You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
AdLike2048/assets/script/battle/ai/runtime/AiHeadlessBagHost.ts

209 lines
8.2 KiB

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