/** * @file HeroMoreCom.ts * @description 英雄信息弹窗组件(UI 视图层) * * 职责: * 1. 展示单个英雄的次级属性(攻速/击晕/暴击/爆伤/冰冻/击退/穿透/风怒)与技能描述, * 展示内容由原 HeroBoxComp 分离而来,计算口径与其完全一致。 * 2. 由 HeroBoxComp 长按英雄盒时同步实例化(instantiate more.prefab)并调用 init(eid)。 * 3. 属性受 buff/光环影响实时变化,打开期间按固定间隔降频刷新;英雄死亡/被卖出自动关闭。 * 4. 点击弹窗内任意位置(含全屏透明遮罩)或 HeroBoxComp 放开手指时由调用方 destroy。 * * 依赖: * - HeroAttrsComp —— 英雄属性数据模型 * - HeroSkillDesc —— 技能描述富文本标准层 */ import { _decorator, Component, Label, NodeEventType, RichText } from "cc"; import { oops } from "db://oops-framework/core/Oops"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { buildSkillDescMoreRich } from "../common/config/HeroSkillDesc"; const { ccclass, property } = _decorator; /** * HeroMoreCom —— 英雄信息弹窗(more.prefab 根节点组件) * * 与长按的英雄盒一一绑定(eid),英雄移除后自动关闭。 * 序列化引用(8 行属性 Label + 技能 RichText)直接写在 more.prefab 中。 */ @ccclass('HeroMoreCom') export class HeroMoreCom extends Component { // ======================== 编辑器绑定节点(more.prefab attr_more 下 8 行) ======================== /** 攻速(攻击间隔,秒)总值 */ @property(Label) private speed_all_label: Label = null; /** 攻速附加值 */ @property(Label) private speed_plus_label: Label = null; /** 击晕率总值(百分点) */ @property(Label) private stun_all_label: Label = null; /** 击晕率附加值 */ @property(Label) private stun_plus_label: Label = null; /** 暴击率总值(百分点) */ @property(Label) private crt_all_label: Label = null; /** 暴击率附加值 */ @property(Label) private crt_plus_label: Label = null; /** 暴击伤害总值(额外暴伤,百分点) */ @property(Label) private crit_dmg_all_label: Label = null; /** 暴击伤害附加值 */ @property(Label) private crit_dmg_plus_label: Label = null; /** 冰冻率总值(百分点) */ @property(Label) private freeze_all_label: Label = null; /** 冰冻率附加值 */ @property(Label) private freeze_plus_label: Label = null; /** 击退率总值(百分点) */ @property(Label) private knockback_all_label: Label = null; /** 击退率附加值 */ @property(Label) private knockback_plus_label: Label = null; /** 穿透率总值(百分点) */ @property(Label) private puncture_all_label: Label = null; /** 穿透率附加值 */ @property(Label) private puncture_plus_label: Label = null; /** 风怒率总值(百分点) */ @property(Label) private wfuny_all_label: Label = null; /** 风怒率附加值 */ @property(Label) private wfuny_plus_label: Label = null; /** 技能描述富文本(全档位合并,数值高亮) */ @property(RichText) private skill_label: RichText = null; // ======================== 运行时状态 ======================== /** 绑定的英雄属性数据模型引用 */ private model: HeroAttrsComp | null = null; /** 是否正在关闭(防止重复调用 remove) */ private isClosing: boolean = false; /** 刷新计时累计(降频刷新实时属性) */ private refreshElapsed: number = 0; /** 实时刷新间隔(秒) */ private static readonly REFRESH_INTERVAL = 0.25; /** 技能描述富文本内容缓存(避免降频内重复重解析排版) */ private skillDescCache: string = ""; // ======================== 生命周期 ======================== onLoad() { // 全屏透明遮罩为根节点子节点,点击任意处触摸事件冒泡至此 → 关闭弹窗(备用交互) this.node.on(NodeEventType.TOUCH_END, this.onTouchEndClose, this); } onDestroy() { if (this.node && this.node.isValid) { this.node.off(NodeEventType.TOUCH_END, this.onTouchEndClose, this); } } /** * 初始化并渲染英雄信息(由 HeroBoxComp 在 instantiate 后同步调用)。 * @param eid 英雄 ECS 实体 ID;实体不存在时立即销毁 */ public init(eid: number) { this.refreshElapsed = 0; this.skillDescCache = ""; if (!eid) { this.closeSelf(); return; } // 按 eid 查找英雄实体(与 HInfoComp 口径一致) 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.closeSelf(); return; } this.model = foundModel; this.refresh(); } /** * 帧更新:降频刷新实时属性;英雄实体失效(死亡/卖出)时自动关闭。 */ update(dt: number) { if (this.isClosing || !this.model) return; if (!(this.model as any).ent) { this.closeSelf(); return; } this.refreshElapsed += dt; if (this.refreshElapsed < HeroMoreCom.REFRESH_INTERVAL) return; this.refreshElapsed = 0; this.refresh(); } // ======================== 显示刷新 ======================== /** * 刷新显示:8 行次级属性(总值 + 附加值)+ 技能描述富文本。 * 附加 = 最终生效值 - 基础配置值,与原 HeroBoxComp 口径一致。 */ private refresh() { if (!this.model) return; // ---- 攻速(攻击间隔,秒;速度提升 → 间隔变短,附加值为负) ---- const skillIds = this.model.getSkillIds(); const displaySkillId = skillIds[1] ?? skillIds[0] ?? 0; const baseCd = displaySkillId ? (this.model.skills[displaySkillId]?.cd ?? 0) : 0; const finalCd = displaySkillId ? this.model.getEffectiveSkillCd(displaySkillId) : 0; this.setStatRow(this.speed_all_label, this.speed_plus_label, finalCd, finalCd - baseCd, "s", 1); // ---- 击晕率 / 暴击率 / 暴击伤害(百分点) ---- const finalStun = this.model.getFinalStunChance(); this.setStatRow(this.stun_all_label, this.stun_plus_label, finalStun, finalStun - (this.model.stun_chance ?? 0), "%"); const finalCrit = this.model.getFinalCritical(); this.setStatRow(this.crt_all_label, this.crt_plus_label, finalCrit, finalCrit - (this.model.critical ?? 0), "%"); const finalCritDmg = this.model.getFinalCritDamage(); this.setStatRow(this.crit_dmg_all_label, this.crit_dmg_plus_label, finalCritDmg, finalCritDmg - (this.model.crit_damage ?? 0), "%"); // ---- 冰冻率 / 击退率 / 穿透率 / 风怒率(百分点) ---- const finalFreeze = this.model.getFinalFreezeChance(); this.setStatRow(this.freeze_all_label, this.freeze_plus_label, finalFreeze, finalFreeze - (this.model.freeze_chance ?? 0), "%"); const finalKnockback = this.model.getFinalKnockbackChance(); this.setStatRow(this.knockback_all_label, this.knockback_plus_label, finalKnockback, finalKnockback - (this.model.knockback_chance ?? 0), "%"); const finalPuncture = this.model.getFinalPunctureChance(); this.setStatRow(this.puncture_all_label, this.puncture_plus_label, finalPuncture, finalPuncture - (this.model.puncture_chance ?? 0), "%"); const finalWfuny = this.model.getFinalWindFury(); this.setStatRow(this.wfuny_all_label, this.wfuny_plus_label, finalWfuny, finalWfuny - (this.model.wfuny ?? 0), "%"); // ---- 技能描述(技能名+技能等级 Lv.x,附升级提示 Lv.x→Lv.y 金色预览) ---- if (this.skill_label && this.skill_label.isValid) { // 缓存未变则跳过重设,避免降频内 RichText 重复重解析排版 const desc = buildSkillDescMoreRich(this.model); if (desc !== this.skillDescCache) { this.skillDescCache = desc; this.skill_label.string = desc; } } } /** * 写入一行属性(总值 + 附加值)。 * 总值始终显示(0 不隐藏);附加值为 0 时隐藏,正/负分别显示 (+x) / (x)。 * * @param allLabel 总值标签(可为空,空时跳过) * @param plusLabel 附加值标签(可为空) * @param total 总数值(最终生效值) * @param bonus 附加数值(最终值 - 基础值) * @param suffix 数值后缀(如 "%") * @param decimals 保留小数位数(默认 0,取整) */ private setStatRow(allLabel: Label | null, plusLabel: Label | null, total: number, bonus: number, suffix: string = "", decimals: number = 0) { const fmt = (v: number) => decimals > 0 ? v.toFixed(decimals) : `${Math.floor(v)}`; if (allLabel && allLabel.isValid) { allLabel.string = `${fmt(total)}${suffix}`; } if (plusLabel && plusLabel.isValid) { if (bonus !== 0) { plusLabel.string = bonus > 0 ? `(+${fmt(bonus)}${suffix})` : `(${fmt(bonus)}${suffix})`; } else { plusLabel.string = ""; } } } // ======================== 交互 ======================== /** 点击面板任意处(含遮罩)关闭(备用交互,主路径由 HeroBoxComp 放开手指 destroy) */ private onTouchEndClose() { oops.audio.playEffect("music/button"); this.closeSelf(); } /** 关闭弹窗(同步销毁节点) */ private closeSelf() { if (this.isClosing) return; this.isClosing = true; if (this.node && this.node.isValid) { this.node.destroy(); } } }