//********************* // create by 流云 // time: Sat Sep 05 2026 // desc: 战斗内点击 type=1 单字 — 详情弹窗 //********************* import { _decorator, 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 { GEvent } from '../../../mx/module/event/GEvent'; import { Auto_BattleWeaponDesc1 } from './Auto_BattleWeaponDesc1'; import type { TileEntitySnapshot, TowerRuntimeSnapshot } from '../../battle/combat/TowerQueryTypes'; import { TowerQueryService } from '../../battle/combat/TowerQueryService'; import { deriveTowerSetupStats, TowerStatDisplay } from '../../battle/combat/TowerStatResolver'; import { heroNormalSkillId, textIconName } from '../../battle/config/BattleHeroTable'; import { applyGlyphSpineSkin } from '../../battle/view/GlyphSpineSkin'; import { TextGrid } from '../../battle/view/TextGrid'; import { PEACH_TREE_WEAPON2_ID } from '../../battle/bag/BagPrepConfig'; import { GBundle, GPath } from '../../game/ConfigRes'; import { TableNames } from '../../game/ConfigTableData'; import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch'; const { ccclass } = _decorator; /** 单字详情弹窗入参 */ export interface BattleWeaponDesc1PanelData { entity: TileEntitySnapshot; runtime?: TowerRuntimeSnapshot | null; /** 蟠桃树(weapon2Id=102) */ isPeachTree?: boolean; /** 敌方阵营(预留;单字详情暂与己方同形) */ enemyCamp?: boolean; weaponWorldPos?: { x: number; y: number; z?: number }; gridStep?: number; trySwitchAtUiPos?: (uiPos: { x: number; y: number }) => boolean; } const LOG = '[UIBattleWeaponDesc1]'; /** 图鉴内隐藏 textGrid 子节点 */ const GLYPH_HIDE_CHILDREN = ['select', 'redSelect', 'canUpEff', 'trail1', 'trail2', 'trail3']; /** ui_node 内字形缩放 */ const GLYPH_SCALE = 0.85; @ccclass('UIBattleWeaponDesc1') export class UIBattleWeaponDesc1 extends Auto_BattleWeaponDesc1 { private static readonly PANEL_GAP_GRIDS = 1.7; private _popupBody: Node | null = null; /** 递增时取消尚未完成的关窗销毁 */ private _panelLifeGen = 0; /** ui_node 下挂的字形节点 */ private _glyphNode: Node | null = null; /** 普通字攻击数值区 */ private _uiAtkNode: Node | null = null; /** 蟠桃树信息区 */ private _uiPeachNode: Node | null = null; /** 蟠桃树技能描述(ui_peachNode/lb_skillDesc) */ private _lbPeachSkillDesc: Label | null = null; /** GEvent 监听是否已绑定 */ private _geventBound = false; onLoad(): void { super.onLoad(); this._popupBody = this.node.getChildByName('弹窗主体'); this._bindStatNodes(); } /** 绑定 ui_atkNode / ui_peachNode 及子 Label(Auto 路径可能未随 prefab 更新) */ private _bindStatNodes(): void { const body = this._popupBody ?? this.node.getChildByName('弹窗主体'); if (!body?.isValid) return; this._uiAtkNode = body.getChildByName('ui_atkNode'); if (this.ui_campNode?.isValid) this.ui_campNode.active = false; const atk = this._uiAtkNode; if (atk?.isValid) { this.lb_attck = atk.getChildByName('lb_attck')?.getComponent(Label) ?? this.lb_attck; this.lb_attckSpeed = atk.getChildByName('lb_attckSpeed')?.getComponent(Label) ?? this.lb_attckSpeed; } this._uiPeachNode = body.getChildByName('ui_peachNode'); const peach = this._uiPeachNode ?? this.ui_peachNode; if (peach?.isValid) { this.ui_peachNode = peach; this.lb_time = peach.getChildByName('lb_time')?.getComponent(Label) ?? this.lb_time; this._lbPeachSkillDesc = peach.getChildByName('lb_peachSkillDesc')?.getComponent(Label) ?? this._lbPeachSkillDesc ?? null; } } 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._deferredRefreshDynamic); this._clearGlyphNode(); super.onDisable(); } /** 订阅棋盘变更,实时刷新攻击力/攻速 */ private _bindLiveRefresh(): void { if (this._geventBound) return; GEvent.Ins.on(GEvent.UpdateBestPlace, this._onLiveDataDirty, this); this._geventBound = true; } /** 取消实时刷新订阅 */ private _unbindLiveRefresh(): void { if (!this._geventBound) return; GEvent.Ins.off(GEvent.UpdateBestPlace, this._onLiveDataDirty, this); this._geventBound = false; } /** 棋盘/攻速符变化后刷新攻击力/攻速 */ private _onLiveDataDirty = (): void => { this.unschedule(this._deferredRefreshDynamic); this.scheduleOnce(this._deferredRefreshDynamic, 0); }; /** 帧末刷新,确保攻速符 modifier 写入后再读 */ private _deferredRefreshDynamic = (): void => { this._refreshDynamicFromLive(); }; /** 仅刷新攻击力/攻速 */ private _refreshDynamicFromLive(): void { if (!this.node?.isValid || !this.node.active) return; const data = this.prema as BattleWeaponDesc1PanelData | null; if (!data?.entity?.tile) return; const tile = data.entity.tile; if (data.isPeachTree || this._isPeachText(tile)) return; const w1 = this._resolveHero(data.entity); const runtime = this._resolveLiveRuntime(data.runtime ?? null, data.entity); data.runtime = runtime; const stats = this._resolveLiveStats(runtime, data.entity); const level = runtime?.level ?? data.entity.level; this._refreshAtkLabels(w1, level, stats); } onInit(): void { this._panelLifeGen++; this._ensurePanelVisible(); this._bindLiveRefresh(); const data = this.prema as BattleWeaponDesc1PanelData | null; if (!data?.entity?.tile) { this._clearLabels(); this._clearGlyphNode(); return; } this._bindStatNodes(); const { entity } = data; const tile = entity.tile; const isPeachText = !!data.isPeachTree || this._isPeachText(tile); const w1 = this._resolveHero(entity); const runtime = this._resolveLiveRuntime(data.runtime ?? null, entity); data.runtime = runtime; const stats = this._resolveLiveStats(runtime, entity); const level = runtime?.level ?? entity.level; const levelLimit = entity.tower?.levelLimit ?? w1?.level_limit ?? level; this._refreshGlyph(tile); this._refreshCommonLabels(w1, level, levelLimit); this._refreshStatPanel(isPeachText); if (isPeachText) { this._refreshPeachLabels(w1, level); } else { this._refreshAtkLabels(w1, level, stats); } this._applyPanelPosition(data); } 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 BattleWeaponDesc1PanelData | null)?.trySwitchAtUiPos; if (uiPos && trySwitch?.(uiPos)) { return; } this.close(2); } /** ui_node:textGrid 预制体按 Text.icon 播 skin 待机(含蟠桃树) */ private _refreshGlyph(tile: ITableText): void { if (!this.ui_node?.isValid) return; this._clearGlyphNode(); const prefab = gg.res.getRes('textGrid', GPath.Prefab, GBundle.BattleGame, Prefab); if (!prefab) { console.log('[UIBattleWeaponDesc1] textGrid 预制体缺失', tile.id); this.ui_node.active = false; return; } const gridNode = instantiate(prefab); gridNode.parent = this.ui_node; gridNode.setPosition(0, 0, 0); gridNode.setScale(GLYPH_SCALE, GLYPH_SCALE, 1); this._glyphNode = gridNode; this.ui_node.active = true; for (let i = 0; i < GLYPH_HIDE_CHILDREN.length; i++) { const child = gridNode.getChildByName(GLYPH_HIDE_CHILDREN[i]); if (child) child.active = false; } const textGrid = gridNode.getComponent(TextGrid); if (textGrid) textGrid.enabled = false; const iconName = textIconName(tile); if (!iconName) { console.log('[UIBattleWeaponDesc1] 字形名为空', tile.id); this.ui_node.active = false; return; } const skinNode = gridNode.getChildByName('skin'); if (skinNode) skinNode.active = true; const sk = skinNode?.getComponent(sp.Skeleton) ?? skinNode?.getComponentInChildren(sp.Skeleton); if (!sk?.isValid) { console.log('[UIBattleWeaponDesc1] 字形 Spine 缺失', iconName); return; } sk.enabled = true; if (!applyGlyphSpineSkin(sk, iconName)) { console.log('[UIBattleWeaponDesc1] 字形皮肤未能应用', iconName); } if (sk.skeletonData) ensureSpineSkeletonDataRuntime(sk.skeletonData); if (sk.skeletonData?.getRuntimeData?.(true)?.findAnimation?.('待机')) { sk.setAnimation(0, '待机', true); } } private _clearGlyphNode(): void { if (this._glyphNode?.isValid) { this._glyphNode.destroy(); } this._glyphNode = null; if (this.ui_node?.isValid) { this.ui_node.removeAllChildren(); } } /** 蟠桃树字(Text.id=102) */ private _isPeachText(tile: ITableText): boolean { return Math.floor(Number(tile.id) || 0) === PEACH_TREE_WEAPON2_ID; } /** 解析 Hero 行(部署快照缺失时按 weapon1Id / 蟠桃 id 兜底) */ private _resolveHero(entity: TileEntitySnapshot): ITableHero | null { const fromTower = entity.tower?.config ?? null; if (fromTower) return fromTower; const heroId = entity.weapon1Id > 0 ? entity.weapon1Id : 0; if (heroId > 0) { return gg.data.table.getTableData(TableNames.Hero, heroId) ?? null; } if (this._isPeachText(entity.tile)) { return gg.data.table.getTableData(TableNames.Hero, PEACH_TREE_WEAPON2_ID) ?? null; } return null; } /** 切换普通攻击区 / 蟠桃树信息区 */ private _refreshStatPanel(isPeachText: boolean): void { if (this._uiAtkNode?.isValid) this._uiAtkNode.active = !isPeachText; if (this.ui_campNode?.isValid) this.ui_campNode.active = false; const peachPanel = this._uiPeachNode ?? this.ui_peachNode; if (peachPanel?.isValid) peachPanel.active = isPeachText; } private _refreshCommonLabels(w1: ITableHero | null, level: number, levelLimit: number): void { if (!w1) { if (this.lb_name) this.lb_name.string = ''; if (this.lb_level) this.lb_level.string = ''; if (this.lb_lvMax) this.lb_lvMax.string = ''; return; } if (this.lb_name) { this.lb_name.string = gg.lang.get(w1.name); } if (this.lb_level) { this.lb_level.string = `Lv.${level}`; } if (this.lb_lvMax) { this.lb_lvMax.string = `满级${levelLimit}级`; } } /** 普通字:攻击力 / 攻速(ui_atkNode) */ private _refreshAtkLabels( w1: ITableHero | null, level: number, stats: TowerRuntimeSnapshot['stats'] | undefined, ): void { if (!w1) { if (this.lb_attck) this.lb_attck.string = ''; if (this.lb_attckSpeed) this.lb_attckSpeed.string = ''; return; } const setup = deriveTowerSetupStats(w1, level); const attack = stats?.attack ?? setup.attack; const attackSpeed = stats?.attackSpeed ?? setup.attackSpeed; if (this.lb_attck) { this.lb_attck.string = TowerStatDisplay.oneDecimal(attack); } if (this.lb_attckSpeed) { this.lb_attckSpeed.string = TowerStatDisplay.attackSpeed(attackSpeed); } } /** 蟠桃树:产桃 CD / 技能描述(ui_peachNode) */ private _refreshPeachLabels(w1: ITableHero | null, level: number): void { if (!w1) { this._clearPeachLabels(); return; } const skillId = heroNormalSkillId(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); } const cdSec = skill && Number(skill.cd) > 0 ? Number(skill.cd) : 0; if (skill && !(cdSec > 0)) { console.log(LOG, '蟠桃产桃 CD 无效', skill.id, skill.cd); } if (this.lb_time) { this.lb_time.string = cdSec > 0 ? `${TowerStatDisplay.attackSpeed(cdSec)}秒` : ''; } if (this._lbPeachSkillDesc) { this._lbPeachSkillDesc.string = skill ? gg.lang.get(skill.desc) : ''; } } private _clearPeachLabels(): void { if (this.lb_time) this.lb_time.string = ''; if (this._lbPeachSkillDesc) this._lbPeachSkillDesc.string = ''; } 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 = ''; this._clearPeachLabels(); if (this._uiAtkNode?.isValid) this._uiAtkNode.active = false; if (this.ui_campNode?.isValid) this.ui_campNode.active = false; const peachPanel = this._uiPeachNode ?? this.ui_peachNode; if (peachPanel?.isValid) peachPanel.active = false; } /** 已部署则重读 ECS 最终数值(含攻速符 aspd_add) */ private _resolveLiveRuntime(runtime: TowerRuntimeSnapshot | null, entity: TileEntitySnapshot): TowerRuntimeSnapshot | null { let live = runtime; const battle = gg.game.CurentBattle; const session = battle?.session ?? null; const entityId = live?.entityId ?? 0; if (entityId > 0 && session) { live = TowerQueryService.instance.getRuntimeSnapshot(session, entityId) ?? live; return live; } if (entity.location?.kind === 'map' && session) { live = TowerQueryService.instance.getRuntimeByGridIndex(entity.location.gridIndex, session, null) ?? live; } return live; } /** 优先 ECS 最终攻速/攻击;未部署才回落配表快照 */ private _resolveLiveStats(runtime: TowerRuntimeSnapshot | null, entity: TileEntitySnapshot): TowerRuntimeSnapshot['stats'] | undefined { const session = gg.game.CurentBattle?.session ?? null; const entityId = runtime?.entityId ?? 0; if (entityId > 0 && session) { const live = TowerQueryService.instance.readLiveStats(session, entityId); if (live) return live; } return runtime?.stats ?? entity.tower?.stats; } private _applyPanelPosition(data: BattleWeaponDesc1PanelData | 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 = UIBattleWeaponDesc1.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) { for (const r of body.getComponentsInChildren(UIRenderer)) { 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', }; } }