Files
pixelheros/assets/script/game/map/HeroInfoComp.ts
panFD 63a9f6cba5 feat(hero): 新增英雄详情弹窗与图鉴页面重构
1.  新增HeroInfoComp英雄详情弹窗,支持英雄属性展示、升级操作、前后切换功能
2.  重构HerosListComp图鉴页面,将详情面板逻辑剥离至弹窗,点击卡片直接打开详情页
3.  新增英雄列表排序工具函数,统一图鉴与弹窗切换的排序逻辑
4.  优化战斗渲染排序逻辑,移除无关的Y轴渲染权重计算
5.  新增英雄详情弹窗UI配置与预制体资源
2026-09-18 11:22:03 +08:00

688 lines
25 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 HeroInfoComp.ts
* @description 英雄详情弹窗(UI 视图层,oops.gui 弹窗)
*
* 职责:
* 1. 展示单个英雄的详情:idle 动画、名称、AP / DEF / HP / CD、技能描述、
* 稀有度底框、局外等级、职业、碎片数量、升级按钮、技能图标列表。
* 2. 提供升级操作(消耗碎片提升局外等级,成功后播放升级特效)。
* 3. 提供前后英雄切换按钮,按图鉴排序顺序循环切换(getSortedHeroList)。
* 4. 提供关闭按钮 / 遮罩点击关闭弹窗。
*
* 使用(oops.gui 模块):
* oops.gui.open(UIID.HeroInfo, { uuid: 5001 });
*
* 关键设计:
* - 详情渲染逻辑与原 HerosListComp 详情面板同源(已迁移至此),
* HerosListComp 仅保留卡片列表,点击卡片改为打开本弹窗。
* - 前后切换按 getSortedHeroList 计算的顺序循环(与图鉴页面卡片顺序一致)。
* - hero_icon 使用 Animation + iconVisualToken 机制防止异步加载竞态。
*
* 依赖:
* - HeroInfo / heroSet —— 英雄静态配置
* - getSortedHeroList(heroUpSet)—— 图鉴排序
* - smc.getHeroGrowth / smc.getHeroOuterLv / smc.upgradeHero —— 成长数据
* - buildHeroSkillDesc(HeroSkillText)—— 技能描述生成器
* - SSkillBoxComp / ChipComp / SmallTipComp —— 子组件
*/
import { _decorator, Animation, AnimationClip, Button, EventTouch, Label, Node, NodeEventType, Prefab, RichText, Sprite, Tween, Vec3, instantiate, resources, tween } 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 { oops } from "db://oops-framework/core/Oops";
import { mLogger } from "../common/Logger";
import { HeroInfo, SkillTriggerType, LvFieldEntry, LvTriggerEntry, TriggerGrouped, HSkillInfo, getHeroDisplayStats, LvBonusAttrsEntry } from "../common/config/heroSet";
import { buildHeroSkillDesc } from "../common/HeroSkillText";
import { outlineRich } from "../common/RichOutline";
import { GameEvent } from "../common/config/GameEvent";
import { HERO_UPGRADE_NEED_FRAGMENTS, RARITY_BG_COLORS } from "../common/config/GameSet";
import { getSortedHeroList } from "../common/config/heroUpSet";
import { ChipComp } from "./ChipComp";
import { SmallTipComp } from "./SmallTipComp";
import { SSkillBoxComp } from "./SSkillBoxComp";
import { smc } from "../common/SingletonModuleComp";
const { property, ccclass } = _decorator;
/** HeroInfo 弹窗初始化参数 */
export interface HeroInfoParams {
/** 英雄模板 uuid */
uuid: number;
}
@ccclass('HeroInfoComp')
@ecs.register('HeroInfoComp', false)
export class HeroInfoComp extends CCComp {
// ======================== 编辑器绑定节点 ========================
@property(Node)
hero_icon = null!
@property(Node)
ap_node = null!
@property(Node)
def_node = null!
@property(Node)
hp_node = null!
@property(Node)
cd_node = null!
/** 卡牌背景底框节点(按卡池等级切换子节点显示) */
@property(Node)
BG_node = null!
@property(Node)
info_node = null!
@property(Node)
name_node = null!
/** 卡池等级 Label,直接显示 X */
@property(Label)
lv_label = null!
/** 职业展示节点(子节点按 HRole 枚举值命名 0~5) */
@property(Node)
role_node = null!
/** 升级按钮节点(smalltip 气泡已作为其子节点挂在下面) */
@property(Node)
upgrade_btn = null!
/** 碎片数量显示节点(Label) */
@property(Node)
fragment_node = null!
/** 碎片图标节点(挂载 ChipComp 用于渲染碎片图标) */
@property(Node)
chip_node = null!
/** 技能描述节点 */
@property(Node)
infos_node = null!
/** 技能图标容器节点 */
@property(Node)
skills_node = null!
/** 技能小图标预制体(根节点挂 SSkillBoxComp) */
@property(Prefab)
skill_icon_prefab = null!
/** 升级特效动画节点(内含升级效果 Animation,点击升级时从头播放) */
@property(Node)
upgrade_anim_node = null!
/** 碎片需求/总量 Label(显示 当前碎片 / 下级需求) */
@property(Label)
fragment_label = null!
/** 升级提升加成说明 RichText(展示升级后的属性提升,数值绿色高亮) */
@property(RichText)
upgrade_info_label = null!
/** 关闭按钮节点 */
@property({ type: Node, displayName: "关闭按钮" })
close_btn: Node = null!
/** 上一个英雄切换按钮节点 */
@property({ type: Node, displayName: "上一个英雄按钮" })
prev_btn: Node = null!
/** 下一个英雄切换按钮节点 */
@property({ type: Node, displayName: "下一个英雄按钮" })
next_btn: Node = null!
/** 全屏遮罩节点(点击弹窗主体以外区域关闭,可选) */
@property({ type: Node, displayName: "遮罩节点" })
mask_node: Node = null!
// ======================== 运行时状态 ========================
/** 当前展示的英雄模板 uuid */
huuid: number = 0
private iconVisualToken: number = 0
debugMode: boolean = false
// ======================== 生命周期 ========================
protected onLoad(): void {
// 关闭按钮点击
if (this.close_btn && this.close_btn.isValid) {
this.close_btn.on(NodeEventType.TOUCH_END, this.onCloseClick, this);
}
// 前后切换按钮
if (this.prev_btn && this.prev_btn.isValid) {
this.prev_btn.on(NodeEventType.TOUCH_END, this.onPrevClick, this);
}
if (this.next_btn && this.next_btn.isValid) {
this.next_btn.on(NodeEventType.TOUCH_END, this.onNextClick, this);
}
// 遮罩点击关闭
if (this.mask_node && this.mask_node.isValid) {
this.mask_node.on(NodeEventType.TOUCH_END, this.onMaskClick, this);
}
}
/**
* oops.gui 添加到舞台回调(框架约定入口)
* @param params 见 HeroInfoParams
*/
onAdded(params: HeroInfoParams) {
this.huuid = params?.uuid ?? 0;
this.bindUpgradeBtn();
if (this.upgrade_anim_node) this.upgrade_anim_node.active = false;
if (this.huuid) {
this.updateHeroDetail(this.huuid);
}
}
protected onEnable(): void {
oops.message.on(GameEvent.HERO_GROWTH_UPDATE, this.onGrowthUpdate, this);
}
protected onDisable(): void {
oops.message.off(GameEvent.HERO_GROWTH_UPDATE, this.onGrowthUpdate, this);
}
onDestroy() {
super.onDestroy();
if (this.close_btn && this.close_btn.isValid) {
this.close_btn.off(NodeEventType.TOUCH_END, this.onCloseClick, this);
}
if (this.prev_btn && this.prev_btn.isValid) {
this.prev_btn.off(NodeEventType.TOUCH_END, this.onPrevClick, this);
}
if (this.next_btn && this.next_btn.isValid) {
this.next_btn.off(NodeEventType.TOUCH_END, this.onNextClick, this);
}
if (this.mask_node && this.mask_node.isValid) {
this.mask_node.off(NodeEventType.TOUCH_END, this.onMaskClick, this);
}
if (this.upgrade_btn && this.upgrade_btn.isValid) {
this.upgrade_btn.off(Button.EventType.CLICK, this.onUpgradeClick, this);
}
}
private onGrowthUpdate() {
if (this.huuid) {
this.updateHeroDetail(this.huuid);
}
}
// ======================== 按钮回调 ========================
/** 关闭按钮点击 */
private onCloseClick(event: EventTouch) {
event.propagationStopped = true;
oops.audio.playEffect("music/button");
this.close();
}
/** 遮罩点击关闭 */
private onMaskClick(event: EventTouch) {
event.propagationStopped = true;
oops.audio.playEffect("music/button");
this.close();
}
/** 关闭弹窗 */
private close() {
if (!this.node || !this.node.isValid || !this.node.parent) return;
oops.gui.removeByNode(this.node);
}
/** 切换到上一个英雄(按图鉴排序循环) */
private onPrevClick(event: EventTouch) {
event.propagationStopped = true;
oops.audio.playEffect("music/button");
this.switchHero(-1);
}
/** 切换到下一个英雄(按图鉴排序循环) */
private onNextClick(event: EventTouch) {
event.propagationStopped = true;
oops.audio.playEffect("music/button");
this.switchHero(1);
}
/**
* 按图鉴排序顺序循环切换英雄
* @param delta 1 下一个,-1 上一个
*/
private switchHero(delta: 1 | -1) {
const sorted = getSortedHeroList(uuid => smc.getHeroGrowth(uuid));
if (sorted.length === 0) return;
const idx = sorted.indexOf(this.huuid);
// 当前 uuid 不在列表中(异常)时回退到第一个
const cur = idx >= 0 ? idx : 0;
const next = (cur + delta + sorted.length) % sorted.length;
this.huuid = sorted[next];
this.updateHeroDetail(this.huuid);
}
// ======================== 升级 ========================
/**
* 绑定升级按钮点击事件
* 在 onAdded 中调用,确保 @property 属性已正确赋值
*/
private bindUpgradeBtn() {
if (!this.upgrade_btn) {
mLogger.error(this.debugMode, "HeroInfoComp", "upgrade_btn 未绑定,请检查编辑器属性面板");
return;
}
// 确保 smalltip 气泡初始隐藏,避免遮挡按钮触摸(组件缺失时兜底补挂)
const tipNode = this.upgrade_btn.getChildByName("smalltip");
if (tipNode) {
tipNode.getComponent(SmallTipComp) ?? tipNode.addComponent(SmallTipComp);
tipNode.active = false;
}
let btn = this.upgrade_btn.getComponent(Button);
if (!btn) btn = this.upgrade_btn.addComponent(Button);
btn.interactable = true;
this.upgrade_btn.off(Button.EventType.CLICK, this.onUpgradeClick, this);
this.upgrade_btn.on(Button.EventType.CLICK, this.onUpgradeClick, this);
}
/**
* 升级按钮点击回调
* 消耗碎片提升英雄局外等级,成功后刷新详情面板
*/
onUpgradeClick() {
const uuid = this.huuid;
if (!uuid) return;
this.playUpgradeBtnAnim();
const success = smc.upgradeHero(uuid);
if (success) {
mLogger.log(this.debugMode, "HeroInfoComp", `英雄 ${uuid} 升级成功`);
this.playUpgradeAnim();
this.updateHeroDetail(uuid);
} else {
const growth = smc.getHeroGrowth(uuid);
const need = HERO_UPGRADE_NEED_FRAGMENTS[growth.lv + 1];
// 碎片不足 / 已满级:气泡提示
const tipText = need === undefined ? "已满级" : "碎片不足";
this.showSmallTip(tipText);
}
}
/**
* 播放升级按钮按压回弹动画
*/
private playUpgradeBtnAnim() {
if (!this.upgrade_btn) return;
Tween.stopAllByTarget(this.upgrade_btn);
this.upgrade_btn.setScale(1, 1, 1);
tween(this.upgrade_btn)
.to(0.08, { scale: new Vec3(0.9, 0.9, 1) }, { easing: 'quadOut' })
.to(0.12, { scale: new Vec3(1.1, 1.1, 1) }, { easing: 'quadOut' })
.to(0.08, { scale: new Vec3(1, 1, 1) }, { easing: 'quadIn' })
.start();
}
/**
* 播放升级特效动画
* 每次升级成功均从头播放;动画未播完时再次升级,立即重新开始
*/
private playUpgradeAnim() {
if (!this.upgrade_anim_node) return;
const anim = this.upgrade_anim_node.getComponent(Animation);
const clip = anim?.clips?.[0];
if (!anim || !clip) {
mLogger.log(this.debugMode, "HeroInfoComp", "playUpgradeAnim: 缺少 Animation 组件或动画剪辑");
return;
}
this.upgrade_anim_node.active = true;
anim.stop();
const animState = anim.getState(clip.name);
if (animState) {
animState.off('finished', this.onUpgradeAnimFinished, this);
animState.once('finished', this.onUpgradeAnimFinished, this);
}
anim.play(clip.name);
}
/**
* 升级特效动画播放结束回调:隐藏特效节点
*/
private onUpgradeAnimFinished() {
if (this.upgrade_anim_node && this.upgrade_anim_node.isValid) {
this.upgrade_anim_node.active = false;
}
}
/**
* 在升级按钮附近弹出 smalltip 气泡提示
* @param text 提示文案
*/
private showSmallTip(text: string) {
const tipNode = this.upgrade_btn?.getChildByName("smalltip");
if (!tipNode || !tipNode.isValid) {
mLogger.log(this.debugMode, "HeroInfoComp", `未在 upgrade_btn 下找到 smalltip 节点,无法提示:${text}`);
return;
}
const tip = tipNode.getComponent(SmallTipComp) ?? tipNode.addComponent(SmallTipComp);
tip.show(text);
}
// ======================== 详情面板 ========================
private updateHeroDetail(uuid: number) {
const hero = HeroInfo[uuid];
if (!hero) return;
// 属性按当前局外等级计算
const heroLv = Math.max(1, smc.getHeroOuterLv(uuid));
this.setLabelText(this.name_node, hero.name || "");
const stats = getHeroDisplayStats(hero, heroLv);
this.setLabelText(this.ap_node, `${stats.ap}`);
this.setLabelText(this.def_node, `${stats.def}`);
this.setLabelText(this.hp_node, `${stats.hp}`);
this.updateCdDisplay(hero);
if (this.info_node) {
// Label 承载:HeroSkillText 输出富文本,剥离 BBCode 标签转纯文本
const desc = buildHeroSkillDesc(hero.uuid, true, true).replace(/<[^>]+>/g, "");
const infoLabel = this.info_node.getChildByName("info")?.getComponent(Label)
|| this.info_node.getComponent(Label)
|| this.info_node.getComponentInChildren(Label);
if (infoLabel) infoLabel.string = desc || hero.info || "";
}
this.updatePoolLvBg(hero.rarity ?? 1);
this.updateLvDisplay(uuid);
this.updateRoleDisplay(hero);
this.updateHeroAnimation(uuid);
this.updateGrowthPanel(uuid);
this.updateSkillIcons(hero);
}
private updatePoolLvBg(rarity: number) {
if (!this.BG_node) return;
// BG_node 下直接放置颜色节点(green/blue/purple/yellow/red),按卡池等级激活对应颜色
const color = RARITY_BG_COLORS[Math.min(rarity, 5) - 1];
this.BG_node.children.forEach(child => {
child.active = (child.name === color);
});
}
private updateCdDisplay(hero: typeof HeroInfo[number]) {
if (!this.cd_node) return;
const skillKeys = Object.keys(hero.skills);
if (skillKeys.length === 0) return;
const firstSkill = hero.skills[Number(skillKeys[0])];
if (firstSkill) {
this.setLabelText(this.cd_node, `${firstSkill.cd}s`);
}
}
private updateLvDisplay(uuid: number) {
if (!this.lv_label) return;
const outerLv = smc.getHeroOuterLv(uuid);
this.lv_label.string = `${outerLv}`;
}
private updateRoleDisplay(hero: typeof HeroInfo[number]) {
if (!this.role_node) return;
this.role_node.active = true;
const roleStr = `${hero.role ?? 0}`;
this.role_node.children.forEach(child => {
child.active = (child.name === roleStr);
});
}
/**
* 更新局外成长面板:碎片数量与升级按钮状态
* @param uuid 英雄模板 uuid
*/
private updateGrowthPanel(uuid: number) {
const growth = smc.getHeroGrowth(uuid);
const needFragments = HERO_UPGRADE_NEED_FRAGMENTS[growth.lv + 1];
const isMaxLv = needFragments === undefined;
// 碎片数量显示:当前拥有 / 下一级所需(满级时显示 MAX)
const fragmentText = isMaxLv
? `${growth.fragments} / MAX`
: `${growth.fragments} / ${needFragments}`;
if (this.fragment_node) {
this.setLabelText(this.fragment_node, fragmentText);
}
if (this.fragment_label) {
this.fragment_label.string = fragmentText;
}
// 升级提升加成说明
this.updateUpgradeInfoLabel(uuid, growth.lv, isMaxLv);
// 碎片图标:使用 ChipComp 渲染英雄图标
if (this.chip_node) {
const chipComp = this.chip_node.getComponent(ChipComp) || this.chip_node.addComponent(ChipComp);
chipComp.setHeroByUuid(uuid);
}
// 升级按钮状态:始终可点击,碎片不足时通过气泡提示
if (this.upgrade_btn) {
this.upgrade_btn.active = true;
const btnComp = this.upgrade_btn.getComponent(Button);
if (btnComp) btnComp.interactable = true;
}
}
// ======================== 技能图标列表 ========================
/**
* 刷新升级提升加成说明 RichText
* 展示口径:对比当前等级与下一等级,仅列出 AP/HP/DEF 属性差值,单行展示
*/
private updateUpgradeInfoLabel(uuid: number, curLv: number, isMaxLv: boolean) {
if (!this.upgrade_info_label) return;
const hero = HeroInfo[uuid];
if (!hero) { this.upgrade_info_label.string = ""; return; }
if (isMaxLv) {
this.upgrade_info_label.string = outlineRich("已满级");
return;
}
const nextLv = curLv + 1;
const cur = getHeroDisplayStats(hero, curLv);
const nxt = getHeroDisplayStats(hero, nextLv);
// 属性提升差值:单行展示 AP/HP/DEF,数值绿色高亮
const parts: string[] = [];
if (nxt.hp > cur.hp) parts.push(`生命<color=#00FF00>+${nxt.hp - cur.hp}</color>`);
if (nxt.ap > cur.ap) parts.push(`攻击<color=#00FF00>+${nxt.ap - cur.ap}</color>`);
if (nxt.def > cur.def) parts.push(`防御<color=#00FF00>+${nxt.def - cur.def}</color>`);
this.upgrade_info_label.string = parts.length > 0
? outlineRich(`升级属性提升:${parts.join(" ")}`)
: outlineRich(`升级至 Lv.${nextLv}`);
}
/** 详情面板展示的触发技能槽位(按触发时机排序) */
private static readonly PANEL_TRIGGER_TYPES: SkillTriggerType[] = [
SkillTriggerType.Call,
SkillTriggerType.FStart,
SkillTriggerType.Atking,
SkillTriggerType.Atked,
SkillTriggerType.Dead,
SkillTriggerType.FEnd,
SkillTriggerType.Field,
];
/**
* 刷新当前英雄的技能图标列表:
* 1. 大招:skills[1] 及以后(首个为普攻,跳过)。
* 2. 触发技能:每个触发槽位整组视为一个技能。
* 以局外等级为当前等级,未达解锁档的显示 locked。
*/
private updateSkillIcons(hero: typeof HeroInfo[number]) {
if (!this.skills_node || !this.skill_icon_prefab) return;
// 清空旧图标(显式销毁,避免节点泄漏)
for (let i = this.skills_node.children.length - 1; i >= 0; i--) {
this.skills_node.children[i].destroy();
}
const lv = Math.max(1, smc.getHeroOuterLv(hero.uuid));
// ---- 大招:skills[1] 及以后 ----
Object.values(hero.skills).slice(1).forEach(hSkill => {
const sLv = hSkill.lv_entries?.length
? (hSkill.lv_entries.filter(e => e.lv <= lv).pop()?.s_lv ?? hSkill.lv)
: hSkill.lv;
this.addSkillIcon(hSkill.uuid, sLv, false, false);
});
// ---- 普攻 lv2 强化档 ----
this.addNormalLv2Icon(Object.values(hero.skills)[0], lv);
// ---- 触发技能 ----
for (const type of HeroInfoComp.PANEL_TRIGGER_TYPES) {
if (type === SkillTriggerType.Field) {
this.addFieldGroupIcon(hero[SkillTriggerType.Field], lv);
} else {
this.addTriggerGroupIcon(hero[type] as TriggerGrouped | undefined, lv, type);
}
}
// ---- bonus 档位 ----
this.addBonusIcons(hero.bonus, lv);
}
/**
* bonus 档位图标:配置了 icon 的档位各出一个图标(追加在技能列表最末)。
*/
private addBonusIcons(bonus: LvBonusAttrsEntry[] | undefined, lv: number) {
if (!bonus?.length) return;
for (const entry of bonus) {
if (!entry.icon) continue;
const node = instantiate(this.skill_icon_prefab);
node.name = `bonus_lv${entry.lv}`;
const comp = node.getComponent(SSkillBoxComp) || node.addComponent(SSkillBoxComp);
comp.initBonus(this.huuid, entry, lv < entry.lv);
this.skills_node.addChild(node);
}
}
/**
* 实例化技能小图标并初始化
*/
private addSkillIcon(s_uuid: number, s_lv: number, locked: boolean, isField: boolean, slot?: SkillTriggerType, isGroup: boolean = false, normalLv2: boolean = false, unlockLv?: number) {
const node = instantiate(this.skill_icon_prefab);
node.name = `skill_${s_uuid}`;
const comp = node.getComponent(SSkillBoxComp) || node.addComponent(SSkillBoxComp);
comp.initHeroSkill(s_uuid, s_lv, locked, isField, this.huuid, slot, isGroup, normalLv2, unlockLv);
this.skills_node.addChild(node);
}
/**
* 触发技能槽位(call/fstart/atking/atked/dead)整组一个图标
*/
private addTriggerGroupIcon(grouped: TriggerGrouped | undefined, lv: number, type: SkillTriggerType) {
if (!grouped) return;
const keys = Object.keys(grouped).map(Number).filter(k => grouped[k]?.length);
if (keys.length === 0) return;
const sorted = [...grouped[keys[0]]].sort((a, b) => a.lv - b.lv);
let active: LvTriggerEntry | undefined;
for (const e of sorted) { if (e.lv <= lv) active = e; }
let anyActive = !!active;
for (const key of keys) {
if (anyActive) break;
for (const e of grouped[key] ?? []) {
if (e.lv <= lv) { anyActive = true; break; }
}
}
const pick = active ?? sorted[0];
this.addSkillIcon(keys[0], pick.s_lv ?? 1, !anyActive, false, type, true, false, anyActive ? undefined : pick.lv);
}
/**
* 驻场光环槽位整组一个图标
*/
private addFieldGroupIcon(field: LvFieldEntry[] | undefined, lv: number) {
if (!field?.length) return;
const sorted = [...field].sort((a, b) => a.lv - b.lv);
let active: LvFieldEntry | undefined;
for (const e of sorted) { if (e.lv <= lv) active = e; }
const pick = active ?? sorted[0];
const firstUuid = pick.uuids[0]?.uuid;
if (!firstUuid) return;
this.addSkillIcon(firstUuid, 1, !active, true, SkillTriggerType.Field, true, false, active ? undefined : pick.lv);
}
/**
* 普攻 lv2+ 强化档作为独立技能图标
*/
private addNormalLv2Icon(normal: HSkillInfo | undefined, lv: number) {
const entry = normal?.lv_entries?.[0];
if (!normal || !entry) return;
this.addSkillIcon(normal.uuid, entry.s_lv ?? 2, lv < entry.lv, false, undefined, false, true, entry.lv);
const icon = this.skills_node.children[this.skills_node.children.length - 1];
if (icon) icon.name = `skill_${normal.uuid}_lv2`;
}
// ======================== 英雄动画 ========================
private updateHeroAnimation(uuid: number) {
if (!this.hero_icon) return;
const hero = HeroInfo[uuid];
if (!hero) return;
const sprite = this.hero_icon.getComponent(Sprite) || this.hero_icon.getComponentInChildren(Sprite);
if (sprite) sprite.spriteFrame = null;
const anim = this.hero_icon.getComponent(Animation) || this.hero_icon.addComponent(Animation);
this.clearAnimationClips(anim);
this.iconVisualToken += 1;
const token = this.iconVisualToken;
const path = `game/heros/hero/${hero.path}/idle`;
resources.load(path, AnimationClip, (err, clip) => {
if (err || !clip) {
mLogger.log(this.debugMode, "HeroInfoComp", `load hero animation failed ${uuid}`, err);
return;
}
if (token !== this.iconVisualToken) return;
this.clearAnimationClips(anim);
anim.addClip(clip);
anim.play("idle");
});
}
private clearAnimationClips(anim: Animation) {
const clips = anim.clips;
if (clips && clips.length > 0) {
for (let i = clips.length - 1; i >= 0; i--) {
const clip = clips[i];
if (clip) anim.removeClip(clip, true);
}
}
}
// ======================== UI 工具 ========================
private setLabelText(node: Node, text: string) {
if (!node) return;
const label = node.getComponent(Label) || node.getComponentInChildren(Label);
if (label) {
label.string = text;
}
}
reset() {
this.node.destroy();
}
}