feat(ui): 新增英雄长按详情弹窗功能
1. 拆分HeroBoxComp的次级属性与技能描述到独立弹窗HeroMoreCom 2. 新增UIID.HeroMore配置与对应预制体资源 3. 实现英雄盒长按0.5秒触发详情弹窗,点击任意位置关闭 4. 弹窗支持实时刷新受buff影响的英雄属性
This commit is contained in:
@@ -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 —— 英雄属性数据模型
|
||||
* - HeroInfo(heroSet)—— 英雄静态配置
|
||||
*/
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user