/** * @file HeroBoxComp.ts * @description 英雄面板单个英雄盒组件(UI 视图层) * * 职责: * 1. 同步显示一个已登场英雄的详细信息(图标 / 名称 / 等级 / AP / HP / 描述)。 * 2. 由 MissionCardComp 实例化到 herosBoxNode 下,最多 3 个,与场上英雄一一对应。 * 3. 英雄卖出 / 死亡后槽位保留,通过 applyEmpty() 显示为空英雄信息。 * * 依赖: * - HeroAttrsComp —— 英雄属性数据模型 * - HeroInfo(heroSet)—— 英雄静态配置 */ import { mLogger } from "../common/Logger"; import { _decorator, Node, Sprite, Label, resources, AnimationClip, SpriteFrame } 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 { HeroInfo } from "../common/config/heroSet"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { getLvColor } from "../common/config/GameSet"; const { ccclass, property } = _decorator; /** * HeroBoxComp —— 英雄面板单个英雄信息盒 * * 与场上英雄一一绑定(eid),英雄移除后显示空状态。 */ @ccclass('HeroBoxComp') @ecs.register('HeroBoxComp', false) export class HeroBoxComp extends CCComp { private debugMode: boolean = false; // ======================== 编辑器绑定节点 ======================== /** 英雄图标节点 */ @property({ type: Node }) private icon_node: Node = null; /** 背景节点 */ @property({ type: Node }) private bg_node: Node = null; /** 英雄名称 */ @property(Label) private name_label: Label = null; /** 等级标签 */ @property(Label) private level_label: Label = null; // ======================== 运行时状态 ======================== /** 绑定的英雄实体 ID(0 表示空槽位) */ private eid: number = 0; /** 绑定的英雄属性数据模型引用 */ private model: HeroAttrsComp | null = null; /** 图标视觉令牌(异步加载竞态保护) */ private iconVisualToken: number = 0; /** 当前显示的英雄 UUID(避免相同 UUID 重复加载动画) */ private iconHeroUuid: number = 0; // ======================== 生命周期 ======================== onLoad() { } /** ECS 组件移除时销毁节点 */ reset() { if (this.node && this.node.isValid) { this.node.destroy(); } } // ======================== 数据绑定 ======================== /** 当前槽位是否绑定了英雄 */ public get hasHero(): boolean { return this.eid > 0 && !!this.model; } /** 当前绑定的英雄实体 ID */ public get bindEid(): number { return this.eid; } /** * 绑定场上英雄并刷新显示。 * @param eid 英雄 ECS 实体 ID * @param model 英雄属性组件 */ public bindHero(eid: number, model: HeroAttrsComp) { this.eid = eid; this.model = model; this.node.active = true; this.refresh(); } /** 清空槽位,显示空英雄信息 */ public applyEmpty() { this.eid = 0; this.model = null; this.iconHeroUuid = 0; this.iconVisualToken += 1; if (this.name_label && this.name_label.isValid) { this.name_label.string = ""; } if (this.level_label && this.level_label.isValid) { this.level_label.string = ""; } if (this.icon_node && this.icon_node.isValid) { const sprite = this.icon_node.getComponent(Sprite) || this.icon_node.getComponentInChildren(Sprite); if (sprite) sprite.spriteFrame = null; } this.setAttrLabels(0, 0); } /** * 刷新显示:同步英雄等级、名称、AP / HP、描述与图标。 * 绑定的英雄实体已失效时自动切换为空状态。 */ public refresh() { if (!this.model || !(this.model as any).ent) { this.applyEmpty(); return; } const heroUuid = this.model.hero_uuid ?? 0; const hero = HeroInfo[heroUuid]; // ---- 名称 ---- if (this.name_label && this.name_label.isValid) { this.name_label.string = this.model.hero_name ?? ""; } // ---- 等级 ---- if (this.level_label && this.level_label.isValid) { const lv = this.model.lv ?? 1; this.level_label.string = `Lv.${lv}`; this.level_label.color = getLvColor(lv); } // ---- AP / HP ---- this.setAttrLabels( Math.max(0, Math.floor(this.model.getFinalAp())), Math.max(0, Math.floor(this.model.getFinalHpMax())) ); // ---- 图标(UUID 变化时重新加载首帧动画) ---- if (hero && heroUuid !== this.iconHeroUuid) { this.iconHeroUuid = heroUuid; this.iconVisualToken += 1; this.updateHeroIcon(hero.path, heroUuid, this.iconVisualToken); } } // ======================== 内部工具 ======================== /** * 写入 AP / HP 数值标签。 * 兼容 herobox.prefab 节点结构(attr/ap|hp → val)。 */ private setAttrLabels(ap: number, hp: number) { const attrNode = this.node.getChildByName("attr"); const apNode = attrNode?.getChildByName("ap"); const hpNode = attrNode?.getChildByName("hp"); const apLabel = apNode?.getChildByName("val")?.getComponent(Label) ?? apNode?.getComponentInChildren(Label) ?? null; const hpLabel = hpNode?.getChildByName("val")?.getComponent(Label) ?? hpNode?.getComponentInChildren(Label) ?? null; if (apLabel) apLabel.string = ap > 0 ? `${ap}` : ""; if (hpLabel) hpLabel.string = hp > 0 ? `${hp}` : ""; } /** * 加载英雄 idle 动画首帧作为静态图标。 * @param path 英雄美术资源名(HeroInfo.path) * @param uuid 英雄 UUID(竞态校验用) * @param token 视觉令牌(竞态保护) */ private updateHeroIcon(path: string, uuid: number, token: number) { if (!this.icon_node || !this.icon_node.isValid) return; const sprite = this.icon_node.getComponent(Sprite) || this.icon_node.getComponentInChildren(Sprite); if (sprite) sprite.spriteFrame = null; resources.load(`game/heros/hero/${path}/idle`, AnimationClip, (err, clip) => { if (err || !clip) return; // 异步加载期间槽位可能已被复用 / 清空 if (token !== this.iconVisualToken) return; if (this.iconHeroUuid !== uuid) return; if (!this.icon_node || !this.icon_node.isValid) return; const target = this.icon_node.getComponent(Sprite) || this.icon_node.getComponentInChildren(Sprite); if (!target || !target.isValid) return; // 帧动画数据存于 spriteFrame 轨道的 _channel._curve._values(裸 SpriteFrame 数组) let firstFrame: SpriteFrame | undefined; for (const track of clip.tracks) { const curve = (track as unknown as { channel?: { curve?: { _values?: unknown[] } } }).channel?.curve; const frame = curve?._values?.[0]; if (frame instanceof SpriteFrame) { firstFrame = frame; break; } } if (firstFrame) { target.spriteFrame = firstFrame; } }); } }