Files
pixelheros/assets/script/game/map/HInfoComp.ts
panw 95ea36651e feat(天赋系统): 实现天赋效果并应用至相关游戏系统
- 在 MissionCardComp 中应用 RefreshDiscount 天赋以减少刷新消耗
- 在 CardComp 中应用 BuyDiscount 天赋以减少英雄购买消耗
- 在 HInfoComp 中应用 SellBonus 天赋以增加英雄出售收益
- 统一 TalentType 枚举类型,增强类型安全性
- 更新 SingletonModuleComp 中 talents 数据结构以支持类型化
- 修改 HeroAttrsComp.getTalentValue 方法参数类型为 TalentType
2026-04-28 15:34:58 +08:00

296 lines
10 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 { smc } from "../common/SingletonModuleComp";
import { TalentType } from "../common/config/TalentSet";
import { Hero } from "../hero/Hero";
import { FieldSkillType } from "../common/config/SkillSet";
import { GameEvent } from "../common/config/GameEvent";
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!
@property(Node)
lv_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();
}
/**
* 设置当前是否处于战斗阶段,控制出售按钮显示/隐藏
* @param isBattlePhase 是否处于战斗阶段
*/
setBattlePhase(isBattlePhase: boolean) {
if (this.sell_node && this.sell_node.isValid) {
this.sell_node.active = !isBattlePhase;
}
}
/**
* 刷新显示:
* 1. 根据英雄等级切换高级 / 普通边框。
* 2. 若英雄 UUID 发生变化,重新加载 idle 动画。
* 3. 更新 AP / HP 数值标签。
*/
refresh() {
if (!this.model) return;
// ---- 卡牌等级显示 ----
if (this.lv_node) {
const currentLv = this.model.lv ?? 1;
this.lv_node.children.forEach(child => {
if (child.name.startsWith('lv')) {
const nodeLv = parseInt(child.name.substring(2));
child.active = !isNaN(nodeLv) && nodeLv <= currentLv;
}
});
}
// ---- 图标动画(仅在 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;
// 卖出英雄金币收益
const baseSellGold = 1; // 基础卖出金币
const goldBoost = HeroAttrsComp.getFieldSkillTotalValue(FieldSkillType.SellGold);
let totalSellGold = baseSellGold + goldBoost;
// 应用天赋 SellBonus (增加数值)
const bonusGold = HeroAttrsComp.getTalentValue(TalentType.SellBonus);
if (bonusGold > 0) {
totalSellGold += bonusGold;
}
totalSellGold = Math.floor(totalSellGold);
oops.message.dispatchEvent(GameEvent.CoinAdd, { delta: totalSellGold });
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();
}
}