feat(ui): 新增英雄长按详情弹窗功能

1. 拆分HeroBoxComp的次级属性与技能描述到独立弹窗HeroMoreCom
2. 新增UIID.HeroMore配置与对应预制体资源
3. 实现英雄盒长按0.5秒触发详情弹窗,点击任意位置关闭
4. 弹窗支持实时刷新受buff影响的英雄属性
This commit is contained in:
pan
2026-08-05 16:54:14 +08:00
parent fb3cf4c619
commit 1d4769b080
8 changed files with 12834 additions and 12157 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "1bc6274e-7a88-46ee-9965-7b3bb2ac13c4",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "more"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,8 @@ export enum UIID {
Guide2,
Guide3,
Guide4,
/** 英雄信息弹窗(长按英雄盒查看次级属性与技能描述) */
HeroMore,
}
/** 打开界面方式的配置数据 */
@@ -51,4 +53,5 @@ export var UIConfigData: { [key: number]: UIConfig } = {
[UIID.Guide2]: { layer: LayerType.PopUp, prefab: "gui/element/guide2" },
[UIID.Guide3]: { layer: LayerType.PopUp, prefab: "gui/element/guide3" },
[UIID.Guide4]: { layer: LayerType.PopUp, prefab: "gui/element/guide4" },
[UIID.HeroMore]: { layer: LayerType.PopUp, prefab: "gui/auie/more" },
}

View File

