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.
299 lines
11 KiB
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;
|
||
|
|
}
|