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.
197 lines
7.9 KiB
197 lines
7.9 KiB
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 });
|
|
}
|
|
}
|
|
|