@@ -3,16 +3,17 @@
* @description 英雄面板单个英雄盒组件UI 视图层)
*
* 职责:
* 1. 同步显示一个已登场英雄的详细信息(图标 / 名称 / 等级 / AP / HP / 描述)。
* 2. 由 MissionCardComp 实例化到 herosBoxNode 下,最多 3 个,与场上英雄一一对应
* 3. 英雄卖出 / 死亡后槽位保留,通过 applyEmpty() 显示为空英雄信息
* 1. 同步显示一个已登场英雄的基础信息(图标 / 名称 / 等级 / AP / HP
* 2. 长按英雄盒弹出信息面板UIID.HeroMore → more.prefab展示次级属性与技能描述
* 3. 由 MissionCardComp 实例化到 herosBoxNode 下,最多 3 个,与场上英雄一一对应
* 4. 英雄卖出 / 死亡后槽位保留,通过 applyEmpty() 显示为空英雄信息。
*
* 依赖:
* - HeroAttrsComp —— 英雄属性数据模型
* - HeroInfoheroSet—— 英雄静态配置
*/
import { mLogger } from "../common/Logger";
import { _decorator, Node, Sprite, Label, RichText, resources, AnimationClip, SpriteFrame, NodeEventType, Tween, tween, Vec3, Color } from "cc";
import { _decorator, Node, Sprite, Label, resources, AnimationClip, SpriteFrame, NodeEventType, EventTouch, Tween, tween, Vec3, Color } 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";
@@ -21,7 +22,7 @@ import { HeroInfo } from "../common/config/heroSet";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { Hero } from "../hero/Hero";
import { FacSet, FightSet, getLvColor } from "../common/config/GameSet";
import { buildSkillDesc, buildSkillDescRich, ISkillDescSource } from "../common/config/HeroSkillDesc";
import { buildSkillDesc, ISkillDescSource } from "../common/config/HeroSkillDesc";
import { MissionEconomy } from "./MissionEconomy";
import { UIID } from "../common/config/GameUIConfig";
@@ -75,12 +76,6 @@ export class HeroBoxComp extends CCComp {
@property(Label)
private upgrade_price_label: Label = null;
/** 技能描述富文本(多行,展示当前等级触发技能,数值高亮) */
@property(RichText)
private skill_label: RichText = null;
/** 技能描述富文本内容缓存(避免 refresh 降频内重复重解析排版) */
private skillDescCache: string = "";
// ======================== 战斗信息标签(总值 + 附加值) ========================
/** 攻击总值 */
@@ -95,63 +90,6 @@ export class HeroBoxComp extends CCComp {
/** 生命附加值 */
@property(Label)
private hp_plus_label: Label = null;
/** 暴击率总值(百分点) */
@property(Label)
private crt_all_label: Label = null;
/** 暴击率附加值 */
@property(Label)
private crt_plus_label: Label = null;
/** 击退率总值(百分点) */
@property(Label)
private knockback_all_label: Label = null;
/** 击退率附加值 */
@property(Label)
private knockback_plus_label: Label = null;
/** 风怒率总值(百分点) */
@property(Label)
private wfuny_all_label: Label = null;
/** 风怒率附加值 */
@property(Label)
private wfuny_plus_label: Label = null;
/** 击晕率总值(百分点) */
@property(Label)
private stun_all_label: Label = null;
/** 击晕率附加值 */
@property(Label)
private stun_plus_label: Label = null;
/** 攻击速度总值(攻击间隔,秒;速度提升时间隔变短,附加值为负) */
@property(Label)
private speed_all_label: Label = null;
/** 攻击速度附加值 */
@property(Label)
private speed_plus_label: Label = null;
/** 冰冻率总值(百分点) */
@property(Label)
private freeze_all_label: Label = null;
/** 冰冻率附加值 */
@property(Label)
private freeze_plus_label: Label = null;
/** 穿透率总值(百分点) */
@property(Label)
private puncture_all_label: Label = null;
/** 穿透率附加值 */
@property(Label)
private puncture_plus_label: Label = null;
/** 暴击伤害总值(额外暴伤,百分点) */
@property(Label)
private crit_dmg_all_label: Label = null;
/** 暴击伤害附加值 */
@property(Label)
private crit_dmg_plus_label: Label = null;
// ======================== 更多属性折叠栏 ========================
/** 次级属性栏容器(默认隐藏,长按更多按钮时显示) */
@property(Node)
private attr_box: Node = null;
/** 更多按钮(长按显示次级属性栏,放开隐藏) */
@property(Node)
private more_btn: Node = null;
// ======================== 运行时状态 ========================
@@ -164,16 +102,24 @@ export class HeroBoxComp extends CCComp {
/** 当前显示的英雄 UUID避免相同 UUID 重复加载动画) */
private iconHeroUuid: number = 0;
/** 长按判定:当前按下触摸 ID-1 表示无) */
private longPressTouchId: number = -1;
/** 长按判定:按下起点 UI 坐标 */
private longPressStartX: number = 0;
private longPressStartY: number = 0;
/** 长按触发时长(秒) */
private static readonly LONG_PRESS_DURATION = 0.5;
/** 长按最大位移容差(像素),超出则取消判定 */
private static readonly LONG_PRESS_SLOP = 30;
// ======================== 生命周期 ========================
onLoad() {
// 次级属性栏默认隐藏,长按更多按钮时显示,放开隐藏
if (this.attr_box && this.attr_box.isValid) {
this.attr_box.active = false;
}
this.more_btn?.on(NodeEventType.TOUCH_START, this.onMoreBtnTouchStart, this);
this.more_btn?.on(NodeEventType.TOUCH_END, this.onMoreBtnTouchEnd, this);
this.more_btn?.on(NodeEventType.TOUCH_CANCEL, this.onMoreBtnTouchEnd, this);
// 长按英雄盒任意区域弹出信息面板UIID.HeroMore → more.prefab出售/升级按钮上的触摸不触发
this.node.on(NodeEventType.TOUCH_START, this.onBoxTouchStart, this);
this.node.on(NodeEventType.TOUCH_MOVE, this.onBoxTouchMove, this);
this.node.on(NodeEventType.TOUCH_END, this.onBoxTouchEnd, this);
this.node.on(NodeEventType.TOUCH_CANCEL, this.onBoxTouchCancel, this);
// 空槽位点击 → 打开英雄卡池
this.noHero?.on(NodeEventType.TOUCH_END, this.onNoHeroClick, this);
// 出售按钮点击 → 出售当前绑定英雄
@@ -183,10 +129,11 @@ export class HeroBoxComp extends CCComp {
}
onDestroy() {
if (this.more_btn && this.more_btn.isValid) {
this.more_btn.off(NodeEventType.TOUCH_START, this.onMoreBtnTouchStart, this);
this.more_btn.off(NodeEventType.TOUCH_END, this.onMoreBtnTouchEnd, this);
this.more_btn.off(NodeEventType.TOUCH_CANCEL, this.onMoreBtnTouchEnd, this);
if (this.node && this.node.isValid) {
this.node.off(NodeEventType.TOUCH_START, this.onBoxTouchStart, this);
this.node.off(NodeEventType.TOUCH_MOVE, this.onBoxTouchMove, this);
this.node.off(NodeEventType.TOUCH_END, this.onBoxTouchEnd, this);
this.node.off(NodeEventType.TOUCH_CANCEL, this.onBoxTouchCancel, this);
}
if (this.noHero && this.noHero.isValid) {
this.noHero.off(NodeEventType.TOUCH_END, this.onNoHeroClick, this);
@@ -376,24 +323,76 @@ export class HeroBoxComp extends CCComp {
this.refresh();
}
/** 更多按钮按下:显示次级属性栏 */
private onMoreBtnTouchStart() {
oops.audio.playEffect("music/button");
this.setAttrBoxVisible(true);
/**
* 英雄盒按下:记录起点并启动长按判定。
* 出售/升级按钮上的触摸不触发长按(按钮有各自的 TOUCH_END 行为)。
*/
private onBoxTouchStart(event: EventTouch) {
if (!this.hasHero) return;
const target = event.target as Node | null;
if (this.isNodeWithin(target, this.sell_btn) || this.isNodeWithin(target, this.upgrade_btn)) return;
this.longPressTouchId = event.touch?.getID() ?? -1;
const pos = event.getUILocation();
this.longPressStartX = pos.x;
this.longPressStartY = pos.y;
this.scheduleOnce(this.onLongPressFired, HeroBoxComp.LONG_PRESS_DURATION);
}
/** 更多按钮放开/取消:隐藏次级属性栏 */
private onMoreBtnTouchEnd() {
this.setAttrBoxVisible(false);
}
/** 切换次级属性栏容器可见性 */
private setAttrBoxVisible(visible: boolean) {
if (this.attr_box && this.attr_box.isValid) {
this.attr_box.active = visible;
/** 英雄盒拖动:位移超出容差取消长按判定 */
private onBoxTouchMove(event: EventTouch) {
if (this.longPressTouchId < 0) return;
if ((event.touch?.getID() ?? -1) !== this.longPressTouchId) return;
const pos = event.getUILocation();
const dx = pos.x - this.longPressStartX;
const dy = pos.y - this.longPressStartY;
if (dx * dx + dy * dy > HeroBoxComp.LONG_PRESS_SLOP * HeroBoxComp.LONG_PRESS_SLOP) {
this.cancelLongPress();
}
}
/** 英雄盒抬起:取消未触发的长按判定 */
private onBoxTouchEnd(event: EventTouch) {
if (this.longPressTouchId < 0) return;
if ((event.touch?.getID() ?? -1) !== this.longPressTouchId) return;
this.cancelLongPress();
}
/** 英雄盒触摸被系统打断:取消长按判定 */
private onBoxTouchCancel(event: EventTouch) {
this.onBoxTouchEnd(event);
}
/** 取消长按判定(复位触摸 ID 并移除定时) */
private cancelLongPress() {
this.longPressTouchId = -1;
this.unschedule(this.onLongPressFired);
}
/** 长按触发弹出英雄信息面板more.prefab展示次级属性与技能描述 */
private onLongPressFired() {
this.longPressTouchId = -1;
if (!this.hasHero || !(this.model as any)?.ent) return;
oops.audio.playEffect("music/button");
oops.gui.open(UIID.HeroMore, { eid: this.eid });
}
/**
* 判断触摸目标节点是否位于指定祖先节点(含自身)之内。
* @param target 触摸命中的节点(可能为空)
* @param ancestor 祖先节点(可能为空,空时返回 false
* @returns 命中节点是否为 ancestor 的后代或自身
*/
private isNodeWithin(target: Node | null, ancestor: Node | null): boolean {
if (!target || !ancestor || !ancestor.isValid) return false;
let cur: Node | null = target;
while (cur) {
if (cur === ancestor) return true;
if (cur === this.node) break;
cur = cur.parent;
}
return false;
}
/** ECS 组件移除时销毁节点 */
reset() {
if (this.node && this.node.isValid) {
@@ -470,22 +469,10 @@ export class HeroBoxComp extends CCComp {
}
this.clearStatRow(this.ap_all_label, this.ap_plus_label);
this.clearStatRow(this.hp_all_label, this.hp_plus_label);
this.clearStatRow(this.crt_all_label, this.crt_plus_label);
this.clearStatRow(this.knockback_all_label, this.knockback_plus_label);
this.clearStatRow(this.wfuny_all_label, this.wfuny_plus_label);
this.clearStatRow(this.stun_all_label, this.stun_plus_label);
this.clearStatRow(this.speed_all_label, this.speed_plus_label);
this.clearStatRow(this.freeze_all_label, this.freeze_plus_label);
this.clearStatRow(this.puncture_all_label, this.puncture_plus_label);
this.clearStatRow(this.crit_dmg_all_label, this.crit_dmg_plus_label);
if (this.skill_label && this.skill_label.isValid) {
this.skill_label.string = "";
}
this.skillDescCache = "";
}
/**
* 刷新显示同步英雄等级、名称、AP / HP、描述与图标
* 刷新显示同步英雄等级、名称、AP / HP 与图标(次级属性与技能描述已分离至 HeroMoreCom 弹窗)
* 绑定的英雄实体已失效时自动切换为空状态。
*/
public refresh() {
@@ -537,48 +524,6 @@ export class HeroBoxComp extends CCComp {
const baseHp = Math.max(0, Math.floor(this.model.base_hp ?? 0));
this.setStatRow(this.hp_all_label, this.hp_plus_label, finalHp, finalHp - baseHp);
// ---- 暴击率 / 击退率 / 风怒率 / 击晕率(百分点,附加 = 最终 - 基础配置值) ----
const finalCrit = this.model.getFinalCritical();
this.setStatRow(this.crt_all_label, this.crt_plus_label, finalCrit, finalCrit - (this.model.critical ?? 0), "%");
const finalKnockback = this.model.getFinalKnockbackChance();
this.setStatRow(this.knockback_all_label, this.knockback_plus_label, finalKnockback, finalKnockback - (this.model.knockback_chance ?? 0), "%");
const finalWfuny = this.model.getFinalWindFury();
this.setStatRow(this.wfuny_all_label, this.wfuny_plus_label, finalWfuny, finalWfuny - (this.model.wfuny ?? 0), "%");
const finalStun = this.model.getFinalStunChance();
this.setStatRow(this.stun_all_label, this.stun_plus_label, finalStun, finalStun - (this.model.stun_chance ?? 0), "%");
// ---- 攻击速度(攻击间隔,秒;速度提升 → 间隔变短,附加值为负) ----
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 finalFreeze = this.model.getFinalFreezeChance();
this.setStatRow(this.freeze_all_label, this.freeze_plus_label, finalFreeze, finalFreeze - (this.model.freeze_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 finalCritDmg = this.model.getFinalCritDamage();
this.setStatRow(this.crit_dmg_all_label, this.crit_dmg_plus_label, finalCritDmg, finalCritDmg - (this.model.crit_damage ?? 0), "%");
// ---- 技能描述(全档位合并:每技能一行,等级置前 + 已/未激活状态,数值富文本高亮) ----
if (this.skill_label && this.skill_label.isValid) {
// 缓存未变则跳过重设,避免 refresh 降频内 RichText 重复重解析排版
// showName=false 不显示技能名仅保留效果showLocked=true 合并全档位(未来档数值灰色拼接,未激活技能整行灰色)
const desc = buildSkillDescRich(this.model, false, true);
if (desc !== this.skillDescCache) {
this.skillDescCache = desc;
this.skill_label.string = desc;
}
}
// ---- 图标UUID 变化时重新加载首帧动画) ----
if (hero && heroUuid !== this.iconHeroUuid) {
this.iconHeroUuid = heroUuid;

View File

@@ -0,0 +1,252 @@
/**
* @file HeroMoreCom.ts
* @description 英雄信息弹窗组件UI 视图层)
*
* 职责:
* 1. 展示单个英雄的次级属性(攻速/击晕/暴击/爆伤/冰冻/击退/穿透/风怒)与技能描述,
* 展示内容由原 HeroBoxComp 分离而来,计算口径与其完全一致。
* 2. 由 HeroBoxComp 长按英雄盒时通过 oops.gui.open(UIID.HeroMore, { eid }) 打开oops.gui 模块)。
* 3. 属性受 buff/光环影响实时变化,打开期间按固定间隔降频刷新;英雄死亡/被卖出自动关闭。
* 4. 点击弹窗内任意位置(含全屏透明遮罩)关闭。
*
* 依赖:
* - 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 { buildSkillDescRich } from "../common/config/HeroSkillDesc";
const { ccclass, property } = _decorator;
/** 英雄信息弹窗参数 */
export interface HeroMoreParams {
/** 英雄 ECS 实体 ID */
eid?: number;
}
/**
* 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);
}
}
/**
* oops.gui 添加到舞台回调(框架约定入口)。
* @param params 见 HeroMoreParamseid 对应实体不存在时立即关闭
*/
onAdded(params: HeroMoreParams = {}) {
this.isClosing = false;
this.refreshElapsed = 0;
this.skillDescCache = "";
const eid = params?.eid ?? 0;
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), "%");
// ---- 技能描述(全档位合并:每技能一行,已/未激活状态 + 数值富文本高亮) ----
if (this.skill_label && this.skill_label.isValid) {
// 缓存未变则跳过重设,避免降频内 RichText 重复重解析排版
// showName=false 不显示技能名仅保留效果showLocked=true 合并全档位
const desc = buildSkillDescRich(this.model, false, true);
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 = "";
}
}
}
// ======================== 交互 ========================
/** 点击面板任意处(含遮罩)关闭 */
private onTouchEndClose() {
oops.audio.playEffect("music/button");
this.closeSelf();
}
/** 关闭弹窗(交还 oops.gui 缓存,下次打开复用节点并重新触发 onAdded */
private closeSelf() {
if (this.isClosing) return;
this.isClosing = true;
oops.gui.removeByNode(this.node);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "6b2e9f14-7a3c-4d85-9e62-c4f8a1b7d3e0",
"files": [],
"subMetas": {},
"userData": {}
}