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.
AdLike2048/assets/script/battle/ai/decision/AiActiveSkillTargetPicker.ts

181 lines
6.8 KiB

2 weeks ago
import { Weapon2TileType, isElementGlyphType, type MapTileRecord } from 'db://assets/mx/module/merge';
3 weeks ago
import type { BattleBagSession } from '../../bag/BattleBagSession';
import { WeaponComposeConfigProvider } from '../../bag/WeaponComposeConfigProvider';
2 weeks ago
import { isElementHero } from '../../config/BattleHeroTable';
import { TableNames } from '../../../game/ConfigTableData';
3 weeks ago
/** 主动技落点(己方字/武将) */
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));
2 weeks ago
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, compose.weapon1Id);
const isHero = isElementHero(hero);
3 weeks ago
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;
2 weeks ago
if (w2Type === Weapon2TileType.Grid) continue;
3 weeks ago
out.push({
target: { kind: 'bag', slotIndex: i },
2 weeks ago
isHero: isElementGlyphType(w2Type),
3 weeks ago
level: Math.max(1, Math.floor(Number(slot.level) || 1)),
quality: this._readQuality(slot.weapon2Id),
});
}
return out;
}
/**
2 weeks ago
* 推入未计入合成的五行字。
3 weeks ago
* @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;
2 weeks ago
if (w2Type === Weapon2TileType.Grid) return;
3 weeks ago
out.push({
target: { kind: 'map', gridIndex },
2 weeks ago
isHero: isElementGlyphType(w2Type),
3 weeks ago
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;
}
}