import { Weapon2TileType, canDragMapTile, MapCellKind } from 'db://assets/mx/module/merge'; import type { AiPlannerContext, AiPlannedAction } from '../types/AiTypes'; import { AiActionKind } from '../types/AiTypes'; import { AiBoardProbe } from './AiBoardProbe'; import { WeaponComposeConfigProvider } from '../../bag/WeaponComposeConfigProvider'; import { aiOp } from '../runtime/AiLog'; /** 需求 6.4 占格互换(空位不足时) */ export class AiSwapPlanner { private readonly _probe = new AiBoardProbe(); /** * 空位不足时尝试互换;优先挤更低档位(1级普通 → 紫)。 * 背包有铲且仍有 Dig 时不互换(本拍应由铲子优先占用);无铲时允许互换,避免 Dig 未开完且刷不起包时长期空转。 * @param ctx 决策上下文 * @param allowWithEmptyPut 摆放已失败时允许有空位仍取舍(解背包积压) */ plan(ctx: AiPlannerContext, allowWithEmptyPut = false): AiPlannedAction | null { const emptyPut = this._probe.countEmptyTextPut(ctx.bagSession, ctx.map); if (emptyPut > 0 && !allowWithEmptyPut) { aiOp(`取舍跳过 emptyPut=${emptyPut}(有空位不互换)`); return null; } if (emptyPut > 0 && allowWithEmptyPut) { aiOp(`取舍放宽 emptyPut=${emptyPut}(摆放失败强制取舍)`); } const digs = this._probe.listEmptyDigGrids(ctx.bagSession, ctx.map); const shovelSlot = this._probe.findShovelSlotIndex(ctx.bagSession); if (digs.length > 0 && shovelSlot >= 0) { aiOp(`取舍跳过 dig=${digs.length} 有铲,留给铲子`); return null; } WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; const slots = ctx.bagSession.getBagSlots(); const grids = ctx.map.mapGrids; if (!Array.isArray(grids)) return null; let best: { slotIndex: number; gridIndex: number; rank: number; inQ: number; occW2: number; occLv: number } | null = null; let skippedCompose = 0; for (let s = 0; s < slots.length; s++) { const slot = slots[s]; if (!slot) continue; const srcType = Number(provider.getWeapon2Type(slot.weapon2Id)) as Weapon2TileType; if (srcType !== Weapon2TileType.Single && srcType !== Weapon2TileType.Combo) continue; for (let i = 0; i < grids.length; i++) { const mg = grids[i]; if (!mg?.node?.isValid || !mg.node.active) continue; if (!ctx.bagSession.validator.isAllowedCampForPlace(mg.Index)) continue; if (ctx.bagSession.merge.map.getCellKind(mg.Index) !== MapCellKind.Put) continue; const tile = ctx.bagSession.merge.map.getTile(mg.Index); if (!tile) continue; // 多格成将占格禁止被橙/紫互换拆掉(否则 DeploySync sell 神将,表现为「合成了但不打」) const compose = this._probe.findComposeAtGrid(ctx.bagSession, mg.Index); if (compose && compose.gridIndices.length > 1) { skippedCompose++; continue; } const dstType = Number(provider.getWeapon2Type(tile.weapon2Id)) as Weapon2TileType; if (dstType !== Weapon2TileType.Single && dstType !== Weapon2TileType.Combo) continue; if (!canDragMapTile(tile, dstType, false)) continue; if (this._probe.canHeroReplaceHero(slot, tile)) { const inQ = Number(provider.getWeapon2Config(slot.weapon2Id)?.quality) || 0; if (!best || -1 < best.rank) { best = { slotIndex: s, gridIndex: mg.Index, rank: -1, inQ, occW2: tile.weapon2Id, occLv: tile.level ?? 1, }; } continue; } const rank = this._probe.qualitySwapTargetRank(slot, tile, ctx.bagSession, mg.Index); if (rank < 0) continue; const inQ = Number(provider.getWeapon2Config(slot.weapon2Id)?.quality) || 0; if (!best || rank < best.rank || (rank === best.rank && s < best.slotIndex) || (rank === best.rank && s === best.slotIndex && mg.Index < best.gridIndex)) { best = { slotIndex: s, gridIndex: mg.Index, rank, inQ, occW2: tile.weapon2Id, occLv: tile.level ?? 1, }; } } } if (!best) { aiOp(`取舍无候选 emptyPut=0 dig=${digs.length} skipCompose=${skippedCompose}`); return null; } aiOp( `取舍执行 bag[${best.slotIndex}] q=${best.inQ}` + ` -> map[${best.gridIndex}] w2=${best.occW2} lv=${best.occLv} rank=${best.rank}` + ` dig=${digs.length} skipCompose=${skippedCompose}`, ); return { kind: AiActionKind.Swap, merge: { source: { kind: 'bag', slotIndex: best.slotIndex }, target: { kind: 'map', gridIndex: best.gridIndex }, }, }; } }