import type { MapCellSnapshot } from 'db://assets/mx/module/merge'; import { isElementGlyphType, type BagSlotRecord, type DragSource, type DropTarget, type MapTileRecord, } from 'db://assets/mx/module/merge'; import type { BattleBagSession } from './BattleBagSession'; import { WeaponComposeConfigProvider } from './WeaponComposeConfigProvider'; import type { MapManager } from '../map/legacy/MapManager'; import { TextListCellState } from '../view/TextListLandHost'; export interface AutoMergeAction { source: DragSource; target: DropTarget; } interface BoardSnapshot { prepActive: boolean; bagSlots: Array; mapCells: Array<{ gridIndex: number; cell: MapCellSnapshot }>; mapTiles: Array<{ gridIndex: number; tile: MapTileRecord }>; } /** 盘面单位快照:用于区分「真合成」与「落子/交换」 */ interface UnitSnapshot { occupiedCount: number; maxLevel: number; } type TileLike = { weapon2Id: number; level?: number }; function exportBoardSnapshot(session: BattleBagSession): BoardSnapshot { const merge = session.merge; const bagSlots: Array = []; for (let i = 0; i < merge.bag.slotCount; i++) { const slot = merge.bag.getSlot(i); bagSlots.push(slot ? { ...slot } : null); } const mapCells: Array<{ gridIndex: number; cell: MapCellSnapshot }> = []; for (const gridIndex of merge.map.gridIndices) { const cell = merge.map.exportCell(gridIndex); if (cell) mapCells.push({ gridIndex, cell }); } const mapTiles: Array<{ gridIndex: number; tile: MapTileRecord }> = []; merge.map.forEachTile((gridIndex, tile) => { mapTiles.push({ gridIndex, tile: { ...tile } }); }); return { prepActive: merge.prepActive, bagSlots, mapCells, mapTiles, }; } function restoreBoardSnapshot(session: BattleBagSession, snapshot: BoardSnapshot): void { session.merge.restoreBoardState(snapshot, { silent: true }); } function captureUnitSnapshot(session: BattleBagSession): UnitSnapshot { const merge = session.merge; let occupiedCount = 0; let maxLevel = 0; for (let i = 0; i < merge.bag.slotCount; i++) { const slot = merge.bag.getSlot(i); if (!slot) continue; occupiedCount++; maxLevel = Math.max(maxLevel, slot.level ?? 1); } merge.map.forEachTile((_idx, tile) => { occupiedCount++; maxLevel = Math.max(maxLevel, tile.level ?? 1); }); return { occupiedCount, maxLevel }; } /** 真合成:等级提升,或消耗单位升级(单位数减少) */ function isActualMerge(before: UnitSnapshot, after: UnitSnapshot): boolean { if (after.maxLevel > before.maxLevel) return true; if (after.occupiedCount < before.occupiedCount) return true; return false; } function isEligibleElementTile(tile: TileLike): boolean { WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; const w2Type = Number(provider.getWeapon2Type(tile.weapon2Id)); return isElementGlyphType(w2Type); } function canElementMergeUpgrade(src: TileLike, dst: TileLike): boolean { WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; const srcType = Number(provider.getWeapon2Type(src.weapon2Id)); const dstType = Number(provider.getWeapon2Type(dst.weapon2Id)); if (!isElementGlyphType(srcType) || !isElementGlyphType(dstType)) return false; if (srcType !== dstType) return false; const srcLv = Math.floor(src.level ?? 1); const dstLv = Math.floor(dst.level ?? 1); if (srcLv !== dstLv) return false; return srcLv < provider.getElementMaxLevel(srcType); } function canMergeInBag(src: BagSlotRecord, dst: BagSlotRecord): boolean { return canElementMergeUpgrade(src, dst); } /** * 背包源槽是否可拖出。 * @param session 会话 * @param slotIndex 槽 */ function canDragBagSlot(session: BattleBagSession, slotIndex: number): boolean { const land = session.landStateOf?.(slotIndex); if (land == null) return true; return land === TextListCellState.Open; } /** * 背包目标槽是否可被合成落到。 * @param session 会话 * @param slotIndex 槽 */ function canMergeOntoBagSlot(session: BattleBagSession, slotIndex: number): boolean { const land = session.landStateOf?.(slotIndex); if (land == null) return true; return land === TextListCellState.Open || land === TextListCellState.OpenNoDrag; } function wouldBagMapMerge(session: BattleBagSession, slotIndex: number, gridIndex: number): boolean { if (!canDragBagSlot(session, slotIndex)) return false; const slot = session.merge.bag.getSlot(slotIndex); if (!slot || !isEligibleElementTile(slot)) return false; if (session.merge.isMergeUpgradeBlocked(gridIndex)) return false; const targetTile = session.merge.map.getTile(gridIndex); if (!targetTile || !isEligibleElementTile(targetTile)) return false; return canElementMergeUpgrade(slot, targetTile); } function wouldBagBagMerge(session: BattleBagSession, fromSlot: number, toSlot: number): boolean { if (fromSlot === toSlot) return false; if (!canDragBagSlot(session, fromSlot)) return false; if (!canMergeOntoBagSlot(session, toSlot)) return false; const src = session.merge.bag.getSlot(fromSlot); const dst = session.merge.bag.getSlot(toSlot); if (!src || !dst) return false; if (!isEligibleElementTile(src) || !isEligibleElementTile(dst)) return false; return canMergeInBag(src, dst); } function tryMergeAction( session: BattleBagSession, source: DragSource, target: DropTarget, ): AutoMergeAction | null { if (!session.merge.prepActive) return null; const snapshot = exportBoardSnapshot(session); const before = captureUnitSnapshot(session); const reason = session.merge.drop(source, target, { silent: true }); if (reason) { restoreBoardSnapshot(session, snapshot); return null; } const after = captureUnitSnapshot(session); const merged = isActualMerge(before, after); restoreBoardSnapshot(session, snapshot); return merged ? { source, target } : null; } function listMapGridIndices(map: MapManager): number[] { const out: number[] = []; const grids = map.mapGrids; if (!Array.isArray(grids)) return out; for (const mg of grids) { if (mg?.node?.isValid && mg.node.active) out.push(mg.Index); } return out; } /** * 查找下一步可执行的合成动作(五行同 type+level 升级;仅背包→地图 / 背包→背包)。 * 优先级:背包→地图(耗尽)→ 背包→背包。 */ export function findNextAutoMergeAction( session: BattleBagSession, map: MapManager, ): AutoMergeAction | null { if (!session.merge.prepActive) return null; const slotCount = session.merge.bag.slotCount; const mapIndices = listMapGridIndices(map); for (let slotIndex = 0; slotIndex < slotCount; slotIndex++) { const slot = session.merge.bag.getSlot(slotIndex); if (!slot || !isEligibleElementTile(slot)) continue; for (const gridIndex of mapIndices) { if (!wouldBagMapMerge(session, slotIndex, gridIndex)) continue; const action = tryMergeAction( session, { kind: 'bag', slotIndex }, { kind: 'map', gridIndex }, ); if (action) return action; } } for (let fromSlot = 0; fromSlot < slotCount; fromSlot++) { const src = session.merge.bag.getSlot(fromSlot); if (!src || !isEligibleElementTile(src)) continue; for (let toSlot = 0; toSlot < slotCount; toSlot++) { if (!wouldBagBagMerge(session, fromSlot, toSlot)) continue; const action = tryMergeAction( session, { kind: 'bag', slotIndex: fromSlot }, { kind: 'bag', slotIndex: toSlot }, ); if (action) return action; } } return null; }