import type { AiPlannerContext, AiPlannedAction } from '../types/AiTypes'; import { AiActionKind } from '../types/AiTypes'; import { AiShovelPlanner } from './AiShovelPlanner'; import { AiSwapPlanner } from './AiSwapPlanner'; import { AiTextPolicyPlanner } from './AiTextPolicyPlanner'; import { AiPlacementScorer } from './AiPlacementScorer'; import { AiBoardProbe } from './AiBoardProbe'; import { aiOp } from '../runtime/AiLog'; import { Weapon2TileType } from 'db://assets/mx/module/merge'; import { WeaponComposeConfigProvider } from '../../bag/WeaponComposeConfigProvider'; import { isNonCombatWeapon1 } from '../../heros/HeroActorRes'; /** 需求 6.1:成将 → 铲 → 升级(含包内) → 摆放 → 取舍/字策略 → 重摆(避免重摆占满拍导致背包积压) */ export class AiActionPlanner { private readonly _probe = new AiBoardProbe(); private readonly _shovel = new AiShovelPlanner(); private readonly _swap = new AiSwapPlanner(); private readonly _text = new AiTextPolicyPlanner(); private readonly _place = new AiPlacementScorer(); /** 普通字策略(供 Controller 在 tick 初刷新 focus) */ get textPlanner(): AiTextPolicyPlanner { return this._text; } /** * AI 空 Put 格数量(供刷新门禁)。 * @param ctx 决策上下文 */ countEmptyTextPut(ctx: AiPlannerContext): number { return this._probe.countEmptyTextPut(ctx.bagSession, ctx.map); } /** * 规划下一步原子动作;无可执行时返回 null。 * 有铲先铲,再升级,再摆放;多格神将可贴路重摆优先于落 1–3 级单字。 * @param ctx 决策上下文 */ planNext(ctx: AiPlannerContext): AiPlannedAction | null { const emptyPut = this._probe.countEmptyTextPut(ctx.bagSession, ctx.map); const dig = this._probe.listEmptyDigGrids(ctx.bagSession, ctx.map).length; const merge = this._probe.findMergeFormAction(ctx.bagSession, ctx.map); if (merge) { return this._logAction('合成', { kind: AiActionKind.Merge, merge }, emptyPut, dig); } const assemble = this._probe.findAssembleHeroAction(ctx.bagSession, ctx.map); if (assemble && assemble.moves.length > 0) { return this._logAction( `拼装 steps=${assemble.moves.length}`, { kind: AiActionKind.Merge, merge: assemble.moves[0], moves: assemble.moves, }, emptyPut, dig, ); } const shovel = this._shovel.plan(ctx); if (shovel) return this._logAction('铲子', shovel, emptyPut, dig); const upgrade = this._probe.findUpgradeAction(ctx.bagSession, ctx.map); if (upgrade) { return this._logAction('升级', { kind: AiActionKind.Upgrade, merge: upgrade }, emptyPut, dig); } const place = this._place.plan(ctx); const multiRepo = this._probe.findMultiCellHeroRepositionAction(ctx.bagSession, ctx.map); // 合成神将贴路优先于 1–3 级单字落子:有可贴路重摆时先挪神将 if (multiRepo && multiRepo.moves.length > 0 && this._shouldDeferPlaceForHeroRepo(ctx, place)) { return this._logAction( `阵型重摆(神将优先) steps=${multiRepo.moves.length}`, { kind: AiActionKind.Reposition, merge: multiRepo.moves[0], moves: multiRepo.moves, }, emptyPut, dig, ); } if (place) return this._logAction('摆放', place, emptyPut, dig); // 有空位却摆放规划失败:仍尝试取舍,避免背包积压空转 const swap = this._swap.plan(ctx, emptyPut > 0); if (swap) return this._logAction(emptyPut > 0 ? '取舍(空位摆失败)' : '取舍', swap, emptyPut, dig); const text = this._text.plan(ctx); if (text) return this._logAction('字策略', text, emptyPut, dig); const dedupe = this._probe.findDuplicateComboRetractAction(ctx.bagSession, ctx.map); if (dedupe) { return this._logAction('散字收回', { kind: AiActionKind.Merge, merge: dedupe }, emptyPut, dig); } const dupReplace = this._probe.findDuplicateComboReplaceAction(ctx.bagSession, ctx.map); if (dupReplace) { return this._logAction('散字替换', { kind: AiActionKind.Swap, merge: dupReplace }, emptyPut, dig); } const reposition = this._probe.findHeroRepositionAction(ctx.bagSession, ctx.map); if (reposition && reposition.moves.length > 0) { return this._logAction( `阵型重摆 steps=${reposition.moves.length}`, { kind: AiActionKind.Reposition, merge: reposition.moves[0], moves: reposition.moves, }, emptyPut, dig, ); } aiOp(`本拍无动作 emptyPut=${emptyPut} dig=${dig}`); return null; } /** * 当前落子是否应让位于多格神将贴路重摆。 * 无落子 / 1–3 级普通单字 / 无攻击功能字 → 让位;高等级单字与组合散字仍可先摆。 * @param ctx 上下文 * @param place 落子规划 */ private _shouldDeferPlaceForHeroRepo(ctx: AiPlannerContext, place: AiPlannedAction | null): boolean { if (!place?.merge || place.merge.source.kind !== 'bag') return true; const slot = ctx.bagSession.getBagSlots()[place.merge.source.slotIndex]; if (!slot) return true; WeaponComposeConfigProvider.instance.load(); const w2Type = Number(WeaponComposeConfigProvider.instance.getWeapon2Type(slot.weapon2Id)) as Weapon2TileType; const w1 = this._probe.resolveWeapon1Id(slot) ?? 0; if (isNonCombatWeapon1(w1)) return true; if (w2Type !== Weapon2TileType.Single) return false; const lv = Math.max(1, Math.floor(Number(slot.level) || 1)); return lv <= 3; } /** * 仅规划落子(互换/升级后同拍回填空位)。 * @param ctx 决策上下文 */ planPlaceOnly(ctx: AiPlannerContext): AiPlannedAction | null { const place = this._place.plan(ctx); if (!place) return null; const emptyPut = this._probe.countEmptyTextPut(ctx.bagSession, ctx.map); return this._logAction('摆放(同拍回填)', place, emptyPut, 0); } /** * 打印并返回规划动作。 * @param label 动作中文名 * @param action 动作 * @param emptyPut 空 Put * @param dig 可铲数 */ private _logAction(label: string, action: AiPlannedAction, emptyPut: number, dig: number): AiPlannedAction { const m = action.merge; let detail = `kind=${action.kind}`; if (m) { const src = m.source.kind === 'bag' ? `bag[${m.source.slotIndex}]` : `map[${m.source.gridIndex}]`; const dst = m.target.kind === 'bag' ? `bag[${m.target.slotIndex}]` : `map[${m.target.gridIndex}]`; detail += ` ${src}->${dst}`; } if (action.moves && action.moves.length > 1) { detail += ` moves=${action.moves.length}`; } aiOp(`决策 ${label} | ${detail} | emptyPut=${emptyPut} dig=${dig}`); return action; } /** * @deprecated 使用 planNext;保留空实现兼容旧调用 * @param ctx 决策上下文 */ planRound(ctx: AiPlannerContext): AiPlannedAction[] { const action = this.planNext(ctx); return action ? [action] : []; } }