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

299 lines
11 KiB

3 weeks ago
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<BagSlotRecord | null>;
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<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 } });
});
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;
}