687 lines
25 KiB
TypeScript
687 lines
25 KiB
TypeScript
/**
|
||
* @file HeroInfoComp.ts
|
||
* @description 英雄详情弹窗(UI 视图层,oops.gui 弹窗)
|
||
*
|
||
* 职责:
|
||
* 1. 展示单个英雄的详情:idle 动画、名称、AP / DEF / HP / CD、技能描述、
|
||
* 稀有度底框、局外等级、职业、碎片数量、升级按钮、技能图标列表。
|
||
* 2. 提供升级操作(消耗碎片提升局外等级,成功后播放升级特效)。
|
||
* 3. 提供前后英雄切换按钮,按图鉴排序顺序循环切换(品质降序 → uuid 升序)。
|
||
* 4. 提供关闭按钮 / 遮罩点击关闭弹窗。
|
||
*
|
||
* 使用(oops.gui 模块):
|
||
* oops.gui.open(UIID.HeroInfo, { uuid: 5001 });
|
||
*
|
||
* 关键设计:
|
||
* - 详情渲染逻辑与原 HerosListComp 详情面板同源(已迁移至此),
|
||
* HerosListComp 仅保留卡片列表,点击卡片改为打开本弹窗。
|
||
* - 前后切换按品质降序 → uuid 升序循环(与图鉴页面卡片顺序一致)。
|
||
* - hero_icon 使用 Animation + iconVisualToken 机制防止异步加载竞态。
|
||
*
|
||
* 依赖:
|
||
* - HeroInfo / HeroList(heroSet)—— 英雄静态配置与全量 UUID 列表
|
||
* - 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, HeroList, 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 { 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!
|
||
|
||
/** 碎片图标节点(挂载 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) {
|
||
// 与图鉴页面排序同源:按职业(HRole)升序 → uuid 升序
|
||
const sorted = [...HeroList].sort((a, b) => {
|
||
const heroA = HeroInfo[a];
|
||
const heroB = HeroInfo[b];
|
||
const roleA = heroA?.role ?? 0;
|
||
const roleB = heroB?.role ?? 0;
|
||
if (roleA !== roleB) return roleA - roleB;
|
||
return a - b;
|
||
});
|
||
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_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();
|
||
}
|
||
}
|