import { BattleBagSession } from '../../bag/BattleBagSession'; import { aiLog, aiOp, aiWarn } from './AiLog'; import { DeploySync } from '../../runtime/DeploySync'; import type { ProjectBattleHandle } from '../../runtime/BattleRuntime'; import type { MapManager } from '../../map/legacy/MapManager'; import type { AiMatchProfile } from '../match/AiMatchProfile'; import { AI_PLACE_CAMP, AI_PLAYER_ID, AI_VIEW_LOCAL_CAMP } from './AiCampArea'; import { WeaponComposeConfigProvider } from '../../bag/WeaponComposeConfigProvider'; import { textDisplayName } from '../../config/BattleHeroTable'; /** playerId=1 无头 BattleBagSession + 字池 + DeploySync(需求 A0) */ export class AiHeadlessBagHost { /** 对手 playerId */ static readonly AI_PLAYER_ID = AI_PLAYER_ID; private _bagSession: BattleBagSession | null = null; private _deploySync: DeploySync | null = null; private _battle: ProjectBattleHandle | null = null; /** 无头背包会话;未 bind 时为 null */ get bagSession(): BattleBagSession | null { return this._bagSession; } /** AI 部署同步 */ get deploySync(): DeploySync | null { return this._deploySync; } /** * 绑定战斗、建池、首轮扣桃刷新。 * @param profile 本局匹配档案 * @param battle 战斗句柄 * @param map 地图 */ bind(profile: AiMatchProfile, battle: ProjectBattleHandle, map: MapManager): boolean { this.dispose(); if (!map?.gridsInitialized) { aiLog('[AiHeadlessBagHost] bind failed: map not ready'); return false; } this._logCampPutGrids(map); const bag = new BattleBagSession(map, battle.levelConfigId, battle.session, 5, { rankId: profile.opponentRankId, ownerPlayerId: AiHeadlessBagHost.AI_PLAYER_ID, }); const result = bag.tryNormalRefresh(battle.levelConfigId, battle); if (result !== 'ok') { aiOp(`首刷失败 ${result} rankId=${profile.opponentRankId} gold=${battle.session.players[AiHeadlessBagHost.AI_PLAYER_ID]?.gold}`); return false; } const deploy = new DeploySync(); deploy.bind(battle, map, bag, AiHeadlessBagHost.AI_PLAYER_ID, false); this._bagSession = bag; this._deploySync = deploy; this._battle = battle; const aiPlayer = battle.getPlayer(AiHeadlessBagHost.AI_PLAYER_ID); if (aiPlayer) { aiPlayer.attachBagSession(bag); } else { aiLog('[AiHeadlessBagHost] AI Player missing, compositions query unavailable'); } aiLog( `[AiHeadlessBagHost] bound aiRank=${profile.opponentRankId}` + ` gold=${battle.session.players[AiHeadlessBagHost.AI_PLAYER_ID]?.gold}` + ` nextCost=${bag.normalRefreshCost}` + ` placeCamp=${AI_PLACE_CAMP}`, ); this._logBagSlots('firstRefresh'); return true; } /** 有桃且字池够则再刷一次(耗尽返回 pool_empty) */ tryRefresh(): 'ok' | 'not_enough' | 'failed' | 'pool_empty' { if (!this._bagSession || !this._battle) return 'failed'; const gold = this._battle.session.players[AiHeadlessBagHost.AI_PLAYER_ID]?.gold ?? -1; const cost = this._bagSession.normalRefreshCost; if (gold < cost) { return 'not_enough'; } const result = this._bagSession.tryNormalRefresh(this._battle.levelConfigId, this._battle); if (result === 'ok') { this._logBagSlots('refresh'); } else { aiOp(`刷新结果 result=${result} aiGold=${gold} need=${cost}`); } return result; } /** 下次刷新费用(Const 4/5) */ get nextRefreshCost(): number { return this._bagSession?.normalRefreshCost ?? -1; } /** AI 当前桃子 */ get aiGold(): number { return this._battle?.session.players[AiHeadlessBagHost.AI_PLAYER_ID]?.gold ?? -1; } /** 打印当前 AI 背包 5 槽内容(中文名);刷新结果始终打印 */ private _logBagSlots(tag: string): void { const bag = this._bagSession; if (!bag) { aiOp(`背包[${tag}] session missing`); return; } WeaponComposeConfigProvider.instance.load(); const slots = bag.getBagSlots(); const parts: string[] = []; let filled = 0; for (let i = 0; i < slots.length; i++) { const s = slots[i]; if (!s) { parts.push(`[${i}]=空`); continue; } filled++; parts.push(`[${i}]=${this._resolveTileName(s.weapon2Id)}`); } const line = `背包[${tag}] filled=${filled}/${slots.length} gold=${this._battle?.session.players[AiHeadlessBagHost.AI_PLAYER_ID]?.gold}` + ` | ${parts.join(' ')}`; // 操作相关背包变动始终打印,便于排查摆放/取舍 aiOp(line); } /** * 背包参与移动后打印源目标与当前槽位。 * @param tag 动作标签 * @param source 拖拽源 * @param target 放置目标 */ logBagMove(tag: string, source: { kind: string; slotIndex?: number; gridIndex?: number }, target: { kind: string; slotIndex?: number; gridIndex?: number }): void { const src = source.kind === 'bag' ? `bag[${source.slotIndex}]` : `map[${source.gridIndex}]`; const dst = target.kind === 'bag' ? `bag[${target.slotIndex}]` : `map[${target.gridIndex}]`; this._logBagSlots(`${tag} ${src}->${dst}`); } /** Text 表中文名;缺表回退 id */ private _resolveTileName(weapon2Id: number): string { const row = WeaponComposeConfigProvider.instance.getWeapon2Config(weapon2Id); if (!row) return `id=${weapon2Id}`; const name = textDisplayName(row).trim(); return name || `id=${weapon2Id}`; } /** 释放背包与会话引用 */ dispose(): void { const aiPlayer = this._battle?.getPlayer(AiHeadlessBagHost.AI_PLAYER_ID); if (aiPlayer) { aiPlayer.attachBagSession(null); aiPlayer.skillController = null; } this._deploySync?.unbind(); this._deploySync = null; this._bagSession = null; this._battle = null; } /** 开战打印各 CampArea 可放字格 / 可摆格数量 */ private _logCampPutGrids(map: MapManager): void { const grids = map.mapGrids; if (!Array.isArray(grids)) { aiLog('[AiHeadlessBagHost] mapGrids missing'); return; } let localText = 0; let enemyText = 0; let localGrid = 0; let enemyGrid = 0; let localWalk = 0; let enemyWalk = 0; let dualPutDig = 0; for (let i = 0; i < grids.length; i++) { const mg = grids[i]; if (!mg?.node?.isValid || !mg.node.active) continue; const camp = mg.CampAreaID | 0; // 可放置优先:勾了 putText 即计可放(含 dig 双标) if (mg.isCanPutText && !mg.isCanWalk) { if (camp === AI_VIEW_LOCAL_CAMP) localText++; else if (camp === AI_PLACE_CAMP) enemyText++; if (mg.isCanBeDig && camp === AI_PLACE_CAMP) dualPutDig++; } if (mg.isCanPutGrid && mg.isCanWalk) { if (camp === AI_VIEW_LOCAL_CAMP) localGrid++; else if (camp === AI_PLACE_CAMP) enemyGrid++; } if (mg.isCanWalk) { if (camp === AI_VIEW_LOCAL_CAMP) localWalk++; else if (camp === AI_PLACE_CAMP) enemyWalk++; } } aiLog( `[AiHeadlessBagHost] mapCamp textPut: local(1)=${localText} enemy(2)=${enemyText}` + ` | gridPut: local(1)=${localGrid} enemy(2)=${enemyGrid}` + ` | walk: local(1)=${localWalk} enemy(2)=${enemyWalk}`, ); if (dualPutDig > 0) { aiWarn(`地图 dig+putText 双标 enemy=${dualPutDig}(已按可放置优先当 Put)`); } if (enemyText <= 0) { aiLog('[AiHeadlessBagHost] enemy CampAreaID=2 has no isCanPutText cell; AI text cannot place'); } } }