|
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|