import type { MapCellSnapshot } from 'db://assets/mx/module/merge'; import { Weapon2TileType, type BagSlotRecord, type ComposedUnit, type DragSource, type DropTarget, type MapTileRecord, } from 'db://assets/mx/module/merge'; import { isAutoMergeWeapon1 } from '../BattleAutoMergeUnlock'; import type { BattleBagSession } from './BattleBagSession'; import { WeaponComposeConfigProvider } from './WeaponComposeConfigProvider'; import type { MapManager } from '../map/legacy/MapManager'; 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 }>; compositions: ComposedUnit[]; } /** 盘面单位快照:用于区分「真合成」与「落子/交换」 */ interface UnitSnapshot { occupiedCount: number; maxLevel: number; /** weapon1Id:level,仅多格组合 */ comboLevels: string; } type TileLike = { weapon2Id: number; weapon1Id?: 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 } }); }); const compositions: ComposedUnit[] = merge.compositions.map(c => ({ weapon1Id: c.weapon1Id, level: c.level, gridIndices: [...c.gridIndices], direction: c.direction, })); return { prepActive: merge.prepActive, bagSlots, mapCells, mapTiles, compositions, }; } function restoreBoardSnapshot(session: BattleBagSession, snapshot: BoardSnapshot): void { session.merge.restoreBoardState(snapshot, { silent: true }); } function composeLevelAt(session: BattleBagSession, compose: ComposedUnit): number { let level = 1; for (const idx of compose.gridIndices) { const lv = session.merge.map.getTile(idx)?.level ?? 1; if (lv > level) level = lv; } return level; } 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); }); const comboLevels = session.compositions .filter(c => c.gridIndices.length > 1) .map(c => `${c.weapon1Id}:${composeLevelAt(session, c)}`) .sort() .join('|'); return { occupiedCount, maxLevel, comboLevels }; } /** 真合成:等级提升,或消耗单位升级(单位数减少) */ function isActualMerge(before: UnitSnapshot, after: UnitSnapshot): boolean { if (after.maxLevel > before.maxLevel) return true; if (after.occupiedCount < before.occupiedCount) return true; if (after.comboLevels !== before.comboLevels) return true; return false; } function resolveWeapon1Id(tile: TileLike): number | null { WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; return tile.weapon1Id ?? provider.getWeapon1ByGroupSequence([tile.weapon2Id]); } /** 仅 herotype===1 的基础武器参与自动合成 */ function isEligibleAutoMergeTile(tile: TileLike): boolean { return isAutoMergeWeapon1(resolveWeapon1Id(tile)); } function isSameSingleWeaponPair(src: TileLike, dst: TileLike): boolean { WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; const srcType = provider.getWeapon2Type(src.weapon2Id) as Weapon2TileType; const dstType = provider.getWeapon2Type(dst.weapon2Id) as Weapon2TileType; if (srcType !== Weapon2TileType.Single || dstType !== Weapon2TileType.Single) return false; const srcW1 = resolveWeapon1Id(src); const dstW1 = resolveWeapon1Id(dst); if (srcW1 == null || dstW1 == null || srcW1 !== dstW1) return false; if (!isAutoMergeWeapon1(srcW1)) return false; return provider.isSingleCellWeapon1(srcW1); } function wouldSingleMergeUpgrade(src: TileLike, dst: TileLike): boolean { if (!isSameSingleWeaponPair(src, dst)) return false; if ((src.level ?? 1) !== (dst.level ?? 1)) return false; WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; const w1 = resolveWeapon1Id(src); if (w1 == null) return false; return (src.level ?? 1) < provider.getMaxWeaponLevel(w1); } function canMergeInBag(src: BagSlotRecord, dst: BagSlotRecord): boolean { return wouldSingleMergeUpgrade(src, dst); } function isComposeUpgradeCandidate( session: BattleBagSession, source: TileLike, dropGridIndex: number, fromMapGrid?: number, ): boolean { const compose = session.compositions.find(c => c.gridIndices.includes(dropGridIndex)); if (!compose) return false; if (!isAutoMergeWeapon1(compose.weapon1Id)) return false; if (fromMapGrid != null && compose.gridIndices.includes(fromMapGrid)) return false; WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; const srcType = provider.getWeapon2Type(source.weapon2Id) as Weapon2TileType; // 自动合成仅单格基础武器(herotype===1),不处理多格 Combo 英雄 if (!provider.isSingleCellWeapon1(compose.weapon1Id)) return false; if (srcType !== Weapon2TileType.Single) return false; const srcW1 = resolveWeapon1Id(source); return srcW1 != null && srcW1 === compose.weapon1Id; } function wouldUpgradeOnComposition( session: BattleBagSession, source: TileLike, dropGridIndex: number, fromMapGrid?: number, ): boolean { if (!isComposeUpgradeCandidate(session, source, dropGridIndex, fromMapGrid)) return false; const compose = session.compositions.find(c => c.gridIndices.includes(dropGridIndex)); if (!compose) return false; WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; const level = composeLevelAt(session, compose); if (level >= provider.getMaxWeaponLevel(compose.weapon1Id)) return false; if (provider.isSingleCellWeapon1(compose.weapon1Id)) { return (source.level ?? 1) === level; } return true; } function wouldBagMapMerge(session: BattleBagSession, slotIndex: number, gridIndex: number): boolean { const slot = session.merge.bag.getSlot(slotIndex); if (!slot || !isEligibleAutoMergeTile(slot)) return false; if (session.merge.isMergeUpgradeBlocked(gridIndex)) return false; if (wouldUpgradeOnComposition(session, slot, gridIndex)) return true; const targetTile = session.merge.map.getTile(gridIndex); if (!targetTile || !isEligibleAutoMergeTile(targetTile)) return false; return wouldSingleMergeUpgrade(slot, targetTile); } function wouldBagBagMerge(session: BattleBagSession, fromSlot: number, toSlot: number): boolean { if (fromSlot === toSlot) return false; const src = session.merge.bag.getSlot(fromSlot); const dst = session.merge.bag.getSlot(toSlot); if (!src || !dst) return false; if (!isEligibleAutoMergeTile(src) || !isEligibleAutoMergeTile(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); // silent:禁止 emit,否则表现层会打点/重建/播特效,而 silent restore 不通知视图 → 每帧死循环 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; } /** * 查找下一步可执行的合成动作(仅 herotype===1;仅升级/合成,不含落子/交换)。 * 优先级:背包→地图(耗尽)→ 背包→背包;背包内合成后若又能对地图升级,下一轮仍优先背包→地图。 * 不做地图→地图。 */ 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 || !isEligibleAutoMergeTile(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 || !isEligibleAutoMergeTile(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; }