Ver Fonte

第1到3步实现

181404010226 há 2 meses atrás
pai
commit
2c2873dbd0

+ 119 - 29
assets/NewbieGuidePlugin-v1.0.0/NewbieGuidePlugin-v1.0.0/scripts/GuideUIController.ts

@@ -5,6 +5,7 @@
 
 import { sp, Node, director, Vec3, find, UITransform, tween, Tween, _decorator, Component } from "cc";
 import EventBus from "../../../scripts/Core/EventBus";
+import BlinkScaleAnimator from "../../../scripts/Animations/BlinkScaleAnimator";
 const { ccclass, property } = _decorator;
 
 @ccclass('GuideUIController')
@@ -30,6 +31,45 @@ export class GuideUIController extends Component {
 
     @property({ type: [Node], tooltip: '每步遮罩区域' })
     public guideStepsMaskAreas: Node[] = [];
+    // 新增:每步Frame装饰器(仅用于显隐控制)
+    @property({ type: [Node], tooltip: '每步Frame装饰器(仅控制显隐)' })
+    public guideStepsFrameDecorators: Node[] = [];
+
+    // 显隐控制:遮罩与Frame
+    private setMaskAndFrameVisibility(stepIndex: number, visible: boolean): void {
+        console.log(`[GuideUIController] 设置第${stepIndex}步的遮罩与Frame可见性为${visible}`);
+        const maskNode = this.guideStepsMaskAreas && this.guideStepsMaskAreas[stepIndex];
+        if (maskNode && maskNode.isValid) {
+            maskNode.active = visible;
+        }
+        const frameNode = this.guideStepsFrameDecorators && this.guideStepsFrameDecorators[stepIndex];
+        if (frameNode && frameNode.isValid) {
+            frameNode.active = visible;
+        }
+    }
+
+    private showMaskAndFrameForStep(stepIndex: number): void {
+        // 先隐藏全部
+        console.log('[GuideUIController] 隐藏所有遮罩与Frame');
+        for (const n of this.guideStepsMaskAreas || []) {
+            if (n && n.isValid) n.active = false;
+        }
+        for (const n of this.guideStepsFrameDecorators || []) {
+            if (n && n.isValid) n.active = false;
+        }
+        // 显示当前步骤
+        this.setMaskAndFrameVisibility(stepIndex, true);
+    }
+
+    private hideAllMasksAndFrames(): void {
+        console.log('[GuideUIController] 隐藏所有遮罩与Frame');
+        for (const n of this.guideStepsMaskAreas || []) {
+            if (n && n.isValid) n.active = false;
+        }
+        for (const n of this.guideStepsFrameDecorators || []) {
+            if (n && n.isValid) n.active = false;
+        }
+    }
 
     @property({ type: [Node], tooltip: '手指动画起始位置节点(按步骤顺序配置)' })
     public fingerStartNodes: Node[] = [];
@@ -37,6 +77,9 @@ export class GuideUIController extends Component {
     @property({ type: [Node], tooltip: '手指动画终止位置节点(按步骤顺序配置)' })
     public fingerTargetNodes: Node[] = [];
 
+    @property({ type: Vec3, tooltip: '手指尖相对按钮右下角偏移' })
+    public fingerTipOffset: Vec3 = new Vec3(-20, 12, 0);
+
     @property({ type: Number, tooltip: 'auto 步骤延时秒数' })
     public autoStepDelay: number = 1.0;
 
@@ -47,10 +90,8 @@ export class GuideUIController extends Component {
     private _autoScheduleCallback: Function | null = null;
     private _listeningTarget: Node | null = null;
     private _listeningEventName: string | null = null;
+    private _activeBlinkComp: BlinkScaleAnimator | null = null;
 
-    // Tutorial-specific properties
-    private _tutorialTargetGrids: string[] = ['Grid_3_1', 'Grid_4_1']; // Target grids for tutorial
-    private _tutorialBlockContainers: string[] = ['Block1', 'Block2']; // Block containers for tutorial
 
 
     /**
@@ -104,10 +145,14 @@ export class GuideUIController extends Component {
         this.guideStepsActions.push('wait_event');
         this.guideStepsEvents.push('TUTORIAL_BLOCK_2_PLACED');
 
-        // Step 2: Tutorial completion
-        this.guideStepsTargets.push(grid4_1);
-        this.guideStepsActions.push('auto');
-        this.guideStepsEvents.push('');
+        // Step 2: Confirm selection step, wait for confirm click
+        const confirmButton = find('Canvas/GameLevelUI/BlockSelectionUI/diban/confirm');
+        if (!confirmButton) {
+            console.warn('[GuideUIController] Confirm button node not found');
+        }
+        this.guideStepsTargets.push(confirmButton ?? grid4_1);
+        this.guideStepsActions.push('wait_event');
+        this.guideStepsEvents.push('BLOCK_SELECTION_CONFIRMED');
 
         console.log('[GuideUIController] Tutorial steps configured');
     }
@@ -357,13 +402,7 @@ export class GuideUIController extends Component {
 
         // 添加缩放动画效果(替换旧 Action)
         Tween.stopAllByTarget(node);
-        tween(node)
-            .repeatForever(
-                tween()
-                    .to(0.5, { scale: 1.2 })
-                    .to(0.5, { scale: 1.0 })
-            )
-            .start();
+
     }
 
     /**
@@ -529,20 +568,6 @@ export class GuideUIController extends Component {
         console.log(`创建拖拽动画: ${nodeId} 从 (${finalStartPos.x}, ${finalStartPos.y}) 到 (${finalTargetPos.x}, ${finalTargetPos.y})`);
     }
 
-    /**
-     * 为目标节点创建高亮效果(已废弃,改为外部遮罩节点管理)
-     */
-    public createHighlightEffect(targetNode: Node): void {
-        // 高亮与遮罩由外部场景节点/guideStepsMaskAreas管理,此处不再创建
-    }
-
-    /**
-     * 移除高亮效果(已废弃,改为外部遮罩节点管理)
-     */
-    public removeHighlightEffect(): void {
-        // 无需清理,本控制器不再管理高亮遮罩
-    }
-
     /**
      * 按数组顺序开始新手引导
      */
@@ -569,6 +594,9 @@ export class GuideUIController extends Component {
             return;
         }
 
+        // 显示当前步骤的遮罩与Frame
+        this.showMaskAndFrameForStep(idx);
+
         const target = this.guideStepsTargets[idx];
         const action = (this.guideStepsActions[idx] || 'tap').toLowerCase();
 
@@ -585,6 +613,8 @@ export class GuideUIController extends Component {
         const hasFingerConfig = this.fingerStartNodes && this.fingerStartNodes[idx] && 
                                this.fingerTargetNodes && this.fingerTargetNodes[idx];
 
+        console.log(`[GuideUIController] 运行步骤 idx=${idx}, action=${action}, hasFingerConfig=${!!hasFingerConfig}, target=${target?.name || 'null'}`);
+
         // 特殊处理第一步:显示从Block1到Grid_3_1的拖拽动画
         if (idx === 0 && action === 'wait_event') {
             if (hasFingerConfig) {
@@ -609,6 +639,50 @@ export class GuideUIController extends Component {
                     console.warn('[GuideUIController] 未找到Grid_3_1,使用普通指向动画');
                 }
             }
+        } else if (idx === 2 && action === 'wait_event') {
+            // 第三步:将手指(guild_1)放到确认按钮右下角,并让手指原地缩放
+            if (this._activeBlinkComp) {
+                this._activeBlinkComp.stop();
+                this._activeBlinkComp = null;
+            }
+            
+            const finger = this.getGuideNode('guild_1');
+            if (finger) {
+                // 先停止手指的Spine动画,避免与缩放冲突
+                this.stopAnimation('guild_1');
+            
+                // 计算目标按钮右下角的世界坐标
+                const cornerWorldPos = ui
+                    ? ui.convertToWorldSpaceAR(new Vec3(ui.width * 0.5, -ui.height * 0.5, 0))
+                    : worldPos;
+            
+                // 转换为手指父节点的本地坐标并叠加指尖偏移
+                const localCorner = this.convertWorldToLocalPosition(cornerWorldPos, finger.parent || finger);
+                const finalPos = new Vec3(
+                    localCorner.x + this.fingerTipOffset.x,
+                    localCorner.y + this.fingerTipOffset.y,
+                    localCorner.z + this.fingerTipOffset.z
+                );
+                finger.setPosition(finalPos);
+                
+                // 显示手指UI(不启动移动动画)
+                this.showGuideUI('guild_1');
+                Tween.stopAllByTarget(finger);
+                
+                // 在手指上播放闪烁缩放动画
+                this._activeBlinkComp = BlinkScaleAnimator.ensure(finger, {
+                    scaleFactor: 1.3,
+                    upDuration: 0.15,
+                    downDuration: 0.15,
+                    easingUp: 'sineOut',
+                    easingDown: 'sineIn'
+                });
+                console.log('[GuideUIController] 第三步:手指位于按钮右下角并进行缩放闪烁');
+            } else {
+                console.warn('[GuideUIController] 未找到 guild_1 手指节点');
+                // 兜底使用普通指向动画
+                this.createPointingAnimation('guild_1', worldPos);
+            }
         } else if (hasFingerConfig && (action === 'wait_event' || action === 'tap')) {
             // 其他步骤如果配置了手指动画节点,也使用拖拽动画
             this.createDragAnimation('guild_1', undefined, undefined, idx);
@@ -659,6 +733,17 @@ export class GuideUIController extends Component {
             Tween.stopAllByTarget(finger);
         }
 
+        // 隐藏当前步骤的遮罩与Frame(例如:第一步完成后隐藏)
+        if (this._currentStepIndex >= 0) {
+            this.setMaskAndFrameVisibility(this._currentStepIndex, false);
+        }
+
+        // 停止第三步的闪烁缩放效果(如果有)
+        if (this._activeBlinkComp) {
+            this._activeBlinkComp.stop();
+            this._activeBlinkComp = null;
+        }
+
         // 清理事件监听/计时器
         this.clearCurrentStepListeners();
 
@@ -699,6 +784,11 @@ export class GuideUIController extends Component {
     public stopGuideSequence(): void {
         this.clearCurrentStepListeners();
         this.hideAllGuideUI();
+        this.hideAllMasksAndFrames();
+        if (this._activeBlinkComp) {
+            this._activeBlinkComp.stop();
+            this._activeBlinkComp = null;
+        }
         this._currentStepIndex = -1;
     }
 
@@ -718,6 +808,6 @@ export class GuideUIController extends Component {
 
     onDestroy() {
         this.stopGuideSequence();
-        // 高亮效果清理由外部负责
+        this.hideAllMasksAndFrames();
     }
 }

Diff do ficheiro suprimidas por serem muito extensas
+ 145 - 157
assets/Scenes/GameLevel.scene


+ 96 - 0
assets/scripts/Animations/BlinkScaleAnimator.ts

@@ -0,0 +1,96 @@
+import { _decorator, Component, Node, tween, Tween, Vec3 } from 'cc';
+import type { TweenEasing } from 'cc';
+const { ccclass, property } = _decorator;
+
+@ccclass('BlinkScaleAnimator')
+export default class BlinkScaleAnimator extends Component {
+    @property({ tooltip: '放大比例因子(相对于初始缩放)' })
+    public scaleFactor: number = 1.3;
+
+    @property({ tooltip: '放大阶段时长(秒)' })
+    public upDuration: number = 0.15;
+
+    @property({ tooltip: '缩小阶段时长(秒)' })
+    public downDuration: number = 0.15;
+
+    @property({ tooltip: '放大阶段缓动' })
+    public easingUp: TweenEasing | ((k: number) => number) = 'sineOut';
+
+    @property({ tooltip: '缩小阶段缓动' })
+    public easingDown: TweenEasing | ((k: number) => number) = 'sineIn';
+
+    @property({ tooltip: '启用组件时自动开始闪烁' })
+    public playOnEnable: boolean = true;
+
+    private _originalScale: Vec3 = Vec3.ONE.clone();
+    private _blinkTween: Tween<Node> | null = null;
+
+    onLoad() {
+        this._originalScale = this.node.scale.clone();
+    }
+
+    onEnable() {
+        if (this.playOnEnable) {
+            this.play();
+        }
+    }
+
+    onDisable() {
+        this.stop();
+    }
+
+    onDestroy() {
+        this.stop();
+    }
+
+    public play(): void {
+        this.stop();
+        const targetScale = new Vec3(
+            this._originalScale.x * this.scaleFactor,
+            this._originalScale.y * this.scaleFactor,
+            this._originalScale.z
+        );
+
+        this._blinkTween = tween(this.node)
+            .to(this.upDuration, { scale: targetScale }, { easing: this.easingUp })
+            .to(this.downDuration, { scale: this._originalScale }, { easing: this.easingDown })
+            .union()
+            .repeatForever()
+            .start();
+    }
+
+    public stop(): void {
+        if (this._blinkTween) {
+            this._blinkTween.stop();
+            this._blinkTween = null;
+        }
+        Tween.stopAllByTarget(this.node);
+        // 恢复初始缩放
+        this.node.scale = this._originalScale.clone();
+    }
+
+    // 自动挂载并启动闪烁
+    public static ensure(node: Node, options?: Partial<{
+        scaleFactor: number,
+        upDuration: number,
+        downDuration: number,
+        easingUp: TweenEasing | ((k: number) => number),
+        easingDown: TweenEasing | ((k: number) => number),
+        playOnEnable: boolean,
+    }>): BlinkScaleAnimator {
+        let comp = node.getComponent(BlinkScaleAnimator);
+        if (!comp) {
+            comp = node.addComponent(BlinkScaleAnimator);
+        }
+        if (options) {
+            if (options.scaleFactor !== undefined) comp.scaleFactor = options.scaleFactor;
+            if (options.upDuration !== undefined) comp.upDuration = options.upDuration;
+            if (options.downDuration !== undefined) comp.downDuration = options.downDuration;
+            if (options.easingUp !== undefined) comp.easingUp = options.easingUp;
+            if (options.easingDown !== undefined) comp.easingDown = options.easingDown;
+            if (options.playOnEnable !== undefined) comp.playOnEnable = options.playOnEnable;
+        }
+        comp.play();
+        return comp;
+    }
+}

+ 9 - 0
assets/scripts/Animations/BlinkScaleAnimator.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "2df70b27-ddbe-4cbe-8ef6-466e10d9b7e9",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 119 - 75
assets/scripts/CombatSystem/BlockManager.ts

@@ -164,6 +164,7 @@ export class BlockManager extends Component {
     // 新手教程相关属性
     private _tutorialStep: number = 0;
     private _tutorialTargetGrids: string[] = ['Grid_3_1', 'Grid_4_1'];
+    private _tutorialTargetGridsStep2: string[] = ['Grid_3_5', 'Grid_3_6'];
     private _tutorialBlockContainers: Node[] = [];
 
     
@@ -311,37 +312,6 @@ export class BlockManager extends Component {
         }, 0);
     }
     
-    /**
-     * 玩家方块受到伤害(已移除,因为抛掷物不攻击玩家方块)
-     */
-    /*
-    private damagePlayerBlock(blockNode: Node, damage: number) {
-        console.log(`[BlockManager] 玩家方块 ${blockNode.name} 受到 ${damage} 点伤害`);
-        
-        // 获取方块信息组件
-        const blockInfo = blockNode.getComponent(BlockInfo);
-        if (!blockInfo) {
-            console.warn(`[BlockManager] 方块 ${blockNode.name} 没有BlockInfo组件,无法处理伤害`);
-            return;
-        }
-        
-        // 减少方块血量
-        const currentHealth = blockInfo.getCurrentHealth();
-        const newHealth = Math.max(0, currentHealth - damage);
-        blockInfo.setCurrentHealth(newHealth);
-        
-        console.log(`[BlockManager] 方块 ${blockNode.name} 血量: ${currentHealth} -> ${newHealth}`);
-        
-        // 检查方块是否被摧毁
-        if (newHealth <= 0) {
-            console.log(`[BlockManager] 方块 ${blockNode.name} 被摧毁`);
-            this.destroyPlayerBlock(blockNode);
-        } else {
-            // 播放受伤效果
-            this.playBlockDamageEffect(blockNode);
-        }
-    }
-    */
     
     /**
      * 摧毁玩家方块
@@ -873,8 +843,9 @@ export class BlockManager extends Component {
             const levelOk = !!this.saveDataManager && typeof this.saveDataManager.getCurrentLevel === 'function' && this.saveDataManager.getCurrentLevel() === 1;
             const sessionOk = !!this.session && typeof this.session.getRefreshUsageCount === 'function' && typeof this.session.getAddBallUsageCount === 'function'
                 && this.session.getRefreshUsageCount() === 0 && this.session.getAddBallUsageCount() === 0;
-            const noPlaced = !this.hasPlacedBlocks();
-            return levelOk && sessionOk && noPlaced;
+            // 教程在两个阶段内都应保持限制,直到第二步完成
+            const tutorialActive = this._tutorialStep < 3;
+            return levelOk && sessionOk && tutorialActive;
         } catch (e) {
             console.warn(`[BlockManager] 新手引导上下文检测失败: ${e}`);
             return false;
@@ -1203,25 +1174,58 @@ export class BlockManager extends Component {
      * 验证新手教程中的方块放置是否有效
      */
     private validateTutorialPlacement(block: Node, targetGrid: Node): boolean {
-        // 确定当前方块是第几个方块
-        let blockIndex = -1;
-        if (block.parent === this.block1Container) {
-            blockIndex = 0;
-        } else if (block.parent === this.block2Container) {
-            blockIndex = 1;
-        } else {
-            // 如果不是来自预期的容器,允许放置(兼容性)
-            return true;
+        // 基础目标对(当前步骤)
+        const basePair = this._tutorialStep >= 2 ? this._tutorialTargetGridsStep2 : this._tutorialTargetGrids;
+        if (!basePair || basePair.length !== 2) {
+            return true; // 配置不完整不强制
         }
 
-        // 检查是否是正确的目标网格
-        const expectedGridName = this._tutorialTargetGrids[blockIndex];
-        if (targetGrid.name !== expectedGridName) {
-            console.log(`[BlockManager] 新手教程:方块${blockIndex + 1}应该放置到${expectedGridName},但尝试放置到${targetGrid.name}`);
-            return false;
+        // 辅助:解析与配对键
+        const parse = (name: string) => {
+            const m = name.match(/^Grid_(\d+)_(\d+)$/);
+            return m ? { row: parseInt(m[1]), col: parseInt(m[2]) } : null;
+        };
+        const pairKey = (arr: string[]) => arr.slice().sort().join('|');
+
+        const p0 = parse(basePair[0]);
+        const p1 = parse(basePair[1]);
+        if (!p0 || !p1) return true;
+
+        // 选择锚点(行不同取较小行,否则取较小列)
+        let anchor = p0;
+        if (p0.row !== p1.row) {
+            anchor = p0.row < p1.row ? p0 : p1;
+        } else if (p0.col !== p1.col) {
+            anchor = p0.col < p1.col ? p0 : p1;
         }
 
-        return true;
+        // 从基础对推导允许的横/竖两种占位对
+        const horizontalPair = [`Grid_${anchor.row}_${anchor.col}`, `Grid_${anchor.row}_${anchor.col + 1}`];
+        const verticalPair = [`Grid_${anchor.row}_${anchor.col}`, `Grid_${anchor.row + 1}_${anchor.col}`];
+        const allowedPairs = new Set<string>([
+            pairKey(horizontalPair),
+            pairKey(verticalPair),
+            pairKey(basePair),
+        ]);
+
+        // 计算实际占用格子
+        const targetRowCol = this.getGridRowCol(targetGrid);
+        if (!targetRowCol) return false;
+        const parts = this.getBlockParts(block);
+        const occupiedNames: string[] = [];
+        for (let i = 0; i < parts.length; i++) {
+            const part = parts[i];
+            const row = targetRowCol.row - part.y;
+            const col = targetRowCol.col + part.x;
+            occupiedNames.push(`Grid_${row}_${col}`);
+        }
+        const occupiedKey = pairKey(occupiedNames);
+
+        const ok = allowedPairs.has(occupiedKey);
+        if (!ok) {
+            console.log(`[BlockManager] 新手教程:需要占用任一允许对,当前占用为 ${occupiedNames.join('、')}`);
+        }
+        return ok;
     }
 
     /**
@@ -1284,6 +1288,9 @@ export class BlockManager extends Component {
             return false;
         }
         
+        // 在放置前缓存一次教程上下文判定,避免放置后 hasPlacedBlocks 改变导致不触发
+        const tutorialContextBeforePlacement = this.isNewbieTutorialContext();
+        
         const gridCenterWorldPos = this.gridContainer.getComponent(UITransform).convertToWorldSpaceAR(targetGrid.position);
         const targetWorldPos = gridCenterWorldPos.clone();
         
@@ -1325,8 +1332,8 @@ export class BlockManager extends Component {
             void 0;
         }
 
-        // 新手教程:触发方块放置事件
-        if (this.isNewbieTutorialContext()) {
+        // 新手教程:触发方块放置事件(使用放置前的上下文判定)
+        if (tutorialContextBeforePlacement) {
             this.handleTutorialBlockPlacement(block, targetGrid);
         }
         
@@ -1337,31 +1344,68 @@ export class BlockManager extends Component {
      * 处理教程方块放置事件
      */
     private handleTutorialBlockPlacement(block: cc.Node, targetGrid: cc.Node): void {
-        // 确定是第几个方块
-        let blockIndex = -1;
-        if (block === this.block1Container) {
-            blockIndex = 0;
-        } else if (block === this.block2Container) {
-            blockIndex = 1;
-        } else if (block === this.block3Container) {
-            blockIndex = 2;
-        }
-
-        // 检查是否放置在正确的目标网格上
-        const targetGridName = targetGrid.name;
-        const expectedGridName = this._tutorialTargetGrids[blockIndex];
-        
-        if (targetGridName === expectedGridName) {
-            // 触发相应的教程事件
-            if (blockIndex === 0) {
-                cc.systemEvent.emit('TUTORIAL_BLOCK_1_PLACED');
-                this._tutorialStep = 1;
-            } else if (blockIndex === 1) {
-                cc.systemEvent.emit('TUTORIAL_BLOCK_2_PLACED');
-                this._tutorialStep = 2;
-            }
-            
-            console.log(`Tutorial: Block ${blockIndex + 1} placed correctly on ${targetGridName}`);
+        // 基础目标对(当前步骤)
+        const basePair = this._tutorialStep >= 2 ? this._tutorialTargetGridsStep2 : this._tutorialTargetGrids;
+        if (!basePair || basePair.length !== 2) {
+            return;
+        }
+
+        // 辅助:解析与配对键
+        const parse = (name: string) => {
+            const m = name.match(/^Grid_(\d+)_(\d+)$/);
+            return m ? { row: parseInt(m[1]), col: parseInt(m[2]) } : null;
+        };
+        const pairKey = (arr: string[]) => arr.slice().sort().join('|');
+
+        const p0 = parse(basePair[0]);
+        const p1 = parse(basePair[1]);
+        if (!p0 || !p1) return;
+
+        // 选择锚点
+        let anchor = p0;
+        if (p0.row !== p1.row) {
+            anchor = p0.row < p1.row ? p0 : p1;
+        } else if (p0.col !== p1.col) {
+            anchor = p0.col < p1.col ? p0 : p1;
+        }
+
+        // 允许的横/竖占位对
+        const horizontalPair = [`Grid_${anchor.row}_${anchor.col}`, `Grid_${anchor.row}_${anchor.col + 1}`];
+        const verticalPair = [`Grid_${anchor.row}_${anchor.col}`, `Grid_${anchor.row + 1}_${anchor.col}`];
+        const allowedPairs = new Set<string>([
+            pairKey(horizontalPair),
+            pairKey(verticalPair),
+            pairKey(basePair),
+        ]);
+
+        // 实际占用
+        const targetRowCol = this.getGridRowCol(targetGrid);
+        if (!targetRowCol) return;
+        const parts = this.getBlockParts(block);
+        const occupiedNames: string[] = [];
+        for (let i = 0; i < parts.length; i++) {
+            const part = parts[i];
+            const row = targetRowCol.row - part.y;
+            const col = targetRowCol.col + part.x;
+            occupiedNames.push(`Grid_${row}_${col}`);
+        }
+        const occupiedKey = pairKey(occupiedNames);
+
+        if (!allowedPairs.has(occupiedKey)) {
+            console.log(`[BlockManager] 教程放置未匹配:需要占用任一允许对,实际 ${occupiedNames.join('、')}`);
+            return;
+        }
+
+        // 阶段推进
+        const isStep2 = this._tutorialStep >= 2;
+        if (!isStep2) {
+            EventBus.getInstance().emit('TUTORIAL_BLOCK_1_PLACED');
+            this._tutorialStep = 2; // 进入第二步(启用第二步的目标格子对)
+            console.log(`[BlockManager] 教程第一步完成,占用 ${occupiedNames.join('、')}`);
+        } else {
+            EventBus.getInstance().emit('TUTORIAL_BLOCK_2_PLACED');
+            this._tutorialStep = 3; // 教程结束,关闭吸附限制
+            console.log(`[BlockManager] 教程第二步完成,占用 ${occupiedNames.join('、')}`);
         }
     }
     

+ 3 - 0
assets/scripts/CombatSystem/BlockSelection/GameBlockSelection.ts

@@ -588,6 +588,9 @@ export class GameBlockSelection extends Component {
         if (this.onConfirmCallback) {
             this.onConfirmCallback();
         }
+
+        // 发出方块选择确认事件(用于新手引导第三步结束)
+        EventBus.getInstance().emit(GameEvents.BLOCK_SELECTION_CONFIRMED);
         
         // 播放下滑diban动画,结束备战进入playing状态
         this.playDibanSlideDownAnimation();

+ 18 - 7
assets/scripts/CombatSystem/SkillSelection/SkillButtonAnimator.ts

@@ -1,4 +1,5 @@
 import { _decorator, Component, Node, Vec3, tween, find, Sprite, Color } from 'cc';
+import BlinkScaleAnimator from '../../Animations/BlinkScaleAnimator';
 const { ccclass, property } = _decorator;
 
 /**
@@ -15,6 +16,7 @@ export class SkillButtonAnimator extends Component {
     private _origScale: Vec3 = new Vec3();
     private _origPos: Vec3 = new Vec3();
     private _blinkTween: any = null;
+    private _starBlinkComp: BlinkScaleAnimator | null = null;
     
     // 星星颜色配置
     private readonly STAR_ACTIVE_COLOR = new Color(255, 255, 255, 255); // 亮起的星星
@@ -68,19 +70,28 @@ export class SkillButtonAnimator extends Component {
         const sprite = nextStar.getComponent(Sprite);
         if (!sprite) return;
         
-        // 创建闪烁动画:在亮起和未亮起之间切换
-        this._blinkTween = tween(sprite)
-            .to(0.5, { color: this.STAR_ACTIVE_COLOR })
-            .to(0.5, { color: this.STAR_INACTIVE_COLOR })
-            .union()
-            .repeatForever()
-            .start();
+        // 使用通用组件在该星星上播放闪烁缩放(不再使用颜色闪烁)
+        if (this._starBlinkComp) {
+            this._starBlinkComp.stop();
+            this._starBlinkComp = null;
+        }
+        this._starBlinkComp = BlinkScaleAnimator.ensure(nextStar, {
+            scaleFactor: 1.3,
+            upDuration: 0.15,
+            downDuration: 0.15,
+            easingUp: 'sineOut',
+            easingDown: 'sineIn'
+        });
     }
 
     /**
      * 停止星星闪烁动画
      */
     private stopBlinkAnimation() {
+        if (this._starBlinkComp) {
+            this._starBlinkComp.stop();
+            this._starBlinkComp = null;
+        }
         if (this._blinkTween) {
             this._blinkTween.stop();
             this._blinkTween = null;

+ 3 - 0
assets/scripts/Core/EventBus.ts

@@ -97,6 +97,9 @@ export enum GameEvents {
     // 方块拖拽事件
     BLOCK_DRAG_START = 'BLOCK_DRAG_START',
     BLOCK_DRAG_END = 'BLOCK_DRAG_END',
+
+    // 方块选择事件
+    BLOCK_SELECTION_CONFIRMED = 'BLOCK_SELECTION_CONFIRMED',
     
     // Toast提示事件// Toast提示事件
     SHOW_TOAST = 'SHOW_TOAST',

+ 45 - 48
assets/scripts/FourUI/UpgradeSystem/UpgradeAni.ts

@@ -1,6 +1,7 @@
 import { _decorator, Component, Node, Tween, tween, Vec3, Material, Sprite, resources, Label, JsonAsset } from 'cc';
 import { JsonConfigLoader } from '../../Core/JsonConfigLoader';
 import { Audio } from '../../AudioManager/AudioManager';
+import BlinkScaleAnimator from '../../Animations/BlinkScaleAnimator';
 
 const { ccclass, property } = _decorator;
 
@@ -20,7 +21,7 @@ export class UpgradeAni extends Component {
     @property(Label)
     currentDamageLabel: Label = null; // Canvas/UpgradePanel/NumberBack/CurrentDamage节点
     
-    private blinkTween: Tween<Node> = null; // 闪烁动画的引用
+    private blinkComp: BlinkScaleAnimator | null = null; // 闪烁动画组件引用
     private weaponsConfig: any = null; // 武器配置数据
     
     /**
@@ -80,38 +81,39 @@ export class UpgradeAni extends Component {
             }
             
             // 创建缩放动画:放大到1.5倍再立即缩小回原始大小
-            const scaleAnimation = tween(weaponIconNode)
-                .to(0.25, { scale: new Vec3(originalScale.x * 1.5, originalScale.y * 1.5, originalScale.z) }, {
-                    easing: 'sineOut'
-                })
-                .to(0.25, { scale: originalScale }, {
-                    easing: 'sineIn'
-                })
-                .call(() => {
-                    // 动画结束后恢复原始材质
-                    if (weaponSprite && originalMaterial) {
-                        // 如果原始材质是customMaterial,则恢复customMaterial
-                        if (weaponSprite.customMaterial === originalMaterial) {
-                            weaponSprite.material = weaponSprite.customMaterial;
-                        } else {
-                            weaponSprite.material = originalMaterial;
-                        }
-                        console.log('[UpgradeAni] 恢复原始材质成功');
-                    } else if (weaponSprite) {
-                        // 如果没有保存的原始材质,尝试恢复到customMaterial
-                        if (weaponSprite.customMaterial) {
-                            weaponSprite.material = weaponSprite.customMaterial;
-                            console.log('[UpgradeAni] 恢复到customMaterial');
-                        } else {
-                            weaponSprite.material = null;
-                            console.log('[UpgradeAni] 恢复到默认材质');
-                        }
+            const comp = BlinkScaleAnimator.ensure(weaponIconNode, {
+                scaleFactor: 1.5,
+                upDuration: 0.25,
+                downDuration: 0.25,
+                easingUp: 'sineOut',
+                easingDown: 'sineIn',
+                playOnEnable: false,
+            });
+            comp.play();
+            this.scheduleOnce(() => {
+                comp.stop();
+                // 动画结束后恢复原始材质
+                if (weaponSprite && originalMaterial) {
+                    if (weaponSprite.customMaterial === originalMaterial) {
+                        weaponSprite.material = weaponSprite.customMaterial;
+                    } else {
+                        weaponSprite.material = originalMaterial;
                     }
-                    resolve();
-                });
+                    console.log('[UpgradeAni] 恢复原始材质成功');
+                } else if (weaponSprite) {
+                    if (weaponSprite.customMaterial) {
+                        weaponSprite.material = weaponSprite.customMaterial;
+                        console.log('[UpgradeAni] 恢复到customMaterial');
+                    } else {
+                        weaponSprite.material = null;
+                        console.log('[UpgradeAni] 恢复到默认材质');
+                    }
+                }
+                resolve();
+            }, 0.25 + 0.25);
             
             // 播放缩放动画
-            scaleAnimation.start();
+            // 使用 BlinkScaleAnimator 播放动画,无需调用 scaleAnimation.start();
         });
     }
     
@@ -205,18 +207,15 @@ export class UpgradeAni extends Component {
         // 重置到原始缩放状态
         this.upgradeBtnNode.setScale(Vec3.ONE);
         
-        // 创建循环闪烁动画:放大到1.5倍再缩小回原始大小,无限循环
-        this.blinkTween = tween(this.upgradeBtnNode)
-            .to(0.5, { scale: new Vec3(1.5, 1.5, 1) }, {
-                easing: 'sineInOut'
-            })
-            .to(0.5, { scale: Vec3.ONE }, {
-                easing: 'sineInOut'
-            })
-            .union() // 将上面的动画组合成一个整体
-            .repeatForever(); // 无限循环
-            
-        this.blinkTween.start();
+        // 使用通用组件进行循环闪烁(放大缩小)
+        this.blinkComp = BlinkScaleAnimator.ensure(this.upgradeBtnNode, {
+            scaleFactor: 1.5,
+            upDuration: 0.5,
+            downDuration: 0.5,
+            easingUp: 'sineInOut',
+            easingDown: 'sineInOut',
+        });
+        this.blinkComp.play();
         
         console.log('[UpgradeAni] 开始升级按钮闪烁动画');
     }
@@ -226,15 +225,13 @@ export class UpgradeAni extends Component {
      * 当钞票不够升级时调用此方法
      */
     public stopUpgradeBtnBlink(): void {
-        if (this.blinkTween) {
-            this.blinkTween.stop();
-            this.blinkTween = null;
+        if (this.blinkComp) {
+            this.blinkComp.stop();
+            this.blinkComp = null;
         }
         
         if (this.upgradeBtnNode) {
-            // 停止节点上的所有动画
             Tween.stopAllByTarget(this.upgradeBtnNode);
-            // 恢复到原始缩放状态
             this.upgradeBtnNode.setScale(Vec3.ONE);
         }
         
@@ -246,7 +243,7 @@ export class UpgradeAni extends Component {
      * @returns 是否正在闪烁
      */
     public isUpgradeBtnBlinking(): boolean {
-        return this.blinkTween !== null;
+        return this.blinkComp !== null;
     }
     
     /**

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff