import { Weapon2TileType } from 'db://assets/mx/module/merge'; import type { AiPlannerContext, AiPlannedAction } from '../types/AiTypes'; import { AiActionKind } from '../types/AiTypes'; import { AiTextPolicyType } from '../runtime/AiBattlePersonality'; import { AiBoardProbe } from './AiBoardProbe'; import { WeaponComposeConfigProvider } from '../../bag/WeaponComposeConfigProvider'; import { aiOp } from '../runtime/AiLog'; /** 需求 6.3 / 6.3.1 普通字策略(含单字冲级) */ export class AiTextPolicyPlanner { private readonly _probe = new AiBoardProbe(); /** * 每个 tick 开始时更新单字冲级目标。 * @param ctx 决策上下文 */ refreshFocus(ctx: AiPlannerContext): void { if (ctx.personality.textPolicy !== AiTextPolicyType.Type2) { ctx.textFocusWeapon2Id = null; return; } ctx.textFocusWeapon2Id = this._pickFocusWeapon2Id(ctx); } /** * 尝试规划一步普通字整理动作。 * @param ctx 决策上下文 */ plan(ctx: AiPlannerContext): AiPlannedAction | null { if (ctx.personality.textPolicy === AiTextPolicyType.Type2) { return this._planFocusPlace(ctx); } return this._planKeepHighLevel(ctx); } /** 类型2:优先把目标字从背包落到空位 */ private _planFocusPlace(ctx: AiPlannerContext): AiPlannedAction | null { const focus = ctx.textFocusWeapon2Id; if (focus == null || focus <= 0) return null; const slots = ctx.bagSession.getBagSlots(); let bestSlot = -1; let bestLv = -1; for (let i = 0; i < slots.length; i++) { const slot = slots[i]; if (!slot || slot.weapon2Id !== focus) continue; const lv = slot.level ?? 1; if (lv > bestLv) { bestLv = lv; bestSlot = i; } } if (bestSlot < 0) return null; const slot = slots[bestSlot]!; const w2Type = Number(WeaponComposeConfigProvider.instance.getWeapon2Type(slot.weapon2Id)) as Weapon2TileType; const grids = ctx.map.mapGrids; if (!Array.isArray(grids)) return null; let bestGrid = -1; let bestScore = -1e99; const w1 = this._probe.resolveWeapon1Id(slot) ?? 0; 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 (!this._probe.canDropTextOnEmptyGrid(ctx.bagSession, mg, w2Type)) continue; const score = this._probe.formationScore(ctx.map, mg, w1); if (score > bestScore) { bestScore = score; bestGrid = mg.Index; } } if (bestGrid < 0) return null; const mg = ctx.map.getMapGridByIndex(bestGrid); if (!mg) return null; aiOp( `字策略(冲级) bag[${bestSlot}] w2=${focus} lv=${bestLv}` + ` -> map[${bestGrid}] rc=(${mg.Row},${mg.Col}) form=${Math.round(bestScore)}`, ); return { kind: AiActionKind.TextPolicy, merge: { source: { kind: 'bag', slotIndex: bestSlot }, target: { kind: 'map', gridIndex: bestGrid }, }, place: { weapon1Id: w1, level: slot.level ?? 1, row: mg.Row, col: mg.Col }, }; } /** 类型1:优先落局内等级最高的普通字 */ private _planKeepHighLevel(ctx: AiPlannerContext): AiPlannedAction | null { WeaponComposeConfigProvider.instance.load(); const slots = ctx.bagSession.getBagSlots(); let bestSlot = -1; let bestLv = -1; for (let i = 0; i < slots.length; i++) { const slot = slots[i]; if (!slot) continue; if (Number(WeaponComposeConfigProvider.instance.getWeapon2Type(slot.weapon2Id)) !== Weapon2TileType.Single) { continue; } const lv = slot.level ?? 1; if (lv > bestLv) { bestLv = lv; bestSlot = i; } } if (bestSlot < 0) return null; const slot = slots[bestSlot]!; const w2Type = Weapon2TileType.Single; const grids = ctx.map.mapGrids; if (!Array.isArray(grids)) return null; let bestGrid = -1; let bestScore = -1e99; const w1 = this._probe.resolveWeapon1Id(slot) ?? 0; 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 (!this._probe.canDropTextOnEmptyGrid(ctx.bagSession, mg, w2Type)) continue; const score = this._probe.formationScore(ctx.map, mg, w1); if (score > bestScore) { bestScore = score; bestGrid = mg.Index; } } if (bestGrid < 0) return null; const mg = ctx.map.getMapGridByIndex(bestGrid); if (!mg) return null; aiOp( `字策略(保高等级) bag[${bestSlot}] w2=${slot.weapon2Id} lv=${bestLv}` + ` -> map[${bestGrid}] rc=(${mg.Row},${mg.Col}) form=${Math.round(bestScore)}`, ); return { kind: AiActionKind.TextPolicy, merge: { source: { kind: 'bag', slotIndex: bestSlot }, target: { kind: 'map', gridIndex: bestGrid }, }, place: { weapon1Id: w1, level: slot.level ?? 1, row: mg.Row, col: mg.Col }, }; } private _pickFocusWeapon2Id(ctx: AiPlannerContext): number | null { WeaponComposeConfigProvider.instance.load(); const bestById: { id: number; level: number }[] = []; const slots = ctx.bagSession.getBagSlots(); for (let i = 0; i < slots.length; i++) { const slot = slots[i]; if (slot) this._accumulateFocusCandidate(bestById, slot.weapon2Id, slot.level ?? 1); } const mapIndices = ctx.bagSession.merge.map.gridIndices; for (let i = 0; i < mapIndices.length; i++) { const tile = ctx.bagSession.merge.map.getTile(mapIndices[i]); if (tile) this._accumulateFocusCandidate(bestById, tile.weapon2Id, tile.level ?? 1); } if (bestById.length <= 0) return null; this._sortFocusCandidates(bestById); return bestById[0].id; } /** * 按等级降序、id 升序排列冲级候选。 * @param bestById 候选表 */ private _sortFocusCandidates(bestById: { id: number; level: number }[]): void { for (let i = 0; i < bestById.length; i++) { for (let j = i + 1; j < bestById.length; j++) { const a = bestById[i]; const b = bestById[j]; const swap = b.level > a.level || (b.level === a.level && b.id < a.id); if (!swap) continue; bestById[i] = b; bestById[j] = a; } } } /** * 累计未满级普通字的最高等级。 * @param bestById 结果表 * @param weapon2Id Text.id * @param level 局内等级 */ private _accumulateFocusCandidate(bestById: { id: number; level: number }[], weapon2Id: number, level: number): void { const provider = WeaponComposeConfigProvider.instance; if (Number(provider.getWeapon2Type(weapon2Id)) !== Weapon2TileType.Single) return; const w1 = this._probe.resolveWeapon1Id({ weapon2Id }); const max = w1 != null ? provider.getMaxWeaponLevel(w1) : 1; if (level >= max) return; for (let i = 0; i < bestById.length; i++) { if (bestById[i].id === weapon2Id) { if (level > bestById[i].level) bestById[i].level = level; return; } } bestById.push({ id: weapon2Id, level }); } }