From d951896799e0d396a54ffb2016c52cb80fdc230c Mon Sep 17 00:00:00 2001 From: panFD Date: Sat, 12 Sep 2026 14:04:18 +0800 Subject: [PATCH] =?UTF-8?q?refactor(hero):=20=E4=BC=98=E5=8C=96=E8=8B=B1?= =?UTF-8?q?=E9=9B=84=E5=8A=A8=E7=94=BB=E7=8A=B6=E6=80=81=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E9=80=BB=E8=BE=91=20=E7=A7=BB=E9=99=A4=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E7=9A=84=E8=8B=B1=E9=9B=84=E6=88=90=E9=95=BF=E6=A8=A1?= =?UTF-8?q?=E6=8B=9F=E6=95=B0=E6=8D=AE=EF=BC=8C=E4=BF=AE=E5=A4=8D=E5=8A=A8?= =?UTF-8?q?=E7=94=BB=E7=8A=B6=E6=80=81=E4=B8=8E=E5=AE=9E=E9=99=85=E8=A1=A8?= =?UTF-8?q?=E7=8E=B0=E4=B8=8D=E4=B8=80=E8=87=B4=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 重构HeroSpine的idle/move方法,新增返回值标记动画是否被拦截 2. 重构HeroViewComp的status_change逻辑,仅在动画切换成功时更新状态 3. 重构HeroAnmComp的动画播放逻辑,修复动画结束后默认播放的异常 4. 移除SingletonModuleComp中的测试用英雄成长数据 5. 简化MoveSystem中状态切换的冗余判断逻辑 --- .../script/game/common/SingletonModuleComp.ts | 12 +----- assets/script/game/hero/HeroAnmComp.ts | 40 ++++++++++++++----- assets/script/game/hero/HeroSpine.ts | 20 ++++++---- assets/script/game/hero/HeroViewComp.ts | 22 +++++++--- assets/script/game/hero/MoveComp.ts | 6 +-- 5 files changed, 63 insertions(+), 37 deletions(-) diff --git a/assets/script/game/common/SingletonModuleComp.ts b/assets/script/game/common/SingletonModuleComp.ts index e780524b..a1b7e3a5 100644 --- a/assets/script/game/common/SingletonModuleComp.ts +++ b/assets/script/game/common/SingletonModuleComp.ts @@ -143,17 +143,7 @@ export class SingletonModuleComp extends ecs.Comp { shop: false, // 购买药品面板按钮 }, /** 英雄局外成长数据:记录每个英雄的局外等级与碎片数量 */ - // TODO: 以下为测试英雄升级功能的模拟数据,验证通过后需移除(避免污染云端存档) - hero_growth: { - // 碎片充足:可连升 2 级(10+20=30,剩余 20 不够升 4 级) - 5001: { uuid: 5001, lv: 1, fragments: 50 }, - // 碎片不足:升 4 级需 40,验证升级失败路径 - 5002: { uuid: 5002, lv: 3, fragments: 15 }, - // 碎片刚好够升到满级:升 6 级需 160 - 5003: { uuid: 5003, lv: 5, fragments: 160 }, - // 已满级:验证满级后 upgradeHero 返回 false - 5004: { uuid: 5004, lv: 6, fragments: 999 }, - } as Record, + hero_growth: {} as Record, /** 天赋等级表:key 为天赋 uuid,value 为当前等级 */ talents: {} as Record, /** 可用天赋点数 */ diff --git a/assets/script/game/hero/HeroAnmComp.ts b/assets/script/game/hero/HeroAnmComp.ts index fc6ee182..513875c1 100644 --- a/assets/script/game/hero/HeroAnmComp.ts +++ b/assets/script/game/hero/HeroAnmComp.ts @@ -10,7 +10,6 @@ export default class HeroAnmComp extends Component { private anmcon: any = null private _hasStop = true; private _atkIndex = 0; - private default_anim: string = 'idle' anms: any[] = ["idle", "move", "atk0", "max0", "max1"] onLoad() { this.anmcon = this.node.getComponent(Animation) @@ -41,25 +40,41 @@ export default class HeroAnmComp extends Component { this._hasStop = true; } onAnimationFinished(type: Animation.EventType, state: AnimationState) { + // HeroAnmComp 与 HeroViewComp 挂在同一节点上,直接 getComponent 即可, + // 不需要走父节点查找(且 Component 没有 getComponentInParent,必须在 Node 上调用) + const viewComp = this.node.getComponent(HeroViewComp); if (state.name === "max0" || state.name === "max1") { // 大招播放结束,清除 HeroViewComp 的置顶标记,恢复 MoveSystem 统一排序 - const viewComp = this.node.getComponent(HeroViewComp) || this.node.getComponentInParent(HeroViewComp); if (viewComp) { viewComp.isCastingMax = false; } } if (state.name != "idle" && state.name != "move") { - this.anmcon.play(this.default_anim) + // 攻击/buff/大招结束:按视图层记录的真实状态回放,而非过期的缓存动画, + // 避免"位置已停但还在播走路"的原地走步(view.status 由 status_change 成功切换时写入) + const target = viewComp?.status === "move" ? "move" : "idle"; + if (!this.maxPlaying && !this.atkPlaying && !this.buffPlaying) { + if (!this.anmcon.getState(target)?.isPlaying) { + this.anmcon.play(target); + } + } } } atked() { this.fsSprite.clickFlash(); } - move() { - if (this.maxPlaying || this.atkPlaying || this.buffPlaying) return - if (this.anmcon.getState("move").isPlaying) return + /** + * 播放移动动画。 + * Why 返回 boolean:普攻/buff/大招播放期间会被拦截(return false), + * 调用方(HeroSpine/HeroViewComp)据此不记录状态,保证记录状态与实际动画一致, + * 下一帧重试时去重条件不成立可再次尝试,避免"位置在动但动画卡死"。 + * @returns true = 动画已处于目标状态(新播或原本在播);false = 被高优先级动画拦截 + */ + move(): boolean { + if (this.maxPlaying || this.atkPlaying || this.buffPlaying) return false + if (this.anmcon.getState("move").isPlaying) return true this.anmcon.play("move") - this.default_anim = 'move' + return true } atk() { if (this.maxPlaying) return @@ -75,11 +90,14 @@ export default class HeroAnmComp extends Component { this.anmcon.play(anm) } - idle() { - if (this.maxPlaying || this.atkPlaying || this.buffPlaying) return - if (this.anmcon.getState("idle").isPlaying) return + /** + * 播放待机动画(返回值语义与 move() 一致:被高优先级动画拦截时返回 false) + */ + idle(): boolean { + if (this.maxPlaying || this.atkPlaying || this.buffPlaying) return false + if (this.anmcon.getState("idle").isPlaying) return true this.anmcon.play("idle") - this.default_anim = 'idle' + return true } buff() { if (this.maxPlaying) return diff --git a/assets/script/game/hero/HeroSpine.ts b/assets/script/game/hero/HeroSpine.ts index a9d6e7da..e99d09e6 100644 --- a/assets/script/game/hero/HeroSpine.ts +++ b/assets/script/game/hero/HeroSpine.ts @@ -35,11 +35,15 @@ export class HeroSpine extends Component { } - idle(){ - - if(this.status=="idle") return + /** + * 播放待机动画:动画层成功切换才记录 status, + * 被普攻/buff/大招拦截时返回 false 且不记录,保证记录与实际动画一致。 + */ + idle(): boolean { + if(this.status=="idle") return true + if(!this.anm.idle()) return false this.status="idle" - this.anm.idle() + return true } atk() { this.anm.atk() @@ -75,10 +79,12 @@ export class HeroSpine extends Component { buff(){ this.anm.buff() } - move(){ - if(this.status=="move") return + /** 播放移动动画(返回值语义与 idle() 一致:拦截时不记录状态) */ + move(): boolean { + if(this.status=="move") return true + if(!this.anm.move()) return false this.status="move" - this.anm.move() + return true } } diff --git a/assets/script/game/hero/HeroViewComp.ts b/assets/script/game/hero/HeroViewComp.ts index ce08ee39..be32345f 100644 --- a/assets/script/game/hero/HeroViewComp.ts +++ b/assets/script/game/hero/HeroViewComp.ts @@ -312,15 +312,24 @@ export class HeroViewComp extends CCComp { get isActive() { return this.ent.has(HeroViewComp) && this.node?.isValid; } - /** 状态切换(动画) */ + /** + * 状态切换(动画)。 + * Why 成功才记录:动画层在普攻/buff/大招播放期间会拦截 idle/move 切换, + * 若先记录状态再播动画,会导致记录与实际动画永久脱钩(位置在动动画却卡死、 + * 或攻击结束回放过期动画)。只在切换成功时写入 status,被拦截时不写入, + * 下一帧 MoveSystem 重试时去重条件不成立,会再次尝试直至成功。 + */ status_change(type: string) { if (this.status === type) return; - this.status = type; - if (this.model.is_dead || this.model.is_reviving) return + if (this.model.is_dead || this.model.is_reviving) { + // 死亡/复活中动画由对应流程接管,仅记录待恢复状态 + this.status = type; + return; + } if (type === "idle") { - this.as.idle(); + if (this.as.idle()) this.status = type; } else if (type === "move") { - this.as.move(); + if (this.as.move()) this.status = type; } } /** 护盾常驻特效节点,避免重复创建 */ @@ -374,6 +383,9 @@ export class HeroViewComp extends CCComp { // 恢复UI this.topBar.show(); + // 死亡期间 status 可能已被记录为 idle(死亡分支只记录不播动画), + // 清空强制重同步,确保复活后动画真实切回待机 + this.status = ""; this.status_change("idle"); // 【新增】仅英雄阵营派发复活成功事件,供卡牌技能(HeroCall 类型)监听 diff --git a/assets/script/game/hero/MoveComp.ts b/assets/script/game/hero/MoveComp.ts index a4bc415f..9f2d7ac4 100644 --- a/assets/script/game/hero/MoveComp.ts +++ b/assets/script/game/hero/MoveComp.ts @@ -131,7 +131,7 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate model.is_atking = false; if (model.fac === FacSet.HERO) { this.processHeroQueue(e, model, move, view); - } else if (view.status !== "atk") { + } else { view.status_change("idle"); } return; @@ -146,7 +146,7 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate /** 无敌人:停止移动,等待战斗结束结算 */ this.clearCombatTarget(model); model.is_atking = false; - if (view.status !== "atk") view.status_change("idle"); + view.status_change("idle"); } /** 渲染层级重排(双阵营统一处理) */ @@ -181,7 +181,7 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate view.node.setPosition(targetX, view.node.position.y, 0); } view.scale = 1; - if (view.status !== "atk") view.status_change("idle"); + view.status_change("idle"); return; }