import { Vec3, _decorator, v3, Collider2D, Contact2DType, Node, Prefab, instantiate, tween, Tween, BoxCollider2D, UITransform } 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 { 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!; // ==================== 直接访问 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, }> = []; 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); this.model.dirty_shield = false; } } public cd_show() { return; } /** 升级特效 */ 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, triggerType: string = "", 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, triggerType); } /** 血量提示(伤害数字) */ private hp_tip(type: number = 1, value: string = "", s_uuid: number = 1001, y: number = 0) { 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); } /** 护盾减免提示 */ shield_tip(absorbed: number) { this.hp_tip(TooltipTypes.shield, NumberFormatter.formatNumber(Math.max(0, Math.floor(absorbed)))); } 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/buff/heathed", 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; } /** 状态切换(动画) */ status_change(type: string) { if (this.status === type) return; this.status = type; if (this.model.is_dead || this.model.is_reviving) return if (type === "idle") { this.as.idle(); } else if (type === "move") { this.as.move(); } } add_shield(shield: number) { // 护盾数据更新由 Model 层处理,这里只负责视图表现 if (this.model && this.model.shield > 0) this.topBar.shieldShow(this.model.shield); } 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; // 恢复碰撞体 const collider = this.node.getComponent(Collider2D); if (collider) { collider.enabled = true; } // 恢复UI this.topBar.show(); 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; // 防止重复触发 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); } private isBackingUp: boolean = false; // 🔥 添加后退状态标记 //后退 back(distance: number = 0) { // 🔥 防止重复调用后退动画 if (this.isBackingUp) return; this.isBackingUp = true; // 🔥 设置后退状态 if (this.model.fac == FacSet.MON) { // 基础击退距离,加上额外的距离强化 let dist = FightSet.BACK_RANG + distance; let tx = this.node.position.x + dist; if (tx > 320) tx = 320; tween(this.node) .to(0.1, { position: v3(tx, this.node.position.y, 0) }) .call(() => { this.isBackingUp = false; // 🔥 动画完成后重置状态 }) .start(); } if (this.model.fac == FacSet.HERO) { let dist = 5 + distance; let tx = this.node.position.x - dist; if (tx < -320) tx = -320; tween(this.node) .to(0.1, { position: v3(tx, this.node.position.y, 0) }) .call(() => { this.isBackingUp = false; // 🔥 动画完成后重置状态 }) .start(); } } // 伤害计算和战斗逻辑已迁移到 HeroBattleSystem playSkillAnm(act: string = "") { mLogger.log(this.debugMode, 'HeroViewComp', '[heroview] act' + act,) if (act === "") return; switch (act) { case "max": this.as.max() break case "atk": this.as.atk() break case "buff": this.as.buff() break } } /** 显示伤害数字 */ showDamage(damage: number, isCrit: boolean) { this.damageQueue.push({ damage, isCrit, }); } /** 处理伤害队列 */ private processDamageQueue() { if (this.isProcessingDamage || this.damageQueue.length === 0) return; this.isProcessingDamage = true; const damageInfo = this.damageQueue.shift()!; this.showDamageImmediate(damageInfo.damage, damageInfo.isCrit); // 设置延时处理下一个伤 this.scheduleOnce(() => { this.isProcessingDamage = false; }, this.damageInterval); } /** 立即显示伤害效果 */ private showDamageImmediate(damage: number, isCrit: boolean) { 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); } } reset() { // 清理残留的定时器和缓动 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(); // } } }