//********************* // 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 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 { 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 { BattleGameBindNetTrapController } from './BattleGameBindNetTrapController'; import { BattleGameFireTrapController } from './BattleGameFireTrapController'; import { BattleGameRecycleToPeachPresenter } from './BattleGameRecycleToPeachPresenter'; import { BattleGameRewriteGlyphPresenter } from './BattleGameRewriteGlyphPresenter'; import { BattleGameAttackSpeedFxPresenter } from './BattleGameAttackSpeedFxPresenter'; import { BattleGameRandomLevelFxPresenter } from './BattleGameRandomLevelFxPresenter'; import { BattleGameUnitLevelUpFxPresenter } from './BattleGameUnitLevelUpFxPresenter'; import { BattleGameAttackRangeFxPresenter } from './BattleGameAttackRangeFxPresenter'; import { BattleGameExtraBossFxPresenter } from './BattleGameExtraBossFxPresenter'; import { BattleGameConfuseHeroFxPresenter } from './BattleGameConfuseHeroFxPresenter'; import { BattleGameStealGlyphFxPresenter } from './BattleGameStealGlyphFxPresenter'; import { BattleGameDailyShopSkillClick } from './BattleGameDailyShopSkillClick'; import { BattlePeachCoinFlyPresenter } from './BattlePeachCoinFlyPresenter'; import { BattleWeaponDropPresenter } from './BattleWeaponDropPresenter'; import { BattleGameItemAdPresenter } from './BattleGameItemAdPresenter'; import { BattleGameMenShenTrapController } from './BattleGameMenShenTrapController'; import { getBattleGameRuoshuiBuffPresenter } from './BattleGameRuoshuiBuffPresenter'; import { getBattleGameMonsterCrisisHintPresenter } from './BattleGameMonsterCrisisHintPresenter'; import { BattleGameKillPeachPresenter } from './BattleGameKillPeachPresenter'; import { getBattleBuffUnitReviveHost } from '../../battle/combat/BattleBuffUnitReviveHost'; import { getBattleBuffBaseThunderHost } from '../../battle/combat/BattleBuffBaseThunderHost'; import { getBattleBuffBossInterruptHost } from '../../battle/combat/BattleBuffBossInterruptHost'; import { BattleGameBaseThunderPresenter } from './BattleGameBaseThunderPresenter'; import { BattleGameBossInterruptFxPresenter } from './BattleGameBossInterruptFxPresenter'; import { BattleGameBossIntroPresenter } from './BattleGameBossIntroPresenter'; import { AiBattleBridge } from '../../battle/ai/bridge/AiBattleBridge'; import type { AiActiveSkillCastFxInfo, AiConfuseHeroFxHandler } 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 { 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; /** 召唤按钮 bg fillRange 变化 tween 时长(秒) */ const REFRESH_FILL_TWEEN_SEC = 0.2; /** btnRefresh/bg2 fillRange 下限(负值,接近凑够时不再小于 -0.1) */ const REFRESH_FILL_RANGE_MIN = -0.1; /** 召唤按钮进度满后 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 = '待机'; /** 砚台 Spine:资源里名字取反,出完播「出字」待机 */ const INKSTONE_SPINE_IDLE = '出字'; @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; /** 召唤按钮填充进度(btnRefresh/bg2,水平从右到左) */ private _refreshFill: Sprite | null = null; /** 召唤按钮 bg2 目标 fillRange(= -(1 - 桃子进度)) */ private _refreshFillTarget = 0; /** 召唤按钮可刷新态(bg3) */ private _refreshBg3: Node | 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 _bindNetTrap: BattleGameBindNetTrapController | null = null; private _fireTrap: BattleGameFireTrapController | null = null; /** 门神盾牌陷阱 */ private _menShenTrap: BattleGameMenShenTrapController | null = null; /** btnItem 门神盾牌广告 */ private _itemAd: BattleGameItemAdPresenter | null = null; /** 九霄神雷被动落雷表现 */ private _baseThunderFx: BattleGameBaseThunderPresenter | null = null; /** 碎法帝钟 Boss 打断表现 */ private _bossInterruptFx: BattleGameBossInterruptFxPresenter | null = null; /** 敌方 Boss 出场弹窗 */ private _bossIntro: BattleGameBossIntroPresenter | null = null; /** 乾坤袋:skill 拖到字块回收 */ private _recycleToPeach: BattleGameRecycleToPeachPresenter | null = null; /** 文昌笔:skill 拖到字块改字前置动画 */ private _rewriteGlyph: BattleGameRewriteGlyphPresenter | null = null; /** 攻速符:skill 拖到字块施法表现 */ private _attackSpeedFx: BattleGameAttackSpeedFxPresenter | null = null; /** 授仙箓:skill 拖到字块升/降级表现 */ private _randomLevelFx: BattleGameRandomLevelFxPresenter | null = null; /** 封神榜:skill 拖到字块升级双层表现 */ private _unitLevelUpFx: BattleGameUnitLevelUpFxPresenter | null = null; /** 千里眼:skill 拖到字块施法表现 */ private _attackRangeFx: BattleGameAttackRangeFxPresenter | null = null; /** 万神幡:skill 拖到己方出怪口召唤 Boss */ private _extraBossFx: BattleGameExtraBossFxPresenter | null = null; /** 幻魂镜:skill 拖到敌方神将迷惑停攻 */ private _confuseHeroFx: BattleGameConfuseHeroFxPresenter | null = null; /** 妙手神偷:skill 拖到敌方单字偷取 */ private _stealGlyphFx: BattleGameStealGlyphFxPresenter | null = null; /** 点击类主动 buff(万神幡等) */ 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(); /** 波间提示:第 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; /** 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_IDLE, true); }; /** 尽早关掉召唤按钮 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._refreshFill = this.btnRefresh?.getChildByName('bg2')?.getComponent(Sprite) ?? this.btnRefresh?.getChildByName('bg')?.getComponent(Sprite) ?? null; this._refreshBg3 = this.btnRefresh?.getChildByName('bg3') ?? null; this._disableRefreshBreathe(); this.btnRefresh?.getScale(this._refreshBtnScaleHome); this._setupRefreshFillSprite(); this._lbWaveTip = this.ui_nextWaveTime?.getChildByName('lb1')?.getComponent(Label) ?? null; this._lbWaveTipTime = this.ui_nextWaveTime?.getChildByName('lb2')?.getComponent(Label) ?? null; 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._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._bindNetTrap?.tick(dt); this._fireTrap?.tick(dt); 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._bindBossInterruptFx(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); } else { console.error('[UIBattleGame] AiBattleBridge.start 失败,对手 AI 未启动'); } } catch (e) { console.error('[UIBattleGame] AiBattleBridge.start 异常', String(e)); } this._campHpBar.bind(battle, map); getBattleGameRuoshuiBuffPresenter().refresh(); getBattleGameMonsterCrisisHintPresenter().bind(battle, map); this._firstGuide.bind(this.ui_guide, battle.bagSession); PerfSampler.beginBattle(); this._listenFirstPlaceCombat(); this._refreshWaveUi(); } /** * 开局不立刻出怪:本机首次放数字开怪,或超时后强制开怪。 */ 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); } } /** * 击杀掉桃表现:本机飞向 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/死亡(优先用节点位,否则用缓存世界坐标) */ private _playMonsterDeathFx(victim: EntityId): void { if (getBattleBuffUnitReviveHost().shouldSkipDeathFxOnKill(victim)) { // 还魂暂缓期间已播过死亡特效,真死时跳过重复播放 return; } 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?: { type?: number } } | null; if (view?.Data?.type === MonsterType.TreasureBox) 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, ); // 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._bindNetTrap?.unbind(); this._bindNetTrap = new BattleGameBindNetTrapController(); this._bindNetTrap.bind(battle, map); this._fireTrap?.unbind(); this._fireTrap = new BattleGameFireTrapController(); this._fireTrap.bind(battle, map); this._menShenTrap?.unbind(); this._menShenTrap = new BattleGameMenShenTrapController(); this._menShenTrap.bind(battle, map, battle.localPlayerId); getBattleGameRuoshuiBuffPresenter().bind(battle, map, this._monsterPresenter, { flyLayer: this.ui_fly ?? this.ui_mapParentNode ?? null, passiveSkillView: this._passiveSkillView, otherSkillView: this._otherSkillView, }); 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._bindRecycleToPeach(); this._bindSkillPeachFly(); this._bindKillPeach(battle); this._bindRewriteGlyph(); this._bindAttackSpeedFx(); this._bindRandomLevelFx(); this._bindUnitLevelUpFx(); this._bindAttackRangeFx(); this._bindExtraBossFx(); this._bindConfuseHeroFx(); this._bindStealGlyphFx(); 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_IDLE, 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 }); } /** 绑定 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); // start→bind 会重建 ActiveSkillHost,须在 AI 启动后再挂神偷表现 AiBattleBridge.inst.setStealGlyphFxHandler(this._onAiStealGlyphFx); } /** AI 主动施放 → 飞图标 */ private readonly _onAiActiveSkillCastFx = (info: AiActiveSkillCastFxInfo): void => { this._aiActiveSkillFx?.play(info); }; /** 绑定碎法帝钟被动打断表现 */ private _bindBossInterruptFx(battle: ProjectBattleHandle): void { this._bossInterruptFx?.unbind(); this._bossInterruptFx = new BattleGameBossInterruptFxPresenter(); this._bossInterruptFx.bind(battle, this._monsterPresenter); getBattleBuffBossInterruptHost().bind(battle, this._bossInterruptFx); } /** 绑定敌方 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); } /** 绑定乾坤袋回收演出 */ private _bindRecycleToPeach(): void { this._recycleToPeach?.unbind(); this._ensurePeachCoinFly(); this._recycleToPeach = new BattleGameRecycleToPeachPresenter(); this._recycleToPeach.bind( this._bagView, this.ui_fly ?? this.ui_mapParentNode, this._peachCoinFly, ); } /** 桃树/技能产桃:从格心飞向 ui_money */ private _bindSkillPeachFly(): void { this._ensurePeachCoinFly(); this._bagView?.setSkillPeachFlyHandler(this._onSkillPeachGain); } /** * 技能产桃:本机 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, ); } /** 绑定文昌笔改字前置动画 */ private _bindRewriteGlyph(): void { this._rewriteGlyph?.unbind(); this._rewriteGlyph = new BattleGameRewriteGlyphPresenter(); this._rewriteGlyph.bind(this._bagView, this.ui_fly ?? this.ui_mapParentNode); } /** 绑定攻速符施法表现 */ private _bindAttackSpeedFx(): void { this._attackSpeedFx?.unbind(); this._attackSpeedFx = new BattleGameAttackSpeedFxPresenter(); this._attackSpeedFx.bind(this._bagView, this.ui_fly ?? this.ui_mapParentNode); } /** 绑定授仙箓升/降级表现 */ private _bindRandomLevelFx(): void { this._randomLevelFx?.unbind(); this._randomLevelFx = new BattleGameRandomLevelFxPresenter(); this._randomLevelFx.bind(this._bagView, this.ui_fly ?? this.ui_mapParentNode); } /** 绑定封神榜升级双层表现 */ private _bindUnitLevelUpFx(): void { this._unitLevelUpFx?.unbind(); this._unitLevelUpFx = new BattleGameUnitLevelUpFxPresenter(); this._unitLevelUpFx.bind(this._bagView, this.ui_fly ?? this.ui_mapParentNode); } /** 绑定千里眼施法表现 */ private _bindAttackRangeFx(): void { this._attackRangeFx?.unbind(); this._attackRangeFx = new BattleGameAttackRangeFxPresenter(); this._attackRangeFx.bind(this._bagView, this.ui_fly ?? this.ui_mapParentNode); } /** 绑定万神幡施法表现 */ private _bindExtraBossFx(): void { this._extraBossFx?.unbind(); this._extraBossFx = new BattleGameExtraBossFxPresenter(); this._extraBossFx.bind(this._battle, this._bagView); } /** 绑定幻魂镜施法表现 */ private _bindConfuseHeroFx(): void { this._confuseHeroFx?.unbind(); this._confuseHeroFx = new BattleGameConfuseHeroFxPresenter(); this._confuseHeroFx.bind(this._battle, this._bagView, this.ui_fly ?? this.ui_mapParentNode); this._battle?.charmController.setEntryReleasedHandler((composeKey) => { this._confuseHeroFx?.abortForComposeKey(composeKey); }); AiBattleBridge.inst.setConfuseHeroFxHandler(this._onAiConfuseHeroFx); } /** 绑定妙手神偷施法表现 */ private _bindStealGlyphFx(): void { this._stealGlyphFx?.unbind(); this._stealGlyphFx = new BattleGameStealGlyphFxPresenter(); this._stealGlyphFx.bind(this._bagView, this.ui_fly ?? this.ui_mapParentNode); AiBattleBridge.inst.setStealGlyphFxHandler(this._onAiStealGlyphFx); } /** AI 幻魂镜 → 对玩家神将播施法与上下层循环 */ private readonly _onAiConfuseHeroFx: AiConfuseHeroFxHandler = (target, durationSec) => { const presenter = this._confuseHeroFx; if (!presenter) return false; return presenter.tryPlayFromAi(target, durationSec); }; /** * AI 妙手神偷 → 神偷手锁定玩家字后抓取飞入。 * @param playerGridIndex 玩家源格 * @param placement AI 落点 * @param onExtracted 提取成功 */ private readonly _onAiStealGlyphFx = ( playerGridIndex: number, placement: { kind: 'map'; gridIndex: number } | { kind: 'bag'; slotIndex: number }, onExtracted: (tile: import('db://assets/mx/module/merge').MapTileRecord) => void, ): boolean => { const presenter = this._stealGlyphFx; if (!presenter) return false; return presenter.tryPlayAiSteal(playerGridIndex, placement, onExtracted); }; /** 绑定每日商店主动 buff 拖拽施放 */ private _bindDailyShopSkillDrag(): void { this._dailyShopSkillDrag?.unbind(); this._dailyShopSkillDrag = new BattleGameDailyShopSkillDrag(); this._dailyShopSkillDrag.bind( this._dailyShopSkillView, this._bagView, this._bindNetTrap, this._fireTrap, this._recycleToPeach, this._rewriteGlyph, this._attackSpeedFx, this._randomLevelFx, this._unitLevelUpFx, this._attackRangeFx, this._extraBossFx, this._confuseHeroFx, this._stealGlyphFx, 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; if (this._refreshBg3?.isValid) { this._refreshBg3.active = enough; } if (this._refreshFill?.isValid) { const fillNode = this._refreshFill.node; if (enough) { if (fillNode.active) { this._tweenRefreshFillRange(0, this._hideRefreshFill.bind(this)); } else { this._stopRefreshFillTween(); this._refreshFillTarget = 0; this._refreshFill.fillRange = 0; } } else { fillNode.active = true; const progress = cost > 0 ? Math.min(1, Math.max(0, gold / cost)) : 1; const fillRange = Math.min(REFRESH_FILL_RANGE_MIN, -(1 - progress)); this._tweenRefreshFillRange(fillRange); } } 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; } /** btnRefresh/bg2:水平从右到左;fillRange = min(-0.1, -(1 - 桃子/消耗)),凑够为 0 */ private _setupRefreshFillSprite(): void { const sprite = this._refreshFill; if (!sprite?.isValid) return; sprite.type = Sprite.Type.FILLED; sprite.fillType = Sprite.FillType.HORIZONTAL; sprite.fillStart = 1; sprite.fillRange = -1; this._refreshFillTarget = -1; } /** btnRefresh/bg2:fillRange 0.2s tween 到目标(不足时为负,凑够为 0) */ private _tweenRefreshFillRange(target: number, onComplete?: () => void): void { const sprite = this._refreshFill; if (!sprite?.isValid) return; if (Math.abs(target - this._refreshFillTarget) < 1e-4) { onComplete?.(); return; } this._refreshFillTarget = target; Tween.stopAllByTarget(sprite); const tw = tween(sprite).to(REFRESH_FILL_TWEEN_SEC, { fillRange: target }); if (onComplete) { tw.call(onComplete); } tw.start(); } /** 桃子已够:fillRange 到 0 后隐藏 bg2 */ private _hideRefreshFill(): void { const fillNode = this._refreshFill?.node; if (fillNode?.isValid) { fillNode.active = false; } } /** 停止召唤按钮 bg fillRange tween */ private _stopRefreshFillTween(): void { if (this._refreshFill?.isValid) { Tween.stopAllByTarget(this._refreshFill); } } /** 关掉预制体上的 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; } 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; } if (this.ui_nextWaveTime?.isValid) { this.ui_nextWaveTime.active = false; } } 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._recycleToPeach?.unbind(); this._recycleToPeach = null; this._bagView?.setSkillPeachFlyHandler(null); this._killPeach?.unbind(); this._killPeach = null; this._weaponDropFx?.unbind(); this._weaponDropFx = null; this._rewriteGlyph?.unbind(); this._rewriteGlyph = null; this._attackSpeedFx?.unbind(); this._attackSpeedFx = null; this._randomLevelFx?.unbind(); this._randomLevelFx = null; this._unitLevelUpFx?.unbind(); this._unitLevelUpFx = null; this._attackRangeFx?.unbind(); this._attackRangeFx = null; this._extraBossFx?.unbind(); this._extraBossFx = null; this._confuseHeroFx?.unbind(); this._confuseHeroFx = null; this._stealGlyphFx?.unbind(); this._stealGlyphFx = null; AiBattleBridge.inst.setStealGlyphFxHandler(null); AiBattleBridge.inst.setConfuseHeroFxHandler(null); AiBattleBridge.inst.setActiveSkillCastFxHandler(null); this._aiActiveSkillFx?.unbind(); this._aiActiveSkillFx = null; this._peachCoinFly?.unbind(); this._peachCoinFly = null; this._bindNetTrap?.unbind(); this._bindNetTrap = null; this._fireTrap?.unbind(); this._fireTrap = null; this._menShenTrap?.unbind(); this._menShenTrap = null; getBattleGameRuoshuiBuffPresenter().unbind(); getBattleGameMonsterCrisisHintPresenter().unbind(); this._itemAd?.unbind(); this._itemAd = null; this._baseThunderFx?.unbind(); this._baseThunderFx = null; getBattleBuffBaseThunderHost().unbind(); this._bossInterruptFx?.unbind(); this._bossInterruptFx = null; getBattleBuffBossInterruptHost().unbind(); this._bossIntro?.unbind(); this._bossIntro = null; this._dailyShopSkillView = null; this._passiveSkillView = null; this._otherSkillView = null; this._otherOwnedDesc = null; this._unbindFirstPlaceCombat(); 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._stopRefreshFillTween(); this._refreshFill = null; this._refreshBg3 = null; this._refreshFillTarget = 0; 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._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()) { // 入场未正常收尾时再点:先解锁再刷新(根因在 Animator.stop 须 settle) this._bagView.clearBagEntranceBusy(); } // 字从刷新按钮抛物线落入本次新补的空格;已有字的格子不重刷 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(); this._bagView?.playNewSlotEntrance(bag.lastFilledSlotIndices(), fromWorld); return; } this._bagView?.clearBagEntranceBusy(); 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(); } }