//********************* // create by 流云 // time: Sat Sep 05 2026 // desc: 战斗内点击组合字武将 — 详情弹窗 //********************* import { _decorator, Color, EventTouch, instantiate, Label, Node, Prefab, sp, Sprite, Tween, UITransform, UIOpacity, UIRenderer, Vec3, } from 'cc'; import { type IPopAnimConfig, type IPopAnimSession } from '../../../mx/module/ui/popAnim'; import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch'; import { BATTLE_EVENT_ALL, BattleEventType, type BattleEvent } from 'db://assets/mx/module/battle'; import type { EntityId } from 'db://assets/mx/module/ecs'; import { GEvent } from '../../../mx/module/event/GEvent'; import { Auto_BattleWeaponDescHero } from './Auto_BattleWeaponDescHero'; import type { TileEntitySnapshot, TowerRuntimeSnapshot } from '../../battle/combat/TowerQueryTypes'; import { TowerQueryService } from '../../battle/combat/TowerQueryService'; import { deriveTowerSetupStats, TowerStatDisplay } from '../../battle/combat/TowerStatResolver'; import { getTowerExpHost, needTowerExpForLevel } from '../../battle/combat/TowerExpHost'; import { resolveHeroBattleWeaponId } from '../../battle/combat/TowerLoadoutService'; import { HeroSkin } from '../../battle/view/HeroSkin'; import { Skill } from '../../battle/skill/Skill'; import { heroUltSkillId } from '../../battle/config/BattleHeroTable'; import { applyWeaponSpineSkin } from '../../battle/view/GlyphSpineSkin'; import { BattleSpinePlayback } from '../../battle/presentation/BattleSpinePlayback'; import { GBundle, GPath } from '../../game/ConfigRes'; import { TableNames } from '../../game/ConfigTableData'; import type { Player } from '../../game/Player'; const { ccclass } = _decorator; const LOG = '[UIBattleWeaponDescHero]'; /** 品质 → 武器名字体色 */ const WEAPON_NAME_FONT_HEX: Record = { 0: '#D8D8D8', 1: '#6FA86F', 2: '#6FA2A8', 3: '#B390D9', 4: '#F9C948', }; /** 品质 → 武器名描边色 */ const WEAPON_NAME_OUTLINE_HEX: Record = { 0: '#4A4A4A', 1: '#386c42', 2: '#285c6d', 3: '#623073', 4: '#734930', }; /** 组合武将详情弹窗入参 */ export interface BattleWeaponDescHeroPanelData { entity: TileEntitySnapshot; runtime?: TowerRuntimeSnapshot | null; /** 敌方阵营皮肤(组合字 皮肤字2_*) */ enemyCamp?: boolean; /** 已解析的展示武器 id(敌方优先 AI 档案) */ weaponId?: number; weaponWorldPos?: { x: number; y: number; z?: number }; gridStep?: number; trySwitchAtUiPos?: (uiPos: { x: number; y: number }) => boolean; /** 监视中的地图/背包格(切换选中格时随 prema 更新) */ watchTarget?: { kind: 'map' | 'bag'; index: number } | null; /** 已部署 ECS 实体(地图格实时刷新 fallback) */ watchEntityId?: EntityId; /** 从棋盘重新读取 entity/runtime(击杀加经验后刷新数值) */ resolveLiveData?: () => Pick | null; } /** 经验条满宽(像素) */ const PROGRESS_MAX_WIDTH = 225; const WEAPON_SPINE_IDLE = '待机'; @ccclass('UIBattleWeaponDescHero') export class UIBattleWeaponDescHero extends Auto_BattleWeaponDescHero { private static readonly PANEL_GAP_GRIDS = 1.7; private _popupBody: Node | null = null; /** 递增时取消尚未完成的关窗销毁 */ private _panelLifeGen = 0; /** ui_node 下挂的武将展示节点 */ private _heroNode: Node | null = null; /** 当前面板已解析的展示武器(defer 复用,避免二次解析失败把 ui_type 隐藏) */ private _displayWeaponRow: ITableWeapon | null = null; /** 静态区(武将/技能/武器名等)缓存键;切换仅刷新数值区时不重建 */ private _staticViewKey = ''; /** 战斗事件监听是否已绑定 */ private _battleEventBound = false; /** GEvent 监听是否已绑定 */ private _geventBound = false; onLoad(): void { super.onLoad(); this._popupBody = this.node.getChildByName('弹窗主体'); } onEnable(): void { super.onEnable(); this.ui_bgMask?.on(Node.EventType.TOUCH_END, this.click_ui_bgMask, this); this._bindLiveRefresh(); } onDisable(): void { if (this.ui_bgMask?.isValid) { this.ui_bgMask.off(Node.EventType.TOUCH_END, this.click_ui_bgMask, this); } this._unbindLiveRefresh(); this.unschedule(this._deferRefreshWeaponType); this.unschedule(this._deferredRefreshDynamic); this._staticViewKey = ''; this._clearHeroNode(); super.onDisable(); } /** 订阅击杀/棋盘变更,实时刷新数值区 */ private _bindLiveRefresh(): void { if (!this._geventBound) { GEvent.Ins.on(GEvent.UpdateBestPlace, this._onLiveDataDirty, this); this._geventBound = true; } const battle = gg.game.CurentBattle; if (battle && !this._battleEventBound) { battle.on(BATTLE_EVENT_ALL, this._onBattleEvent); this._battleEventBound = true; } } /** 取消实时刷新订阅 */ private _unbindLiveRefresh(): void { if (this._geventBound) { GEvent.Ins.off(GEvent.UpdateBestPlace, this._onLiveDataDirty, this); this._geventBound = false; } const battle = gg.game.CurentBattle; if (battle && this._battleEventBound) { battle.off(BATTLE_EVENT_ALL, this._onBattleEvent); this._battleEventBound = false; } } private _onBattleEvent = (event: BattleEvent): void => { if (event.type === BattleEventType.EntityKilled) { this._scheduleRefreshDynamicFromLive(); } }; private _onLiveDataDirty = (): void => { this._scheduleRefreshDynamicFromLive(); }; /** 帧末刷新,确保击杀经验/升级写入后再读 */ private _scheduleRefreshDynamicFromLive(): void { this.unschedule(this._deferredRefreshDynamic); this.scheduleOnce(this._deferredRefreshDynamic, 0); } private _deferredRefreshDynamic = (): void => { this._refreshDynamicFromLive(); }; onInit(): void { this._panelLifeGen++; this._ensurePanelVisible(); this._bindLiveRefresh(); const ctx = this._resolveRefreshContext(); if (!ctx) { this._staticViewKey = ''; this._clearLabels(); this._clearHeroNode(); this._hideWeaponTypes(); return; } const { data, w1, level, levelLimit, stats, weaponRow, runtime, enemyCamp } = ctx; const staticViewKey = this._buildStaticViewKey(w1.id, weaponRow?.id ?? 0, enemyCamp); const needStaticRefresh = staticViewKey !== this._staticViewKey || !this._heroNode?.isValid; if (needStaticRefresh) { this._staticViewKey = staticViewKey; this._displayWeaponRow = weaponRow; this._refreshHero(w1, level, enemyCamp); this._refreshStaticLabels(w1, level, levelLimit, weaponRow); this._refreshWeaponType(weaponRow); this._scheduleRefreshWeaponType(weaponRow); } this._refreshDynamicStats(w1, level, levelLimit, stats, weaponRow, runtime); this._applyPanelPosition(data); } /** 合并 prema 与 resolveLiveData,得到最新等级/属性 */ private _resolveRefreshContext(): { data: BattleWeaponDescHeroPanelData; w1: ITableHero; level: number; levelLimit: number; stats: TowerRuntimeSnapshot['stats'] | undefined; weaponRow: ITableWeapon | null; runtime: TowerRuntimeSnapshot | null; enemyCamp: boolean; } | null { const data = this.prema as BattleWeaponDescHeroPanelData | null; if (!data?.entity) return null; const live = data.resolveLiveData?.() ?? null; if (live) { data.entity = live.entity; data.runtime = live.runtime ?? null; if (live.weaponId != null && live.weaponId > 0) { data.weaponId = live.weaponId; } if (live.runtime?.entityId) { data.watchEntityId = live.runtime.entityId; } } const w1 = data.entity.tower?.config ?? null; if (!w1) return null; let runtime = data.runtime ?? null; const watchEntityId = data.watchEntityId ?? runtime?.entityId ?? 0; const battle = gg.game.CurentBattle; if (watchEntityId > 0 && battle?.session) { runtime = TowerQueryService.instance.getRuntimeSnapshot(battle.session, watchEntityId) ?? runtime; data.runtime = runtime; } const comp = data.entity.composition; const level = runtime?.level ?? comp?.level ?? data.entity.level; const levelLimit = data.entity.tower?.levelLimit ?? w1.level_limit ?? level; const liveStats = watchEntityId > 0 && battle?.session ? TowerQueryService.instance.readLiveStats(battle.session, watchEntityId) : null; const stats = liveStats ?? runtime?.stats ?? data.entity.tower?.stats; const enemyCamp = !!data.enemyCamp; const weaponRow = this._resolveWeaponRow(w1.id, runtime, enemyCamp, data.weaponId ?? 0); return { data, w1, level, levelLimit, stats, weaponRow, runtime, enemyCamp }; } /** 击杀/升级后仅刷新 lb_level 等数值区 */ private _refreshDynamicFromLive(): void { if (!this.node?.isValid || !this.node.active) return; const ctx = this._resolveRefreshContext(); if (!ctx) return; const { w1, level, levelLimit, stats, weaponRow, runtime, enemyCamp } = ctx; const staticViewKey = this._buildStaticViewKey(w1.id, weaponRow?.id ?? 0, enemyCamp); if (staticViewKey !== this._staticViewKey) { this.onInit(); return; } this._refreshDynamicStats(w1, level, levelLimit, stats, weaponRow, runtime); } /** 入场动效结束后再刷一次武器 Spine(避免首帧节点未就绪) */ protected onPopOpenRevealEnd(): void { if (this._displayWeaponRow) { this._refreshWeaponType(this._displayWeaponRow); } } destroyPanel(tweenType = 1) { return new Promise((resolve) => { const lifeGen = this._panelLifeGen; const finish = () => { if (lifeGen !== this._panelLifeGen) { resolve(true); return; } this.prepareDestroyForRender(); if (this.node.isValid) { this.node.destroy(); } resolve(true); this.notifyPanelClose(); }; if (tweenType === 2 || tweenType === 3) { gg.ui.popAnim.playClose(this.node, this._resolvePopSession(), tweenType, finish); return; } finish(); }); } click_ui_bgMask(event?: EventTouch): void { const uiPos = event?.getUILocation(); const trySwitch = (this.prema as BattleWeaponDescHeroPanelData | null)?.trySwitchAtUiPos; if (uiPos && trySwitch?.(uiPos)) { return; } this.close(2); } /** ui_node:heroTemplate + HeroSkin 播待机 */ private _refreshHero(w1: ITableHero, level: number, enemyCamp = false): void { if (!this.ui_node?.isValid) return; this._clearHeroNode(); const prefab = gg.res.getRes('heroTemplate', GPath.Prefab, GBundle.Heros, Prefab); if (!prefab) { console.log('[UIBattleWeaponDescHero] heroTemplate 预制体缺失', w1.id); this.ui_node.active = false; return; } const heroNode = instantiate(prefab); heroNode.parent = this.ui_node; heroNode.setPosition(0, 0, 0); heroNode.setScale(1, 1, 1); this._heroNode = heroNode; this.ui_node.active = true; const skill = heroNode.getComponent(Skill); if (skill) skill.enabled = false; const heroSkin = heroNode.getComponent(HeroSkin); if (!heroSkin) { console.log('[UIBattleWeaponDescHero] HeroSkin 缺失', w1.id); return; } heroSkin.setQualityFrameVisible(false); heroSkin.setEnemyCamp(enemyCamp); heroSkin.setData(w1); heroSkin.setSkinByLevelAndQuality(level, enemyCamp); heroSkin.playIdleAnim(); } private _clearHeroNode(): void { if (this._heroNode?.isValid) { const heroSkin = this._heroNode.getComponent(HeroSkin); heroSkin?.releaseDisplay(); this._heroNode.destroy(); } this._heroNode = null; if (this.ui_node?.isValid) { this.ui_node.removeAllChildren(); } } /** 可展示的武器配表行(Weapon 表且 type 1~4) */ private _tryDisplayWeaponRow(weaponId: number): ITableWeapon | null { const id = Math.floor(Number(weaponId) || 0); if (id <= 0) return null; const row = gg.equip.getEquip(id) ?? gg.data.table.getTableData(TableNames.Weapon, id); if (!row) return null; const type = Number(row.type) | 0; if (type < 1 || type > 4) return null; return row; } private _resolveWeaponRow( heroId: number, runtime?: TowerRuntimeSnapshot | null, enemyCamp = false, presetWeaponId = 0, ): ITableWeapon | null { const fromPreset = this._tryDisplayWeaponRow(presetWeaponId); if (fromPreset) return fromPreset; const loadout = runtime?.loadout; if (loadout && loadout.weapon1Id === heroId) { const fromRuntime = this._tryDisplayWeaponRow(loadout.weaponId ?? 0); if (fromRuntime) return fromRuntime; } const fallbackId = enemyCamp ? resolveHeroBattleWeaponId(heroId, this._resolveEnemyPlayer()) : gg.equip.getHeroDisplayWeaponId(heroId); const fromFallback = this._tryDisplayWeaponRow(fallbackId); if (fromFallback) return fromFallback; console.log( LOG, enemyCamp ? '敌方武器 id 缺失' : '武器 id 缺失', heroId, 'preset=', presetWeaponId, 'runtime=', loadout?.weaponId ?? 0, 'fallback=', fallbackId, ); return null; } /** 弹窗入场后再刷一次武器 Spine(使用 onInit 缓存,避免二次解析失败清空 ui_type) */ private _deferRefreshWeaponType = (): void => { if (!this.node?.isValid) return; const weaponRow = this._displayWeaponRow; if (!weaponRow) return; this._refreshWeaponType(weaponRow); }; /** 延后刷新武器 Spine(本帧末 + 下一帧各一次) */ private _scheduleRefreshWeaponType(weaponRow: ITableWeapon | null): void { this.unschedule(this._deferRefreshWeaponType); if (!weaponRow) return; this.scheduleOnce(this._deferRefreshWeaponType, 0); this.scheduleOnce(this._deferRefreshWeaponType, 0.05); } /** 敌方 Player(与 TowerLoadout 解析同源) */ private _resolveEnemyPlayer(): Player | null { const battle = gg.game.CurentBattle; if (!battle) return null; const localPid = battle.player?.playerId ?? 0; return battle.getPlayer(localPid === 0 ? 1 : 0) ?? null; } /** 静态区缓存键:同武将+武器+阵营切换时仅刷新数值 */ private _buildStaticViewKey(heroId: number, weaponId: number, enemyCamp: boolean): string { return `${Math.floor(Number(heroId) || 0)}|${Math.floor(Number(weaponId) || 0)}|${enemyCamp ? 1 : 0}`; } /** 武将名/满级/技能/武器文案与 Spine(打开或切换武将时刷新) */ private _refreshStaticLabels( w1: ITableHero, level: number, levelLimit: number, weaponRow: ITableWeapon | null, ): void { if (this.lb_name) { this.lb_name.string = gg.lang.get(w1.name); } if (this.lb_lvMax) { this.lb_lvMax.string = `满级${levelLimit}级`; } this._refreshSkillLabels(w1, level); if (!weaponRow) { if (this.lb_weaponName) { this.lb_weaponName.string = ''; this._resetWeaponNameStyle(); } if (this.lb_weaponDesc) this.lb_weaponDesc.string = ''; return; } if (this.lb_weaponName) { this.lb_weaponName.string = gg.lang.get(weaponRow.name); this._applyWeaponNameStyle(weaponRow.quality); } if (this.lb_weaponDesc) { this.lb_weaponDesc.string = gg.lang.get(weaponRow.desc); } } /** * 实时数值:等级、攻击、攻速、武器攻击、经验条(每次打开/切换格子都读最新 runtime) */ private _refreshDynamicStats( w1: ITableHero, level: number, levelLimit: number, stats: TowerRuntimeSnapshot['stats'] | undefined, weaponRow: ITableWeapon | null, runtime?: TowerRuntimeSnapshot | null, ): void { const setup = deriveTowerSetupStats(w1, level, weaponRow?.id); const attack = stats?.attack ?? setup.attack; const attackSpeed = stats?.attackSpeed ?? setup.attackSpeed; if (this.lb_level) { this.lb_level.string = `Lv.${level}`; } if (this.lb_attck) { this.lb_attck.string = TowerStatDisplay.oneDecimal(attack); } if (this.lb_attckSpeed) { this.lb_attckSpeed.string = TowerStatDisplay.attackSpeed(attackSpeed); } if (this.lb_weaponAtk) { if (!weaponRow) { this.lb_weaponAtk.string = ''; } else { const exclusiveMul = gg.equip.getEquipExclusiveMul(w1.id, weaponRow); const weaponAtk = Math.floor((weaponRow.atk ?? 0) * exclusiveMul); this.lb_weaponAtk.string = `${weaponAtk}`; } } this._refreshExpProgress(w1, level, levelLimit, runtime); } /** 刷新当前等级武将技能名与描述 */ private _refreshSkillLabels(w1: ITableHero, level: number): void { const skillId = heroUltSkillId(w1, level); const skill = skillId > 0 ? gg.data.table.getTableData(TableNames.Skill, skillId) : null; if (skillId > 0 && !skill) { console.log(LOG, 'Skill 缺失', skillId, w1.id, level); } if (this.lb_skillName) { this.lb_skillName.string = skill ? gg.lang.get(skill.name) : ''; } if (this.lb_skillDesc) { this.lb_skillDesc.string = skill ? gg.lang.get(skill.desc) : ''; } } /** 按武器品质设置武器名字体色与描边 */ private _applyWeaponNameStyle(quality: number): void { const label = this.lb_weaponName; if (!label) return; const q = Math.floor(Number(quality) || 0); const fontHex = WEAPON_NAME_FONT_HEX[q]; const outlineHex = WEAPON_NAME_OUTLINE_HEX[q]; if (!fontHex || !outlineHex) { console.log(LOG, '未知武器品质色', quality); this._resetWeaponNameStyle(); return; } label.color = Color.fromHEX(new Color(), fontHex); label.enableOutline = true; label.outlineColor = Color.fromHEX(new Color(), outlineHex); } /** 重置武器名样式 */ private _resetWeaponNameStyle(): void { const label = this.lb_weaponName; if (!label) return; label.color = Color.WHITE; label.enableOutline = false; } /** 从弹窗主体取 ui_type 节点(不依赖 Auto 缓存引用) */ private _getWeaponTypeNode(weaponType: number): Node | null { const body = this._popupBody ?? this.node.getChildByName('弹窗主体'); if (!body?.isValid) return null; const t = Math.floor(Number(weaponType) || 0); if (t < 1 || t > 4) return null; return body.getChildByName(`ui_type${t}`); } /** 同步 Auto 节点引用(供外部工具链;展示以 _getWeaponTypeNode 为准) */ private _ensureWeaponTypeNodes(): void { if (!this.ui_type1?.isValid) this.ui_type1 = this._getWeaponTypeNode(1); if (!this.ui_type2?.isValid) this.ui_type2 = this._getWeaponTypeNode(2); if (!this.ui_type3?.isValid) this.ui_type3 = this._getWeaponTypeNode(3); if (!this.ui_type4?.isValid) this.ui_type4 = this._getWeaponTypeNode(4); } /** ui_type1~4:按武器 type 显隐并切肤 */ private _refreshWeaponType(weaponRow: ITableWeapon | null): void { this._ensureWeaponTypeNodes(); this._hideWeaponTypes(); if (!weaponRow) return; const weaponType = Number(weaponRow.type) | 0; const skin = String(weaponRow.skin ?? '').trim(); if (weaponType < 1 || weaponType > 4) { console.log(LOG, '武器 type 无效', weaponRow.id, weaponType); return; } if (!skin) { console.log(LOG, '武器 skin 缺失', weaponRow.id); } const node = this._getWeaponTypeNode(weaponType); if (!node?.isValid) { console.log(LOG, '武器类型节点缺失', weaponType, weaponRow.id); return; } node.active = true; node.setScale(1, 1, 1); this._applyWeaponSpine(node, skin, weaponRow.id); } private _hideWeaponTypes(): void { for (let t = 1; t <= 4; t++) { const node = this._getWeaponTypeNode(t); if (node?.isValid) node.active = false; } } private _applyWeaponSpine(typeNode: Node, skin: string, weaponId: number): void { typeNode.active = true; const skel = typeNode.getComponent(sp.Skeleton) ?? typeNode.getComponentInChildren(sp.Skeleton); if (!skel?.isValid) { console.log(LOG, '武器 Spine 缺失', typeNode.name, weaponId); return; } if (skel.node?.isValid) skel.node.active = true; skel.enabled = true; if (skel.skeletonData) ensureSpineSkeletonDataRuntime(skel.skeletonData); const resolvedSkin = applyWeaponSpineSkin(skel, skin, LOG); if (skin && !resolvedSkin) { console.log(LOG, '武器 skin 不可用', weaponId, skin); } BattleSpinePlayback.resetTracks(skel); try { skel.setAnimation(0, WEAPON_SPINE_IDLE, true); } catch (e) { console.log(LOG, 'setAnimation 失败', WEAPON_SPINE_IDLE, typeNode.name, weaponId, String(e)); } const renderers = typeNode.getComponentsInChildren(UIRenderer); for (let i = 0; i < renderers.length; i++) { const r = renderers[i]; if (r?.isValid) r.enabled = true; } const op = typeNode.getComponent(UIOpacity) ?? typeNode.getComponentInChildren(UIOpacity); if (op?.isValid) op.opacity = 255; } private _refreshExpProgress( w1: ITableHero, level: number, levelLimit: number, runtime?: TowerRuntimeSnapshot | null, ): void { const progressUt = this.ui_progress?.getComponent(UITransform); if (!progressUt?.isValid) return; if (level >= levelLimit) { progressUt.setContentSize(PROGRESS_MAX_WIDTH, progressUt.contentSize.height); if (this.lb_progress) this.lb_progress.string = '满级'; if (this.ui_progress) this.ui_progress.active = true; return; } const need = needTowerExpForLevel(w1, level); const curRaw = runtime?.entityId ? getTowerExpHost().getExp(runtime.entityId) : 0; const ratio = need > 0 ? Math.min(1, Math.max(0, curRaw / need)) : 0; progressUt.setContentSize(PROGRESS_MAX_WIDTH * ratio, progressUt.contentSize.height); if (this.lb_progress) { const curText = TowerStatDisplay.oneDecimal(curRaw); const needText = TowerStatDisplay.oneDecimal(need); this.lb_progress.string = need > 0 ? `${curText}/${needText}` : `${curText}/0`; } if (this.ui_progress) this.ui_progress.active = true; } private _clearLabels(): void { if (this.lb_name) this.lb_name.string = ''; if (this.lb_level) this.lb_level.string = ''; if (this.lb_lvMax) this.lb_lvMax.string = ''; if (this.lb_attck) this.lb_attck.string = ''; if (this.lb_attckSpeed) this.lb_attckSpeed.string = ''; if (this.lb_weaponName) { this.lb_weaponName.string = ''; this._resetWeaponNameStyle(); } if (this.lb_weaponAtk) this.lb_weaponAtk.string = ''; if (this.lb_weaponDesc) this.lb_weaponDesc.string = ''; if (this.lb_skillName) this.lb_skillName.string = ''; if (this.lb_skillDesc) this.lb_skillDesc.string = ''; if (this.lb_progress) this.lb_progress.string = ''; if (this.ui_progress?.isValid) { const ut = this.ui_progress.getComponent(UITransform); if (ut) ut.setContentSize(0, ut.contentSize.height); } } private _applyPanelPosition(data: BattleWeaponDescHeroPanelData | null): void { const weaponWorld = data?.weaponWorldPos; if (!weaponWorld || !this._popupBody?.isValid) return; const rootUt = this.node.getComponent(UITransform); const bodyUt = this._popupBody.getComponent(UITransform); if (!rootUt || !bodyUt) return; const step = data?.gridStep ?? 93; const halfW = bodyUt.contentSize.width * 0.5; const halfH = bodyUt.contentSize.height * 0.5; const gap = UIBattleWeaponDescHero.PANEL_GAP_GRIDS; const weaponLocal = rootUt.convertToNodeSpaceAR( new Vec3(weaponWorld.x, weaponWorld.y, weaponWorld.z ?? 0), ); const x = 0; const y = weaponLocal.y + step * (0.5 + gap) + halfH; const rootHalfW = rootUt.contentSize.width * 0.5; const rootHalfH = rootUt.contentSize.height * 0.5; const clampedX = Math.max(-rootHalfW + halfW, Math.min(rootHalfW - halfW, x)); const clampedY = Math.max(-rootHalfH + halfH, Math.min(rootHalfH - halfH, y)); this._popupBody.setPosition(clampedX, clampedY, 0); } private _ensurePanelVisible(): void { if (!this.node?.isValid) return; this.node.active = true; Tween.stopAllByTarget(this.node); const body = this._popupBody; if (body?.isValid) { body.active = true; const renderers = body.getComponentsInChildren(UIRenderer); for (let i = 0; i < renderers.length; i++) { const r = renderers[i]; if (r?.isValid) r.enabled = true; } } this._keepMaskTouchOnly(); } private _keepMaskTouchOnly(): void { if (!this.ui_bgMask?.isValid) return; Tween.stopAllByTarget(this.ui_bgMask); const maskSpr = this.ui_bgMask.getComponent(Sprite); if (maskSpr) maskSpr.enabled = false; const op = this.ui_bgMask.getComponent(UIOpacity); if (op) op.opacity = 0; } private _resolvePopSession(): IPopAnimSession { const cfg = this.uiInfo?.uiconf?.popAnim as IPopAnimConfig | undefined; return { mask: cfg?.mask ?? 'none', center: cfg?.center ?? 'none', }; } }