1. 新增HeroBoxComp组件用于展示单个已登场英雄信息 2. 实现MissionCardComp的英雄信息盒槽位管理逻辑 3. 新增英雄升级事件监听与面板同步逻辑 4. 适配英雄召唤、死亡、出售后的UI刷新
208 lines
7.5 KiB
TypeScript
208 lines
7.5 KiB
TypeScript
/**
|
||
* @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;
|
||
}
|
||
});
|
||
}
|
||
}
|