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/bag/AutoMergePlanner.ts

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;
}