//********************* // create by 流云 // time: Tue Aug 18 2026 // desc: 战斗界面 — 地图、出怪、背包刷新(召唤) //********************* import { _decorator, find, game, Label, Node, Sprite, Tween, tween, v3, Vec3, sp } from 'cc'; import { Auto_BattleGame } from './Auto_BattleGame'; import { CocosBattleMapHost } from '../../battle/map/CocosBattleMapHost'; import { applyMapTopologyToSession } from '../../battle/map/importTopologyFromMapManager'; import type { MapManager } from '../../battle/map/legacy/MapManager'; import { CocosBattleMovementBridge } from '../../battle/bridge/CocosBattleMovementBridge'; import { CocosBattleMonsterPresenter } from '../../battle/view/CocosBattleMonsterPresenter'; import { CampHpBarPresenter } from '../../battle/view/CampHpBarPresenter'; import { CocosBattleCombatPresenter } from '../../battle/view/CocosBattleCombatPresenter'; import { DamageTextSpawner } from '../../battle/view/DamageTextSpawner'; import { CocosBagBoardView } from '../../battle/view/CocosBagBoardView'; import { HitEffectResolver } from '../../battle/view/HitEffectResolver'; import { BATTLE_MONSTER_CCCLASS } from '../../battle/view/IBattleMonsterView'; import { getDiscountedBagNormalRefreshCost } from '../../battle/bag/BagPrepConfig'; import { resolveCarriedSummonCostReduceWan } from '../../manager/battleBuff/BattleBuffPassiveSkillUtil'; import { BattleBuffFunctionType } from '../../manager/battleBuff/BattleBuffTypes'; import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime'; import { BattleAudio } from '../../battle/audio/BattleAudio'; import { BattleUiPauseGate } from '../../battle/BattleUiPauseGate'; import { SkillController } from '../../battle/skill/SkillController'; import { SkillPresentationEnv } from '../../battle/skill/SkillPresentationEnv'; import { MonsterDataManager, MonsterType } from '../../manager/MonsterDataManager'; import { GEvent } from '../../../mx/module/event/GEvent'; import { BattleIllustratedGuideManager } from '../../manager/BattleIllustratedGuideManager'; import { UIID } from '../../game/ConfigRes'; import type { BattleEvent, EntityId } from 'db://assets/mx/module/battle'; import { BATTLE_EVENT_ALL, BattleEventType } from 'db://assets/mx/module/battle'; import { BattleGameDailyShopSkillView } from './BattleGameDailyShopSkillView'; import { BattleGamePassiveSkillView } from './BattleGamePassiveSkillView'; import { BattleGameOtherSkillView } from './BattleGameOtherSkillView'; import { BattleGameAiActiveSkillFxPresenter } from './BattleGameAiActiveSkillFxPresenter'; import { BattleGameDailyShopSkillDrag } from './BattleGameDailyShopSkillDrag'; import { BattleGameDailyShopSkillClick } from './BattleGameDailyShopSkillClick'; import { BattlePeachCoinFlyPresenter } from './BattlePeachCoinFlyPresenter'; import { BattleWeaponDropPresenter } from './BattleWeaponDropPresenter'; import { BattleGameItemAdPresenter } from './BattleGameItemAdPresenter'; import { BattleGameMenShenTrapController } from './BattleGameMenShenTrapController'; import { getBattleGameMonsterCrisisHintPresenter } from './BattleGameMonsterCrisisHintPresenter'; import { BattleGameKillPeachPresenter } from './BattleGameKillPeachPresenter'; import { getBattleBuffBaseThunderHost } from '../../battle/combat/BattleBuffBaseThunderHost'; import { BattleBuffSelectService } from '../../manager/battleBuff/BattleBuffSelectService'; import { BattleBuffInGameRefresh } from '../../battle/combat/BattleBuffInGameRefresh'; import { BattleGameBaseThunderPresenter } from './BattleGameBaseThunderPresenter'; import { BattleGameBossIntroPresenter } from './BattleGameBossIntroPresenter'; import { AiBattleBridge } from '../../battle/ai/bridge/AiBattleBridge'; import { AiHeadlessBagHost } from '../../battle/ai/runtime/AiHeadlessBagHost'; import type { AiActiveSkillCastFxInfo } from '../../battle/ai/runtime/AiActiveSkillHost'; import { BattleSpinePlayback } from '../../battle/presentation/BattleSpinePlayback'; import { TweenBreathe } from 'db://assets/mx/components/tween/TweenBreathe'; import { DailyShopOwnedDesc } from '../PopDailyShopLottery/DailyShopLotteryIcon'; import { BattleFirstGuidePresenter } from './BattleFirstGuidePresenter'; import { LangTxt } from 'db://assets/mx/module/lang/LangTxt'; import { ConstNumType } from '../../game/GameDataEnums'; import { ClientErrorReporter } from '../../../mx/tools/ClientErrorReporter'; import { PerfSampler } from '../../game/PerfSampler'; import { isElementGlyphType, MergeEventType, type MergeEvent } from 'db://assets/mx/module/merge'; import { WeaponComposeConfigProvider } from '../../battle/bag/WeaponComposeConfigProvider'; const { ccclass } = _decorator; /** 召唤按钮进度满后 Q 弹第一下相对缩放增量 */ const REFRESH_BOUNCE_AMP = 0.1; /** 单段弹起/回落时长(秒) */ const REFRESH_BOUNCE_SEC = 0.22; /** 开局未放置数字则强制开怪的等待秒数 */ const FIRST_PLACE_COMBAT_TIMEOUT_SEC = 10; /** 看视频加金币期间暂停 token */ const COIN_AD_PAUSE_TOKEN = 'ad:battleCoin'; /** 砚台 Spine:出字过程/待机均播「待机」(资源内「出字」为空动画) */ const INKSTONE_SPINE_SPAWN = '待机'; @ccclass('UIBattleGame') export class UIBattleGame extends Auto_BattleGame { private _battle: ProjectBattleHandle | null = null; private _mapHost: CocosBattleMapHost | null = null; private readonly _monsterPresenter = new CocosBattleMonsterPresenter(); /** 己方/敌方营地血条 */ private readonly _campHpBar = new CampHpBarPresenter(); private _combatPresenter: CocosBattleCombatPresenter | null = null; private _movementBridge: CocosBattleMovementBridge | null = null; private _bagView: CocosBagBoardView | null = null; /** 普攻/技能表现:监听 PresentationCue → HeroActor */ private _skillController: SkillController | null = null; /** 召唤按钮基准缩放 */ private readonly _refreshBtnScaleHome = new Vec3(1, 1, 1); /** 本段「桃子足够」是否已播过满进度 Q 弹 */ private _refreshBounceHinted = false; /** 战斗 skill 描述浮层 */ private _ownedDesc: DailyShopOwnedDesc | null = null; /** 敌方 skill 描述(ui_otherDesc) */ private _otherOwnedDesc: DailyShopOwnedDesc | null = null; private _combatStarted = false; private _onMapGridsReady: ((map: MapManager) => void) | null = null; private readonly _monsterDeathFx = new HitEffectResolver(); private _deathFxParent: Node | null = null; private _onBattleEventBound: ((e: BattleEvent) => void) | null = null; /** 每日商店主动 buff:skill1 / skill2 */ private _dailyShopSkillView: BattleGameDailyShopSkillView | null = null; /** 每日商店被动 buff:ui_selfskill2List */ private _passiveSkillView: BattleGamePassiveSkillView | null = null; /** 敌方 AI 主/被动:ui_otherskill1List / ui_otherskill2List */ private _otherSkillView: BattleGameOtherSkillView | null = null; /** AI 主动施放:复制图标飞向受体 */ private _aiActiveSkillFx: BattleGameAiActiveSkillFxPresenter | null = null; /** 主动 buff 拖到地图施放 */ private _dailyShopSkillDrag: BattleGameDailyShopSkillDrag | null = null; /** 门神盾牌陷阱 */ private _menShenTrap: BattleGameMenShenTrapController | null = null; /** btnItem 门神盾牌广告 */ private _itemAd: BattleGameItemAdPresenter | null = null; /** 御雷珠被动落雷表现 */ private _baseThunderFx: BattleGameBaseThunderPresenter | null = null; /** 敌方 Boss 出场弹窗 */ private _bossIntro: BattleGameBossIntroPresenter | null = null; /** 点击类主动 buff(Phase1 占位) */ private _dailyShopSkillClick: BattleGameDailyShopSkillClick | null = null; /** 回收/奖励:coinFly 飘仙桃 */ private _peachCoinFly: BattlePeachCoinFlyPresenter | null = null; /** 赏金令:击杀小怪概率额外金币 */ private _killPeach: BattleGameKillPeachPresenter | null = null; /** 击杀武器碎片掉落 */ private _weaponDropFx: BattleWeaponDropPresenter | null = null; /** 掉落表现用死亡坐标 */ private readonly _resolveWeaponDropPos = (victim: EntityId): Vec3 | null => this._monsterDeathWorldPos(victim); /** 首次进战点召唤 / 拖字弱引导 */ private readonly _firstGuide = new BattleFirstGuidePresenter(); /** 倍增卡:本次刷新待入场结束后掷骰升级的槽 */ private _pendingDigitDoubleSlots: number[] = []; /** 波间提示:第 N 波怪物来袭 */ private _lbWaveTip: Label | null = null; /** 波间倒计时 mm:ss */ private _lbWaveTipTime: Label | null = null; /** btnIllustrated 子节点 spine */ private _inkstoneSkel: sp.Skeleton | null = null; /** 本局是否已触发过第 5 波图鉴引导 */ private _illustratedHintShownThisBattle = false; /** 桃 UI 缓存,避免每帧写 Label */ private _peachUiGold = -1; /** 召唤消耗缓存 */ private _peachUiCost = -1; /** 金币广告已用次数缓存 */ private _peachUiCoinAdUsed = -1; /** btnCoin/视频标 */ private _coinAdVideoIcon: Node | null = null; /** 看视频加金币进行中 */ private _coinAdPending = false; /** 波次 UI 缓存 */ private _waveUiIdx = -999; /** 已提示过配置缺失的下一波下标 */ private _nextWaveMiss = -2; /** 已发过结算金币的已结束波次下标;首波倒计时 waveIndex 为 -1,不发 */ private _waveGoldIndex = -2; /** AI 技能条降频 */ private _aiSkillUiFrame = 0; /** AI 未运行告警节流(秒) */ private _aiWatchdogSec = 0; /** 刷新按钮世界坐标,供背包出字抛物线起点 */ private readonly _inkstoneWorldPos = (): Readonly | null => { const n = this.btnRefresh; return n?.isValid ? n.worldPosition : null; }; /** 出字开始:播资源名「待机」 */ private readonly _onInkstoneEntranceStart = (): void => { this._playInkstoneSpine(INKSTONE_SPINE_SPAWN, true); }; /** 出字结束:回待机(资源「出字」为空动画,勿强播),再结算倍增卡 */ private readonly _onInkstoneEntranceEnd = (): void => { this._playInkstoneSpine(INKSTONE_SPINE_SPAWN, true); this._flushPendingDigitDouble(); }; /** 飞入结束后(或入场未播/被清锁时)对本次刷新字掷骰 level×2 */ private _flushPendingDigitDouble(): void { const slots = this._pendingDigitDoubleSlots; this._pendingDigitDoubleSlots = []; if (!slots.length) return; const bag = this._battle?.bagSession; if (!bag) return; bag.applyDigitDoubleAfterRefresh(slots); } /** 尽早关掉召唤按钮 TweenBreathe,避免 onEnable 抢缩放 */ onLoad(): void { super.onLoad(); this._disableRefreshBreathe(); } onInit(): void { this.initUI(); } onEnable(): void { super.onEnable(); this._ownedDesc?.bindDismiss(); GEvent.Ins.on(GEvent.HideBattleIllustratedHint, this._onHideIllustratedHint, this); } onDisable(): void { GEvent.Ins.off(GEvent.HideBattleIllustratedHint, this._onHideIllustratedHint, this); this._ownedDesc?.unbindDismiss(); this._otherOwnedDesc?.unbindDismiss(); this._teardownBattleView(); } /** 关页/end 对局前拆视图,避免 session.dispose 后 Spine/怪物仍同步 */ teardownBeforeDestroy(): void { this._teardownBattleView(); } /** 初始化 skill 描述浮层(ui_descLeft / ui_descRight) */ private _ensureOwnedDesc(): void { if (this._ownedDesc) return; const uiDescLeft = find('ui_descLeft', this.node); const uiDescRight = find('ui_descRight', this.node); if (!uiDescLeft?.isValid && !uiDescRight?.isValid) { console.log('[UIBattleGame] ui_descLeft/ui_descRight 缺失'); return; } this._ownedDesc = new DailyShopOwnedDesc(uiDescLeft, uiDescRight, { activeYOffset: 60, passiveYOffset: 40, }); this._ownedDesc.init(); } /** 初始化敌方 skill 描述浮层(ui_otherDesc) */ private _ensureOtherOwnedDesc(): void { if (this._otherOwnedDesc) return; const uiOtherDesc = find('ui_otherDesc', this.node); if (!uiOtherDesc?.isValid) { console.log('[UIBattleGame] ui_otherDesc 缺失'); return; } this._otherOwnedDesc = new DailyShopOwnedDesc(uiOtherDesc, uiOtherDesc, { activeYOffset: -30, passiveYOffset: -20, }); this._otherOwnedDesc.init(); } /** 互斥显示我方/敌方 skill 描述 */ private _linkSkillDescPeers(): void { this._ownedDesc?.setPeerDesc(this._otherOwnedDesc); this._otherOwnedDesc?.setPeerDesc(this._ownedDesc); } initUI(): void { this._battle = gg.game.CurentBattle as ProjectBattleHandle | null; if (!this._battle) { console.error('[UIBattleGame] CurentBattle missing'); return; } this._ensureOwnedDesc(); this._ensureOtherOwnedDesc(); this._linkSkillDescPeers(); this._showRefreshBg1(this._isRefreshGoldEnough()); this._disableRefreshBreathe(); this.btnRefresh?.getScale(this._refreshBtnScaleHome); this._lbWaveTip = this.ui_nextWaveTime?.getChildByName('lb1')?.getComponent(Label) ?? null; this._lbWaveTipTime = this.ui_nextWaveTime?.getChildByName('lb2')?.getComponent(Label) ?? null; if (!this.ui_nextWaveTime?.isValid) { console.log('[UIBattleGame] ui_nextWaveTime 节点缺失'); } else if (!this._lbWaveTip || !this._lbWaveTipTime) { console.log('[UIBattleGame] ui_nextWaveTime 缺少 lb1 或 lb2'); } this._dailyShopSkillView = new BattleGameDailyShopSkillView(this.ui_selfskill1List); this._dailyShopSkillView.bindBattle(this._battle); this._dailyShopSkillView.setOwnedDesc(this._ownedDesc); this._dailyShopSkillView.setOnDisplayChanged(() => this._onDailyShopSkillCastChanged()); this._dailyShopSkillView.refresh(); this._passiveSkillView = new BattleGamePassiveSkillView(this.ui_selfskill2List); this._passiveSkillView.bindBattle(this._battle); this._passiveSkillView.setOwnedDesc(this._ownedDesc); this._bindGoldMineCoinFly(); this._passiveSkillView.refresh(); this._otherSkillView = new BattleGameOtherSkillView(this.ui_otherskill1List, this.ui_otherskill2List); this._otherSkillView.setOwnedDesc(this._otherOwnedDesc); this._otherSkillView.refresh(); if (this.ui_money?.isValid) this.ui_money.active = false; this._coinAdVideoIcon = this.btnCoin?.getChildByName('视频标') ?? null; this._waveUiIdx = -999; this._peachUiGold = -1; this._peachUiCost = -1; this._peachUiCoinAdUsed = -1; this._refreshPeachUi(); this._refreshWaveUi(); this._syncIllustratedHint(false); this._ownedDesc?.bindDismiss(); this._otherOwnedDesc?.bindDismiss(); this._setupMapAndSpawn(); } update(dt: number): void { try { gg.battle.tick(dt); } catch (e) { ClientErrorReporter.report('UIBattleGame_battleTick', e); } try { AiBattleBridge.inst.tick(dt); if (this._battle && !this._battle.IsGameOver && !AiBattleBridge.inst.running) { this._aiWatchdogSec += dt; if (this._aiWatchdogSec >= 3) { this._aiWatchdogSec = 0; console.error('[UIBattleGame] 对手 AI 未运行(start 失败或已 stop)'); } } else { this._aiWatchdogSec = 0; } if (!this._battle?.IsGamePause) { this._dailyShopSkillView?.tick(dt); this._passiveSkillView?.tick(dt); this._aiSkillUiFrame++; if (this._aiSkillUiFrame >= 6) { this._aiSkillUiFrame = 0; this._otherSkillView?.syncFromAi(); } this._bagView?.tickPeachTreePresentations(); getBattleGameMonsterCrisisHintPresenter().tick(); this._campHpBar.tick(); } this._menShenTrap?.tick(dt); this._itemAd?.tick(dt); this._refreshPeachUi(); this._refreshWaveUi(); } catch (e) { ClientErrorReporter.report('UIBattleGame_uiTick', e); } } onDestroy(): void { this._teardownBattleView(); } private _setupMapAndSpawn(): void { const battle = this._battle; const parent = this.ui_mapParentNode; if (!battle || !parent) { console.error('[UIBattleGame] battle or map parent missing'); return; } this._mapHost = parent.getComponent(CocosBattleMapHost) ?? parent.addComponent(CocosBattleMapHost); this._mapHost.mapRoot = parent; const mapNode = this._mapHost.loadMap(battle.mapPrefabName); if (!mapNode) return; const mapMgr = mapNode.getComponent('MapManager') as MapManager | null; if (mapMgr?.gridsInitialized) { this._onMapReady(mapMgr, mapNode); return; } this._onMapGridsReady = (map: MapManager) => { this._onMapReady(map, mapNode); }; GEvent.Ins.on(GEvent.MapGridsReady, this._onMapGridsReady, this); } private _onMapReady(map: MapManager, mapNode: Node): void { if (this._onMapGridsReady) { GEvent.Ins.off(GEvent.MapGridsReady, this._onMapGridsReady, this); this._onMapGridsReady = null; } const battle = this._battle; const parent = this.ui_mapParentNode; if (!battle || !parent?.isValid) return; if (!applyMapTopologyToSession(battle.session, map, 'pvp')) { console.error('[UIBattleGame] topology import failed'); return; } this._deathFxParent = mapNode; battle.setDeathFxParent(mapNode); this._monsterPresenter.bind(battle, mapNode, parent); this._bindBaseThunder(battle); this._bindBossIntro(battle); this._movementBridge = new CocosBattleMovementBridge(this._monsterPresenter); battle.session.setBridge(this._movementBridge); this._combatPresenter?.unbind(); this._combatPresenter = new CocosBattleCombatPresenter(); this._combatPresenter.bind(battle, this._monsterPresenter, parent); this._bindSkillPresentationEnv(battle, map); this._bindBagBoard(map); this._ensurePeachCoinFly(); this._bindWeaponDrop(battle); this._bindSkillController(map); this._bindBattleEvents(battle); this._refreshPeachUi(); this._refreshWaveUi(); // 须在 AI start 之后再挂血条:AI 镇宅牌会改 wallMaxHp,挂早了格宽会按 3 算 AiBattleBridge.inst.setMapTextFlyLayer(this.ui_fly ?? this.ui_mapParentNode); AiBattleBridge.inst.setTextGridLayer(this.ui_textGrids); try { if (AiBattleBridge.inst.start(battle, map, undefined, this.node, this.ui_textList)) { this._otherSkillView?.refresh(); this._bindAiActiveSkillFx(map); this._bindAiGoldMineCoinFly(); } else { console.error('[UIBattleGame] AiBattleBridge.start 失败,对手 AI 未启动'); } } catch (e) { console.error('[UIBattleGame] AiBattleBridge.start 异常', String(e)); } this._bindUnlockCoinFly(battle); this._bindBattleBuffSelect(battle); this._campHpBar.bind(battle, map); getBattleGameMonsterCrisisHintPresenter().bind(battle, map); this._firstGuide.bind(this.ui_guide, battle.bagSession, this.btnRefresh); // Widget 布局完成后再对齐一次,避免手指仍停在预制体旧坐标 this.scheduleOnce(this._realignFirstGuideFinger, 0); PerfSampler.beginBattle(); this._listenFirstPlaceCombat(); this._refreshWaveUi(); } /** 引导手指对齐召唤按钮 */ private readonly _realignFirstGuideFinger = (): void => { this._firstGuide.realignFinger(); }; /** * 开局不立刻出怪:本机首次放数字开怪,或超时后强制开怪。 */ private _listenFirstPlaceCombat(): void { this._unbindFirstPlaceCombat(); const bag = this._battle?.bagSession; if (!bag) { console.error('[UIBattleGame] 无背包会话,无法按首次放字开怪'); return; } bag.on(MergeEventType.TilePlaced, this._onFirstPlayerNumberPlaced); this.scheduleOnce(this._onFirstPlaceCombatTimeout, FIRST_PLACE_COMBAT_TIMEOUT_SEC); } /** 卸掉首次放字开怪监听与超时 */ private _unbindFirstPlaceCombat(): void { this.unschedule(this._onFirstPlaceCombatTimeout); this._battle?.bagSession?.off(MergeEventType.TilePlaced, this._onFirstPlayerNumberPlaced); } /** * 本机首次放下五行数字后开波。 * @param event 地图落子事件 */ private readonly _onFirstPlayerNumberPlaced = (event: MergeEvent): void => { if (this._combatStarted) return; if (event.type !== MergeEventType.TilePlaced) return; const tileType = Math.floor(Number(WeaponComposeConfigProvider.instance.getWeapon2Type(event.weapon2Id))); if (!isElementGlyphType(tileType)) return; this._startCombatFromFirstPlaceGate(); }; /** 超时仍未放数字:强制开怪 */ private readonly _onFirstPlaceCombatTimeout = (): void => { if (this._combatStarted) return; this._startCombatFromFirstPlaceGate(); }; /** 首次放字 / 超时共用:开波并卸监听 */ private _startCombatFromFirstPlaceGate(): void { if (this._combatStarted) return; this._unbindFirstPlaceCombat(); this._combatStarted = true; gg.battle.startCombat(); this._refreshWaveUi(); }; /** 绑定 HitAfter/Buff/死亡等战斗事件(须在 monsterPresenter 之后) */ private _bindBattleEvents(battle: ProjectBattleHandle): void { if (this._onBattleEventBound) { battle.off(BATTLE_EVENT_ALL, this._onBattleEventBound); } this._onBattleEventBound = (e) => this._onBattleEvent(e); battle.on(BATTLE_EVENT_ALL, this._onBattleEventBound); } private _onBattleEvent(event: BattleEvent): void { if (event.type === BattleEventType.EntityKilled) { this._playMonsterDeathFx(event.victim); this._playKillPeachFly(event.killer, event.victim); this._killPeach?.onEntityKilled(event.killer, event.victim); this._weaponDropFx?.onEntityKilled(event.victim); return; } if (event.type === BattleEventType.WaveStart) { this._tryShowIllustratedHintOnWave(event.waveIndex); } } /** * 非首波倒计时开始时,本机与 AI 各发一次金币。首波倒计时 waveIndex 仍是 -1。 * 飞金币只播本机。 * @param endedWaveIndex 刚结束、正在等待下一波的波次下标 * @param remain 倒计时剩余秒 */ private _grantWaveCountdownGold(endedWaveIndex: number, remain: number): void { if (endedWaveIndex < 0 || !(remain > 0)) return; if (this._waveGoldIndex === endedWaveIndex) return; this._waveGoldIndex = endedWaveIndex; const battle = this._battle; if (!battle || battle.IsGameOver) return; const gold = this._waveSettleGold(); if (!(gold > 0)) return; battle.addPlayerGold(battle.localPlayerId, gold); this._refreshPeachUi(); this._playWaveSettleCoin(gold); this._grantWaveGoldToOthers(gold); } /** * 非本机玩家槽入账同样的波次金币,不播飞金币。 * @param gold 本次入账金币 */ private _grantWaveGoldToOthers(gold: number): void { const battle = this._battle; if (!battle) return; const players = battle.session.players; for (let i = 0; i < players.length; i++) { const id = players[i].id; if (id === battle.localPlayerId) continue; battle.addPlayerGold(id, gold); } } /** * 常量表 41 para1:每波结算金币。缺失或非法时打日志并返回 0。 */ private _waveSettleGold(): number { const list = gg.data.table.getConstArray(ConstNumType.WaveSettleGold); if (!list.length) { console.log('[UIBattleGame] Const 每波结算金钱为空', ConstNumType.WaveSettleGold); return 0; } const gold = Math.floor(list[0]); if (!Number.isFinite(gold) || gold < 0) { console.log('[UIBattleGame] Const 每波结算金钱非法', list[0]); return 0; } return gold; } /** * 常量表 42 para1:每波飘金币个数。缺失或非法时打日志并返回 0。 */ private _waveSettleCoinCount(): number { const list = gg.data.table.getConstArray(ConstNumType.WaveSettleCoinCount); if (!list.length) { console.log('[UIBattleGame] Const 飘金币数量为空', ConstNumType.WaveSettleCoinCount); return 0; } const count = Math.floor(list[0]); if (!Number.isFinite(count) || count <= 0) { console.log('[UIBattleGame] Const 飘金币数量非法', list[0]); return 0; } return count; } /** * 按次数生成金币挂到 ui_fly,先在不同位置间隔冒出,再一起飞向 btnCoin。 * 单枚显示为结算金币除以生成次数;入账仍是结算金币总额。 * @param gold 本次入账的金币总数 */ private _playWaveSettleCoin(gold: number): void { const layer = this.ui_fly; if (!layer?.isValid) { console.log('[UIBattleGame] 波次结算金币缺少 ui_fly'); return; } const count = this._waveSettleCoinCount(); if (!(count > 0)) return; const each = gold / count; if (each !== Math.floor(each)) { console.log('[UIBattleGame] 每波结算金钱不能被飘金币数量整除', gold, count); } const show = Math.floor(each); if (!(show > 0)) { console.log('[UIBattleGame] 单枚金币显示数量为 0', gold, count); return; } this._ensurePeachCoinFly(); if (!layer.active) layer.active = true; this._peachCoinFly?.playWaveCoin(layer, layer.worldPosition, show, count); } /** * 击杀掉桃表现:本机飞向 ui_money;AI 归属原地弹跳(金已入 AI 槽)。 * @param killer 击杀者 * @param victim 死亡实体 */ private _playKillPeachFly(killer: EntityId, victim: EntityId): void { const battle = this._battle; const fly = this._peachCoinFly; if (!battle || !fly) return; const peach = battle.resolveKillPeach(victim); if (!(peach > 0)) return; const from = this._monsterDeathWorldPos(victim); if (!from) { console.log('[UIBattleGame] 击杀掉桃无死亡坐标', victim); return; } const ownerId = battle.resolveKillPeachOwnerPlayerId(killer, victim); if (ownerId === battle.localPlayerId) { fly.playKillDrop(from, peach); return; } fly.playKillDropLocal(from, peach); } /** * 击杀时怪物世界坐标(节点优先,否则 presenter 缓存)。 * @param victim 死亡实体 */ private _monsterDeathWorldPos(victim: EntityId): Vec3 | null { const node = this._monsterPresenter.getEntityNodeMap().get(victim); if (node?.isValid) { const view = node.getComponent(BATTLE_MONSTER_CCCLASS) as { getPosByAtkPosIndex?(idx: number): Vec3; } | null; return (view?.getPosByAtkPosIndex?.(2) ?? node.worldPosition).clone(); } return this._monsterPresenter.getEntityWorldPos(victim); } /** 击杀时在怪物坐标播 prefab/死亡;五行小怪走自身「系小怪2」轨,不播外部死亡特效 */ private _playMonsterDeathFx(victim: EntityId): void { const parent = this._deathFxParent; if (!parent?.isValid) return; const node = this._monsterPresenter.getEntityNodeMap().get(victim); if (node?.isValid) { const view = node.getComponent(BATTLE_MONSTER_CCCLASS) as { Data?: ITableMonster } | null; if (view?.Data?.type === MonsterType.TreasureBox) return; if (view?.Data && MonsterDataManager.isElementSmallMonster(view.Data)) return; } const worldPos = this._monsterDeathWorldPos(victim); if (!worldPos) return; this._monsterDeathFx.playMonsterDeathAt(parent, worldPos); } /** 离场时回收未播完的死亡特效 */ private _recycleOrphanMonsterDeathFx(): void { const parent = this._deathFxParent; if (!parent?.isValid) return; for (const child of parent.children.slice()) { if (!child?.isValid || child.name !== '死亡') continue; const sk = child.getComponent(sp.Skeleton) ?? child.getComponentInChildren(sp.Skeleton); sk?.setCompleteListener(null); gg.res.putNode(child); } } /** 绑定 Skill 表现层共享环境(ui_fly / 怪物坐标) */ private _bindSkillPresentationEnv(battle: ProjectBattleHandle, map: MapManager): void { SkillPresentationEnv.bind({ flyLayer: this.ui_fly?.isValid ? this.ui_fly : null, scoreLayer: null, monsterPresenter: this._monsterPresenter, map, battle, }); } /** 绑定背包槽与拖放视图 */ private _bindBagBoard(map: MapManager): void { const battle = this._battle; if (!battle || !this.ui_textList?.isValid) { console.log('[UIBattleGame] ui_textList missing,无法绑定背包'); return; } this._bagView?.unbind(); this._bagView = new CocosBagBoardView(); const rangeNode = this.ui_range?.isValid ? this.ui_range : find('ui_range', this.node); this.ui_range = rangeNode; this._resolveInkstoneSpine(); this._bagView.bind( battle, map, this.ui_textList, this.ui_mapParentNode, this.ui_fly ?? this.ui_mapParentNode, this.ui_line, rangeNode, this.ui_textGrids, ); if (!this.ui_delete?.isValid) { console.log('[UIBattleGame] ui_delete 缺失,无法拖字删除'); } this._bagView.setDeleteNode(this.ui_delete); // bind() 开头会 unbind,入场回调必须在 bind 之后再挂 this._bagView.setOtherBagAnchor(this.ui_otherBag); this._bagView.setInkstoneWorldPosProvider(this._inkstoneWorldPos); this._bagView.setBagEntranceHandler(this._onInkstoneEntranceStart, this._onInkstoneEntranceEnd); if (this._bagView.isBagEntranceBusy()) { this._onInkstoneEntranceStart(); } this._menShenTrap?.unbind(); this._menShenTrap = new BattleGameMenShenTrapController(); this._menShenTrap.bind(battle, map, battle.localPlayerId); this._itemAd?.unbind(); this._itemAd = new BattleGameItemAdPresenter(); const btnItem = find('ui_bottom/ui_ready/btnItem', this.node); this._itemAd.bind(battle, btnItem, this._menShenTrap); this._ensurePeachCoinFly(); this._bindSkillPeachFly(); this._bindKillPeach(battle); this._bindDailyShopSkillDrag(); this._bindDailyShopSkillClick(); this._passiveSkillView?.bindBagView(this._bagView); } /** 解析 btnIllustrated 上的砚台 Spine */ private _resolveInkstoneSpine(): void { const btn = this.btnIllustrated; if (!btn?.isValid) { console.log('[UIBattleGame] btnIllustrated 缺失'); this._inkstoneSkel = null; return; } const spineNode = btn.getChildByName('spine'); const skel = spineNode?.getComponent(sp.Skeleton) ?? btn.getComponentInChildren(sp.Skeleton); if (!skel?.isValid) { console.log('[UIBattleGame] btnIllustrated 缺少 Spine'); this._inkstoneSkel = null; return; } this._inkstoneSkel = skel; this._playInkstoneSpine(INKSTONE_SPINE_SPAWN, true); } /** 释放砚台 Spine 播放回调 */ private _clearInkstoneSpine(): void { if (this._inkstoneSkel?.isValid) { BattleSpinePlayback.clearPendingComplete(this._inkstoneSkel); } this._inkstoneSkel = null; } /** * 播放砚台 Spine(资源动画名与表现含义取反) * @param animName 资源内动画名 * @param loop 是否循环 */ private _playInkstoneSpine(animName: string, loop: boolean): void { const skel = this._inkstoneSkel; if (!skel?.isValid) return; BattleSpinePlayback.play(skel, animName, { loop, optional: true }); } /** 绑定 AI 主动施放飞图标表现 */ private _bindAiActiveSkillFx(map: MapManager): void { this._aiActiveSkillFx?.unbind(); this._aiActiveSkillFx = new BattleGameAiActiveSkillFxPresenter(); this._aiActiveSkillFx.bind( this._otherSkillView, this._bagView, map, this.ui_fly ?? this.ui_mapParentNode, ); AiBattleBridge.inst.setActiveSkillCastFxHandler(this._onAiActiveSkillCastFx); } /** AI 主动施放 → 飞图标 */ private readonly _onAiActiveSkillCastFx = (info: AiActiveSkillCastFxInfo): void => { this._aiActiveSkillFx?.play(info); }; /** 绑定敌方 Boss 出场弹窗 */ private _bindBossIntro(battle: ProjectBattleHandle): void { this._bossIntro?.unbind(); this._bossIntro = new BattleGameBossIntroPresenter(); this._bossIntro.bind(battle); } /** 绑定御雷珠被动表现与逻辑 Host */ private _bindBaseThunder(battle: ProjectBattleHandle): void { this._baseThunderFx?.unbind(); this._baseThunderFx = new BattleGameBaseThunderPresenter(); this._baseThunderFx.bind(this._monsterPresenter, this.ui_mapParentNode ?? this.ui_fly); getBattleBuffBaseThunderHost().bind(battle, this._baseThunderFx); } /** 绑定底部金币飞币(击杀 / 回收共用) */ private _ensurePeachCoinFly(): void { if (!this._peachCoinFly) { this._peachCoinFly = new BattlePeachCoinFlyPresenter(); } const moneyTarget = this.btnCoin?.getChildByName('金币') ?? this.btnCoin; this._peachCoinFly.bind(moneyTarget, this.ui_fly ?? this.ui_mapParentNode); } /** 金矿:本机被动图标掉落 coinFly 飞向 btnCoin/金币 */ private _bindGoldMineCoinFly(): void { this._ensurePeachCoinFly(); this._passiveSkillView?.setGoldMineCoinFlyHandler(this._onGoldMineCoinFly); } /** AI 金矿飞币(须在 AiBattleBridge.start 之后,避免 bind 清掉回调) */ private _bindAiGoldMineCoinFly(): void { this._ensurePeachCoinFly(); AiBattleBridge.inst.setGoldMineCoinFlyHandler(this._onAiGoldMineCoinFly); } /** * 本机金矿飞币。 * @param fromWorld 金矿图标世界坐标 */ private readonly _onGoldMineCoinFly = (fromWorld: Readonly): void => { this._ensurePeachCoinFly(); this._peachCoinFly?.playKillDrop(fromWorld, 1); this._refreshPeachUi(); }; /** AI 金矿飞币:敌方被动金矿图标原地弹跳 */ private readonly _onAiGoldMineCoinFly = (): void => { this._ensurePeachCoinFly(); const icon = this._otherSkillView?.getPassiveIconNode(BattleBuffFunctionType.GoldMine); const from = icon?.isValid ? icon.worldPosition.clone() : null; if (!from) { console.log('[UIBattleGame] AI 金矿飞币无坐标'); return; } this._peachCoinFly?.playKillDropLocal(from, 1); }; /** 技能产金币:从格心飞向 ui_money */ private _bindSkillPeachFly(): void { this._ensurePeachCoinFly(); this._bagView?.setSkillPeachFlyHandler(this._onSkillPeachGain); } /** * 寻金尺:解锁地块随机金币飞币(本机飞向 ui_money,AI 原地弹跳)。 * @param battle 战斗 */ private _bindUnlockCoinFly(battle: ProjectBattleHandle): void { this._ensurePeachCoinFly(); const handler = this._onUnlockCoinFly; if (battle.bagSession) { battle.bagSession.onUnlockCoinFly = handler; } const aiBag = AiBattleBridge.inst.aiBagSession; if (aiBag) { aiBag.onUnlockCoinFly = handler; } } /** * 地图解锁达 Const27 次数:本机弹三选一,AI 自动选。 * @param battle 战斗 */ private _bindBattleBuffSelect(battle: ProjectBattleHandle): void { const handler = this._onBattleBuffSelect; if (battle.bagSession) { battle.bagSession.onBattleBuffSelect = handler; } const aiBag = AiBattleBridge.inst.aiBagSession; if (aiBag) { aiBag.onBattleBuffSelect = handler; } } /** * @param ownerPlayerId 归属 * @param unlockCount 解锁次数 */ private readonly _onBattleBuffSelect = (ownerPlayerId: number, unlockCount: number): void => { const battle = this._battle; if (!battle) return; if (ownerPlayerId === AiHeadlessBagHost.AI_PLAYER_ID) { const picked = BattleBuffSelectService.autoPickOne(ownerPlayerId); if (picked) { BattleBuffInGameRefresh.afterPick(battle, ownerPlayerId); this.refreshBattleBuffSkills(); } return; } if (ownerPlayerId !== (battle.localPlayerId ?? 0)) { console.warn('[UIBattleGame] 未知 player 三选一', ownerPlayerId, unlockCount); return; } void gg.ui.openPanel(UIID.PopSelect); }; /** 本局获得 buff 后刷新技能栏与基地血条 */ refreshBattleBuffSkills(): void { this._dailyShopSkillView?.refresh(); this._passiveSkillView?.refresh(); this._otherSkillView?.refresh(); this._campHpBar.syncFromSlots(); } /** * 寻金尺飞币:金币已入账,仅播表现。 * @param gridIndex 解锁锚点格 * @param coin 本次金币 * @param ownerPlayerId 归属 */ private readonly _onUnlockCoinFly = (gridIndex: number, coin: number, ownerPlayerId: number): void => { if (!(coin > 0)) return; const battle = this._battle; const fly = this._peachCoinFly; const from = this._bagView?.getMapGridWorldPosition(gridIndex); if (ownerPlayerId === battle?.localPlayerId) { this._refreshPeachUi(); if (!from) { console.log('[UIBattleGame] 寻金尺无飞币坐标', gridIndex); return; } fly?.playKillDrop(from, coin); return; } if (!from) { console.log('[UIBattleGame] 寻金尺 AI 无飞币坐标', gridIndex); return; } fly?.playKillDropLocal(from, coin); }; /** * 技能产桃:本机 coinFly 飞向 ui_money;敌方格心 coinFly 原地弹跳后回池。 * @param gridIndex 施法格 * @param peachCount 蟠桃数量 */ private readonly _onSkillPeachGain = (gridIndex: number, peachCount: number, ownerPlayerId = 0): void => { const battle = this._battle; const bagView = this._bagView; const fly = this._peachCoinFly; const from = bagView?.getMapGridWorldPosition(gridIndex); if (ownerPlayerId === battle?.localPlayerId) { if (!from) { console.log('[UIBattleGame] 产桃坐标缺失', gridIndex, ownerPlayerId); bagView?.grantBattlePeach(peachCount); return; } fly?.playKillDrop(from, peachCount); bagView?.grantBattlePeach(peachCount); return; } if (battle && ownerPlayerId >= 0) { battle.addPlayerGold(ownerPlayerId, peachCount); if (!from) { console.log('[UIBattleGame] 产桃坐标缺失', gridIndex, ownerPlayerId); return; } fly?.playKillDropLocal(from, peachCount); } }; /** 绑定赏金令击杀额外金币 */ private _bindKillPeach(battle: ProjectBattleHandle): void { this._killPeach?.unbind(); this._ensurePeachCoinFly(); this._killPeach = new BattleGameKillPeachPresenter(); this._killPeach.bind( battle, this._bagView, this._peachCoinFly, (victim) => this._monsterDeathWorldPos(victim), ); } /** 绑定击杀武器碎片掉落 */ private _bindWeaponDrop(battle: ProjectBattleHandle): void { this._weaponDropFx?.unbind(); this._weaponDropFx = new BattleWeaponDropPresenter(); this._weaponDropFx.bind( battle, this.ui_fly ?? this.ui_mapParentNode, this.btnPause, this._resolveWeaponDropPos, ); } /** 绑定每日商店主动 buff 拖拽施放(Phase1 占位) */ private _bindDailyShopSkillDrag(): void { this._dailyShopSkillDrag?.unbind(); this._dailyShopSkillDrag = new BattleGameDailyShopSkillDrag(); this._dailyShopSkillDrag.bind( this._dailyShopSkillView, this._bagView, null, null, null, null, null, null, null, null, null, null, null, this._battle, this.ui_fly ?? this.ui_mapParentNode, this.ui_line, this._ownedDesc, ); this._dailyShopSkillView?.bindSkillDesc(); } /** 绑定每日商店主动 buff 点击施放 */ private _bindDailyShopSkillClick(): void { this._dailyShopSkillClick?.unbind(); this._dailyShopSkillClick = new BattleGameDailyShopSkillClick(); this._dailyShopSkillClick.bind(this._dailyShopSkillView, this._bagView, this._battle); } /** 七十二变 roll / 施放后:刷新 skill 交互绑定(不重建 Drag,避免 Presenter 引用断裂) */ private _onDailyShopSkillCastChanged(): void { if (!this._bagView) return; this._dailyShopSkillDrag?.refreshSlotBindings(); this._dailyShopSkillClick?.refreshSlotBindings(); this._dailyShopSkillView?.bindSkillDesc(); } /** 绑定技能表现控制器(缺绑则 PlayPresentation 上锁后无人 finish,塔永不攻击) */ private _bindSkillController(map: MapManager): void { const battle = this._battle; const player = battle?.player ?? gg.game.localPlayer; if (!battle || !player) { console.error('[UIBattleGame] SkillController 绑定失败:无 battle/player'); return; } this._skillController?.unbind(); let sc = this.node.getComponent(SkillController); if (!sc) sc = this.node.addComponent(SkillController); sc.bind(player, battle); this._skillController = sc; sc.refreshEntitySkillBindings(map, this._bagView?.getMapGrids()); } /** 刷新底部金币数、召唤消耗、金币广告与按钮填充状态 */ private _refreshPeachUi(): void { const battle = this._battle; if (!battle) return; const gold = battle.localGold; const cost = battle.bagSession ? battle.bagSession.normalRefreshCost : getDiscountedBagNormalRefreshCost(0, resolveCarriedSummonCostReduceWan().reduceWan); const coinAdUsed = battle.coinAdUsed; if (gold === this._peachUiGold && cost === this._peachUiCost && coinAdUsed === this._peachUiCoinAdUsed) return; this._peachUiGold = gold; this._peachUiCost = cost; this._peachUiCoinAdUsed = coinAdUsed; if (this.lb_coin?.isValid) { this.lb_coin.string = `${gold}`; } this._syncCoinAdUi(battle); if (this.lb_RefreshNeed?.isValid) { this.lb_RefreshNeed.string = `${cost}`; } const enough = cost <= 0 || gold >= cost; this._showRefreshBg1(enough); this._syncRefreshBounce(enough); } /** * 同步 btnCoin 广告区:次数未满显示下一次 +数量与 used+1/max;用尽则隐藏。 * @param battle 当前对局 */ private _syncCoinAdUi(battle: ProjectBattleHandle): void { const canAd = battle.canUseCoinAd(); const amount = battle.nextCoinAdAmount(); const max = battle.coinAdRewards.length; this._setNodeActive(this._coinAdVideoIcon, canAd); this._setNodeActive(this.lb_coinAdLimit?.node ?? null, canAd); this._setNodeActive(this.lb_coinAdNum?.node ?? null, canAd); if (!canAd) return; if (this.lb_coinAdNum?.isValid) { this.lb_coinAdNum.string = `+${amount}`; } if (this.lb_coinAdLimit?.isValid) { this.lb_coinAdLimit.string = `${battle.coinAdUsed + 1}/${max}`; } } /** * 安全设置节点显隐。 * @param node 目标节点 * @param active 是否显示 */ private _setNodeActive(node: Node | null, active: boolean): void { if (node?.isValid && node.active !== active) node.active = active; } /** * 当前金币是否够付一次召唤。 */ private _isRefreshGoldEnough(): boolean { const battle = this._battle; if (!battle) return false; const gold = battle.localGold; const cost = battle.bagSession ? battle.bagSession.normalRefreshCost : getDiscountedBagNormalRefreshCost(0, resolveCarriedSummonCostReduceWan().reduceWan); return cost <= 0 || gold >= cost; } /** * btnRefresh 只显示 bg1,隐藏进度底 bg2 和可刷新底 bg3。金币不够时 bg1 置灰。 * @param enough 金币是否够付一次召唤 */ private _showRefreshBg1(enough: boolean): void { const btn = this.btnRefresh; if (!btn?.isValid) return; const bg1 = btn.getChildByName('bg1'); const bg2 = btn.getChildByName('bg2'); const bg3 = btn.getChildByName('bg3'); if (!bg1?.isValid) { console.log('[UIBattleGame] btnRefresh 缺少 bg1'); } else { this._setNodeActive(bg1, true); const sp = bg1.getComponent(Sprite); if (!sp) { console.log('[UIBattleGame] btnRefresh bg1 缺少 Sprite'); } else if (sp.grayscale === enough) { sp.grayscale = !enough; } } if (bg2?.isValid) Tween.stopAllByTarget(bg2); this._setNodeActive(bg2, false); this._setNodeActive(bg3, false); } /** 关掉预制体上的 TweenBreathe,避免与 Q 弹抢缩放 */ private _disableRefreshBreathe(): void { const breathe = this.btnRefresh?.getComponent(TweenBreathe); if (!breathe?.isValid) return; breathe.playing = false; breathe.enabled = false; } /** 桃子足够时召唤按钮 Q 弹两下(第二下幅度减半),每段足够只播一次 */ private _syncRefreshBounce(enough: boolean): void { if (!enough) { this._stopRefreshBounce(); this._refreshBounceHinted = false; return; } if (this._refreshBounceHinted) return; this._refreshBounceHinted = true; BattleAudio.playRefreshReady(); this._playRefreshBounce(); } /** 召唤按钮小幅度 Q 弹两下 */ private _playRefreshBounce(): void { const btn = this.btnRefresh; if (!btn?.isValid) return; this._stopRefreshBounce(); const hx = this._refreshBtnScaleHome.x; const hy = this._refreshBtnScaleHome.y; const hz = this._refreshBtnScaleHome.z; const a1 = 1 + REFRESH_BOUNCE_AMP; const a2 = 1 + REFRESH_BOUNCE_AMP * 0.5; tween(btn) .to(REFRESH_BOUNCE_SEC, { scale: v3(hx * a1, hy * a1, hz) }, { easing: 'quadOut' }) .to(REFRESH_BOUNCE_SEC, { scale: v3(hx, hy, hz) }, { easing: 'quadIn' }) .to(REFRESH_BOUNCE_SEC, { scale: v3(hx * a2, hy * a2, hz) }, { easing: 'quadOut' }) .to(REFRESH_BOUNCE_SEC, { scale: v3(hx, hy, hz) }, { easing: 'quadIn' }) .start(); } /** 停止召唤按钮 Q 弹并恢复基准缩放 */ private _stopRefreshBounce(): void { const btn = this.btnRefresh; if (!btn?.isValid) return; Tween.stopAllByTarget(btn); btn.setScale(this._refreshBtnScaleHome); } /** 刷新顶部波次:第 N 波 */ private _refreshWaveUi(): void { const battle = this._battle; if (!battle) return; const session = battle.session; const level = session.config.getLevel(session.levelId); if (!level || !(level.waves.length > 0)) { console.log('[UIBattleGame] 关卡波次缺失', session.levelId); return; } this._refreshNextWaveTime(); const idx = session.spawner.waveIndex; const cur = idx < 0 ? 1 : idx + 1; const text = gg.lang.get('第{1}波', [cur]); if (idx === this._waveUiIdx && this.lb_wave?.isValid && this.lb_wave.string === text) return; this._waveUiIdx = idx; if (this.lb_wave?.isValid) { this.lb_wave.node.getComponent(LangTxt)?.destroy(); this.lb_wave.string = text; } } /** * 下一波还没开打时显示 ui_nextWaveTime。 * 倒计时已开始用剩余秒数;开打前先显示该波准备时间。出怪中、没有下一波、战斗结束时隐藏。 */ private _refreshNextWaveTime(): void { const node = this.ui_nextWaveTime; if (!node?.isValid) return; const battle = this._battle; if (!battle || battle.IsGameOver) { if (node.active) node.active = false; return; } const session = battle.session; const spawner = session.spawner; const waiting = !spawner.isWaveCombatActive() && spawner.hasMoreWaves(); if (!waiting) { if (node.active) node.active = false; return; } if (!node.active) node.active = true; const nextIndex = spawner.waveIndex + 1; const tip = gg.lang.get('第{1}波怪物来袭', [nextIndex + 1]); const wave = session.config.getWave(session.levelId, nextIndex); if (!wave && this._nextWaveMiss !== nextIndex) { this._nextWaveMiss = nextIndex; console.log('[UIBattleGame] 下一波配置缺失', session.levelId, nextIndex); } const scheduled = session.waveIntervalRemaining; this._grantWaveCountdownGold(spawner.waveIndex, scheduled); const remain = scheduled > 0 ? scheduled : (wave?.prepTime ?? 0); const timeText = this._formatWaveCountdown(remain); const tipLabel = this._lbWaveTip; if (tipLabel?.isValid && tipLabel.string !== tip) { tipLabel.node.getComponent(LangTxt)?.destroy(); tipLabel.string = tip; } const timeLabel = this._lbWaveTipTime; if (timeLabel?.isValid && timeLabel.string !== timeText) { timeLabel.string = timeText; } } /** * 波间剩余秒数格式化为 mm:ss。向上取整,避免最后不足 1 秒变成 00:00。 * @param remainSec 剩余秒 */ private _formatWaveCountdown(remainSec: number): string { const total = Math.max(0, Math.ceil(remainSec)); const min = Math.floor(total / 60); const sec = total % 60; const mm = min < 10 ? `0${min}` : String(min); const ss = sec < 10 ? `0${sec}` : String(sec); return `${mm}:${ss}`; } private _teardownBattleView(): void { PerfSampler.endBattle(); DamageTextSpawner.resetBurst(); HitEffectResolver.resetBurst(); if (this._onMapGridsReady) { GEvent.Ins.off(GEvent.MapGridsReady, this._onMapGridsReady, this); this._onMapGridsReady = null; } if (this._battle && this._onBattleEventBound) { this._battle.off(BATTLE_EVENT_ALL, this._onBattleEventBound); } this._onBattleEventBound = null; this._recycleOrphanMonsterDeathFx(); this._deathFxParent = null; SkillPresentationEnv.unbind(); this._combatPresenter?.unbind(); this._combatPresenter = null; this._skillController?.unbind(); this._skillController = null; this._dailyShopSkillDrag?.unbind(); this._dailyShopSkillDrag = null; this._dailyShopSkillClick?.unbind(); this._dailyShopSkillClick = null; this._bagView?.setSkillPeachFlyHandler(null); this._passiveSkillView?.setGoldMineCoinFlyHandler(null); AiBattleBridge.inst.setGoldMineCoinFlyHandler(null); if (this._battle?.bagSession) { this._battle.bagSession.onUnlockCoinFly = null; this._battle.bagSession.onBattleBuffSelect = null; } const aiBagTeardown = AiBattleBridge.inst.aiBagSession; if (aiBagTeardown) { aiBagTeardown.onUnlockCoinFly = null; aiBagTeardown.onBattleBuffSelect = null; } this._killPeach?.unbind(); this._killPeach = null; this._weaponDropFx?.unbind(); this._weaponDropFx = null; AiBattleBridge.inst.setActiveSkillCastFxHandler(null); this._aiActiveSkillFx?.unbind(); this._aiActiveSkillFx = null; this._peachCoinFly?.unbind(); this._peachCoinFly = null; this._menShenTrap?.unbind(); this._menShenTrap = null; getBattleGameMonsterCrisisHintPresenter().unbind(); this._itemAd?.unbind(); this._itemAd = null; this._baseThunderFx?.unbind(); this._baseThunderFx = null; getBattleBuffBaseThunderHost().unbind(); this._bossIntro?.unbind(); this._bossIntro = null; this._dailyShopSkillView = null; this._passiveSkillView = null; this._otherSkillView = null; this._otherOwnedDesc = null; this._unbindFirstPlaceCombat(); this.unschedule(this._realignFirstGuideFinger); this._firstGuide.unbind(); this._bagView?.unbind(); this._bagView = null; this._clearInkstoneSpine(); AiBattleBridge.inst.stop(); this._monsterPresenter.unbind(); this._campHpBar.unbind(); this._battle?.session.setBridge(undefined); this._movementBridge = null; this._mapHost?.unloadMap(); this._mapHost = null; this._battle = null; this._combatStarted = false; this._stopRefreshBounce(); this._refreshBounceHinted = false; this._illustratedHintShownThisBattle = false; this._peachUiGold = -1; this._peachUiCost = -1; this._peachUiCoinAdUsed = -1; this._coinAdVideoIcon = null; if (this._coinAdPending) { this._coinAdPending = false; BattleUiPauseGate.release(COIN_AD_PAUSE_TOKEN); } this._waveUiIdx = -999; this._nextWaveMiss = -2; this._waveGoldIndex = -2; this._aiSkillUiFrame = 0; this._lbWaveTip = null; this._lbWaveTipTime = null; } /** 第 5 波且未点击过图鉴时显示 ui_illustrated */ private _tryShowIllustratedHintOnWave(waveIndex: number): void { if (this._illustratedHintShownThisBattle) return; if (!BattleIllustratedGuideManager.shouldShowHintAtWave(waveIndex)) return; this._illustratedHintShownThisBattle = true; this._syncIllustratedHint(true); } /** 同步 ui_illustrated 显隐 */ private _syncIllustratedHint(visible: boolean): void { if (!this.ui_illustrated?.isValid) return; this.ui_illustrated.active = visible; } /** 监听:隐藏图鉴引导 */ private _onHideIllustratedHint = (): void => { this._syncIllustratedHint(false); }; click_btnAutoM(): void { } click_btnBattleSpeed(): void { } /** 底部金币:看视频加币 */ click_btnCoin(): void { this._tryWatchCoinAd(); } /** 旧入口:转调看视频加币 */ click_btnGetMoneyAd(): void { this._tryWatchCoinAd(); } /** 观看激励视频,按 Const38 下一次数量加金币 */ private _tryWatchCoinAd(): void { const battle = this._battle; if (!battle || this._coinAdPending) return; if (!battle.canUseCoinAd()) return; this._coinAdPending = true; BattleUiPauseGate.acquire(COIN_AD_PAUSE_TOKEN); gg.sdk.showVideoAd((res) => { if (!this._coinAdPending) return; this._coinAdPending = false; BattleUiPauseGate.release(COIN_AD_PAUSE_TOKEN); if (game.isPaused()) game.resume(); if (!res) { gg.ui.showToast('观看视频失败'); return; } const b = this._battle; if (!b?.canUseCoinAd()) return; b.IsWatchAd = true; const amount = b.consumeCoinAd(); if (!(amount > 0)) { console.log('[UIBattleGame] 金币广告入账失败'); return; } const curWaveNum = gg.game.CurentBattle.CurentWaveNum; gg.sdk.reportDY('inLevel', `${gg.game.getReportDYBattleCountLabel(curWaveNum)}-AD-广告获得金币`); this._refreshPeachUi(); }); } click_btnIllustrated(): void { BattleIllustratedGuideManager.markClicked(); gg.ui.openPanel(UIID.PopIllustrated); } click_btnPause(): void { gg.ui.popPanel(UIID.BattlePause); } /** 召唤:按当前空开启格补字,最多 5 个 */ click_btnRefresh(): void { const battle = this._battle; const bag = battle?.bagSession; if (!battle || !bag) { console.log('[UIBattleGame] 背包会话未就绪'); return; } if (this._bagView?.isBagEntranceBusy()) { // 入场未正常收尾时再点:先结算待定倍增,再解锁刷新 this._bagView.clearBagEntranceBusy(); this._flushPendingDigitDouble(); } // 字从刷新按钮抛物线落入本次新补的空格;已有字的格子不重刷 const ink = this._bagView?.getInkstoneWorldPosition(); const fromWorld = ink ? new Vec3(ink.x, ink.y, ink.z) : (this.btnRefresh?.isValid ? this.btnRefresh.worldPosition.clone() : null); const result = bag.tryFillEmptyOpenSlots(battle); if (result === 'ok') { this._refreshPeachUi(); this._firstGuide.onSummonOk(); const filled = bag.lastFilledSlotIndices(); this._pendingDigitDoubleSlots = filled.slice(); this._bagView?.playNewSlotEntrance(filled, fromWorld); // 无飞入动画时入场结束回调不会触发,立刻结算倍增 if (!this._bagView?.isBagEntranceBusy()) { this._flushPendingDigitDouble(); } return; } this._bagView?.clearBagEntranceBusy(); this._pendingDigitDoubleSlots = []; if (result === 'no_slot') { gg.ui.showToast('暂无空格'); } else if (result === 'not_enough') { gg.ui.showToast('金币不足'); } else { gg.ui.showToast('刷新失败'); console.log('[UIBattleGame] tryFillEmptyOpenSlots failed', result); } this._refreshPeachUi(); } }