Files
pixelheros/assets/script/game/hero/HeroViewComp.ts
panFD d951896799 refactor(hero): 优化英雄动画状态同步逻辑
移除测试用的英雄成长模拟数据,修复动画状态与实际表现不一致的问题

1. 重构HeroSpine的idle/move方法,新增返回值标记动画是否被拦截
2. 重构HeroViewComp的status_change逻辑,仅在动画切换成功时更新状态
3. 重构HeroAnmComp的动画播放逻辑,修复动画结束后默认播放的异常
4. 移除SingletonModuleComp中的测试用英雄成长数据
5. 简化MoveSystem中状态切换的冗余判断逻辑
2026-09-12 14:04:18 +08:00

573 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Vec3, _decorator, v3, Collider2D, Contact2DType, Node, Prefab, instantiate, tween, Tween, BoxCollider2D, UITransform, Color } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { mLogger } from "../common/Logger";
import { HeroSpine } from "./HeroSpine";
import { HeroTopBarComp } from "./HeroTopBarComp";
import { BuffComp } from "./BuffComp";
import { BoxSet, FacSet, FightSet, NumberFormatter, TooltipTypes } from "../common/config/GameSet";
import { smc } from "../common/SingletonModuleComp";
import { SkillSet, } from "../common/config/SkillSet";
import { HeroInfo } from "../common/config/heroSet";
import { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { Tooltip } from "../skill/Tooltip";
import { timedCom } from "../skill/timedCom";
import { oneCom } from "../skill/oncend";
import { FlashSprite } from "./hit-flash-white/scripts/FlashSprite";
const { ccclass, property } = _decorator;
/** 角色显示组件 */
export interface BuffInfo {
value: number;
remainTime?: number;
}
@ccclass('HeroViewComp') // 定义Cocos Creator 组件
@ecs.register('HeroView', false) // 定义ECS 组件
export class HeroViewComp extends CCComp {
@property({ tooltip: "是否启用调试日志" })
private debugMode: boolean = false; // 是否启用调试模式
// ==================== View 层属性(表现相关)====================
as: HeroSpine = null!
status: String = ""
scale: number = 1; // 显示方向
box_group: number = BoxSet.HERO; // 碰撞组
realDeadTime: number = 0.1
deadCD: number = 0
monDeadTime: number = 0.1
// 血条显示相关
lastBarUpdateTime: number = 0; // 最后一次血条/蓝条/护盾更新时间
// ==================== UI 节点引用 ====================
private topBar: HeroTopBarComp = null!;
/** 是否正在播放大招动画(用于渲染层级置顶,由 HeroAnmComp 播放结束时清除) */
public isCastingMax: boolean = false;
// ==================== 直接访问 HeroAttrsComp ====================
get model() {
// 🔥 修复:添加安全检查,防止ent为null时的访问异常
if (!this.ent) {
mLogger.warn(this.debugMode, 'HeroViewComp', "[HeroViewComp] ent is null, returning null for model");
return null;
}
return this.ent.get(HeroAttrsComp);
}
private damageQueue: Array<{
damage: number,
isCrit: boolean,
debuffIcon: string,
}> = [];
private isProcessingDamage: boolean = false;
private damageInterval: number = 0.01; // 伤害数字显示间隔
private effectLifeTime: number = 0.8;
onLoad() {
this.as = this.getComponent(HeroSpine);
const collider = this.node.getComponent(BoxCollider2D);
this.scheduleOnce(() => {
if (collider) {
collider.enabled = true; // 先禁
collider.group = this.box_group; // 设置为英雄组
}
}, 0.1)
// let anm = this.node.getChildByName("anm")
// anm.setScale(anm.scale.x*0.8,anm.scale.y*0.8);
}
/** 视图层逻辑代码分离演示 */
start() {
this.init();
}
/** 初始化/重置视图状态 */
init() {
this.status = "";
this.deadCD = 0;
this.lastBarUpdateTime = 0;
this.status_change("idle");
// 初始化头顶状态栏
const topNode = this.node.getChildByName("top");
this.topBar = topNode.getComponent(HeroTopBarComp) || topNode.addComponent(HeroTopBarComp);
this.topBar.init(this.model, this.scale);
/** 方向 */
this.node.setScale(this.scale * Math.abs(this.node.scale.x), 1 * this.node.scale.y); // 确保 scale.x 为正后再乘方向
// 🔥 重置描边
if (this.model) {
let flashSprite = this.node.getComponent(FlashSprite);
if (!flashSprite) {
flashSprite = this.node.getComponentInChildren(FlashSprite);
}
if (flashSprite) {
flashSprite.setOutlineByLevel(this.model.lv);
}
}
}
/**
* View 层每帧更新
* 注意:数据更新逻辑已移到 HeroAttrSystem,这里只负责显示
*/
update(dt: number) {
if (!smc.mission.play) return;
if (smc.mission.pause) return
// 🔥 修复:添加安全检查,防止在实体销毁过程中访问null的model
if (!this.ent) return;
if (!this.model) return;
this.processDamageQueue();
if (this.model.is_dead) {
this.deadCD += dt
if (this.deadCD >= this.realDeadTime) {
this.deadCD = 0
this.realDead()
}
return
};
// ✅ 按需更新 UI(脏标签模式)- 只在属性变化时更新
if (this.model.dirty_hp) {
this.topBar.hpShow();
this.model.dirty_hp = false;
}
if (this.model.dirty_shield) {
this.topBar.shieldShow(this.model.shield);
if (this.model.shield <= 0) {
// 护盾耗尽时隐藏护盾特效
this.hideShieldFx();
} else {
// 护盾存在时显示常驻特效(内部有 isValid 防重复,不会重复创建)
this.showShieldFx();
}
this.model.dirty_shield = false;
}
// ✅ buff 变更脏标记:刷新 debuff 图标显隐(灼烧等)
const buffComp = this.ent.has(BuffComp) ? this.ent.get(BuffComp) : null;
if (buffComp && buffComp.dirty_buffs) {
this.topBar.debuffShow(buffComp);
// hp_max 计时 buff 的生效/到期会改变血条上限,同步刷新血条(内部走 syncHpWithFinalMax)
this.topBar.hpShow();
buffComp.dirty_buffs = false;
}
}
/** 技能 CD 显示:由 SCastSystem 每帧 updateCD 后调用,刷新能量条进度 */
public cd_show() {
this.topBar.cdShow();
}
/** 升级特效 */
public lv_up() {
this.spawnTimedFx("game/skill/buff/buff_lvup", this.node, 1.0);
// 升级时同步更新描边
if (this.model) {
let flashSprite = this.node.getComponent(FlashSprite);
if (!flashSprite) {
flashSprite = this.node.getComponentInChildren(FlashSprite);
}
if (flashSprite) {
flashSprite.setOutlineByLevel(this.model.lv);
}
}
}
/** 攻击力提升特效 */
private ap_up() {
this.spawnTimedFx("game/skill/buff/buff_apup", this.node, 1.0);
}
/** 受击特效 */
private in_atked(anm: string = "atked", scale: number = 1) {
this.as.do_atked()
// var path = "game/skill/end/" + anm;
// var prefab: Prefab = oops.res.get(path, Prefab)!;
// var node = instantiate(prefab);
// node.setScale(node.scale.x * scale, node.scale.y);
// node.setPosition(this.node.position.x, this.node.position.y+50, this.node.position.z);
// node.parent = this.node.parent;
}
/** 冰冻特效 */
in_iced(t: number = 1) {
this.spawnTimedFx("game/skill/buff/iced", this.node, t);
}
/** 击晕特效 */
in_stun(t: number = 1) {
this.spawnTimedFx("game/skill/buff/stun", this.node, t);
}
/** 技能提示 */
private tooltip(type: number = 1, value: string = "", s_uuid: number = 1001, y: number = 50) {
let pos = v3(0, 60);
pos.y = pos.y + y;
Tooltip.load(pos, type, value, s_uuid, this.node);
}
/** 技能提示 */
public skill_name(value: string = "", s_uuid: number = 1001, y: number = 50) {
let pos = v3(0, 60);
pos.y = pos.y + y;
Tooltip.load(pos, TooltipTypes.skill, value, s_uuid, this.node, 1, this.model?.fac ?? FacSet.MON);
}
/** 血量提示(伤害数字) */
private hp_tip(type: number = 1, value: string = "", s_uuid: number = 1001, y: number = 0, debuffIcon: string = "", tipColor: Color | null = null) {
let x = this.node.position.x;
// 获取怪物高度的一半,定位到中心点
let halfHeight = 0;
const transform = this.node.getComponent(UITransform);
if (transform) {
halfHeight = transform.height / 2;
}
// 起点设为怪物中心偏下位置,使其在血条下方
let ny = this.node.position.y + halfHeight - 15;
let pos = v3(x, ny, 0);
Tooltip.load(pos, type, value, s_uuid, this.node.parent, 1, this.model?.fac ?? FacSet.MON, debuffIcon, tipColor);
}
/** 护盾吸收提示:蓝色 "吸收 N" */
shield_tip(absorbed: number) {
this.hp_tip(TooltipTypes.shield, NumberFormatter.formatNumber(Math.max(0, Math.floor(absorbed))));
}
/** 闪避提示:白色 "闪避" */
showMiss() {
this.hp_tip(TooltipTypes.miss, "闪避");
}
public playBuff(anm: string = "") {
if (anm === "") return;
var path = "game/skill/buff/" + anm;
this.spawnTimedFx(path, this.node, this.effectLifeTime);
}
public playReady(anm: string = "") {
if (anm === "") return;
var path = "game/skill/ready/" + anm;
this.spawnAnimEndFx(path, this.node, undefined);
}
public playOther(anm: string = "") {
if (anm === "") return;
var path = "game/skill/ready/" + anm;
// 以自身当前位置为基准,挂载到父节点,即使自身节点销毁动画也能继续播放
this.spawnAnimEndFx(path, this.node.parent, this.node.position);
}
public playEnd(anm: string = "") {
if (anm === "") return;
var path = "game/skill/end/" + anm;
this.spawnAnimEndFx(path, this.node, undefined);
}
public playAllTime(anm: string = "") {
if (anm === "") return;
var path = "game/skill/buff/" + anm;
// 常驻特效直接创建节点,不挂载生命周期销毁组件,随父节点(this.node)一起销毁
this.createFxNode(path, this.node, undefined);
}
/** 治疗特效 */
private heathed() {
this.spawnAnimEndFx("game/skill/end/heal", this.node, undefined);
}
private deaded() {
this.spawnAnimEndFx("game/skill/end/dead", this.node.parent, this.node.position);
}
private createFxNode(path: string, parent: Node | null, worldPos?: Vec3): Node | null {
if (!parent || !parent.isValid) return null;
const prefab: Prefab = oops.res.get(path, Prefab)!;
if (!prefab) return null;
const node = instantiate(prefab);
if (!node || !node.isValid) return null;
node.parent = parent;
if (worldPos) {
node.setPosition(worldPos);
}
return node;
}
private spawnTimedFx(path: string, parent: Node | null, life: number = 0.8, worldPos?: Vec3): Node | null {
const node = this.createFxNode(path, parent, worldPos);
if (!node) return null;
const timer = node.getComponent(timedCom) || node.addComponent(timedCom);
timer.time = Math.max(0.2, life);
return node;
}
private spawnAnimEndFx(path: string, parent: Node | null, worldPos?: Vec3): Node | null {
const node = this.createFxNode(path, parent, worldPos);
if (!node) return null;
node.getComponent(oneCom) || node.addComponent(oneCom);
return node;
}
// 注意:BaseUp 逻辑已移到 HeroAttrSystem.update()
// 注意:updateTemporaryBuffsDebuffs 逻辑已移到 HeroAttrSystem.update()
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;
if (this.model.is_dead || this.model.is_reviving) {
// 死亡/复活中动画由对应流程接管,仅记录待恢复状态
this.status = type;
return;
}
if (type === "idle") {
if (this.as.idle()) this.status = type;
} else if (type === "move") {
if (this.as.move()) this.status = type;
}
}
/** 护盾常驻特效节点,避免重复创建 */
private shieldFxNode: Node | null = null;
add_shield(shield: number) {
// 护盾数据更新由 Model 层处理,这里只负责视图表现
if (this.model && this.model.shield > 0) {
this.topBar.shieldShow(this.model.shield);
this.showShieldFx();
}
}
/** 显示护盾常驻特效(动态加载并挂载到 this.node 下) */
private showShieldFx() {
if (this.shieldFxNode && this.shieldFxNode.isValid) return;
this.shieldFxNode = this.createFxNode("game/skill/buff/shielded", this.node, undefined);
}
/** 隐藏护盾常驻特效 */
private hideShieldFx() {
if (this.shieldFxNode && this.shieldFxNode.isValid) {
this.shieldFxNode.destroy();
}
this.shieldFxNode = null;
}
health(hp: number = 0) {
// ✅ 仅显示特效和提示,不调用 hp_show()
if (hp <= 20) return;
this.heathed();
this.hp_tip(TooltipTypes.health, hp.toFixed(0));
this.lastBarUpdateTime = Date.now() / 1000;
}
alive() {
// 重置复活标记 - 必须最先重置,否则status_change会被拦截
this.model.is_reviving = false;
this.model.is_dead = false
this.model.is_count_dead = false
this.deadCD = 0;
this.hideShieldFx();
// 恢复碰撞体
const collider = this.node.getComponent(Collider2D);
if (collider) {
collider.enabled = true;
}
// 恢复UI
this.topBar.show();
// 死亡期间 status 可能已被记录为 idle(死亡分支只记录不播动画),
// 清空强制重同步,确保复活后动画真实切回待机
this.status = "";
this.status_change("idle");
// 【新增】仅英雄阵营派发复活成功事件,供卡牌技能(HeroCall 类型)监听
// 统一在此派发可覆盖两条复活路径:复活技能触发 + 关卡战斗准备阶段恢复
if (this.model && this.model.fac === FacSet.HERO && this.ent) {
oops.message.dispatchEvent(GameEvent.ReviveSuccess, { eid: this.ent.eid });
}
}
/**
* 死亡视图表现
* 由 HeroAtkSystem 调用,只负责视觉效果和事件通知
*/
do_dead() {
// 添加安全检查
if (!this.model) return;
this.isCastingMax = false;
// 防止重复触发
if (this.model.is_count_dead) return;
this.model.is_count_dead = true; // 防止重复触发,必须存在防止重复调用
// 怪物使用0.5秒死亡时间,英雄使用realDeadTime
if (this.model.fac === FacSet.MON) {
this.realDeadTime = this.monDeadTime;
}
// 如果角色带有死亡触发技能,则由 SCastSystem 播放 playReady("dead")
if (this.model && this.model.runtime_dead && this.model.runtime_dead.length > 0) {
// SCastSystem will handle the "dead" ready animation
} else {
// 播放默认死亡特效
this.deaded();
}
}
realDead() {
// 🔥 修复:添加model安全检查,防止实体销毁过程中的空指针异常
if (!this.model) {
mLogger.warn(this.debugMode, 'HeroViewComp', "[HeroViewComp] realDead called but model is null, skipping");
return;
}
// 隐藏UI
this.topBar.hide();
// 在销毁实体前先禁用碰撞体,从源头减少"尸体"参与碰撞
const collider = this.node.getComponent(Collider2D);
if (collider) {
collider.enabled = false;
}
// 根据阵营决定飞出方向:英雄向左(负),怪物向右(正)
let isHero = this.model.fac === FacSet.HERO;
let dirX = isHero ? -800 : 800;
// 死亡往后飞出屏幕动画,结束后统一销毁实体
// 英雄为真实死亡:快照已在 doDead 阶段写入 smc.mission.dead_heroes,此处直接销毁
tween(this.node)
.by(0.5, { position: v3(dirX, 700, 0) }, { easing: "quadOut" })
.call(() => {
this.ent.destroy();
})
.start();
}
/** 受击表现(伤害数字、受击动画、击退) */
do_atked(damage: number, isCrit: boolean, s_uuid: number, knockbackExtra: number = 0) {
// 受到攻击时更新最后更新时间
this.topBar.activateTopBar();
this.lastBarUpdateTime = Date.now() / 1000;
if (damage <= 0) return;
// 播放受击音效
if (isCrit) {
oops.audio.playEffect("music/Critical");
} else {
oops.audio.playEffect("music/Hit");
}
// 视图层表现
let SConf = SkillSet[s_uuid]
const hitAnm = SConf?.DAnm || "atked";
this.back(knockbackExtra)
this.in_atked(hitAnm, this.model.fac == FacSet.HERO ? 1 : -1);
this.showDamage(damage, isCrit);
}
//后退
back(distance: number = 0) {
// 击退位移由 MoveSystem 统一处理,视图层只负责触发,不直接操作 position
// 具体击退速度由 HeroAtkSystem 在伤害结算后写入 MoveComp.knockbackVelocity
}
// 伤害计算和战斗逻辑已迁移到 HeroBattleSystem
playSkillAnm(act: string = "") {
mLogger.log(this.debugMode, 'HeroViewComp', '[heroview] act' + act,)
if (act === "") return;
switch (act) {
case "max":
this.isCastingMax = true;
this.as.max()
break
case "atk":
this.as.atk()
break
case "buff":
this.as.buff()
break
}
}
/** 显示伤害数字 */
showDamage(damage: number, isCrit: boolean, debuffIcon: string = "") {
this.damageQueue.push({
damage,
isCrit,
debuffIcon,
});
}
/** 处理伤害队列 */
private processDamageQueue() {
if (this.isProcessingDamage || this.damageQueue.length === 0) return;
this.isProcessingDamage = true;
const damageInfo = this.damageQueue.shift()!;
this.showDamageImmediate(damageInfo.damage, damageInfo.isCrit, damageInfo.debuffIcon);
// 设置延时处理下一个伤
this.scheduleOnce(() => {
this.isProcessingDamage = false;
}, this.damageInterval);
}
/** 立即显示伤害效果 */
private showDamageImmediate(damage: number, isCrit: boolean, debuffIcon: string = "") {
if (!this.model) return;
const damageText = NumberFormatter.formatNumber(Math.max(0, Math.floor(damage)));
this.topBar.hpShow();
if (isCrit) {
this.hp_tip(TooltipTypes.crit, damageText);
} else {
this.hp_tip(TooltipTypes.life, damageText, 1001, 0, debuffIcon);
}
}
reset() {
this.isCastingMax = false;
this.hideShieldFx();
// 清理残留的定时器和缓动
this.unscheduleAllCallbacks();
Tween.stopAllByTarget(this.node);
this.topBar.reset();
// 清理碰撞器事件监听
const collider = this.getComponent(Collider2D);
if (collider) {
collider.off(Contact2DType.BEGIN_CONTACT);
}
this.deadCD = 0
this.lastBarUpdateTime = 0
// 清理伤害队列
this.damageQueue.length = 0;
this.isProcessingDamage = false;
// 节点生命周期由 Monster 对象池管理,此处不再销毁
// if (this.node && this.node.isValid) {
// this.node.destroy();
// }
}
}