Files
pixelheros/assets/script/game/map/HeroBoxComp.ts
pan 2a5051d46b feat(map): add hero info box panel for mission scene
1. 新增HeroBoxComp组件用于展示单个已登场英雄信息
2. 实现MissionCardComp的英雄信息盒槽位管理逻辑
3. 新增英雄升级事件监听与面板同步逻辑
4. 适配英雄召唤、死亡、出售后的UI刷新
2026-07-28 14:47:08 +08:00

208 lines
7.5 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.
/**
* @file HeroBoxComp.ts
* @description 英雄面板单个英雄盒组件UI 视图层)
*
* 职责:
* 1. 同步显示一个已登场英雄的详细信息(图标 / 名称 / 等级 / AP / HP / 描述)。
* 2. 由 MissionCardComp 实例化到 herosBoxNode 下,最多 3 个,与场上英雄一一对应。
* 3. 英雄卖出 / 死亡后槽位保留,通过 applyEmpty() 显示为空英雄信息。
*
* 依赖:
* - HeroAttrsComp —— 英雄属性数据模型
* - HeroInfoheroSet—— 英雄静态配置
*/
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;
// ======================== 运行时状态 ========================
/** 绑定的英雄实体 ID0 表示空槽位) */
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;
}
});
}
}