2 Commits

Author SHA1 Message Date
pan
1d4769b080 feat(ui): 新增英雄长按详情弹窗功能
1. 拆分HeroBoxComp的次级属性与技能描述到独立弹窗HeroMoreCom
2. 新增UIID.HeroMore配置与对应预制体资源
3. 实现英雄盒长按0.5秒触发详情弹窗,点击任意位置关闭
4. 弹窗支持实时刷新受buff影响的英雄属性
2026-08-05 16:54:14 +08:00
pan
fb3cf4c619 refactor(missionCard): 重构面板滑动逻辑为底部划出模式
重构了任务卡牌面板的布局与动画系统:
1. 移除原水平轮播逻辑,改为覆盖面板从底部划入/划出的交互
2. 统一使用场景读取的真实坐标作为显示/隐藏基准位置
3. 重命名相关变量与注释,匹配新的底部划出交互逻辑
4. 调整所有面板切换按钮的注释描述,贴合新交互行为
2026-08-05 14:45:22 +08:00
9 changed files with 12887 additions and 12207 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": {}
}

View File

@@ -179,14 +179,14 @@ export class MissionCardComp extends CCComp {
/** 是否已召唤过英雄(用于控制其他面板按钮是否可点击) */
private hasCalledHero: boolean = false;
// ======================== 面板轮播状态 ========================
// ======================== 面板划出状态 ========================
/** 面板滑动间距(单面板宽度,运行时由 initPanelLayout 推导) */
private panelSlideWidth: number = 720;
/** 当前激活的面板索引0=英雄, 1=装备, 2=技能, 3=药品 */
private currentPanelIndex: number = 0;
/** 各面板基准 Y 坐标缓存 */
private panelBaseYs: number[] = [];
/** 面板显示位置(英雄面板基准位置,运行时从场景读取) */
private panelShowPos: Vec3 = new Vec3();
/** 各覆盖面板(装备/技能/药品)的隐藏 Y 坐标(屏幕外底部,从场景初始位置读取) */
private panelHideYs: number[] = [];
/** 是否已初始化面板布局 */
private hasInitPanelLayout: boolean = false;
/** 是否已缓存卡牌面板基准缩放 */
@@ -197,10 +197,8 @@ export class MissionCardComp extends CCComp {
private cardsShowScale: Vec3 = new Vec3(1, 1, 1);
/** 卡牌面板收起态缩放scale=0 隐藏) */
private cardsHideScale: Vec3 = new Vec3(0, 0, 1);
/** 卡牌面板显示位置 Y 坐标 */
private readonly cardPanelShowY: number = 0;
/** 卡牌面板隐藏位置 Y 坐标(向下 700 */
private readonly cardPanelHideY: number = -700;
/** 卡牌面板隐藏 Y 坐标(场景初始位置,屏幕外底部,由 initPanelLayout 缓存) */
private cardsPanHideY: number = 0;
/** 卡牌面板显示/隐藏动画时长 */
private readonly cardPanelAnimDuration: number = 0.25;
@@ -504,7 +502,7 @@ export class MissionCardComp extends CCComp {
}
/**
* 技能面板按钮回调:滑动到技能面板(index=2)。
* 技能面板按钮回调:技能面板从底部划出(index=2)。
*/
private onShowSkillsClick() {
oops.audio.playEffect("music/button");
@@ -514,7 +512,7 @@ export class MissionCardComp extends CCComp {
// ======================== 商店面板 ========================
/**
* 商店面板按钮回调:滑动到药品商店面板(index=3)。
* 商店面板按钮回调:药品商店面板从底部划出(index=3)。
*/
private onShowShopClick() {
oops.audio.playEffect("music/button");
@@ -854,7 +852,7 @@ export class MissionCardComp extends CCComp {
this.playButtonResetAnim(this.cards_chou);
}
// ======================== 面板轮播系统 ========================
// ======================== 面板底部划出系统 ========================
/** 面板节点数组(顺序:英雄→装备→技能→药品) */
private getPanelNodes(): Node[] {
@@ -867,46 +865,51 @@ export class MissionCardComp extends CCComp {
}
/**
* 初始化面板轮播布局:
* 1. 缓存各面板基准 Y 坐标
* 2. 推导面板滑动间距(取场景设计分辨率宽度或默认 720)。
* 3. 将所有面板排列为水平队列初始显示英雄面板index=0
* 初始化面板布局:
* 1. 以英雄面板位置为基准显示位置(英雄面板固定不动)
* 2. 缓存各覆盖面板(装备/技能/药品)的初始位置作为隐藏位置(屏幕外底部)。
* 3. 将覆盖面板归位到隐藏位置初始显示英雄面板index=0
*/
private initPanelLayout() {
if (this.hasInitPanelLayout) return;
const panels = this.getPanelNodes();
this.panelBaseYs = [];
this.panelHideYs = [];
// 以第一个有效面板的位置为基准,推导滑动间距
let refX = 0;
for (const p of panels) {
if (p && p.isValid) {
refX = p.getPosition().x;
break;
}
const heroNode = panels[0];
if (heroNode && heroNode.isValid) {
heroNode.active = true;
this.panelShowPos.set(heroNode.getPosition());
}
this.panelSlideWidth = Math.abs(refX) * 2 || 720;
if (this.panelSlideWidth < 400) this.panelSlideWidth = 720;
for (let i = 0; i < panels.length; i++) {
// 覆盖面板缓存场景初始位置屏幕外底部作为隐藏位置X 对齐显示位置
for (let i = 1; i < panels.length; i++) {
const node = panels[i];
if (!node || !node.isValid) {
this.panelBaseYs.push(0);
this.panelHideYs.push(this.panelShowPos.y);
continue;
}
node.active = true;
const pos = node.getPosition();
this.panelBaseYs.push(pos.y);
// 初始位置:当前面板(英雄)居中,其余按序排列
const targetX = (i - this.currentPanelIndex) * this.panelSlideWidth;
node.setPosition(targetX, pos.y, pos.z);
this.panelHideYs.push(pos.y);
// index 0 为英雄面板,隐藏列表从 index 1 起
node.setPosition(this.panelShowPos.x, pos.y, pos.z);
}
// 卡牌面板与其他覆盖面板同级缓存场景初始位置屏幕外底部作为隐藏位置X 对齐显示位置
if (this.cardPanNode && this.cardPanNode.isValid) {
const pos = this.cardPanNode.getPosition();
this.cardsPanHideY = pos.y;
this.cardPanNode.setPosition(this.panelShowPos.x, pos.y, pos.z);
}
this.hasInitPanelLayout = true;
this.updatePanelButtonStates();
}
/**
* 滑动到指定面板
* 切换到指定面板
* 英雄面板index=0固定不动
* 选中的覆盖面板从底部划入到显示位置,其余覆盖面板划出回底部隐藏位置。
* @param index 0=英雄, 1=装备, 2=技能, 3=药品
*/
private slideToPanel(index: number) {
@@ -916,14 +919,14 @@ export class MissionCardComp extends CCComp {
this.currentPanelIndex = index;
const duration = 0.25;
for (let i = 0; i < panels.length; i++) {
// 覆盖面板从 index 1 开始index 0 英雄面板固定不动)
for (let i = 1; i < panels.length; i++) {
const node = panels[i];
if (!node || !node.isValid) continue;
const targetX = (i - index) * this.panelSlideWidth;
const baseY = this.panelBaseYs[i] ?? node.getPosition().y;
const targetY = i === index ? this.panelShowPos.y : (this.panelHideYs[i - 1] ?? node.getPosition().y);
Tween.stopAllByTarget(node);
tween(node)
.to(duration, { position: new Vec3(targetX, baseY, 0) }, { easing: 'quadOut' })
.to(duration, { position: new Vec3(this.panelShowPos.x, targetY, 0) }, { easing: 'quadOut' })
.start();
}
@@ -960,7 +963,7 @@ export class MissionCardComp extends CCComp {
}
/**
* 英雄面板按钮回调:滑动到英雄面板(index=0)。
* 英雄面板按钮回调:收起覆盖面板,回到英雄面板(index=0)。
*/
private onShowHerosClick() {
oops.audio.playEffect("music/button");
@@ -1020,7 +1023,7 @@ export class MissionCardComp extends CCComp {
}
/**
* 显示卡牌面板:从底部向上滑入到 Y=0
* 显示卡牌面板:从底部向上滑入到面板显示位置panelShowPos
*/
private onShowCardsClick() {
oops.audio.playEffect("music/button");
@@ -1028,24 +1031,24 @@ export class MissionCardComp extends CCComp {
this.cardPanNode.active = true;
Tween.stopAllByTarget(this.cardPanNode);
tween(this.cardPanNode)
.to(this.cardPanelAnimDuration, { position: new Vec3(0, this.cardPanelShowY, 0) }, { easing: 'quadOut' })
.to(this.cardPanelAnimDuration, { position: new Vec3(this.panelShowPos.x, this.panelShowPos.y, 0) }, { easing: 'quadOut' })
.start();
}
/**
* 隐藏卡牌面板:向下滑出到 Y=-700
* 隐藏卡牌面板:向下滑出到其隐藏位置cardsPanHideY屏幕外底部
*/
private onCloseCardsClick() {
oops.audio.playEffect("music/button");
this.hideCardsPanel();
}
/** 隐藏卡牌面板:向下滑出到 Y=-700(不含按钮音效,供系统逻辑复用) */
/** 隐藏卡牌面板:向下滑出到隐藏位置(不含按钮音效,供系统逻辑复用) */
private hideCardsPanel() {
if (!this.cardPanNode || !this.cardPanNode.isValid) return;
Tween.stopAllByTarget(this.cardPanNode);
tween(this.cardPanNode)
.to(this.cardPanelAnimDuration, { position: new Vec3(0, this.cardPanelHideY, 0) }, { easing: 'quadIn' })
.to(this.cardPanelAnimDuration, { position: new Vec3(this.panelShowPos.x, this.cardsPanHideY, 0) }, { easing: 'quadIn' })
.call(() => {
if (this.cardPanNode && this.cardPanNode.isValid) {
this.cardPanNode.active = false;
@@ -1057,7 +1060,7 @@ export class MissionCardComp extends CCComp {
// ======================== 装备商店面板 ========================
/**
* 装备面板按钮回调:滑动到装备商店面板(index=1)。
* 装备面板按钮回调:装备商店面板从底部划出(index=1)。
*/
private onShowEquipsClick() {
oops.audio.playEffect("music/button");
@@ -1451,8 +1454,8 @@ export class MissionCardComp extends CCComp {
/**
* 进入准备阶段:
* - 初始化面板轮播布局(如尚未初始化)。
* - 滑动到英雄面板index=0作为默认展示。
* - 初始化面板布局(如尚未初始化)。
* - 收起覆盖面板回到英雄面板index=0作为默认展示。
* - 激活 showHeros 按钮可见性。
* - 显示卡牌面板(从底部滑入)。
*/
@@ -1469,10 +1472,10 @@ export class MissionCardComp extends CCComp {
}
// 刷新所有刷新按钮的费用显示(含刷新石/金币切换与置灰状态)
this.updateDrawCostUI();
// 显示卡牌面板(默认进入准备阶段时展开)
// 显示卡牌面板(默认进入准备阶段时展开,位置与其他面板保持一致
if (this.cardPanNode && this.cardPanNode.isValid) {
this.cardPanNode.active = true;
this.cardPanNode.setPosition(0, this.cardPanelShowY, 0);
this.cardPanNode.setPosition(this.panelShowPos.x, this.panelShowPos.y, 0);
mLogger.log(this.debugMode, "MissionCardComp", "enterPreparePhase cardPanNode", {
active: this.cardPanNode.active,
position: this.cardPanNode.position,
@@ -1485,7 +1488,7 @@ export class MissionCardComp extends CCComp {
cards_nodeChildren: this.cards_node?.children.length
});
}
// 滑动到英雄面板
// 收起覆盖面板,回到英雄面板
this.slideToPanel(0);
mLogger.log(this.debugMode, "MissionCardComp", "enterPreparePhase herosPanNode", {
@@ -1493,7 +1496,7 @@ export class MissionCardComp extends CCComp {
herosPanNodePos: this.herosPanNode?.position,
herosPanNodeWorldPos: this.herosPanNode?.worldPosition,
currentPanelIndex: this.currentPanelIndex,
panelSlideWidth: this.panelSlideWidth
panelShowPos: this.panelShowPos
});
}