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.
116 lines
5.5 KiB
116 lines
5.5 KiB
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 },
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|