Files
pixelheros/assets/script/game/map/HeroMoreCom.ts
panFD ac14b8563c feat(hero): 优化英雄详情页技能展示逻辑
1. 新增带等级和升级预览的技能描述渲染函数
2. 为技能档位添加专属命名,支持按触发语境显示技能名
3. 给所有技能档位补充技能等级标注,优化面板展示
4. 新增200个游戏技能名称配置文件
2026-08-08 23:27:47 +08:00

246 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 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();
}
}
}