Files
pixelheros/assets/script/game/map/HInfoComp.ts
panFD 450a8b81e5 refactor(field): 重构驻场光环数据结构,分离元数据与数值
1.  新增FieldEntry类型,将驻场词条的uuid与生效数值分离
2.  重构FieldSkillSet,移除各档位重复配置,仅保留基础元数据
3.  更新所有使用驻场词条的业务代码,适配新的数据结构
4.  修复数值渲染逻辑,使用词条携带的实际数值而非配置固化值
2026-08-11 22:36:28 +08:00

716 lines
28 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, AnimationClip, Button, Event, EventTouch, Input, Label, Layout, Node, NodeEventType, Sprite, UITransform, input, resources, CCInteger, 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, HeroInfo } from "../common/config/heroSet";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { smc } from "../common/SingletonModuleComp";
import { Hero } from "../hero/Hero";
import { FieldSkillType, FieldSkillSet } from "../common/config/SkillSet";
import { buildSkillDesc } from "../common/config/HeroSkillDesc";
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";
import { MissionHeroComp } from "./MissionHeroComp";
import { MoveComp } from "../hero/MoveComp";
import { FacSet, getLvColor } from "../common/config/GameSet";
import { CKind, CardPoolList, CardConfig, CardTriggerType } from "../common/config/CardSet";
import { SkillCardList } from "../common/config/SCardSet";
import { EquipPoolList } from "../common/config/EquipSet";
import { SkillSet } from "../common/config/SkillSet";
import { CardBgComp } from "./CardBgComp";
import { MissionEconomy } from "./MissionEconomy";
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)
skill_icon_node = null!
/** 出售按钮节点 */
@property(Node)
sell_node = null!
/** 卡牌背景底框节点(按卡池等级切换子节点显示) */
@property(Node)
BG_node = null!
/** 关闭窗口按钮节点 */
@property(Node)
close_node = null!
/** 英雄名字节点 */
@property(Node)
Name_node = null!
/** 技能信息节点 */
@property(Node)
info_node = null!
@property(Label)
lv_node: Label = null!
@property(Node)
ap_node = null!
@property(Node)
hp_node = null!
@property(Node)
cd_node = null!
/** 绑定的英雄 ECS 实体 ID */
private eid: number = 0;
/** 绑定的英雄属性数据模型引用 */
private model: HeroAttrsComp | null = null;
/** 是否为静态预览模式(卡牌点击查看,非场上英雄) */
private isPreview: boolean = false;
/** 静态预览数据 */
private previewUuid: number = 0;
private previewLv: number = 1;
private previewPoolLv: number = 1;
private isSkillCard: boolean = false;
/** 是否为装备卡预览(决定从 EquipPoolList 查询配置) */
private isEquipCard: boolean = false;
/** 英雄名字标签缓存引用 */
private nameLabel: Label | null = null;
/** 技能信息标签缓存引用 */
private infoLabel: Label | null = null;
/** infos 逐条展示的 Label 池索引0复用预制体 Label其余动态创建 */
private infoLineLabels: Label[] = [];
/** AP 标签缓存引用 */
private apLabel: Label | null = null;
/** AP 加成标签缓存引用 */
private apPlusLabel: Label | null = null;
/** HP 标签缓存引用 */
private hpLabel: Label | null = null;
/** HP 加成标签缓存引用 */
private hpPlusLabel: Label | null = null;
/** CD 标签缓存引用 */
private cdLabel: Label | null = null;
/** 图标视觉令牌(异步加载竞态保护) */
private iconVisualToken: number = 0;
/** 当前显示的英雄 UUID避免相同 UUID 重复加载动画) */
private iconHeroUuid: number = 0;
/** 调试日志开关 */
private debugMode: boolean = true;
onLoad() {
this.cacheLabels();
this.bindEvents();
}
onAdded(args: { eid?: number; heroUuid?: number; heroLv?: number; poolLv?: number; isSkillCard?: boolean, isEquipCard?: boolean }) {
if (args?.heroUuid) {
this.bindPreviewData(args.heroUuid, args.heroLv ?? 1, args.poolLv ?? 1, args.isSkillCard ?? false, args.isEquipCard ?? false);
return;
}
const eid = args?.eid ?? 0;
if (!eid) return;
let foundModel: HeroAttrsComp | null = null;
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
if (entity.eid === eid) {
foundModel = entity.get(HeroAttrsComp);
}
});
if (foundModel) {
this.bindData(eid, foundModel);
} else {
this.isClosing = true;
oops.gui.remove(UIID.HInfo);
}
}
/**
* 静态预览模式刷新:从 HeroInfo 配置读取初始数据。
*/
private refreshPreview() {
const heroUuid = this.previewUuid;
const heroLv = Math.max(1, this.previewLv);
// 技能卡无等级概念,隐藏等级节点;英雄卡显示等级
if (this.lv_node) {
if (this.isSkillCard) {
this.lv_node.node.active = false;
} else {
this.lv_node.node.active = true;
this.lv_node.string = `Lv.${heroLv}`;
this.lv_node.color = getLvColor(heroLv);
}
}
const kindName = this.isSkillCard ? CKind[CKind.Skill] : CKind[CKind.Hero];
if (this.BG_node) {
this.BG_node.children.forEach(child => {
child.active = (child.name === kindName);
const bg = child.getComponent(CardBgComp);
if (bg) child.active ? bg.apply(this.previewPoolLv) : bg.clear();
});
}
if (this.isSkillCard) {
// ================= 技能卡预览 =================
// 互斥:技能卡只显示技能图标,隐藏英雄图标
if (this.icon_node) this.icon_node.active = false;
if (this.skill_icon_node) this.skill_icon_node.active = true;
// 根据来源从对应卡池查询配置
const config = this.isEquipCard
? EquipPoolList.find(c => c.uuid === heroUuid)
: (SkillCardList.find(c => c.uuid === heroUuid) ?? CardPoolList.find(c => c.uuid === heroUuid));
if (!config) return;
if (this.nameLabel) this.nameLabel.string = config.name ?? "";
if (this.apLabel) {
this.apLabel.string = "-";
if (this.apPlusLabel) this.apPlusLabel.node.active = false;
}
if (this.hpLabel) {
this.hpLabel.string = "-";
if (this.hpPlusLabel) this.hpPlusLabel.node.active = false;
}
if (this.infoLabel) {
this.renderFallbackLines(config.info ?? "");
}
if (this.cdLabel) {
this.cdLabel.string = config.t_inv ? `${config.t_inv}s` : "0s";
}
// 更新技能图标
if (heroUuid !== this.iconHeroUuid) {
this.iconHeroUuid = heroUuid;
this.iconVisualToken += 1;
this.updateSkillAnimation(this.skill_icon_node, config, this.iconVisualToken);
}
} else {
// ================= 英雄卡预览 =================
// 互斥:英雄卡只显示英雄图标,隐藏技能图标
if (this.icon_node) this.icon_node.active = true;
if (this.skill_icon_node) this.skill_icon_node.active = false;
const hero = HeroInfo[heroUuid];
if (!hero) return;
if (heroUuid !== this.iconHeroUuid) {
this.iconHeroUuid = heroUuid;
this.iconVisualToken += 1;
this.updateHeroAnimation(this.icon_node, heroUuid, this.iconVisualToken);
}
if (this.nameLabel) this.nameLabel.string = hero.name ?? "";
if (this.apLabel) {
const ap = Math.max(0, Math.floor((hero.ap ?? 0) * heroLv));
this.apLabel.string = `${ap}`;
if (this.apPlusLabel) this.apPlusLabel.node.active = false;
}
if (this.hpLabel) {
const hp = Math.max(0, Math.floor((hero.hp ?? 0) * heroLv));
this.hpLabel.string = `${hp}`;
if (this.hpPlusLabel) this.hpPlusLabel.node.active = false;
}
if (this.infoLabel) {
this.renderHeroInfoLines(hero);
}
if (this.cdLabel) {
const skillKeys = hero.skills ? Object.keys(hero.skills) : [];
const displaySkill = skillKeys.length > 1 ? hero.skills[skillKeys[1]] : (skillKeys.length > 0 ? hero.skills[skillKeys[0]] : null);
this.cdLabel.string = displaySkill?.cd ? `${displaySkill.cd.toFixed(1)}s` : "0s";
}
}
}
/** 是否正在关闭中,防止重复调用 remove */
private isClosing: boolean = false;
update(dt: number) {
if (this.isClosing) return;
if (this.isPreview) return;
if (!this.isModelAlive()) {
this.isClosing = true;
oops.gui.remove(UIID.HInfo);
return;
}
this.refresh();
}
onDestroy() {
super.onDestroy();
this.unbindEvents();
}
/**
* 绑定英雄数据:关联实体 ID 和属性模型,并立即刷新显示。
* @param eid 英雄 ECS 实体 ID
* @param model 英雄属性组件引用
*/
bindData(eid: number, model: HeroAttrsComp) {
this.eid = eid;
this.model = model;
this.isPreview = false;
if (this.sell_node) this.sell_node.active = true;
if (this.close_node) this.close_node.active = true;
this.cacheLabels();
this.refresh();
}
/**
* 绑定静态预览数据(卡牌点击查看)。
* 通过 HeroInfo/CardPoolList 配置 × 等级计算初始值,不绑定 ECS 实体。
*/
bindPreviewData(heroUuid: number, heroLv: number, poolLv: number, isSkillCard: boolean = false, isEquipCard: boolean = false) {
this.isPreview = true;
this.previewUuid = heroUuid;
this.previewLv = heroLv;
this.previewPoolLv = poolLv;
this.isSkillCard = isSkillCard;
this.isEquipCard = isEquipCard;
if (this.sell_node) this.sell_node.active = false;
if (this.close_node) this.close_node.active = true;
this.cacheLabels();
this.refresh();
}
/**
* 刷新显示:
* 1. 根据英雄等级切换高级 / 普通边框。
* 2. 若英雄 UUID 发生变化,重新加载 idle 动画。
* 3. 更新 AP / HP 数值标签。
*/
refresh() {
if (this.isPreview) {
this.refreshPreview();
return;
}
if (!this.model) return;
// ---- 卡牌等级显示 ----
if (this.lv_node) {
const currentLv = this.model.lv ?? 1;
this.lv_node.string = `Lv.${currentLv}`;
this.lv_node.color = getLvColor(currentLv);
}
// ---- 卡池等级标识 ----
const kindName = CKind[CKind.Hero];
if (this.BG_node) {
this.BG_node.children.forEach(child => {
child.active = (child.name === kindName);
const bg = child.getComponent(CardBgComp);
if (bg) child.active ? bg.apply(this.model.base_card_lv ?? this.model.card_lv ?? 1) : bg.clear();
});
}
// ---- 图标动画(仅在 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.nameLabel) {
this.nameLabel.string = this.model.hero_name ?? "";
}
// ---- 技能信息标签 ----
if (this.infoLabel) {
const heroData = HeroInfo[heroUuid];
if (heroData) {
this.renderHeroInfoLines(heroData);
} else {
this.renderFallbackLines("");
}
}
// ---- 数值标签 ----
// 攻击力显示最终值(含计时 buff 修饰),实时反映药水等限时强化
if (this.apLabel) {
const currentAp = Math.max(0, Math.floor(this.model.getFinalAp()));
const baseAp = Math.max(0, Math.floor(this.model.base_ap ?? 0));
this.apLabel.string = `${currentAp}`;
if (this.apPlusLabel) {
const diff = currentAp - baseAp;
if (diff !== 0) {
this.apPlusLabel.string = diff > 0 ? `(+${diff})` : `(${diff})`;
this.apPlusLabel.node.active = true;
} else {
this.apPlusLabel.node.active = false;
}
}
}
if (this.hpLabel) {
const currentHp = Math.max(0, Math.floor(this.model.getFinalHpMax()));
const baseHp = Math.max(0, Math.floor(this.model.base_hp ?? 0));
this.hpLabel.string = `${currentHp}`;
if (this.hpPlusLabel) {
const diff = currentHp - baseHp;
if (diff !== 0) {
this.hpPlusLabel.string = diff > 0 ? `(+${diff})` : `(${diff})`;
this.hpPlusLabel.node.active = true;
} else {
this.hpPlusLabel.node.active = false;
}
}
}
if (this.cdLabel) {
const skillIds = this.model.getSkillIds();
const displaySkillId = skillIds[1] ?? skillIds[0] ?? 0;
if (displaySkillId) {
const effectiveCd = this.model.getEffectiveSkillCd(displaySkillId);
this.cdLabel.string = effectiveCd > 0 ? `${effectiveCd.toFixed(1)}s` : "0s";
} else {
this.cdLabel.string = "0s";
}
}
}
/**
* 检测绑定的英雄实体是否仍存活。
* 通过检查 model 上的 ent 引用判断 ECS 实体是否已被回收。
*/
isModelAlive(): boolean {
return !!(this.model as any)?.ent;
}
/**
* 逐条展示英雄能力说明infos 每项一行(自动加 Lv 前缀Lv 着色)。
* 复用预制体首条 Label其余按需创建/隐藏,避免频繁增删节点。
* 未配置 infos 时(如怪物)回退为单行 buildSkillDesc 描述。
* @param hero 英雄静态配置
*/
private renderHeroInfoLines(hero: heroInfo) {
if (!this.infoLabel) return;
const hasInfos = !!(hero.infos?.length);
const count = hasInfos ? hero.infos!.length : 1;
// 按需扩容 Label 池(克隆首条保持样式一致)
for (let i = this.infoLineLabels.length; i < count; i++) {
const node = new Node(`info_${i}`);
node.layer = this.info_node.layer;
this.info_node.addChild(node);
const label = node.addComponent(Label);
label.fontSize = this.infoLabel.fontSize;
label.lineHeight = this.infoLabel.lineHeight;
label.horizontalAlign = Label.HorizontalAlign.LEFT;
label.verticalAlign = Label.VerticalAlign.TOP;
label.color = this.infoLabel.color;
this.infoLineLabels.push(label);
}
for (let i = 0; i < this.infoLineLabels.length; i++) {
const label = this.infoLineLabels[i];
if (i < count) {
label.node.active = true;
if (hasInfos) {
label.string = `Lv${i + 1}. ${hero.infos![i]}`;
// 无富文本时用白色突出 Lv 前缀外的正文,保持简洁
label.color = this.infoLabel.color;
} else {
label.string = buildSkillDesc(hero);
}
} else {
label.node.active = false;
}
}
}
/**
* 单行文本展示(技能卡/无配置英雄):隐藏逐条 Label 池,只保留首行。
* @param text 展示文本
*/
private renderFallbackLines(text: string) {
if (!this.infoLabel) return;
this.infoLabel.node.active = true;
this.infoLabel.string = text;
for (let i = 1; i < this.infoLineLabels.length; i++) {
this.infoLineLabels[i].node.active = false;
}
}
// ======================== 内部工具方法 ========================
/** 缓存 AP / HP Label 引用,避免每次刷新都遍历节点树 */
private cacheLabels() {
if (!this.nameLabel && this.Name_node) {
this.nameLabel = this.Name_node.getComponent(Label) || this.Name_node.getComponentInChildren(Label);
}
if (!this.infoLabel && this.info_node) {
this.infoLabel = this.info_node.getComponentInChildren(Label);
if (!this.infoLabel) {
const child = new Node("info_0");
child.layer = this.info_node.layer;
this.info_node.addChild(child);
this.infoLabel = child.addComponent(Label);
this.infoLabel.fontSize = 20;
this.infoLabel.lineHeight = 28;
this.infoLabel.horizontalAlign = Label.HorizontalAlign.LEFT;
this.infoLabel.verticalAlign = Label.VerticalAlign.TOP;
}
// 垂直 Layout 让多条 Label 自动逐行排布
let layout = this.info_node.getComponent(Layout);
if (!layout) layout = this.info_node.addComponent(Layout);
layout.type = Layout.Type.VERTICAL;
layout.resizeMode = Layout.ResizeMode.CONTAINER;
layout.spacingY = 4;
this.infoLineLabels = [this.infoLabel];
}
if (!this.apLabel && this.ap_node) {
this.apLabel = this.ap_node.getChildByName("val")?.getComponent(Label) || null;
this.apPlusLabel = this.ap_node.getChildByName("plus")?.getComponent(Label) || null;
}
if (!this.hpLabel && this.hp_node) {
this.hpLabel = this.hp_node.getChildByName("val")?.getComponent(Label) || null;
this.hpPlusLabel = this.hp_node.getChildByName("plus")?.getComponent(Label) || null;
}
if (!this.cdLabel && this.cd_node) {
this.cdLabel = this.cd_node.getChildByName("val")?.getComponent(Label) || null;
}
}
/**
* 按节点路径查找 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 动画剪辑,提取首帧 SpriteFrame 静止展示。
* 不经 Animation 组件采样,零运行时开销;帧名各异也无需关心。
* @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) return;
const path = `game/heros/hero/${hero.path}/idle`;
resources.load(path, AnimationClip, (err, clip) => {
if (err || !clip) return;
if (token !== this.iconVisualToken) return;
if (this.isPreview) {
if (this.previewUuid !== uuid) return;
} else {
if (!this.model || this.model.hero_uuid !== uuid) return;
}
// 异步加载期间面板可能已关闭销毁
if (!node || !node.isValid) return;
const target = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
if (!target || !target.isValid) return;
// 帧动画数据存于 spriteFrame 轨道的 _channel._curve._values裸 SpriteFrame 数组)。
// 注_values 为引擎私有字段,但本版本公开 values getter 反序列化后为空,只能读私有字段取首帧。
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;
}
});
}
/**
* 为技能图标加载静态图片。
* 图标来源优先级config.icon > 按 trigger_type 自动取Interval→SkillSetField→FieldSkillSet
* @param node 图标节点
* @param config 卡牌配置
* @param token 视觉令牌
*/
private updateSkillAnimation(node: Node, config: CardConfig, token: number) {
if (!node) return;
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
if (!sprite) return;
sprite.spriteFrame = null;
// 优先使用卡牌自定义 icon未定义则按 trigger_type 自动取
let iconId: string | undefined = config.icon;
if (!iconId) {
if (config.trigger_type === CardTriggerType.Interval) {
iconId = config.skill ? SkillSet[config.skill]?.icon : undefined;
} else if (config.trigger_type === CardTriggerType.Field) {
const fieldUuid = config.field?.[0]?.uuid;
iconId = fieldUuid ? FieldSkillSet[fieldUuid]?.icon : undefined;
}
}
if (!iconId) return;
// 与 CardComp / SCardComp 一致:仅信任全局缓存的 uiconsAtlas
if (smc.uiconsAtlas) {
const frame = smc.uiconsAtlas.getSpriteFrame(iconId);
if (token === this.iconVisualToken) {
sprite.spriteFrame = frame || null;
}
}
}
// ======================== 交互 ========================
private bindEvents() {
this.sell_node?.on(Button.EventType.CLICK, this.onSellHero, this);
this.close_node?.on(Button.EventType.CLICK, this.onClosePanel, this);
// this.node.on(NodeEventType.TOUCH_END, this.onOpenIBox, this);
// 监听全局触摸结束:点击本体节点以外区域时关闭面板
input.on(Input.EventType.TOUCH_END, this.onGlobalTouchEnd, this);
}
private unbindEvents() {
if (this.sell_node && this.sell_node.isValid) {
this.sell_node.off(Button.EventType.CLICK, this.onSellHero, this);
}
if (this.close_node && this.close_node.isValid) {
this.close_node.off(Button.EventType.CLICK, this.onClosePanel, this);
}
// if (this.node && this.node.isValid) {
// this.node.off(NodeEventType.TOUCH_END, this.onOpenIBox, this);
// }
input.off(Input.EventType.TOUCH_END, this.onGlobalTouchEnd, this);
}
/**
* 全局触摸结束处理:点击落点不在本体节点包围盒内时关闭面板。
* Why: 弹窗常见的"点击空白处关闭"交互,用全局 input + 包围盒命中检测实现,
* 无需额外遮罩节点,也不会误关面板内部(含子按钮)的点击。
* @param event 全局触摸事件
*/
private onGlobalTouchEnd(event: EventTouch) {
if (this.isClosing) return;
const transform = this.node.getComponent(UITransform);
if (!transform) {
// 缺少 UITransform 时保守处理为关闭
this.onClosePanel();
return;
}
const worldRect = transform.getBoundingBoxToWorld();
const uiPos = event.getUILocation();
if (!worldRect.contains(uiPos)) {
this.onClosePanel();
}
}
/**
* 点击关闭按钮时关闭英雄信息面板
*/
private onClosePanel() {
if (this.isClosing) return;
this.isClosing = true;
oops.gui.remove(UIID.HInfo);
}
/**
* 点击面板时打开英雄详情弹窗IBox
* 传入英雄 UUID、等级和技能列表。
*/
private onOpenIBox() {
// if (this.isClosing) return;
// 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));
// this.isClosing = true;
// oops.gui.remove(UIID.HInfo); // 打开 IBox 前关闭自身
// 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.isClosing) return;
if (!this.eid) return;
const heroLv = Math.max(1, Math.floor(this.model?.lv ?? 1));
const removed = Hero.removeByEid(this.eid);
mLogger.log(this.debugMode, "HInfoComp", "onSellHero", {
eid: this.eid,
heroLv,
isAlive: this.isModelAlive(),
removed
});
if (!removed) return;
// 使用统一经济管理入口出售英雄(按等级计算卖价)
MissionEconomy.executeSellHero(heroLv);
// 派发英雄出售事件,通知 MissionCardComp 更新场上英雄数量
oops.message.dispatchEvent(GameEvent.HeroSell, { eid: this.eid });
this.isClosing = true;
oops.gui.remove(UIID.HInfo);
}
/** ECS 组件移除时的释放钩子:重置绑定状态 */
reset() {
this.iconVisualToken = 0;
this.iconHeroUuid = 0;
this.model = null;
this.eid = 0;
this.isPreview = false;
this.previewUuid = 0;
this.previewLv = 1;
this.previewPoolLv = 1;
}
}