Files
pixelheros/assets/script/game/map/HInfoComp.ts
walkpan e880613f8f docs: 为游戏地图模块添加详细的代码注释
为游戏地图模块的脚本文件添加全面的注释,说明每个组件的职责、关键设计、依赖关系和使用方式。注释覆盖了英雄信息面板、技能卡槽位管理器、排行榜弹窗、卡牌控制器、背景滚动组件等核心功能模块,提高了代码的可读性和维护性。

同时修复了英雄预制体的激活状态和技能效果预制体的尺寸参数。
2026-04-07 19:00:30 +08:00

267 lines
9.3 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 HInfoComp.ts
* @description 场上英雄信息卡片组件UI 视图层)
*
* 职责:
* 1. 显示单个场上英雄的实时属性信息AP / HP / 边框品质 / idle 动画)。
* 2. 由 MissionCardComp.ensureHeroInfoPanel() 在英雄上场时实例化并绑定数据。
* 3. 支持点击打开英雄详情弹窗IBox、出售英雄等交互当前已注释
*
* 关键设计:
* - 通过 bindData(eid, model) 将组件与某个英雄 ECS 实体绑定。
* - refresh() 被 MissionCardComp 定时调用以同步实时属性变化。
* - iconVisualToken 机制与 CardComp 一致,用于异步动画加载的竞态保护。
* - isModelAlive() 检测绑定的英雄实体是否仍存活ECS ent 引用是否有效)。
*
* 依赖:
* - HeroAttrsComp —— 英雄属性数据模型
* - HeroInfoheroSet—— 英雄静态配置
* - Hero —— 英雄 ECS 实体类(用于出售删除)
* - UIID.IBox —— 英雄详情弹窗 ID
*/
import { _decorator, Animation, AnimationClip, Button, Event, Label, Node, NodeEventType, Sprite, resources } 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 { Hero } from "../hero/Hero";
import { oops } from "db://oops-framework/core/Oops";
import { UIID } from "../common/config/GameUIConfig";
import { mLogger } from "../common/Logger";
const {property, ccclass } = _decorator;
/**
* HInfoComp —— 场上英雄信息面板视图组件
*
* 每个实例对应一个出战英雄,在卡牌面板下方横向排列。
* 实时显示英雄的 AP / HP 和 idle 动画图标。
*/
@ccclass('HInfoComp')
@ecs.register('HInfoComp', false)
export class HInfoComp extends CCComp {
/** 英雄 idle 动画图标节点 */
@property(Node)
icon_node=null!
/** 出售按钮节点(预留,当前交互已注释) */
@property(Node)
sell_node=null!
/** 普通品质边框 */
@property(Node)
NF_node=null!
/** 高品质边框 */
@property(Node)
HF_node=null!
/** 绑定的英雄 ECS 实体 ID */
private eid: number = 0;
/** 绑定的英雄属性数据模型引用 */
private model: HeroAttrsComp | null = null;
/** AP 标签缓存引用 */
private apLabel: Label | null = null;
/** HP 标签缓存引用 */
private hpLabel: Label | null = null;
/** 图标视觉令牌(异步加载竞态保护) */
private iconVisualToken: number = 0;
/** 当前显示的英雄 UUID避免相同 UUID 重复加载动画) */
private iconHeroUuid: number = 0;
/** 调试日志开关 */
private debugMode: boolean = true;
onLoad() {
this.cacheLabels();
}
onDestroy() {
}
/**
* 绑定英雄数据:关联实体 ID 和属性模型,并立即刷新显示。
* @param eid 英雄 ECS 实体 ID
* @param model 英雄属性组件引用
*/
bindData(eid: number, model: HeroAttrsComp) {
this.eid = eid;
this.model = model;
this.cacheLabels();
this.refresh();
}
/**
* 刷新显示:
* 1. 根据英雄等级切换高级 / 普通边框。
* 2. 若英雄 UUID 发生变化,重新加载 idle 动画。
* 3. 更新 AP / HP 数值标签。
*/
refresh() {
if (!this.model) return;
// ---- 品质边框切换 ----
const isHighLevel = (this.model.lv ?? 0) > 1;
if (this.HF_node) this.HF_node.active = isHighLevel;
if (this.NF_node) this.NF_node.active = !isHighLevel;
// 按卡池等级显示对应子节点
const activeFrameNode = isHighLevel ? this.HF_node : this.NF_node;
if (activeFrameNode) {
const cardLvStr = `lv${this.model.pool_lv ?? 1}`;
activeFrameNode.children.forEach(child => {
child.active = (child.name === cardLvStr);
});
}
// ---- 图标动画(仅在 UUID 变化时重新加载) ----
const heroUuid = this.model.hero_uuid ?? 0;
if (heroUuid !== this.iconHeroUuid) {
this.iconHeroUuid = heroUuid;
this.iconVisualToken += 1;
this.updateHeroAnimation(this.icon_node, heroUuid, this.iconVisualToken);
}
// ---- 数值标签 ----
if (this.apLabel) {
this.apLabel.string = `${Math.max(0, Math.floor(this.model.ap ?? 0))}`;
}
if (this.hpLabel) {
this.hpLabel.string = `${Math.max(0, Math.floor(this.model.hp_max ?? 0))}`;
}
}
/**
* 检测绑定的英雄实体是否仍存活。
* 通过检查 model 上的 ent 引用判断 ECS 实体是否已被回收。
*/
isModelAlive(): boolean {
return !!(this.model as any)?.ent;
}
// ======================== 内部工具方法 ========================
/** 缓存 AP / HP Label 引用,避免每次刷新都遍历节点树 */
private cacheLabels() {
if (!this.apLabel) {
this.apLabel = this.findLabelByPath(["ap", "val"]);
}
if (!this.hpLabel) {
this.hpLabel = this.findLabelByPath(["hp", "val"]);
}
}
/**
* 按节点路径查找 Label 组件
* @param path 从当前节点开始的子节点名称路径数组
* @returns 找到的 Label 组件,或 null
*/
private findLabelByPath(path: string[]): Label | null {
let current: Node | null = this.node;
for (let i = 0; i < path.length; i++) {
current = current?.getChildByName(path[i]) ?? null;
if (!current) return null;
}
return current.getComponent(Label) || current.getComponentInChildren(Label);
}
// ======================== 英雄动画 ========================
/**
* 为英雄图标加载并播放 idle 动画(带竞态保护)。
* @param node 图标节点
* @param uuid 英雄 UUID
* @param token 视觉令牌
*/
private updateHeroAnimation(node: Node, uuid: number, token: number) {
if (!node) return;
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
if (sprite) sprite.spriteFrame = null;
const hero = HeroInfo[uuid];
if (!hero) {
this.clearIconAnimation(node);
return;
}
const anim = node.getComponent(Animation) || node.addComponent(Animation);
this.clearAnimationClips(anim);
const path = `game/heros/hero/${hero.path}/idle`;
resources.load(path, AnimationClip, (err, clip) => {
if (err || !clip) return;
// 竞态保护
if (token !== this.iconVisualToken || !this.model || this.model.hero_uuid !== uuid) {
return;
}
this.clearAnimationClips(anim);
anim.addClip(clip);
anim.play("idle");
});
}
/** 停止并清除图标节点上的动画 */
private clearIconAnimation(node: Node) {
const anim = node?.getComponent(Animation);
if (!anim) return;
anim.stop();
this.clearAnimationClips(anim);
}
/** 移除 Animation 组件上的全部 AnimationClip */
private clearAnimationClips(anim: Animation) {
const clips = (anim as any).clips as AnimationClip[] | undefined;
if (!clips || clips.length === 0) return;
[...clips].forEach(clip => anim.removeClip(clip, true));
}
// ======================== 交互(当前已注释) ========================
// private bindEvents() {
// this.sell_node?.on(Button.EventType.CLICK, this.onSellHero, this);
// this.node.on(NodeEventType.TOUCH_END, this.onOpenIBox, this);
// }
// private unbindEvents() {
// this.sell_node?.off(Button.EventType.CLICK, this.onSellHero, this);
// this.node.off(NodeEventType.TOUCH_END, this.onOpenIBox, this);
// }
/**
* 点击面板时打开英雄详情弹窗IBox
* 传入英雄 UUID、等级和技能列表。
*/
private onOpenIBox() {
if (!this.model) return;
if (!this.isModelAlive()) return;
const heroUuid = this.model.hero_uuid ?? 0;
if (!heroUuid || !HeroInfo[heroUuid]) return;
const heroLv = Math.max(1, Math.floor(this.model.lv ?? 1));
oops.gui.remove(UIID.IBox);
oops.gui.open(UIID.IBox, {
heroUuid,
heroLv,
skills: this.model.skills
});
}
/**
* 出售英雄:通过 Hero.removeByEid 移除 ECS 实体,
* 并关闭详情弹窗。
*/
private onSellHero(event?: Event) {
if (!this.eid) return;
const removed = Hero.removeByEid(this.eid);
mLogger.log(this.debugMode, "HInfoComp", "onSellHero", {
eid: this.eid,
isAlive: this.isModelAlive(),
removed
});
if (!removed) return;
oops.gui.remove(UIID.IBox);
}
/** ECS 组件移除时的释放钩子:清理动画资源并销毁节点 */
reset() {
this.clearIconAnimation(this.icon_node);
this.iconVisualToken = 0;
this.iconHeroUuid = 0;
this.model = null;
this.eid = 0;
this.node.destroy();
}
}