//********************* // create by 流云 // time: Fri Jul 10 2026 // desc: 战斗内点击字块 — 武器详情弹窗 //********************* import { _decorator, BlockInputEvents, EventTouch, instantiate, Label, Layout, Node, RichText, ScrollView, Sprite, Tween, UITransform, UIOpacity, UIRenderer, v2, Vec3, } from 'cc'; import { type IPopAnimConfig, type IPopAnimSession } from '../../../mx/module/ui/popAnim'; import { Auto_BattleWeaponDesc } from './Auto_BattleWeaponDesc'; import type { TileEntitySnapshot, TowerRuntimeSnapshot } from '../../battle/combat/TowerQueryTypes'; import { deriveTowerSetupStats, TowerStatDisplay } from '../../battle/combat/TowerStatResolver'; import { GBundle, GPath } from '../../game/ConfigRes'; import { SpriteLoad } from 'db://assets/mx/components/SpriteLoad'; const { ccclass } = _decorator; /** 局外 pageWeapon.iconSpr 尺寸 */ const HERO_ICON_W = 200; const HERO_ICON_H = 107; const HERO_ICON_SCALE = 0.7; export interface BattleWeaponDescPanelData { entity: TileEntitySnapshot; runtime?: TowerRuntimeSnapshot | null; weaponWorldPos?: { x: number; y: number; z?: number }; gridStep?: number; trySwitchAtUiPos?: (uiPos: { x: number; y: number }) => boolean; } @ccclass('UIBattleWeaponDesc') export class UIBattleWeaponDesc extends Auto_BattleWeaponDesc { private static readonly ITEM_HEIGHT_COLLAPSED = 82; private static readonly ITEM_HEIGHT_EXPANDED = 152; private static readonly PANEL_GAP_GRIDS = 1.7; private _popupBody: Node | null = null; private _heroIconSpr: SpriteLoad | null = null; /** 递增时取消尚未完成的关窗销毁(关窗动效中复开) */ private _panelLifeGen = 0; onLoad(): void { super.onLoad(); this._popupBody = this.node.getChildByName('弹窗主体'); if (this.sv_list) { this.sv_list.cancelInnerEvents = false; } this._setupHeroIconSlot(); } onEnable(): void { super.onEnable(); this.ui_bgMask?.on(Node.EventType.TOUCH_END, this.click_ui_bgMask, this); } onDisable(): void { if (this.ui_bgMask?.isValid) { this.ui_bgMask.off(Node.EventType.TOUCH_END, this.click_ui_bgMask, this); } super.onDisable(); } onInit(): void { this._panelLifeGen++; this._ensurePanelVisible(); const data = this.prema as BattleWeaponDescPanelData | null; if (!data?.entity) { this._clearList(); this._clearHeroIcon(); return; } const { entity, runtime } = data; const tower = entity.tower; const w1 = tower?.config; const stats = runtime?.stats ?? tower?.stats; const level = runtime?.level ?? entity.level; const levelLimit = tower?.levelLimit ?? w1?.level_limit ?? level; this._refreshHeroIcon(w1); this._refreshStatLabels(w1, level, levelLimit, stats, entity.weapon1Id); this._refreshSkillList(entity.selectedEntries ?? []); 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(); }); } /** 仅英雄头像(无品质框),与局外 pageWeapon.iconSpr 同源 */ private _setupHeroIconSlot(): void { if (!this.ui_node?.isValid) return; this.ui_node.removeAllChildren(); this.ui_node.setScale(1, 1, 1); const rootUt = this.ui_node.getComponent(UITransform) ?? this.ui_node.addComponent(UITransform); rootUt.setContentSize(100, 100); const iconNode = new Node('iconSpr'); iconNode.parent = this.ui_node; iconNode.setScale(HERO_ICON_SCALE, HERO_ICON_SCALE, 1); iconNode.addComponent(UITransform).setContentSize(HERO_ICON_W, HERO_ICON_H); iconNode.addComponent(Sprite); this._heroIconSpr = iconNode.addComponent(SpriteLoad); } private _refreshHeroIcon(w1: ITableHero | null | undefined): void { if (!this.ui_node?.isValid || !this._heroIconSpr) return; if (!w1) { this._clearHeroIcon(); return; } const itemData = gg.data.table.getItemData(gg.data.project.getWeaponScrapId(w1.id)); if (!itemData) { this._clearHeroIcon(); return; } this.ui_node.active = true; this._heroIconSpr.setSprite(GPath.Item(itemData.icon), GBundle.Items); } private _clearHeroIcon(): void { if (this.ui_node?.isValid) { this.ui_node.active = false; } } /** 关窗动效未完成时复开:仅恢复弹窗主体渲染,遮罩保持透明触摸层 */ 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; } for (const layout of body.getComponentsInChildren(Layout)) { if (layout?.isValid) layout.enabled = true; } } this._keepMaskTouchOnly(); } /** ui_bgMask 预制体:Sprite 关闭 = 透明,仅 BlockInputEvents 接收点击做字块切换 */ 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', }; } private _refreshStatLabels( w1: ITableHero | null | undefined, level: number, levelLimit: number, stats: TowerRuntimeSnapshot['stats'] | undefined, weapon1Id: number, ): void { if (!w1) { if (this.lb_level) this.lb_level.string = ''; if (this.lb_attck) this.lb_attck.string = ''; if (this.lb_attck2?.node) this.lb_attck2.node.active = false; if (this.lb_attckMethod) this.lb_attckMethod.string = ''; if (this.lb_attckSpeed) this.lb_attckSpeed.string = ''; if (this.lb_attckSpeed2?.node) this.lb_attckSpeed2.node.active = false; return; } const setup = deriveTowerSetupStats(w1, level); const attack = stats?.attack ?? setup.attack; const attackSpeed = stats?.attackSpeed ?? setup.attackSpeed; if (this.lb_level) { this.lb_level.string = `Lv.${level}(满级${levelLimit}级)`; } if (this.lb_attck) { this.lb_attck.string = `攻击力:${TowerStatDisplay.oneDecimal(attack)}`; } if (this.lb_attckMethod) { this.lb_attckMethod.string = `攻击方式:${w1.type ?? ''}`; } if (this.lb_attckSpeed) { this.lb_attckSpeed.string = `攻速:${TowerStatDisplay.attackSpeed(attackSpeed)}`; } const nextLevel = level + 1; const canLevelUp = nextLevel <= levelLimit; if (this.lb_attck2) { this.lb_attck2.node.active = canLevelUp; if (canLevelUp) { const nextSetup = deriveTowerSetupStats(w1, nextLevel); const attackDelta = (nextSetup.attack) - setup.attack; this.lb_attck2.string = `→Lv.${nextLevel}+${TowerStatDisplay.oneDecimal(attackDelta)}`; } } if (this.lb_attckSpeed2) { this.lb_attckSpeed2.node.active = canLevelUp; if (canLevelUp) { const nextSetup = deriveTowerSetupStats(w1, nextLevel); const speedDelta = nextSetup.attackSpeed - setup.attackSpeed; this.lb_attckSpeed2.string = `→Lv.${nextLevel}+${TowerStatDisplay.attackSpeed(speedDelta)}`; } } this.layAttack?.getComponent(Layout)?.updateLayout(); this.laySpeed?.getComponent(Layout)?.updateLayout(); } private _applyPanelPosition(data: BattleWeaponDescPanelData | 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 = UIBattleWeaponDesc.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); } click_ui_bgMask(event?: EventTouch): void { const uiPos = event?.getUILocation(); const trySwitch = (this.prema as BattleWeaponDescPanelData | null)?.trySwitchAtUiPos; if (uiPos && trySwitch?.(uiPos)) { return; } this.close(2); } private _refreshSkillList(entries: readonly ITableSkill[]): void { if (!this.ui_content?.isValid || !this.ui_itemTemplate?.isValid) return; this._clearList(); for (const entry of entries) { const item = instantiate(this.ui_itemTemplate); item.parent = this.ui_content; item.active = true; this._setupSkillItem(item, entry); } this._refreshContentLayout(); this.sv_list?.scrollToOffset(v2(0, 0), 0); } private _setupSkillItem(item: Node, entry: ITableSkill): void { const bg = item.getChildByName('bg'); const bg2 = item.getChildByName('bg2'); if (!bg?.isValid || !bg2?.isValid) return; bg.getOrAddComponent(SpriteLoad).setSprite(GPath.popRes('battleGameResult', `texture/q${entry.quality}`), GBundle.PopBattle); bg2.getOrAddComponent(SpriteLoad).setSprite(GPath.popRes('battleGameResult', `texture/q${entry.quality}开`), GBundle.PopBattle); const name = gg.lang.get(entry.name ?? ''); const desc = gg.lang.get(entry.desc ?? ''); const nameLb = bg2.getChildByName('name')?.getComponent(Label); const descNode = bg2.getChildByName('desc'); const descLb = descNode?.getComponent(RichText); const name2Lb = bg.getChildByName('name2')?.getComponent(Label); if (nameLb) nameLb.string = name; if (descLb) descLb.string = desc; if (descNode?.isValid) descNode.active = true; if (name2Lb) name2Lb.string = name; this._setSkillItemExpanded(item, false); item.getOrAddComponent(BlockInputEvents); item.on(Node.EventType.TOUCH_END, (event: EventTouch) => { event.propagationStopped = true; const expanded = bg2.active; this._setSkillItemExpanded(item, !expanded); this._refreshContentLayout(); }, this); } private _setSkillItemExpanded(item: Node, expanded: boolean): void { const bg = item.getChildByName('bg'); const bg2 = item.getChildByName('bg2'); if (!bg?.isValid || !bg2?.isValid) return; bg.active = !expanded; bg2.active = expanded; const ut = item.getComponent(UITransform); if (!ut) return; const height = expanded ? UIBattleWeaponDesc.ITEM_HEIGHT_EXPANDED : UIBattleWeaponDesc.ITEM_HEIGHT_COLLAPSED; ut.setContentSize(ut.contentSize.width, height); } private _refreshContentLayout(): void { this.ui_content?.getComponent(Layout)?.updateLayout(); } private _clearList(): void { if (!this.ui_content?.isValid) return; for (const child of [...this.ui_content.children]) { child.destroy(); } } }