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.
179 lines
6.8 KiB
179 lines
6.8 KiB
|
3 weeks ago
|
import { Weapon2TileType, type MapTileRecord } from 'db://assets/mx/module/merge';
|
||
|
|
import type { BattleBagSession } from '../../bag/BattleBagSession';
|
||
|
|
import { WeaponComposeConfigProvider } from '../../bag/WeaponComposeConfigProvider';
|
||
|
|
|
||
|
|
/** 主动技落点(己方字/武将) */
|
||
|
|
export type AiActiveSkillTileTarget =
|
||
|
|
| { kind: 'map'; gridIndex: number }
|
||
|
|
| { kind: 'bag'; slotIndex: number };
|
||
|
|
|
||
|
|
/** 紫字品质(赌橙合成) */
|
||
|
|
const QUALITY_PURPLE = 3;
|
||
|
|
/** 橙字品质(已可合成,文昌笔垫底) */
|
||
|
|
const QUALITY_ORANGE = 4;
|
||
|
|
|
||
|
|
/** 候选打分条目 */
|
||
|
|
type ScoredTarget = {
|
||
|
|
/** 落点 */
|
||
|
|
target: AiActiveSkillTileTarget;
|
||
|
|
/** 是否合成武将 */
|
||
|
|
isHero: boolean;
|
||
|
|
/** 等级 */
|
||
|
|
level: number;
|
||
|
|
/** Text.quality */
|
||
|
|
quality: number;
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 主动道具目标选择(需求 §8.1)。
|
||
|
|
* 默认:合成武将 → 普通字,同档等级高优先。
|
||
|
|
* 文昌笔:紫字赌橙优先,其余等级低优先,橙字垫底。
|
||
|
|
*/
|
||
|
|
export class AiActiveSkillTargetPicker {
|
||
|
|
/**
|
||
|
|
* 在己方盘面按默认优先级选落点;`canUse` 过滤不可施放目标。
|
||
|
|
* @param session AI bag
|
||
|
|
* @param canUse 是否可对本落点施放
|
||
|
|
*/
|
||
|
|
pick(session: BattleBagSession, canUse: (target: AiActiveSkillTileTarget) => boolean): AiActiveSkillTileTarget | null {
|
||
|
|
const scored = this._collect(session);
|
||
|
|
scored.sort((a, b) => {
|
||
|
|
if (a.isHero !== b.isHero) return a.isHero ? -1 : 1;
|
||
|
|
if (a.level !== b.level) return b.level - a.level;
|
||
|
|
return this._targetStableKey(a.target) - this._targetStableKey(b.target);
|
||
|
|
});
|
||
|
|
return this._firstUsable(scored, canUse);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 文昌笔:紫字(赌变橙可合成)优先;其余等级低优先;橙字垫底。
|
||
|
|
* @param session AI bag
|
||
|
|
* @param canUse 是否可对本落点施放
|
||
|
|
*/
|
||
|
|
pickRewrite(session: BattleBagSession, canUse: (target: AiActiveSkillTileTarget) => boolean): AiActiveSkillTileTarget | null {
|
||
|
|
const scored = this._collect(session);
|
||
|
|
scored.sort((a, b) => {
|
||
|
|
const ta = this._rewriteTier(a.quality);
|
||
|
|
const tb = this._rewriteTier(b.quality);
|
||
|
|
if (ta !== tb) return ta - tb;
|
||
|
|
if (a.level !== b.level) return a.level - b.level;
|
||
|
|
return this._targetStableKey(a.target) - this._targetStableKey(b.target);
|
||
|
|
});
|
||
|
|
return this._firstUsable(scored, canUse);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 文昌笔品质档:紫=0,普通=1,橙=2。
|
||
|
|
* @param quality Text.quality
|
||
|
|
*/
|
||
|
|
private _rewriteTier(quality: number): number {
|
||
|
|
if (quality === QUALITY_PURPLE) return 0;
|
||
|
|
if (quality === QUALITY_ORANGE) return 2;
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 按排序取首个可施放目标。
|
||
|
|
* @param scored 已排序候选
|
||
|
|
* @param canUse 过滤
|
||
|
|
*/
|
||
|
|
private _firstUsable(scored: ScoredTarget[], canUse: (target: AiActiveSkillTileTarget) => boolean): AiActiveSkillTileTarget | null {
|
||
|
|
for (let i = 0; i < scored.length; i++) {
|
||
|
|
if (canUse(scored[i].target)) return scored[i].target;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 收集己方地图+背包字块候选。
|
||
|
|
* @param session AI bag
|
||
|
|
*/
|
||
|
|
private _collect(session: BattleBagSession): ScoredTarget[] {
|
||
|
|
WeaponComposeConfigProvider.instance.load();
|
||
|
|
const provider = WeaponComposeConfigProvider.instance;
|
||
|
|
const out: ScoredTarget[] = [];
|
||
|
|
const coveredGrids: number[] = [];
|
||
|
|
|
||
|
|
const compositions = session.merge.compositions;
|
||
|
|
for (let i = 0; i < compositions.length; i++) {
|
||
|
|
const compose = compositions[i];
|
||
|
|
if (!compose.gridIndices || compose.gridIndices.length <= 0) continue;
|
||
|
|
const anchor = compose.gridIndices[0];
|
||
|
|
if (!session.validator.isAllowedCampForPlace(anchor)) continue;
|
||
|
|
for (let g = 0; g < compose.gridIndices.length; g++) {
|
||
|
|
coveredGrids.push(compose.gridIndices[g]);
|
||
|
|
}
|
||
|
|
const level = Math.max(1, Math.floor(Number(compose.level) || 1));
|
||
|
|
const isHero = compose.gridIndices.length > 1
|
||
|
|
|| !provider.isSingleCellWeapon1(compose.weapon1Id);
|
||
|
|
const tile = session.merge.map.getTile(anchor);
|
||
|
|
const weapon2Id = tile ? tile.weapon2Id : 0;
|
||
|
|
out.push({
|
||
|
|
target: { kind: 'map', gridIndex: anchor },
|
||
|
|
isHero,
|
||
|
|
level,
|
||
|
|
quality: this._readQuality(weapon2Id),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
session.merge.map.forEachTile((gridIndex, tile) => {
|
||
|
|
if (coveredGrids.indexOf(gridIndex) >= 0) return;
|
||
|
|
this._pushLooseMapTile(session, gridIndex, tile, out);
|
||
|
|
});
|
||
|
|
|
||
|
|
const slots = session.getBagSlots();
|
||
|
|
for (let i = 0; i < slots.length; i++) {
|
||
|
|
const slot = slots[i];
|
||
|
|
if (!slot) continue;
|
||
|
|
const w2Type = Number(provider.getWeapon2Type(slot.weapon2Id)) as Weapon2TileType;
|
||
|
|
if (w2Type === Weapon2TileType.Shovel || w2Type === Weapon2TileType.Grid) continue;
|
||
|
|
out.push({
|
||
|
|
target: { kind: 'bag', slotIndex: i },
|
||
|
|
isHero: w2Type === Weapon2TileType.Combo,
|
||
|
|
level: Math.max(1, Math.floor(Number(slot.level) || 1)),
|
||
|
|
quality: this._readQuality(slot.weapon2Id),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 推入未计入合成的散字。
|
||
|
|
* @param session 会话
|
||
|
|
* @param gridIndex 格
|
||
|
|
* @param tile 字
|
||
|
|
* @param out 输出
|
||
|
|
*/
|
||
|
|
private _pushLooseMapTile(session: BattleBagSession, gridIndex: number, tile: MapTileRecord, out: ScoredTarget[]): void {
|
||
|
|
if (!session.validator.isAllowedCampForPlace(gridIndex)) return;
|
||
|
|
WeaponComposeConfigProvider.instance.load();
|
||
|
|
const provider = WeaponComposeConfigProvider.instance;
|
||
|
|
const w2Type = Number(provider.getWeapon2Type(tile.weapon2Id)) as Weapon2TileType;
|
||
|
|
if (w2Type === Weapon2TileType.Shovel || w2Type === Weapon2TileType.Grid) return;
|
||
|
|
out.push({
|
||
|
|
target: { kind: 'map', gridIndex },
|
||
|
|
isHero: w2Type === Weapon2TileType.Combo,
|
||
|
|
level: Math.max(1, Math.floor(Number(tile.level) || 1)),
|
||
|
|
quality: this._readQuality(tile.weapon2Id),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 读 Text.quality。
|
||
|
|
* @param weapon2Id 字 id
|
||
|
|
*/
|
||
|
|
private _readQuality(weapon2Id: number): number {
|
||
|
|
WeaponComposeConfigProvider.instance.load();
|
||
|
|
return Math.floor(Number(WeaponComposeConfigProvider.instance.getWeapon2Config(weapon2Id)?.quality) || 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 稳定排序键。
|
||
|
|
* @param target 落点
|
||
|
|
*/
|
||
|
|
private _targetStableKey(target: AiActiveSkillTileTarget): number {
|
||
|
|
if (target.kind === 'map') return target.gridIndex;
|
||
|
|
return 100000 + target.slotIndex;
|
||
|
|
}
|
||
|
|
}
|