import { Weapon2TileType, isElementGlyphType, type MapTileRecord } from 'db://assets/mx/module/merge'; import type { BattleBagSession } from '../../bag/BattleBagSession'; import { WeaponComposeConfigProvider } from '../../bag/WeaponComposeConfigProvider'; import { isElementHero } from '../../config/BattleHeroTable'; import { TableNames } from '../../../game/ConfigTableData'; /** 主动技落点(己方字/武将) */ 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 hero = gg.data.table.getTableData(TableNames.Hero, compose.weapon1Id); const isHero = isElementHero(hero); 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.Grid) continue; out.push({ target: { kind: 'bag', slotIndex: i }, isHero: isElementGlyphType(w2Type), 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.Grid) return; out.push({ target: { kind: 'map', gridIndex }, isHero: isElementGlyphType(w2Type), 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; } }