import type { Node } from 'cc'; import { aiLog, aiOp, aiWarn } from '../runtime/AiLog'; import type { ProjectBattleHandle } from '../../runtime/BattleRuntime'; import type { MapManager } from '../../map/legacy/MapManager'; import type { AiMatchProfile } from '../match/AiMatchProfile'; import type { BattleBagSession } from '../../bag/BattleBagSession'; import type { DeploySync } from '../../runtime/DeploySync'; import { AiMatchService } from '../match/AiMatchService'; import { AiBattlePersonalityFactory, AiTextPolicyType, } from '../runtime/AiBattlePersonality'; import { AiHeadlessBagHost } from '../runtime/AiHeadlessBagHost'; import { AiOpponentController } from '../runtime/AiOpponentController'; import { AiHeroWeaponRegistry } from '../runtime/AiHeroWeaponRegistry'; import { AiHeroWeaponAssigner } from '../runtime/AiHeroWeaponAssigner'; import { AiSkillHost } from '../runtime/AiSkillHost'; import { AiMapTextHost } from '../runtime/AiMapTextHost'; import { AiPassiveCdHost, type AiPassiveCdUiSlot } from '../runtime/AiPassiveCdHost'; import { AiActiveSkillHost, type AiActiveSkillUiSlot, type AiActiveSkillCastFxHandler } from '../runtime/AiActiveSkillHost'; import { AiAdHost } from '../runtime/AiAdHost'; import type { AiMatchBuildInput } from '../types/AiTypes'; import { setAiHeroWeaponResolver } from '../../combat/TowerLoadoutService'; import { SkillAtkBuffBoardViewBind } from '../../combat/SkillAtkBuffBoardBridge'; import { AiMatchPlayerBuffCounts } from '../match/AiMatchPlayerBuffCounts'; import { BattleBuffCarryStore } from '../../../manager/battleBuff/BattleBuffCarryStore'; import { getBattleBuffMoveSpeedHost } from '../../combat/BattleBuffMoveSpeedHost'; import { getBattleBuffUnitAttackSpeedHost } from '../../combat/BattleBuffUnitAttackSpeedHost'; import { getBattleBuffBaseThunderHost } from '../../combat/BattleBuffBaseThunderHost'; import { getBattleBuffDigitStatHost } from '../../combat/BattleBuffDigitStatHost'; /** AI 模块唯一对外集成层:匹配档案 -> 局内 tick */ export class AiBattleBridge { private static _instance: AiBattleBridge | null = null; private readonly _matchService = new AiMatchService(); private readonly _personalityFactory = new AiBattlePersonalityFactory(); private readonly _bagHost = new AiHeadlessBagHost(); private readonly _controller = new AiOpponentController(); private readonly _weaponRegistry = new AiHeroWeaponRegistry(); private readonly _weaponAssigner = new AiHeroWeaponAssigner(); private readonly _skillHost = new AiSkillHost(); private readonly _mapText = new AiMapTextHost(); private readonly _passiveCd = new AiPassiveCdHost(); private readonly _activeSkill = new AiActiveSkillHost(); private readonly _adHost = new AiAdHost(); private _battle: ProjectBattleHandle | null = null; private _map: MapManager | null = null; private _profile: AiMatchProfile | null = null; /** 单例入口 */ static get inst(): AiBattleBridge { if (!AiBattleBridge._instance) { AiBattleBridge._instance = new AiBattleBridge(); } return AiBattleBridge._instance; } /** 匹配服务 */ get matchService(): AiMatchService { return this._matchService; } /** 当前局匹配档案;未开战为 null */ get profile(): AiMatchProfile | null { return this._profile; } /** AI 无头背包会话;未 bind 时为 null */ get aiBagSession(): BattleBagSession | null { return this._bagHost.bagSession; } /** AI 部署同步;未 bind 时为 null */ get aiDeploySync(): DeploySync | null { return this._bagHost.deploySync; } /** * 妙手神偷:摘除 AI 地图字块视图(不销毁)。 * @param gridIndex 地图格索引 */ detachAiMapTextGrid(gridIndex: number): import('../../view/TextGrid').TextGrid | null { return this._mapText.detachGridForSteal(gridIndex); } /** * AI 妙手神偷落地:挂接飞行字块到 AI 地图格。 * @param gridIndex 落点格 * @param grid 飞行字块 */ attachAiStolenMapTextGrid(gridIndex: number, grid: import('../../view/TextGrid').TextGrid): void { this._mapText.attachStolenGrid(gridIndex, grid); } /** * 遍历 AI 地图托管字块。 * @param visitor 回调 */ forEachAiMapTextGrid(visitor: (grid: import('../../view/TextGrid').TextGrid, gridIndex: number) => void): void { this._mapText.forEachGrid(visitor); } /** * 敌方地图字块点击回调(详情 / 攻击范围)。 * @param handler 点击回调;null 清除 */ setAiMapTextClickHandler(handler: ((grid: import('../../view/TextGrid').TextGrid) => void) | null): void { this._mapText.setMapClickHandler(handler); } /** * 绑定 AI 字块跳飞层(与玩家 ui_fly 相同)。 * @param layer 飞行层 */ setMapTextFlyLayer(layer: Node | null): void { this._mapText.setFlyLayer(layer); } /** * 绑定 AI 地图字专用层(与玩家 ui_textGrids 相同)。 * @param layer 字块层 */ setTextGridLayer(layer: Node | null): void { this._mapText.setTextGridLayer(layer); } /** AI 盘面变更后刷新地图字与技能绑定 */ notifyAiBoardChanged(): void { this._onBoardChanged(); } /** * AI 主动技能 UI 快照(CD + 展示 buff)。 * @returns 槽列表;未启动为空 */ snapshotActiveSkillUi(): AiActiveSkillUiSlot[] { return this._activeSkill.snapshotUiSlots(); } /** * 注册 AI 主动施放表现(图标飞向受体)。 * @param handler 回调;null 清除 */ setActiveSkillCastFxHandler(handler: AiActiveSkillCastFxHandler | null): void { this._activeSkill.setCastFxHandler(handler); } /** * 注册 AI 金矿飞币表现。 * @param handler 回调;null 清除 */ setGoldMineCoinFlyHandler(handler: import('../runtime/AiPassiveCdHost').AiGoldMineCoinFlyHandler | null): void { this._passiveCd.setGoldMineCoinFlyHandler(handler); } /** * 注册 AI 妙手神偷神偷手表现。 * @param handler 回调;null 清除 */ setStealGlyphFxHandler(handler: import('../runtime/AiActiveSkillHost').AiStealGlyphFxHandler | null): void { this._activeSkill.setStealFxHandler(handler); } /** * 注册 AI 幻魂镜表现。 * @param handler 回调;null 清除 */ setConfuseHeroFxHandler(handler: import('../runtime/AiActiveSkillHost').AiConfuseHeroFxHandler | null): void { this._activeSkill.setConfuseFxHandler(handler); } /** * AI CD 被动 UI 快照。 * @returns 槽列表;未启动为空 */ snapshotPassiveCdUi(): AiPassiveCdUiSlot[] { return this._passiveCd.snapshotUiSlots(); } /** * 开战前构建匹配档案。 * @param input 玩家 buff 数量等;缺省读 DailyShop 携带数 */ buildMatchProfile(input?: AiMatchBuildInput): AiMatchProfile | null { const counts = AiMatchPlayerBuffCounts.read('匹配读携带'); const profile = this._matchService.buildProfile(input ?? { playerActiveBuffCount: counts.active, playerPassiveBuffCount: counts.passive, }); this._profile = profile; return profile; } /** * 建波前预挂:局内三选一后赛前不再灌携带(保留空实现供旧调用兼容)。 */ prefetchCarryForWaveBuild(): void { // 局内解锁三选一获得 buff,不再赛前写入 } /** * 匹配页已抽出的对手写入档案(开战重抽 buff 后保留展示对手)。 * @param opponentRankId 对手段位 * @param opponentStars 对手总星 */ applyPendingOpponent(opponentRankId: number, opponentStars: number): void { const p = this._profile; if (!p) return; const rankId = Math.floor(Number(opponentRankId) || 0); if (rankId <= 0) return; const stars = Math.max(0, Math.floor(Number(opponentStars) || 0)); this._profile = { playerRankId: p.playerRankId, playerStars: p.playerStars, opponentRankId: rankId, opponentStars: stars, starOffset: p.starOffset, activeBuffCount: p.activeBuffCount, passiveBuffCount: p.passiveBuffCount, activeBuffIds: p.activeBuffIds, passiveBuffIds: p.passiveBuffIds, seed: p.seed, }; aiOp(`锁定匹配页对手 rank=${rankId} stars=${stars}(AI buff 用开战重抽结果)`); } /** 是否已绑定战斗并在跑决策 */ get running(): boolean { return !!this._battle && this._controller.running; } /** * 战斗就绪后启动 AI。 * @param battle 战斗运行时句柄 * @param map 地图 * @param profile 本局档案;缺省使用已缓存 profile * @param skillParent 挂载 AI SkillController 的 UI 节点 * @param bagLayout 玩家 ui_textList(AI 大地块槽位/邻格几何) */ start(battle: ProjectBattleHandle, map: MapManager, profile?: AiMatchProfile, skillParent?: Node | null, bagLayout?: Node | null): boolean { try { return this._startInner(battle, map, profile, skillParent, bagLayout); } catch (e) { console.error('[AiBattleBridge] start 异常,对手 AI 未启动', String(e)); return false; } } /** * start 主体(异常由 start 捕获)。 * @param battle 战斗 * @param map 地图 * @param profile 档案 * @param skillParent 技能父节点 * @param bagLayout 背包布局 */ private _startInner(battle: ProjectBattleHandle, map: MapManager, profile?: AiMatchProfile, skillParent?: Node | null, bagLayout?: Node | null): boolean { let resolved = profile ?? this._profile; if (!resolved) { resolved = this.buildMatchProfile(); } if (!resolved) { console.warn('[AiBattleBridge] profile missing,使用应急档案'); resolved = this._emergencyProfile(battle); this._profile = resolved; } if (resolved.opponentRankId !== battle.opponentRankId) { console.warn( '[AiBattleBridge] profile.opponentRankId != battle.opponentRankId', resolved.opponentRankId, battle.opponentRankId, ); } this._mountAiBuffs(resolved, battle); let personality = this._personalityFactory.roll(resolved.opponentRankId, resolved.seed); if (!personality) { console.warn('[AiBattleBridge] personality 失败,使用默认', resolved.opponentRankId); personality = { textPolicy: AiTextPolicyType.Type1, }; } if (!this._bagHost.bind(resolved, battle, map, bagLayout ?? null)) { console.error('[AiBattleBridge] start aborted: headless bag bind failed'); return false; } this._weaponRegistry.clear(); this._battle = battle; this._map = map; this._profile = resolved; this._mapText.setAfterFlush(this._refreshAiSkills); if (skillParent) { this._skillHost.bind(battle, map, skillParent, this._mapText.getMapGrids()); } else { console.warn('[AiBattleBridge] skillParent missing: AI towers may not attack'); } this._controller.start( resolved, personality, this._bagHost, battle, map, this._onBoardChanged, this._tryRecycleSurplusBag, this._mapText, ); this._passiveCd.bind(this._bagHost, battle, this._onBoardChanged); this._adHost.bind(this._bagHost, battle, map, resolved.opponentRankId, this._onBoardChanged); this._onBoardChanged(); setAiHeroWeaponResolver(this._resolveAiWeaponForLoadout); SkillAtkBuffBoardViewBind.bindEnemyNotify(this._onBoardChanged); console.warn( '[AiBattleBridge] 就绪', 'rank=' + resolved.opponentRankId, 'gold=' + this._bagHost.aiGold, 'nextCost=' + this._bagHost.nextRefreshCost, 'text=' + personality.textPolicy, ); return true; } /** * 匹配建档全失败时的最小档案(无 buff,仍可走数字合成/解锁)。 * @param battle 战斗 */ private _emergencyProfile(battle: ProjectBattleHandle): AiMatchProfile { const rankId = Math.max(1, Math.floor(Number(battle.opponentRankId) || Number(battle.playerRankId) || 1)); return { playerRankId: Math.max(1, Math.floor(Number(battle.playerRankId) || 1)), playerStars: 1, opponentRankId: rankId, opponentStars: 1, starOffset: 0, activeBuffCount: 0, passiveBuffCount: 0, activeBuffIds: [], passiveBuffIds: [], seed: (Date.now() % 100000) || 1, }; } /** 棋盘被外部改写后刷新 AI 字块与技能绑定 */ notifyBoardChanged(): void { this._onBoardChanged(); } /** 部署/落子后:刷新技能绑定 + 地图字 TextGrid */ private readonly _onBoardChanged = (): void => { const map = this._map; const bag = this._bagHost.bagSession; const battle = this._battle; if (!map || !bag || !battle) return; const aiPlayer = battle.getPlayer(AiHeadlessBagHost.AI_PLAYER_ID) ?? null; this._mapText.requestSync(bag, map, aiPlayer); this._refreshAiSkills(); }; /** 字块表就绪后刷新 AI Skill 绑定 */ private readonly _refreshAiSkills = (): void => { const map = this._map; if (!map) return; this._skillHost.refresh(map, this._mapText.getMapGrids()); }; /** 刷新拍:乾坤袋回收背包多余字 */ private readonly _tryRecycleSurplusBag = (): boolean => { return this._activeSkill.tryRecycleSurplusBag(); }; /** * 供 TowerLoadout 注册的 AI 武器解析(避免循环 require)。 * @param heroId Hero.id */ private readonly _resolveAiWeaponForLoadout = (heroId: number): number | null => { return this.getHeroBattleWeaponId(heroId); }; /** 战斗结束或切场景时停止 AI */ stop(): void { setAiHeroWeaponResolver(null); SkillAtkBuffBoardViewBind.bindEnemyNotify(null); this._controller.stop(); this._passiveCd.dispose(); this._activeSkill.dispose(); this._adHost.dispose(); this._mapText.setAfterFlush(null); this._mapText.clear(); this._skillHost.dispose(); this._bagHost.dispose(); this._weaponRegistry.clear(); BattleBuffCarryStore.clear(); this._battle = null; this._map = null; } /** * 局内三选一:赛前不灌匹配 buff,仅重绑 Host。 * @param profile 档案 * @param battle 战斗 */ private _mountAiBuffs(profile: AiMatchProfile, battle: ProjectBattleHandle): void { const aiPid = AiHeadlessBagHost.AI_PLAYER_ID; BattleBuffCarryStore.setFromBuffIds(aiPid, [], []); const gmPassive = gg.game.gmAiPassiveBuffIds; const gmActive = gg.game.gmAiActiveBuffIds; let seeded = 0; if (gmActive?.length) { for (let i = 0; i < gmActive.length; i++) { if (BattleBuffCarryStore.appendBuffId(aiPid, gmActive[i])) seeded++; } } if (gmPassive?.length) { for (let i = 0; i < gmPassive.length; i++) { if (BattleBuffCarryStore.appendBuffId(aiPid, gmPassive[i])) seeded++; } } if (seeded > 0) { aiOp(`GM 覆盖 AI buff 灌入 count=${seeded} profilePassive=[${profile.passiveBuffIds.join(',')}]`); } else { aiOp(`AI buff 赛前清空(局内解锁三选一)profilePassive=[${profile.passiveBuffIds.join(',')}]`); } getBattleBuffMoveSpeedHost().bind(battle); getBattleBuffUnitAttackSpeedHost().bind(battle); getBattleBuffBaseThunderHost().refreshCarry(battle); getBattleBuffDigitStatHost().bind(battle); } /** * AI 战斗武器 id(旧 aiweapon 分配已下线,非 null 时亦可能为 0)。 * @param heroId Hero.id */ getHeroBattleWeaponId(heroId: number): number | null { if (!this._profile) { aiWarn(`[AiHeroWeapon] skip: profile missing heroId=${heroId}`); return null; } const record = this._weaponAssigner.resolveOrAssign( this._weaponRegistry, heroId, this._profile.opponentRankId, this._profile.seed, ); return record != null ? record.weaponId : null; } /** * 每帧 tick。 * @param dtSec 帧间隔(秒) */ tick(dtSec: number): void { if (!this._battle) return; // 结算/会话已结束:只停决策,盘面留给 UI teardown 再 dispose if (this._battle.IsGameOver || this._battle.session.ended) { if (this._controller.running) { this._controller.stop(); this._passiveCd.dispose(); this._activeSkill.dispose(); this._adHost.dispose(); aiOp('局结束,停止决策'); } return; } // 暂停、等复活:保留状态但不决策 / 不走 CD if (this._battle.IsGamePause) return; this._passiveCd.tick(dtSec); this._adHost.tick(dtSec); this._controller.tick(dtSec); } }