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.
233 lines
7.9 KiB
233 lines
7.9 KiB
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<BagSlotRecord | null>;
|
|
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<BagSlotRecord | null> = [];
|
|
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;
|
|
}
|
|
|