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.
1931 lines
79 KiB
1931 lines
79 KiB
import { BagBoard } from '../board/BagBoard';
|
|
import { MapTileBoard } from '../board/MapTileBoard';
|
|
import type { IComposeConfigProvider } from '../config/IComposeConfigProvider';
|
|
import { MergeEventFactory, type MergeEvent, type MergeEventListener } from '../events/MergeEvents';
|
|
import { MergeEventType, MERGE_EVENT_ALL, type MergeEventSubscription } from '../events/MergeEventType';
|
|
import {
|
|
canDragMapTile,
|
|
canDragPrepType3Cell,
|
|
canDropWeapon2OnCell,
|
|
cellKindAfterReturnToBag,
|
|
cellKindAfterType3Pickup,
|
|
cellKindAfterType3Place,
|
|
isBagToolTextType,
|
|
isElementGlyphType,
|
|
isShovelTextType,
|
|
} from '../rules/MapPlaceRules';
|
|
import { UpgradeSystem } from '../upgrade/UpgradeSystem';
|
|
import {
|
|
MapCellKind,
|
|
PlaceRejectReason,
|
|
TileSource,
|
|
Weapon2TileType,
|
|
type BagSlotRecord,
|
|
type ComposedUnit,
|
|
type DragSource,
|
|
type DropTarget,
|
|
type GlyphHeroBind,
|
|
type GlyphHeroBuff,
|
|
type GridCoord,
|
|
type MapTileRecord,
|
|
} from '../types/MergeTypes';
|
|
import {
|
|
createMembershipSet,
|
|
membershipAdd,
|
|
membershipForEach,
|
|
type MembershipSet,
|
|
} from '../../../tools/MembershipSet';
|
|
|
|
/** Weapon2.id — 扩展地图可放置区域(type3 仅改格状态,不挂字块) */
|
|
const GRID_WEAPON2_ID = 5;
|
|
|
|
/** 地图路径校验(项目层注入 MapManager) */
|
|
export interface IMapPathChecker {
|
|
/** 假设 gridIndex 被 type3 占用后路径是否仍通 */
|
|
canPlaceType3Grid(gridIndex: number): boolean;
|
|
/** 该格是否允许放 type3(读 live MapGrid,避免 MapTileBoard 快照过期) */
|
|
isCanPutGrid?(gridIndex: number): boolean;
|
|
/** 该格是否被蜘蛛精蛛网占用 */
|
|
isSpiderWebBlocked?(gridIndex: number): boolean;
|
|
/** 读 MapGrid 现场格语义 */
|
|
getLiveCellKind?(gridIndex: number): MapCellKind | null;
|
|
}
|
|
|
|
/**
|
|
* 背包合成 + 地图放置会话(纯 TS)
|
|
* 仅在 PREP 阶段由项目层驱动。
|
|
*/
|
|
export class MergeSession {
|
|
readonly bag: BagBoard;
|
|
readonly map: MapTileBoard;
|
|
|
|
private readonly _config: IComposeConfigProvider;
|
|
private readonly _upgrade: UpgradeSystem;
|
|
private readonly _pathChecker: IMapPathChecker | null;
|
|
private readonly _coordOf: (gridIndex: number) => GridCoord | null;
|
|
|
|
private _prepActive = false;
|
|
private _compositions: ComposedUnit[] = [];
|
|
private _compositionLevels = new Map<string, number>();
|
|
private _listeners = new Map<MergeEventSubscription, MembershipSet<MergeEventListener>>();
|
|
/** type3 拖起后暂存,直到落子或还原 */
|
|
private _pendingDragTile: MapTileRecord | null = null;
|
|
/** 规划仿真等:抑制 emit,避免表现层打点/重建 */
|
|
private _emitSuppressed = false;
|
|
/** 降级等:该格禁止合成升级 */
|
|
private _mergeBlocker: { isMergeUpgradeBlocked(gridIndex: number): boolean } | null = null;
|
|
|
|
constructor(options: {
|
|
config: IComposeConfigProvider;
|
|
pathChecker?: IMapPathChecker;
|
|
coordOf: (gridIndex: number) => GridCoord | null;
|
|
bagSlotCount?: number;
|
|
}) {
|
|
this._config = options.config;
|
|
this._pathChecker = options.pathChecker ?? null;
|
|
this._coordOf = options.coordOf;
|
|
this.bag = new BagBoard(options.bagSlotCount ?? 5);
|
|
this.map = new MapTileBoard();
|
|
this._upgrade = new UpgradeSystem();
|
|
}
|
|
|
|
get compositions(): readonly ComposedUnit[] {
|
|
return this._compositions;
|
|
}
|
|
|
|
get prepActive(): boolean {
|
|
return this._prepActive;
|
|
}
|
|
|
|
/**
|
|
* 注入合成升级拦截。
|
|
* @param blocker 查询某格是否禁合成;null 清除
|
|
*/
|
|
setMergeBlocker(blocker: { isMergeUpgradeBlocked(gridIndex: number): boolean } | null): void {
|
|
this._mergeBlocker = blocker;
|
|
}
|
|
|
|
/**
|
|
* 该格是否禁止合成升级。
|
|
* @param gridIndex 地图格
|
|
*/
|
|
isMergeUpgradeBlocked(gridIndex: number): boolean {
|
|
return this._mergeBlocker?.isMergeUpgradeBlocked(gridIndex) === true;
|
|
}
|
|
|
|
beginPrepRound(): void {
|
|
this._prepActive = true;
|
|
}
|
|
|
|
endPrepRound(): void {
|
|
this._prepActive = false;
|
|
}
|
|
|
|
on(event: MergeEventSubscription, listener: MergeEventListener): void {
|
|
if (!this._listeners.has(event)) {
|
|
this._listeners.set(event, createMembershipSet());
|
|
}
|
|
membershipAdd(this._listeners.get(event)!, listener);
|
|
}
|
|
|
|
off(event: MergeEventSubscription, listener: MergeEventListener): void {
|
|
this._listeners.get(event)?.delete(listener);
|
|
}
|
|
|
|
refreshBag(slots: BagSlotRecord[]): void {
|
|
this.bag.fillSlots(slots);
|
|
// 入场动画由 UI(rebuildBagSlots + fromWorld)负责;此处只同步数据,避免抢播/双重建
|
|
this.emit(MergeEventFactory.bagRefreshed(false));
|
|
}
|
|
|
|
/**
|
|
* 局内产卡作用到地图五行字:写回等级/字 id,重组并派发 MapUpgraded。
|
|
* @param gridIndex 地图格
|
|
* @param weapon2Id 结果字
|
|
* @param level 结果显示等级
|
|
*/
|
|
applyBattleCardToMapTile(gridIndex: number, weapon2Id: number, level: number): PlaceRejectReason | null {
|
|
if (!this._prepActive) return PlaceRejectReason.NotPrepPhase;
|
|
const tile = this.map.getTile(gridIndex);
|
|
if (!tile || tile.battleCardKind) return PlaceRejectReason.InvalidMerge;
|
|
const w2 = Math.floor(Number(weapon2Id) || 0);
|
|
const lv = Math.max(1, Math.floor(Number(level) || 0));
|
|
if (w2 <= 0) return PlaceRejectReason.InvalidMerge;
|
|
this.map.placeTile(gridIndex, { ...tile, weapon2Id: w2, level: lv });
|
|
this._recompose();
|
|
const heroId = this._resolveElementHeroId(w2, lv) ?? 0;
|
|
this.emit(MergeEventFactory.mapUpgraded(gridIndex, heroId, lv));
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 局内产卡作用到背包五行字:写回等级/字 id 并派发 BagMerged。
|
|
* @param slotIndex 背包槽
|
|
* @param weapon2Id 结果字
|
|
* @param level 结果显示等级
|
|
*/
|
|
applyBattleCardToBagSlot(slotIndex: number, weapon2Id: number, level: number): PlaceRejectReason | null {
|
|
if (!this._prepActive) return PlaceRejectReason.NotPrepPhase;
|
|
const dst = this.bag.getSlot(slotIndex);
|
|
if (!dst || dst.battleCardKind) return PlaceRejectReason.InvalidMerge;
|
|
const w2 = Math.floor(Number(weapon2Id) || 0);
|
|
const lv = Math.max(1, Math.floor(Number(level) || 0));
|
|
if (w2 <= 0) return PlaceRejectReason.InvalidMerge;
|
|
this.bag.setSlot(slotIndex, { ...dst, weapon2Id: w2, level: lv });
|
|
const heroId = this._resolveElementHeroId(w2, lv) ?? 0;
|
|
this.emit(MergeEventFactory.bagMerged(slotIndex, heroId, lv));
|
|
return null;
|
|
}
|
|
|
|
/** 免费格子:放入背包第一个空槽 */
|
|
grantGridInBag(weapon2Id: number): boolean {
|
|
const slot = this.bag.findEmptySlot();
|
|
if (slot < 0) return false;
|
|
return this.grantWeaponInBagSlot(slot, weapon2Id) == null;
|
|
}
|
|
|
|
/**
|
|
* 指定背包槽放入 weapon2(槽位须为空)。
|
|
* @returns null 表示成功
|
|
*/
|
|
grantWeaponInBagSlot(slotIndex: number, weapon2Id: number, level?: number): PlaceRejectReason | null {
|
|
if (!this._prepActive) return PlaceRejectReason.NotPrepPhase;
|
|
if (slotIndex < 0 || slotIndex >= this.bag.slotCount) {
|
|
return PlaceRejectReason.InvalidMerge;
|
|
}
|
|
if (this.bag.getSlot(slotIndex)) return PlaceRejectReason.BagFull;
|
|
this.bag.setSlot(slotIndex, level != null ? { weapon2Id, level } : { weapon2Id });
|
|
this.emit(MergeEventFactory.bagRefreshed(false));
|
|
return null;
|
|
}
|
|
|
|
/** 免费格子:直接扩展 type3 到指定地图格(仅改格状态) */
|
|
placeGridOnMap(gridIndex: number, weapon2Id: number): PlaceRejectReason | null {
|
|
if (!this._prepActive) return PlaceRejectReason.NotPrepPhase;
|
|
|
|
const w2Type = this._config.getWeapon2Type(weapon2Id) as Weapon2TileType;
|
|
if (w2Type !== Weapon2TileType.Grid) return PlaceRejectReason.WrongCellKind;
|
|
|
|
const cellKind = this.map.getCellKind(gridIndex);
|
|
if (!this._canDropOnCell(gridIndex, Weapon2TileType.Grid)) {
|
|
return this._dropRejectReason(gridIndex, Weapon2TileType.Grid);
|
|
}
|
|
const ok = this._pathChecker?.canPlaceType3Grid(gridIndex) ?? true;
|
|
if (!ok) return PlaceRejectReason.PathBlocked;
|
|
|
|
this._applyType3Expand(gridIndex);
|
|
this.emit(MergeEventFactory.tilePlaced(gridIndex, weapon2Id));
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 被动 buff:在空 Dig 或空 Put 格生成字块。
|
|
* 默认 Dig→Put + placeTile;蟠桃树落到 Put 则 Put→Dig,Dig 格保持 Dig。
|
|
* @param gridIndex 目标地图格
|
|
* @param weapon2Id Weapon2 文字 id
|
|
*/
|
|
grantPassiveTileOnDigGrid(gridIndex: number, weapon2Id: number): PlaceRejectReason | null {
|
|
if (!this._prepActive) return PlaceRejectReason.NotPrepPhase;
|
|
|
|
const w2Type = this._config.getWeapon2Type(weapon2Id) as Weapon2TileType;
|
|
if (!this._config.allowsPassiveDigSpawn(weapon2Id)) {
|
|
console.log('[MergeSession] 被动产字 weapon2 类型非法', weapon2Id, w2Type);
|
|
return PlaceRejectReason.WrongCellKind;
|
|
}
|
|
|
|
const keepDigCell = this._config.passiveSpawnKeepsDigCell(weapon2Id);
|
|
if (keepDigCell) {
|
|
if (!this._canDropKeepDigOnCell(gridIndex)) return PlaceRejectReason.WrongCellKind;
|
|
this._convertPutToDig(gridIndex);
|
|
} else {
|
|
const cellKind = this.map.getCellKind(gridIndex);
|
|
if (cellKind !== MapCellKind.Dig) return PlaceRejectReason.WrongCellKind;
|
|
if (this.map.hasTile(gridIndex)) return PlaceRejectReason.CellOccupied;
|
|
this._applyType4DigConvert(gridIndex);
|
|
}
|
|
const weapon1Id = this._config.getWeapon1ByGroupSequence([weapon2Id]) ?? undefined;
|
|
const tile: MapTileRecord = {
|
|
weapon2Id,
|
|
source: TileSource.PassiveFunction3,
|
|
weapon1Id,
|
|
level: 1,
|
|
};
|
|
this.map.placeTile(gridIndex, tile);
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.tilePlaced(gridIndex, weapon2Id));
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 从地图拖起校验。
|
|
* type3:仅 prepType3 格状态,无字块节点;拖起后恢复可行走。
|
|
*/
|
|
pickupFromMap(gridIndex: number): { error: PlaceRejectReason } | { tile: MapTileRecord; wasType3: boolean } {
|
|
const tile = this.map.getTile(gridIndex);
|
|
|
|
if (this.map.isPrepType3(gridIndex) && !tile) {
|
|
if (!canDragPrepType3Cell(true, false, this.map.getPrepType3PlacedAtMs(gridIndex))) {
|
|
return { error: PlaceRejectReason.NotDraggable };
|
|
}
|
|
this._applyType3Revert(gridIndex);
|
|
const virtualTile: MapTileRecord = {
|
|
weapon2Id: GRID_WEAPON2_ID,
|
|
source: TileSource.PrepPlaced,
|
|
};
|
|
this._pendingDragTile = { ...virtualTile };
|
|
return { tile: virtualTile, wasType3: true };
|
|
}
|
|
|
|
if (!tile) return { error: PlaceRejectReason.SlotEmpty };
|
|
|
|
if (this._config.passiveSpawnKeepsDigCell(tile.weapon2Id)) {
|
|
return { tile: { ...tile }, wasType3: false };
|
|
}
|
|
|
|
const w2Type = this._config.getWeapon2Type(tile.weapon2Id) as Weapon2TileType;
|
|
const isBuiltin = this.map.isBuiltinPut(gridIndex) && !this.map.hasTile(gridIndex);
|
|
if (!canDragMapTile(tile, w2Type, isBuiltin)) {
|
|
return { error: PlaceRejectReason.NotDraggable };
|
|
}
|
|
|
|
return { tile: { ...tile }, wasType3: false };
|
|
}
|
|
|
|
/** type3 拖放失败后还原格状态 */
|
|
revertType3Pickup(gridIndex: number, _tile: MapTileRecord): void {
|
|
this._pendingDragTile = null;
|
|
this._applyType3Expand(gridIndex);
|
|
}
|
|
|
|
/**
|
|
* 落子(背包→地图 或 地图→地图 或 地图→背包)
|
|
* @param opts.silent 仅改盘面数据、不 emit(自动合成规划仿真用;调用方须自行 restore)
|
|
*/
|
|
drop(source: DragSource, target: DropTarget, opts?: { silent?: boolean }): PlaceRejectReason | null {
|
|
if (!this._prepActive) {
|
|
return PlaceRejectReason.NotPrepPhase;
|
|
}
|
|
|
|
const silent = !!opts?.silent;
|
|
if (silent) this._emitSuppressed = true;
|
|
try {
|
|
if (source.kind === 'bag' && target.kind === 'bag') {
|
|
return this._dropBagToBag(source.slotIndex, target.slotIndex);
|
|
}
|
|
if (source.kind === 'bag' && target.kind === 'map') {
|
|
return this._dropBagToMap(source.slotIndex, target.gridIndex);
|
|
}
|
|
if (source.kind === 'map' && target.kind === 'map') {
|
|
return this._dropMapToMap(source.gridIndex, target.gridIndex);
|
|
}
|
|
if (source.kind === 'map' && target.kind === 'bag') {
|
|
return this._dropMapToBag(source.gridIndex, target.slotIndex);
|
|
}
|
|
return PlaceRejectReason.WrongCellKind;
|
|
} finally {
|
|
if (silent) this._emitSuppressed = false;
|
|
}
|
|
}
|
|
|
|
private _applyType3Expand(gridIndex: number): void {
|
|
this.map.setCellKind(gridIndex, cellKindAfterType3Place());
|
|
this.map.setPrepType3(gridIndex, true);
|
|
}
|
|
|
|
private _applyType3Revert(gridIndex: number): void {
|
|
this.map.setPrepType3(gridIndex, false);
|
|
this.map.setCellKind(gridIndex, cellKindAfterType3Pickup());
|
|
}
|
|
|
|
/** Dig→Put:可开挖格变为可放置(被动产字等;不挂字块) */
|
|
private _applyType4DigConvert(gridIndex: number): void {
|
|
this.map.markCellAsPut(gridIndex);
|
|
}
|
|
|
|
private _canDropOnCell(gridIndex: number, w2Type: Weapon2TileType): boolean {
|
|
if (this._pathChecker?.isSpiderWebBlocked?.(gridIndex)) {
|
|
return false;
|
|
}
|
|
const cellKind = this._pathChecker?.getLiveCellKind?.(gridIndex)
|
|
?? this.map.getCellKind(gridIndex);
|
|
if (cellKind == null) return false;
|
|
const canPutGrid = this._pathChecker?.isCanPutGrid?.(gridIndex)
|
|
?? this.map.canPutGrid(gridIndex);
|
|
return canDropWeapon2OnCell(
|
|
w2Type,
|
|
cellKind,
|
|
this.map.hasTile(gridIndex),
|
|
canPutGrid,
|
|
);
|
|
}
|
|
|
|
private _dropRejectReason(gridIndex: number, w2Type: Weapon2TileType): PlaceRejectReason {
|
|
if (this._pathChecker?.isSpiderWebBlocked?.(gridIndex)) {
|
|
return w2Type === Weapon2TileType.Grid
|
|
? PlaceRejectReason.SpiderWebBlocked
|
|
: PlaceRejectReason.WrongCellKind;
|
|
}
|
|
if (w2Type === Weapon2TileType.Grid) {
|
|
return this._type3DropRejectReason(gridIndex);
|
|
}
|
|
return PlaceRejectReason.WrongCellKind;
|
|
}
|
|
|
|
private _type3DropRejectReason(gridIndex: number): PlaceRejectReason {
|
|
if (this._pathChecker?.isSpiderWebBlocked?.(gridIndex)) {
|
|
return PlaceRejectReason.SpiderWebBlocked;
|
|
}
|
|
return PlaceRejectReason.WrongCellKind;
|
|
}
|
|
|
|
/**
|
|
* 五行同 Text.type、同显示等级:目标格等级 +1。
|
|
* 不是五行对,或类型/等级不同,返回 skip。
|
|
*/
|
|
private _mergeElementOntoBag(
|
|
source: { weapon2Id: number; level?: number },
|
|
toSlot: number,
|
|
target: BagSlotRecord,
|
|
clearSource: () => void,
|
|
): PlaceRejectReason | null | 'skip' {
|
|
const srcType = Math.floor(this._config.getWeapon2Type(source.weapon2Id));
|
|
const dstType = Math.floor(this._config.getWeapon2Type(target.weapon2Id));
|
|
if (srcType < 1 || srcType > 5 || dstType < 1 || dstType > 5) return 'skip';
|
|
const srcLevel = Math.floor(source.level ?? 1);
|
|
const dstLevel = Math.floor(target.level ?? 1);
|
|
if (srcType !== dstType || srcLevel !== dstLevel) return 'skip';
|
|
const max = this._config.getElementMaxLevel(srcType);
|
|
if (srcLevel >= max) return PlaceRejectReason.MaxLevel;
|
|
const newLevel = srcLevel + 1;
|
|
clearSource();
|
|
this.bag.setSlot(toSlot, { ...target, level: newLevel });
|
|
const heroId = this._config.getElementHeroId(srcType, newLevel) ?? 0;
|
|
this.emit(MergeEventFactory.bagMerged(toSlot, heroId, newLevel));
|
|
return null;
|
|
}
|
|
|
|
private _dropBagToBag(fromSlot: number, toSlot: number): PlaceRejectReason | null {
|
|
if (fromSlot === toSlot) return null;
|
|
|
|
const src = this.bag.getSlot(fromSlot);
|
|
const dst = this.bag.getSlot(toSlot);
|
|
if (!src) return PlaceRejectReason.SlotEmpty;
|
|
if (src.battleCardKind) {
|
|
if (!dst) {
|
|
this.bag.setSlot(toSlot, src);
|
|
this.bag.clearSlot(fromSlot);
|
|
this.emit(MergeEventFactory.bagSlotMoved(fromSlot, toSlot));
|
|
return null;
|
|
}
|
|
return PlaceRejectReason.InvalidMerge;
|
|
}
|
|
if (dst?.battleCardKind) return PlaceRejectReason.InvalidMerge;
|
|
if (!dst) {
|
|
this.bag.setSlot(toSlot, src);
|
|
this.bag.clearSlot(fromSlot);
|
|
this.emit(MergeEventFactory.bagSlotMoved(fromSlot, toSlot));
|
|
return null;
|
|
}
|
|
const element = this._mergeElementOntoBag(src, toSlot, dst, () => {
|
|
this.bag.clearSlot(fromSlot);
|
|
});
|
|
if (element !== 'skip') return element;
|
|
if (this._config.passiveSpawnKeepsDigCell(src.weapon2Id)
|
|
&& this._config.passiveSpawnKeepsDigCell(dst.weapon2Id)) {
|
|
const merged = this._tryKeepDigBagBagMerge(fromSlot, toSlot, src, dst);
|
|
if (merged === null) return null;
|
|
if (merged !== 'skip') return merged;
|
|
this.bag.setSlot(toSlot, src);
|
|
this.bag.setSlot(fromSlot, dst);
|
|
this.emit(MergeEventFactory.bagSlotMoved(fromSlot, toSlot));
|
|
return null;
|
|
}
|
|
|
|
// 不能合成(异级/满级等)→ 交换位置
|
|
this.bag.setSlot(toSlot, src);
|
|
this.bag.setSlot(fromSlot, dst);
|
|
this.emit(MergeEventFactory.bagSlotMoved(fromSlot, toSlot));
|
|
return null;
|
|
}
|
|
|
|
private _dropBagToMap(slotIndex: number, gridIndex: number): PlaceRejectReason | null {
|
|
const slot = this.bag.getSlot(slotIndex);
|
|
if (!slot) return PlaceRejectReason.SlotEmpty;
|
|
// 产卡须由 BattleBagSession 先消费,禁止落到地图当字块
|
|
if (slot.battleCardKind) return PlaceRejectReason.InvalidMerge;
|
|
|
|
if (this._config.passiveSpawnKeepsDigCell(slot.weapon2Id)) {
|
|
return this._dropKeepDigBagToMap(slotIndex, gridIndex, slot);
|
|
}
|
|
|
|
const w2Type = Number(this._config.getWeapon2Type(slot.weapon2Id)) as Weapon2TileType;
|
|
// 功能符须由 BattleBagSession 消费,禁止当字块落地图
|
|
if (isBagToolTextType(w2Type)) return PlaceRejectReason.InvalidMerge;
|
|
|
|
if (!isElementGlyphType(w2Type) && w2Type === Weapon2TileType.Grid) {
|
|
if (!this._canDropOnCell(gridIndex, w2Type)) {
|
|
return this._dropRejectReason(gridIndex, w2Type);
|
|
}
|
|
const ok = this._pathChecker?.canPlaceType3Grid(gridIndex) ?? true;
|
|
if (!ok) return PlaceRejectReason.PathBlocked;
|
|
this._applyType3Expand(gridIndex);
|
|
this.bag.clearSlot(slotIndex);
|
|
this.emit(MergeEventFactory.tilePlaced(gridIndex, slot.weapon2Id));
|
|
return null;
|
|
}
|
|
|
|
if (isShovelTextType(w2Type)) {
|
|
if (!this._canDropOnCell(gridIndex, w2Type)) {
|
|
return this._dropRejectReason(gridIndex, w2Type);
|
|
}
|
|
this._applyType4DigConvert(gridIndex);
|
|
this.bag.clearSlot(slotIndex);
|
|
this.emit(MergeEventFactory.tilePlaced(gridIndex, slot.weapon2Id));
|
|
return null;
|
|
}
|
|
|
|
const targetTile = this.map.getTile(gridIndex);
|
|
if (targetTile) {
|
|
const element = this._mergeElementOntoMap(slot, gridIndex, targetTile, () => {
|
|
this.bag.clearSlot(slotIndex);
|
|
});
|
|
if (element !== 'skip') return element;
|
|
|
|
const swapped = this._tryBagMapSwap(slotIndex, gridIndex, slot, targetTile);
|
|
if (swapped !== 'skip') return swapped;
|
|
}
|
|
|
|
if (!this._canDropOnCell(gridIndex, w2Type)) {
|
|
return this._dropRejectReason(gridIndex, w2Type);
|
|
}
|
|
|
|
const tile = this._mapTileFromBagSlot(slot, TileSource.BagRefresh);
|
|
this.map.placeTile(gridIndex, tile);
|
|
this.bag.clearSlot(slotIndex);
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.tilePlaced(gridIndex, slot.weapon2Id));
|
|
return null;
|
|
}
|
|
|
|
private _dropMapToMap(fromGrid: number, toGrid: number): PlaceRejectReason | null {
|
|
const pendingType3 = this._pendingDragTile?.source === TileSource.PrepPlaced
|
|
&& !isElementGlyphType(this._config.getWeapon2Type(this._pendingDragTile.weapon2Id))
|
|
&& this._config.getWeapon2Type(this._pendingDragTile.weapon2Id) === Weapon2TileType.Grid;
|
|
|
|
if (fromGrid === toGrid) {
|
|
if (pendingType3) {
|
|
this._applyType3Expand(fromGrid);
|
|
}
|
|
this._pendingDragTile = null;
|
|
return null;
|
|
}
|
|
|
|
const tile = this.map.getTile(fromGrid) ?? this._pendingDragTile;
|
|
if (!tile) return PlaceRejectReason.SlotEmpty;
|
|
|
|
const w2Type = this._config.getWeapon2Type(tile.weapon2Id) as Weapon2TileType;
|
|
|
|
if (!isElementGlyphType(w2Type) && w2Type === Weapon2TileType.Grid && tile.source === TileSource.PrepPlaced) {
|
|
if (!this._canDropOnCell(toGrid, Weapon2TileType.Grid)) {
|
|
return this._dropRejectReason(toGrid, Weapon2TileType.Grid);
|
|
}
|
|
const ok = this._pathChecker?.canPlaceType3Grid(toGrid) ?? true;
|
|
if (!ok) return PlaceRejectReason.PathBlocked;
|
|
|
|
if (this.map.isPrepType3(fromGrid)) {
|
|
this._applyType3Revert(fromGrid);
|
|
}
|
|
this._applyType3Expand(toGrid);
|
|
this._pendingDragTile = null;
|
|
this.emit(MergeEventFactory.tileMoved(fromGrid, toGrid));
|
|
return null;
|
|
}
|
|
|
|
this._pendingDragTile = null;
|
|
|
|
if (isShovelTextType(Number(w2Type))) {
|
|
if (!this._canDropOnCell(toGrid, w2Type)) {
|
|
return this._dropRejectReason(toGrid, w2Type);
|
|
}
|
|
this.map.removeTile(fromGrid);
|
|
this.map.setCellKind(fromGrid, cellKindAfterReturnToBag());
|
|
this._applyType4DigConvert(toGrid);
|
|
this.emit(MergeEventFactory.tilePlaced(toGrid, tile.weapon2Id));
|
|
return null;
|
|
}
|
|
|
|
if (this._keepsDigCell(tile)) {
|
|
return this._dropMapToMapKeepDig(fromGrid, toGrid, tile);
|
|
}
|
|
|
|
const targetTile = this.map.getTile(toGrid);
|
|
if (targetTile) {
|
|
const element = this._mergeElementOntoMap(tile, toGrid, targetTile, () => {
|
|
this.map.removeTile(fromGrid);
|
|
this.map.setCellKind(fromGrid, cellKindAfterReturnToBag());
|
|
});
|
|
if (element !== 'skip') return element;
|
|
|
|
const swapped = this._tryMapToMapSwap(fromGrid, toGrid, tile, targetTile);
|
|
if (swapped !== 'skip') return swapped;
|
|
}
|
|
|
|
if (!this._canDropOnCell(toGrid, w2Type)) {
|
|
return this._dropRejectReason(toGrid, w2Type);
|
|
}
|
|
|
|
this.map.removeTile(fromGrid);
|
|
this.map.setCellKind(fromGrid, cellKindAfterReturnToBag());
|
|
this.map.placeTile(toGrid, { ...tile });
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.tileMoved(fromGrid, toGrid));
|
|
return null;
|
|
}
|
|
|
|
private _dropMapToBag(fromGrid: number, toSlot: number): PlaceRejectReason | null {
|
|
const tile = this.map.getTile(fromGrid);
|
|
const bagSlot = this.bag.getSlot(toSlot);
|
|
|
|
if (this.map.isPrepType3(fromGrid) && !tile) {
|
|
this._applyType3Revert(fromGrid);
|
|
this.bag.setSlot(toSlot, { weapon2Id: GRID_WEAPON2_ID });
|
|
this._pendingDragTile = null;
|
|
this.emit(MergeEventFactory.tileReturned(fromGrid, toSlot));
|
|
return null;
|
|
}
|
|
|
|
if (!tile) return PlaceRejectReason.SlotEmpty;
|
|
|
|
if (this._config.passiveSpawnKeepsDigCell(tile.weapon2Id)) {
|
|
return this._dropKeepDigMapToBag(fromGrid, toSlot, tile, bagSlot);
|
|
}
|
|
|
|
const w2Type = this._config.getWeapon2Type(tile.weapon2Id) as Weapon2TileType;
|
|
const isBuiltin = false;
|
|
if (!canDragMapTile(tile, w2Type, isBuiltin)) {
|
|
return PlaceRejectReason.NotDraggable;
|
|
}
|
|
|
|
if (bagSlot) {
|
|
const element = this._mergeElementOntoBag(tile, toSlot, bagSlot, () => {
|
|
this.map.removeTile(fromGrid);
|
|
this.map.setCellKind(fromGrid, cellKindAfterReturnToBag());
|
|
this._recompose();
|
|
});
|
|
if (element !== 'skip') return element;
|
|
|
|
const swapped = this._tryMapBagSwap(fromGrid, toSlot, tile, bagSlot);
|
|
if (swapped !== 'skip') return swapped;
|
|
return PlaceRejectReason.WrongCellKind;
|
|
}
|
|
|
|
this.map.removeTile(fromGrid);
|
|
this.map.setCellKind(fromGrid, cellKindAfterReturnToBag());
|
|
|
|
this.bag.setSlot(toSlot, this._bagSlotFromMapTile(tile));
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.tileReturned(fromGrid, toSlot));
|
|
return null;
|
|
}
|
|
|
|
/** 组合内所有字块设为同一等级,并同步绑定快照等级 */
|
|
private _applyLevelToCompositionTiles(compose: ComposedUnit, level: number): void {
|
|
for (const idx of compose.gridIndices) {
|
|
const tile = this.map.getTile(idx);
|
|
if (!tile) continue;
|
|
if ((tile.level ?? 1) === level && tile.heroBind?.level === level) continue;
|
|
const rec: MapTileRecord = { ...tile, level };
|
|
if (tile.heroBind) rec.heroBind = { ...this._cloneHeroBind(tile.heroBind), level };
|
|
this.map.placeTile(idx, rec);
|
|
}
|
|
}
|
|
|
|
private _maxTileLevelInCompose(compose: ComposedUnit): number {
|
|
let max = 1;
|
|
for (const idx of compose.gridIndices) {
|
|
const tile = this.map.getTile(idx);
|
|
const lv = Math.max(tile?.heroBind?.level ?? 1, tile?.level ?? 1);
|
|
if (lv > max) max = lv;
|
|
}
|
|
return max;
|
|
}
|
|
|
|
/** 文字是否来自 functiontype=3,可在未开挖格移动并参与同类合成 */
|
|
private _isFunction3Tile(tile: MapTileRecord): boolean {
|
|
return tile.source === TileSource.PassiveFunction3;
|
|
}
|
|
|
|
/** functiontype=3 产字:默认仅空 Dig;蟠桃树另含空 Put */
|
|
private _canDropFunction3OnCell(gridIndex: number, weapon2Id: number): boolean {
|
|
if (this._config.passiveSpawnKeepsDigCell(weapon2Id)) {
|
|
return this._canDropKeepDigOnCell(gridIndex);
|
|
}
|
|
if (this._pathChecker?.isSpiderWebBlocked?.(gridIndex)) return false;
|
|
const cellKind = this._pathChecker?.getLiveCellKind?.(gridIndex)
|
|
?? this.map.getCellKind(gridIndex);
|
|
return cellKind === MapCellKind.Dig && !this.map.hasTile(gridIndex);
|
|
}
|
|
|
|
/** functiontype=3 产字:空格移动或与目标文字合成 */
|
|
private _dropFunction3MapToMap(fromGrid: number, toGrid: number, tile: MapTileRecord): PlaceRejectReason | null {
|
|
const targetTile = this.map.getTile(toGrid);
|
|
if (targetTile) {
|
|
const merged = this._tryKeepDigMerge(fromGrid, toGrid, tile, targetTile);
|
|
if (merged === null) return null;
|
|
if (merged !== 'skip') return merged;
|
|
}
|
|
if (!this._canDropFunction3OnCell(toGrid, tile.weapon2Id)) return PlaceRejectReason.WrongCellKind;
|
|
this.map.removeTile(fromGrid);
|
|
if (this._config.passiveSpawnKeepsDigCell(tile.weapon2Id)) {
|
|
this._convertPutToDig(toGrid);
|
|
}
|
|
this.map.placeTile(toGrid, { ...tile });
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.tileMoved(fromGrid, toGrid));
|
|
return null;
|
|
}
|
|
|
|
/** 蟠桃树及 functiontype=3 产字均保持 Dig 格 */
|
|
private _keepsDigCell(tile: MapTileRecord): boolean {
|
|
return this._isFunction3Tile(tile) || this._config.passiveSpawnKeepsDigCell(tile.weapon2Id);
|
|
}
|
|
|
|
/**
|
|
* functiontype=3 产字可在未开挖格之间拖动,并复用已有合成规则。
|
|
*/
|
|
private _dropMapToMapKeepDig(fromGrid: number, toGrid: number, tile: MapTileRecord): PlaceRejectReason | null {
|
|
if (this._isFunction3Tile(tile)) {
|
|
return this._dropFunction3MapToMap(fromGrid, toGrid, tile);
|
|
}
|
|
const targetTile = this.map.getTile(toGrid);
|
|
if (targetTile) {
|
|
const merged = this._tryKeepDigMerge(fromGrid, toGrid, tile, targetTile);
|
|
if (merged === null) return null;
|
|
if (merged !== 'skip') return merged;
|
|
const swapped = this._tryKeepDigSwap(fromGrid, toGrid, tile, targetTile);
|
|
if (swapped !== 'skip') return swapped;
|
|
return PlaceRejectReason.WrongCellKind;
|
|
}
|
|
if (!this._canDropKeepDigOnCell(toGrid)) {
|
|
return PlaceRejectReason.WrongCellKind;
|
|
}
|
|
this.map.removeTile(fromGrid);
|
|
this._convertPutToDig(toGrid);
|
|
this.map.placeTile(toGrid, { ...tile });
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.tileMoved(fromGrid, toGrid));
|
|
return null;
|
|
}
|
|
|
|
/** 空 Dig 或空 Put,且无蛛网 */
|
|
private _canDropKeepDigOnCell(gridIndex: number): boolean {
|
|
if (this._pathChecker?.isSpiderWebBlocked?.(gridIndex)) return false;
|
|
const cellKind = this._pathChecker?.getLiveCellKind?.(gridIndex)
|
|
?? this.map.getCellKind(gridIndex);
|
|
if (cellKind !== MapCellKind.Dig && cellKind !== MapCellKind.Put) return false;
|
|
return !this.map.hasTile(gridIndex);
|
|
}
|
|
|
|
/** 空 Put 被蟠桃树占用后转为 Dig */
|
|
private _convertPutToDig(gridIndex: number): void {
|
|
const kind = this.map.getCellKind(gridIndex);
|
|
if (kind !== MapCellKind.Put) return;
|
|
this.map.markCellAsDig(gridIndex);
|
|
}
|
|
|
|
/**
|
|
* 背包蟠桃树落到空 Dig/Put,或与地图蟠桃树合成/交换。
|
|
* @param slotIndex 背包槽
|
|
* @param gridIndex 目标格
|
|
* @param slot 背包字
|
|
*/
|
|
private _dropKeepDigBagToMap(slotIndex: number, gridIndex: number, slot: BagSlotRecord): PlaceRejectReason | null {
|
|
const tile = this._mapTileFromBagSlot(slot, TileSource.PassiveFunction3);
|
|
const targetTile = this.map.getTile(gridIndex);
|
|
if (targetTile) {
|
|
const merged = this._tryKeepDigBagMapMerge(slotIndex, gridIndex, tile, targetTile);
|
|
if (merged === null) return null;
|
|
if (merged !== 'skip') return merged;
|
|
const swapped = this._tryKeepDigBagMapSwap(slotIndex, gridIndex, tile, targetTile);
|
|
if (swapped !== 'skip') return swapped;
|
|
return PlaceRejectReason.WrongCellKind;
|
|
}
|
|
if (!this._canDropKeepDigOnCell(gridIndex)) return PlaceRejectReason.WrongCellKind;
|
|
this._convertPutToDig(gridIndex);
|
|
this.map.placeTile(gridIndex, tile);
|
|
this.bag.clearSlot(slotIndex);
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.tilePlaced(gridIndex, slot.weapon2Id));
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 地图蟠桃树拖回背包;源格保持 Dig。
|
|
* @param fromGrid 源格
|
|
* @param toSlot 背包槽
|
|
* @param tile 地图字
|
|
* @param bagSlot 目标槽现有字
|
|
*/
|
|
private _dropKeepDigMapToBag(fromGrid: number, toSlot: number, tile: MapTileRecord, bagSlot: BagSlotRecord | null): PlaceRejectReason | null {
|
|
if (bagSlot) {
|
|
const merged = this._tryKeepDigMapBagMerge(fromGrid, toSlot, tile, bagSlot);
|
|
if (merged === null) return null;
|
|
if (merged !== 'skip') return merged;
|
|
const swapped = this._tryKeepDigMapBagSwap(fromGrid, toSlot, tile, bagSlot);
|
|
if (swapped !== 'skip') return swapped;
|
|
return PlaceRejectReason.WrongCellKind;
|
|
}
|
|
this.map.removeTile(fromGrid);
|
|
this.bag.setSlot(toSlot, this._bagSlotFromMapTile(tile));
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.tileReturned(fromGrid, toSlot));
|
|
return null;
|
|
}
|
|
|
|
/** 背包蟠桃树与地图蟠桃树同级合成 */
|
|
private _tryKeepDigBagMapMerge(slotIndex: number, gridIndex: number, sourceTile: MapTileRecord, targetTile: MapTileRecord): PlaceRejectReason | null | 'skip' {
|
|
if (!this._isSameKeepDigSource(sourceTile, targetTile)) return 'skip';
|
|
if (sourceTile.weapon2Id !== targetTile.weapon2Id) return 'skip';
|
|
const srcLevel = sourceTile.level ?? 1;
|
|
const dstLevel = targetTile.level ?? 1;
|
|
if (srcLevel !== dstLevel) return 'skip';
|
|
const max = this._keepDigMergeMax(sourceTile);
|
|
if (srcLevel >= max) return 'skip';
|
|
const newLevel = Math.min(max, this._upgrade.upgradeLevel(srcLevel));
|
|
this.bag.clearSlot(slotIndex);
|
|
this.map.placeTile(gridIndex, { ...targetTile, level: newLevel });
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.mapUpgraded(gridIndex, targetTile.weapon1Id ?? 0, newLevel));
|
|
return null;
|
|
}
|
|
|
|
/** 背包与地图蟠桃树互换 */
|
|
private _tryKeepDigBagMapSwap(slotIndex: number, gridIndex: number, sourceTile: MapTileRecord, targetTile: MapTileRecord): PlaceRejectReason | null | 'skip' {
|
|
if (!this._isSameKeepDigSource(sourceTile, targetTile)) return 'skip';
|
|
this.map.placeTile(gridIndex, { ...sourceTile });
|
|
this.bag.setSlot(slotIndex, this._bagSlotFromMapTile(targetTile));
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.tileMoved(gridIndex, gridIndex));
|
|
return null;
|
|
}
|
|
|
|
/** 地图蟠桃树与背包蟠桃树同级合成 */
|
|
private _tryKeepDigMapBagMerge(fromGrid: number, toSlot: number, mapTile: MapTileRecord, bagSlot: BagSlotRecord): PlaceRejectReason | null | 'skip' {
|
|
const bagTile: MapTileRecord = {
|
|
weapon2Id: bagSlot.weapon2Id,
|
|
source: TileSource.PassiveFunction3,
|
|
weapon1Id: bagSlot.weapon1Id,
|
|
level: bagSlot.level,
|
|
};
|
|
if (!this._isSameKeepDigSource(mapTile, bagTile)) return 'skip';
|
|
if (mapTile.weapon2Id !== bagSlot.weapon2Id) return 'skip';
|
|
const srcLevel = mapTile.level ?? 1;
|
|
const dstLevel = bagSlot.level ?? 1;
|
|
if (srcLevel !== dstLevel) return 'skip';
|
|
const max = this._keepDigMergeMax(mapTile);
|
|
if (srcLevel >= max) return 'skip';
|
|
const newLevel = Math.min(max, this._upgrade.upgradeLevel(srcLevel));
|
|
this.map.removeTile(fromGrid);
|
|
this.bag.setSlot(toSlot, {
|
|
weapon2Id: bagSlot.weapon2Id,
|
|
weapon1Id: bagSlot.weapon1Id,
|
|
level: newLevel,
|
|
});
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.bagMerged(toSlot, bagSlot.weapon1Id ?? 0, newLevel));
|
|
return null;
|
|
}
|
|
|
|
/** 地图与背包蟠桃树互换;源格保持 Dig */
|
|
private _tryKeepDigMapBagSwap(fromGrid: number, toSlot: number, mapTile: MapTileRecord, bagSlot: BagSlotRecord): PlaceRejectReason | null | 'skip' {
|
|
const bagTile = this._mapTileFromBagSlot(bagSlot, TileSource.PassiveFunction3);
|
|
if (!this._isSameKeepDigSource(mapTile, bagTile)) return 'skip';
|
|
this.map.placeTile(fromGrid, bagTile);
|
|
this.bag.setSlot(toSlot, this._bagSlotFromMapTile(mapTile));
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.tileMoved(fromGrid, fromGrid));
|
|
return null;
|
|
}
|
|
|
|
/** 背包内两棵蟠桃树同级合成 */
|
|
private _tryKeepDigBagBagMerge(fromSlot: number, toSlot: number, source: BagSlotRecord, target: BagSlotRecord): PlaceRejectReason | null | 'skip' {
|
|
if (source.weapon2Id !== target.weapon2Id) return 'skip';
|
|
const srcLevel = source.level ?? 1;
|
|
const dstLevel = target.level ?? 1;
|
|
if (srcLevel !== dstLevel) return 'skip';
|
|
const max = this._config.getKeepDigTileMaxLevel(source.weapon2Id);
|
|
if (srcLevel >= max) return 'skip';
|
|
const newLevel = Math.min(max, this._upgrade.upgradeLevel(srcLevel));
|
|
this.bag.setSlot(toSlot, {
|
|
weapon2Id: target.weapon2Id,
|
|
weapon1Id: target.weapon1Id,
|
|
level: newLevel,
|
|
});
|
|
this.bag.clearSlot(fromSlot);
|
|
this.emit(MergeEventFactory.bagMerged(toSlot, target.weapon1Id ?? 0, newLevel));
|
|
return null;
|
|
}
|
|
|
|
/** 蟠桃树合成等级上限 */
|
|
private _keepDigMergeMax(tile: MapTileRecord): number {
|
|
return this._isFunction3Tile(tile)
|
|
? this._config.getMaxWeaponLevel(tile.weapon1Id
|
|
?? this._config.getWeapon1ByGroupSequence([tile.weapon2Id])!)
|
|
: this._config.getKeepDigTileMaxLevel(tile.weapon2Id);
|
|
}
|
|
|
|
/** 保持 Dig 的同来源文字是否可合成 */
|
|
private _isSameKeepDigSource(sourceTile: MapTileRecord, targetTile: MapTileRecord): boolean {
|
|
const sourceFunction3 = this._isFunction3Tile(sourceTile);
|
|
const targetFunction3 = this._isFunction3Tile(targetTile);
|
|
if (sourceFunction3 || targetFunction3) return sourceFunction3 && targetFunction3;
|
|
return this._config.passiveSpawnKeepsDigCell(sourceTile.weapon2Id)
|
|
&& this._config.passiveSpawnKeepsDigCell(targetTile.weapon2Id);
|
|
}
|
|
|
|
/** 同来源保持 Dig 文字同级合成 */
|
|
private _tryKeepDigMerge(fromGrid: number, toGrid: number, sourceTile: MapTileRecord, targetTile: MapTileRecord): PlaceRejectReason | null | 'skip' {
|
|
if (!this._isSameKeepDigSource(sourceTile, targetTile)) return 'skip';
|
|
if (sourceTile.weapon2Id !== targetTile.weapon2Id) return 'skip';
|
|
|
|
const srcLevel = sourceTile.level ?? 1;
|
|
const dstLevel = targetTile.level ?? 1;
|
|
if (srcLevel !== dstLevel) return 'skip';
|
|
|
|
const max = this._isFunction3Tile(sourceTile)
|
|
? this._config.getMaxWeaponLevel(sourceTile.weapon1Id
|
|
?? this._config.getWeapon1ByGroupSequence([sourceTile.weapon2Id])!)
|
|
: this._config.getKeepDigTileMaxLevel(sourceTile.weapon2Id);
|
|
if (srcLevel >= max) return 'skip';
|
|
|
|
const newLevel = Math.min(max, this._upgrade.upgradeLevel(srcLevel));
|
|
this.map.removeTile(fromGrid);
|
|
this.map.placeTile(toGrid, {
|
|
...targetTile,
|
|
level: newLevel,
|
|
});
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.mapUpgraded(toGrid, targetTile.weapon1Id ?? 0, newLevel));
|
|
return null;
|
|
}
|
|
|
|
/** 两棵蟠桃树不能合成时互换位置(均保持 Dig) */
|
|
private _tryKeepDigSwap(fromGrid: number, toGrid: number, sourceTile: MapTileRecord, targetTile: MapTileRecord): PlaceRejectReason | null | 'skip' {
|
|
if (!this._config.passiveSpawnKeepsDigCell(sourceTile.weapon2Id)) return 'skip';
|
|
if (!this._config.passiveSpawnKeepsDigCell(targetTile.weapon2Id)) return 'skip';
|
|
this.map.placeTile(toGrid, { ...sourceTile });
|
|
this.map.placeTile(fromGrid, { ...targetTile });
|
|
this.emit(MergeEventFactory.tileMoved(fromGrid, toGrid));
|
|
return null;
|
|
}
|
|
|
|
/** 背包 ↔ 地图:目标格已有字块且不能合成时交换(含已激活组合格,交换后 _recompose 拆组合) */
|
|
private _tryBagMapSwap(
|
|
slotIndex: number,
|
|
gridIndex: number,
|
|
bagSlot: BagSlotRecord,
|
|
mapTile: MapTileRecord,
|
|
): PlaceRejectReason | null | 'skip' {
|
|
if (!this._canSwapWeaponTile(bagSlot.weapon2Id, mapTile.weapon2Id)) return 'skip';
|
|
|
|
const cellKind = this._pathChecker?.getLiveCellKind?.(gridIndex)
|
|
?? this.map.getCellKind(gridIndex);
|
|
if (cellKind !== MapCellKind.Put) return 'skip';
|
|
|
|
const mapW2Type = this._config.getWeapon2Type(mapTile.weapon2Id) as Weapon2TileType;
|
|
if (!canDragMapTile(mapTile, mapW2Type, false)) return 'skip';
|
|
|
|
this.map.placeTile(gridIndex, this._mapTileFromBagSlot(bagSlot, TileSource.BagRefresh));
|
|
this.bag.setSlot(slotIndex, this._bagSlotFromMapTile(mapTile));
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.bagRefreshed(false));
|
|
this.emit(MergeEventFactory.tileMoved(gridIndex, gridIndex));
|
|
return null;
|
|
}
|
|
|
|
/** 地图 ↔ 地图:两格均有字块且不能合成时交换(含已激活组合格,交换后拆组合) */
|
|
private _tryMapToMapSwap(
|
|
fromGrid: number,
|
|
toGrid: number,
|
|
sourceTile: MapTileRecord,
|
|
targetTile: MapTileRecord,
|
|
): PlaceRejectReason | null | 'skip' {
|
|
if (!this._canSwapWeaponTile(sourceTile.weapon2Id, targetTile.weapon2Id)) return 'skip';
|
|
|
|
const fromKind = this._pathChecker?.getLiveCellKind?.(fromGrid) ?? this.map.getCellKind(fromGrid);
|
|
const toKind = this._pathChecker?.getLiveCellKind?.(toGrid) ?? this.map.getCellKind(toGrid);
|
|
if (fromKind !== MapCellKind.Put || toKind !== MapCellKind.Put) return 'skip';
|
|
|
|
const srcW2Type = this._config.getWeapon2Type(sourceTile.weapon2Id) as Weapon2TileType;
|
|
const tgtW2Type = this._config.getWeapon2Type(targetTile.weapon2Id) as Weapon2TileType;
|
|
if (!canDragMapTile(sourceTile, srcW2Type, false)) return 'skip';
|
|
if (!canDragMapTile(targetTile, tgtW2Type, false)) return 'skip';
|
|
|
|
this.map.placeTile(toGrid, { ...sourceTile });
|
|
this.map.placeTile(fromGrid, { ...targetTile });
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.tileMoved(fromGrid, toGrid));
|
|
return null;
|
|
}
|
|
|
|
/** 地图 ↔ 背包:背包槽已有字块且不能合成时交换(含已激活组合格,交换后拆组合) */
|
|
private _tryMapBagSwap(
|
|
fromGrid: number,
|
|
toSlot: number,
|
|
mapTile: MapTileRecord,
|
|
bagSlot: BagSlotRecord,
|
|
): PlaceRejectReason | null | 'skip' {
|
|
if (!this._canSwapWeaponTile(bagSlot.weapon2Id, mapTile.weapon2Id)) return 'skip';
|
|
|
|
const cellKind = this._pathChecker?.getLiveCellKind?.(fromGrid)
|
|
?? this.map.getCellKind(fromGrid);
|
|
if (cellKind !== MapCellKind.Put) return 'skip';
|
|
|
|
this.map.placeTile(fromGrid, this._mapTileFromBagSlot(bagSlot, TileSource.BagRefresh));
|
|
this.bag.setSlot(toSlot, this._bagSlotFromMapTile(mapTile));
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.bagRefreshed(false));
|
|
this.emit(MergeEventFactory.tileMoved(fromGrid, fromGrid));
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 五行同 Text.type、同显示等级:目标格等级 +1。
|
|
* 不是五行对,或类型/等级不同,返回 skip。
|
|
*/
|
|
private _mergeElementOntoMap(
|
|
source: { weapon2Id: number; level?: number },
|
|
toGrid: number,
|
|
target: MapTileRecord,
|
|
clearSource: () => void,
|
|
): PlaceRejectReason | null | 'skip' {
|
|
const srcType = Math.floor(this._config.getWeapon2Type(source.weapon2Id));
|
|
const dstType = Math.floor(this._config.getWeapon2Type(target.weapon2Id));
|
|
if (srcType < 1 || srcType > 5 || dstType < 1 || dstType > 5) return 'skip';
|
|
const srcLevel = Math.floor(source.level ?? 1);
|
|
const dstLevel = Math.floor(target.level ?? 1);
|
|
if (srcType !== dstType || srcLevel !== dstLevel) return 'skip';
|
|
const max = this._config.getElementMaxLevel(srcType);
|
|
if (srcLevel >= max) return PlaceRejectReason.MaxLevel;
|
|
const newLevel = srcLevel + 1;
|
|
clearSource();
|
|
this.map.placeTile(toGrid, { ...target, level: newLevel });
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.mapUpgraded(toGrid, target.weapon1Id ?? 0, newLevel));
|
|
return null;
|
|
}
|
|
|
|
/** 仅五行字可参与交换 */
|
|
private _canSwapWeaponTile(weapon2IdA: number, weapon2IdB: number): boolean {
|
|
const typeA = this._config.getWeapon2Type(weapon2IdA);
|
|
const typeB = this._config.getWeapon2Type(weapon2IdB);
|
|
return isElementGlyphType(typeA) && isElementGlyphType(typeB);
|
|
}
|
|
|
|
/** 背包槽 → 地图字块(含损毁与英雄绑定) */
|
|
private _mapTileFromBagSlot(slot: BagSlotRecord, source: TileSource): MapTileRecord {
|
|
const tile: MapTileRecord = {
|
|
weapon2Id: slot.weapon2Id,
|
|
source,
|
|
weapon1Id: slot.weapon1Id,
|
|
level: slot.level,
|
|
};
|
|
if (slot.ruined) tile.ruined = true;
|
|
if (slot.heroBind) tile.heroBind = this._cloneHeroBind(slot.heroBind);
|
|
if (slot.battleCardKind) tile.battleCardKind = slot.battleCardKind;
|
|
return tile;
|
|
}
|
|
|
|
/** 地图字块 → 背包槽(含损毁与英雄绑定) */
|
|
private _bagSlotFromMapTile(tile: MapTileRecord): BagSlotRecord {
|
|
const slot: BagSlotRecord = {
|
|
weapon2Id: tile.weapon2Id,
|
|
weapon1Id: tile.weapon1Id,
|
|
level: tile.level,
|
|
};
|
|
if (tile.ruined) slot.ruined = true;
|
|
if (tile.heroBind) slot.heroBind = this._cloneHeroBind(tile.heroBind);
|
|
if (tile.battleCardKind) slot.battleCardKind = tile.battleCardKind;
|
|
return slot;
|
|
}
|
|
|
|
/**
|
|
* 五行 Hero.id:Text.type + 显示等级。
|
|
* @param weapon2Id Text.id
|
|
* @param level 显示等级
|
|
*/
|
|
private _resolveElementHeroId(weapon2Id: number, level: number): number | null {
|
|
const textType = Math.floor(this._config.getWeapon2Type(weapon2Id));
|
|
if (!isElementGlyphType(textType)) return null;
|
|
const lv = Math.max(1, Math.floor(level));
|
|
return this._config.getElementHeroId(textType, lv);
|
|
}
|
|
|
|
private _recompose(): void {
|
|
const layout = this.map.getWeapon2Layout();
|
|
const found: ComposedUnit[] = [];
|
|
this._appendElementCompositions(layout, found);
|
|
this._compositions = found;
|
|
|
|
const activeKeys = createMembershipSet<string>();
|
|
for (const c of this._compositions) {
|
|
const key = this._composeKey(c);
|
|
membershipAdd(activeKeys, key);
|
|
const level = this._inferCompositionLevel(c);
|
|
this._compositionLevels.set(key, level);
|
|
c.level = level;
|
|
this._applyLevelToCompositionTiles(c, level);
|
|
}
|
|
this._compositionLevels.forEach((_level, key) => {
|
|
if (!activeKeys.has(key)) {
|
|
this._compositionLevels.delete(key);
|
|
}
|
|
});
|
|
|
|
this.emit(MergeEventFactory.composeChanged(this._compositions));
|
|
}
|
|
|
|
/**
|
|
* 五行单格:按 Text.type + 显示等级解析 Hero.id,作为可部署攻击单位。
|
|
* @param layout 当前地图字块布局
|
|
* @param found 匹配结果(就地追加)
|
|
*/
|
|
private _appendElementCompositions(layout: Map<number, number>, found: ComposedUnit[]): void {
|
|
const occupied = createMembershipSet<number>();
|
|
for (let i = 0; i < found.length; i++) {
|
|
const indices = found[i].gridIndices;
|
|
for (let j = 0; j < indices.length; j++) membershipAdd(occupied, indices[j]);
|
|
}
|
|
layout.forEach((weapon2Id, gridIndex) => {
|
|
if (occupied.has(gridIndex)) return;
|
|
const type = Math.floor(this._config.getWeapon2Type(weapon2Id));
|
|
if (!isElementGlyphType(type)) return;
|
|
const tile = this.map.getTile(gridIndex);
|
|
const level = Math.max(1, Math.floor(tile?.level ?? 1));
|
|
const heroId = this._config.getElementHeroId(type, level);
|
|
if (!(heroId > 0)) return;
|
|
membershipAdd(occupied, gridIndex);
|
|
found.push({
|
|
weapon1Id: heroId,
|
|
level,
|
|
gridIndices: [gridIndex],
|
|
direction: 'horizontal',
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 封神榜:是否可升级地图五行字块。
|
|
* @param gridIndex 地图格索引
|
|
* @param levelDelta functionpar 等级增量
|
|
* @returns null 表示可升级;否则为拒绝原因
|
|
*/
|
|
canApplyBattleBuffUnitLevelUp(gridIndex: number, levelDelta: number): PlaceRejectReason | null {
|
|
return this._evaluateBattleBuffUnitLevelUp(gridIndex, levelDelta);
|
|
}
|
|
|
|
/** 封神榜:是否可升级背包五行字块 */
|
|
canApplyBattleBuffUnitLevelUpBag(slotIndex: number, levelDelta: number): PlaceRejectReason | null {
|
|
return this._evaluateBattleBuffUnitLevelUpBag(slotIndex, levelDelta);
|
|
}
|
|
|
|
/** 拖到字块类主动 buff 通用地图落点(五行字块) */
|
|
canApplyBattleBuffSkillDropMap(gridIndex: number): PlaceRejectReason | null {
|
|
const target = this._resolveBattleBuffMapLevelTarget(gridIndex);
|
|
if ('reason' in target) return target.reason;
|
|
return null;
|
|
}
|
|
|
|
/** 拖到字块类主动 buff 通用背包落点(五行字块) */
|
|
canApplyBattleBuffSkillDropBag(slotIndex: number): PlaceRejectReason | null {
|
|
const target = this._resolveBattleBuffBagSingleTextTarget(slotIndex);
|
|
if ('reason' in target) return target.reason;
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 封神榜:提升地图组合字武将 / 普通 type1 单字等级。
|
|
* @param gridIndex 地图格索引
|
|
* @param levelDelta functionpar 等级增量
|
|
* @returns null 表示成功
|
|
*/
|
|
applyBattleBuffUnitLevelUp(gridIndex: number, levelDelta: number): PlaceRejectReason | null {
|
|
const reason = this._evaluateBattleBuffUnitLevelUp(gridIndex, levelDelta);
|
|
if (reason != null) return reason;
|
|
|
|
const target = this._resolveBattleBuffMapLevelTarget(gridIndex);
|
|
if ('reason' in target) return target.reason;
|
|
return this._commitBattleBuffMapSingleTextLevelDelta(gridIndex, target.weapon1Id, levelDelta);
|
|
}
|
|
|
|
/**
|
|
* 封神榜:提升背包普通 type1 单字等级。
|
|
* @param levelDelta functionpar 等级增量
|
|
*/
|
|
applyBattleBuffUnitLevelUpBag(slotIndex: number, levelDelta: number): PlaceRejectReason | null {
|
|
const reason = this._evaluateBattleBuffUnitLevelUpBag(slotIndex, levelDelta);
|
|
if (reason != null) return reason;
|
|
|
|
const target = this._resolveBattleBuffBagSingleTextTarget(slotIndex);
|
|
if ('reason' in target) return target.reason;
|
|
|
|
const currentLevel = target.slot.level ?? 1;
|
|
const newLevel = Math.min(target.maxLevel, currentLevel + levelDelta);
|
|
if (newLevel === currentLevel) return PlaceRejectReason.MaxLevel;
|
|
|
|
this.bag.setSlot(slotIndex, {
|
|
...target.slot,
|
|
weapon1Id: target.weapon1Id,
|
|
level: newLevel,
|
|
});
|
|
this.emit(MergeEventFactory.bagMerged(slotIndex, target.weapon1Id, newLevel));
|
|
return null;
|
|
}
|
|
|
|
/** 授仙箓:是否可作用于地图组合字武将 / 普通 type1 单字 */
|
|
canApplyBattleBuffRandomLevel(gridIndex: number): PlaceRejectReason | null {
|
|
return this._evaluateBattleBuffRandomLevel(gridIndex);
|
|
}
|
|
|
|
/** 授仙箓:是否可作用于背包普通 type1 单字(type2 未合成散字不可) */
|
|
canApplyBattleBuffRandomLevelBag(slotIndex: number): PlaceRejectReason | null {
|
|
return this._evaluateBattleBuffRandomLevelBag(slotIndex);
|
|
}
|
|
|
|
/**
|
|
* 授仙箓:掷骰升/降级增量(不写盘)。
|
|
* @param rollWan 0~9999
|
|
*/
|
|
rollBattleBuffRandomLevelMap(
|
|
gridIndex: number,
|
|
levelUpProbsWan: readonly number[],
|
|
rollWan: number,
|
|
): PlaceRejectReason | number {
|
|
const target = this._resolveBattleBuffMapLevelTarget(gridIndex);
|
|
if ('reason' in target) return target.reason;
|
|
return this._rollRandomLevelDelta(target.level, target.maxLevel, levelUpProbsWan, rollWan, true);
|
|
}
|
|
|
|
/**
|
|
* 授仙箓:掷骰升/降级增量(背包普通 type1 单字,不写盘)。
|
|
* @param rollWan 0~9999
|
|
*/
|
|
rollBattleBuffRandomLevelBag(
|
|
slotIndex: number,
|
|
levelUpProbsWan: readonly number[],
|
|
rollWan: number,
|
|
): PlaceRejectReason | number {
|
|
const target = this._resolveBattleBuffBagSingleTextTarget(slotIndex);
|
|
if ('reason' in target) return target.reason;
|
|
return this._rollRandomLevelDelta(target.level, target.maxLevel, levelUpProbsWan, rollWan, false);
|
|
}
|
|
|
|
/**
|
|
* 授仙箓:按已知增量写地图组合字武将 / 普通 type1 单字。
|
|
* @param levelDelta 等级变化(-1 / 0 / +1)
|
|
*/
|
|
applyBattleBuffRandomLevelMapDelta(gridIndex: number, levelDelta: number): PlaceRejectReason | number {
|
|
if (levelDelta === 0) return 0;
|
|
|
|
const target = this._resolveBattleBuffMapLevelTarget(gridIndex);
|
|
if ('reason' in target) return target.reason;
|
|
|
|
const reason = this._commitBattleBuffMapSingleTextLevelDelta(gridIndex, target.weapon1Id, levelDelta);
|
|
if (reason != null) return reason;
|
|
return levelDelta;
|
|
}
|
|
|
|
/**
|
|
* 授仙箓:按已知增量写背包普通 type1 单字。
|
|
* @param levelDelta 等级变化(-1 / 0 / +1)
|
|
*/
|
|
applyBattleBuffRandomLevelBagDelta(slotIndex: number, levelDelta: number): PlaceRejectReason | number {
|
|
if (levelDelta === 0) return 0;
|
|
|
|
const target = this._resolveBattleBuffBagSingleTextTarget(slotIndex);
|
|
if ('reason' in target) return target.reason;
|
|
if (levelDelta < 0) return PlaceRejectReason.InvalidMerge;
|
|
|
|
const currentLevel = target.slot.level ?? 1;
|
|
const newLevel = Math.max(1, Math.min(target.maxLevel, currentLevel + levelDelta));
|
|
if (newLevel === currentLevel) return PlaceRejectReason.MaxLevel;
|
|
|
|
this.bag.setSlot(slotIndex, {
|
|
...target.slot,
|
|
weapon1Id: target.weapon1Id,
|
|
level: newLevel,
|
|
});
|
|
this.emit(MergeEventFactory.bagMerged(slotIndex, target.weapon1Id, newLevel));
|
|
return levelDelta;
|
|
}
|
|
|
|
/**
|
|
* 授仙箓:随机升/降级地图单字或已合成武将。
|
|
* @param rollWan 0~9999
|
|
*/
|
|
applyBattleBuffRandomLevel(
|
|
gridIndex: number,
|
|
levelUpProbsWan: readonly number[],
|
|
rollWan: number,
|
|
): PlaceRejectReason | number {
|
|
const rolled = this.rollBattleBuffRandomLevelMap(gridIndex, levelUpProbsWan, rollWan);
|
|
if (typeof rolled !== 'number') return rolled;
|
|
return this.applyBattleBuffRandomLevelMapDelta(gridIndex, rolled);
|
|
}
|
|
|
|
/**
|
|
* 授仙箓:随机升/降级背包普通单字。
|
|
* @param rollWan 0~9999
|
|
*/
|
|
applyBattleBuffRandomLevelBag(
|
|
slotIndex: number,
|
|
levelUpProbsWan: readonly number[],
|
|
rollWan: number,
|
|
): PlaceRejectReason | number {
|
|
const rolled = this.rollBattleBuffRandomLevelBag(slotIndex, levelUpProbsWan, rollWan);
|
|
if (typeof rolled !== 'number') return rolled;
|
|
return this.applyBattleBuffRandomLevelBagDelta(slotIndex, rolled);
|
|
}
|
|
|
|
/** 文昌笔:是否可改地图单字 */
|
|
canApplyBattleBuffRewriteGlyphMap(gridIndex: number): PlaceRejectReason | null {
|
|
return this._evaluateBattleBuffRewriteGlyphMap(gridIndex);
|
|
}
|
|
|
|
/** 文昌笔:是否可改背包单字 */
|
|
canApplyBattleBuffRewriteGlyphBag(slotIndex: number): PlaceRejectReason | null {
|
|
return this._evaluateBattleBuffRewriteGlyphBag(slotIndex);
|
|
}
|
|
|
|
/**
|
|
* 文昌笔:改写地图单字 weapon2Id(保留等级)。
|
|
* @returns null 表示成功
|
|
*/
|
|
applyBattleBuffRewriteGlyphMap(gridIndex: number, newWeapon2Id: number): PlaceRejectReason | null {
|
|
const target = this._resolveBattleBuffRewriteGlyphMapTarget(gridIndex);
|
|
if ('reason' in target) return target.reason;
|
|
if (newWeapon2Id <= 0) {
|
|
console.log('[MergeSession] 改字 newWeapon2Id 无效', newWeapon2Id);
|
|
return PlaceRejectReason.InvalidMerge;
|
|
}
|
|
if (target.weapon2Id === newWeapon2Id) return PlaceRejectReason.InvalidMerge;
|
|
const newType = this._config.getWeapon2Type(newWeapon2Id) as Weapon2TileType;
|
|
if (!isElementGlyphType(newType)) {
|
|
console.log('[MergeSession] 改字结果须为五行字', newWeapon2Id, newType);
|
|
return PlaceRejectReason.InvalidMerge;
|
|
}
|
|
|
|
this.map.placeTile(
|
|
gridIndex,
|
|
this._buildBattleBuffRewriteGlyphMapTile(target.tile, newWeapon2Id, target.level),
|
|
);
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.glyphRewritten({ kind: 'map', gridIndex }, newWeapon2Id, target.level));
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 文昌笔:改写背包单字 weapon2Id(保留等级)。
|
|
* @returns null 表示成功
|
|
*/
|
|
applyBattleBuffRewriteGlyphBag(slotIndex: number, newWeapon2Id: number): PlaceRejectReason | null {
|
|
const reason = this._evaluateBattleBuffRewriteGlyphBag(slotIndex);
|
|
if (reason != null) return reason;
|
|
if (newWeapon2Id <= 0) {
|
|
console.log('[MergeSession] 改字 newWeapon2Id 无效', newWeapon2Id);
|
|
return PlaceRejectReason.InvalidMerge;
|
|
}
|
|
|
|
const target = this._resolveBattleBuffRewriteBagTarget(slotIndex);
|
|
if ('reason' in target) return target.reason;
|
|
if (target.slot.weapon2Id === newWeapon2Id) return PlaceRejectReason.InvalidMerge;
|
|
const newType = this._config.getWeapon2Type(newWeapon2Id) as Weapon2TileType;
|
|
if (!isElementGlyphType(newType)) {
|
|
console.log('[MergeSession] 改字结果须为五行字', newWeapon2Id, newType);
|
|
return PlaceRejectReason.InvalidMerge;
|
|
}
|
|
|
|
const level = target.slot.level ?? 1;
|
|
this.bag.setSlot(
|
|
slotIndex,
|
|
this._buildBattleBuffRewriteGlyphBagSlot(target.slot, newWeapon2Id, level),
|
|
);
|
|
this.emit(MergeEventFactory.glyphRewritten({ kind: 'bag', slotIndex }, newWeapon2Id, level));
|
|
return null;
|
|
}
|
|
|
|
/** 校验主动 buff 回收目标(地图单字/组合字/单格武将) */
|
|
canApplyBattleBuffRecycleMap(gridIndex: number): PlaceRejectReason | null {
|
|
return this._evaluateBattleBuffRecycleMap(gridIndex);
|
|
}
|
|
|
|
/**
|
|
* 主动 buff:回收地图字块/单格武将。
|
|
* @returns null 表示成功
|
|
*/
|
|
applyBattleBuffRecycleMap(gridIndex: number): PlaceRejectReason | null {
|
|
const reason = this._evaluateBattleBuffRecycleMap(gridIndex);
|
|
if (reason != null) return reason;
|
|
|
|
this.map.removeTile(gridIndex);
|
|
this.map.setCellKind(gridIndex, cellKindAfterReturnToBag());
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.composeChanged(this._compositions));
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 拖到空装饰格:删掉该地图字。type3 已在拖起时撤销,这里只清挂起,不再还原。
|
|
* @param gridIndex 来源地图格
|
|
*/
|
|
discardDraggedMapGlyph(gridIndex: number): boolean {
|
|
const tile = this.map.getTile(gridIndex);
|
|
this._pendingDragTile = null;
|
|
if (!tile) {
|
|
console.log('[MergeSession] 地图格无字可删', gridIndex);
|
|
return false;
|
|
}
|
|
this.map.removeTile(gridIndex);
|
|
this.map.setCellKind(gridIndex, cellKindAfterReturnToBag());
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.composeChanged(this._compositions));
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 技能损毁:移除单格字块(不限 Weapon2 类型;多格组合免疫)。
|
|
* @returns null 表示成功
|
|
*/
|
|
applySkillDestroyMap(gridIndex: number): PlaceRejectReason | null {
|
|
const tile = this.map.getTile(gridIndex);
|
|
if (!tile) return PlaceRejectReason.InvalidMerge;
|
|
this.map.removeTile(gridIndex);
|
|
this.map.setCellKind(gridIndex, cellKindAfterReturnToBag());
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.composeChanged(this._compositions));
|
|
return null;
|
|
}
|
|
|
|
/** 校验主动 buff 回收背包字块 */
|
|
canApplyBattleBuffRecycleBag(slotIndex: number): PlaceRejectReason | null {
|
|
return this._evaluateBattleBuffRecycleBag(slotIndex);
|
|
}
|
|
|
|
/**
|
|
* 主动 buff:回收背包字块。
|
|
* @returns null 表示成功
|
|
*/
|
|
applyBattleBuffRecycleBag(slotIndex: number): PlaceRejectReason | null {
|
|
const reason = this._evaluateBattleBuffRecycleBag(slotIndex);
|
|
if (reason != null) return reason;
|
|
|
|
this.bag.clearSlot(slotIndex);
|
|
this.emit(MergeEventFactory.bagRefreshed(false));
|
|
return null;
|
|
}
|
|
|
|
/** 校验妙手神偷目标(Put 区五行字块) */
|
|
canApplyStealGlyphMap(gridIndex: number): PlaceRejectReason | null {
|
|
return this._evaluateStealGlyphMap(gridIndex);
|
|
}
|
|
|
|
/** 指定格是否为多格合成武将(合将已移除,恒为 false) */
|
|
isMultiCellCompositionAt(_gridIndex: number): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 妙手神偷:从地图提取未合成武将散字或普通单字(保留 tile 副本);只清字块,不改格子语义。
|
|
* @returns tile 或拒绝原因
|
|
*/
|
|
extractMapTileForSteal(gridIndex: number): { tile: MapTileRecord } | { reason: PlaceRejectReason } {
|
|
const reason = this._evaluateStealGlyphMap(gridIndex);
|
|
if (reason != null) return { reason };
|
|
const tile = this.map.getTile(gridIndex);
|
|
if (!tile) return { reason: PlaceRejectReason.InvalidMerge };
|
|
const copy: MapTileRecord = { ...tile };
|
|
this.map.removeTile(gridIndex);
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.composeChanged(this._compositions));
|
|
return { tile: copy };
|
|
}
|
|
|
|
/**
|
|
* 妙手神偷:偷取字块落入己方空 Put 格。
|
|
* @returns null 表示成功
|
|
*/
|
|
grantStolenTileOnPutGrid(gridIndex: number, tile: MapTileRecord): PlaceRejectReason | null {
|
|
if (!this._prepActive) return PlaceRejectReason.NotPrepPhase;
|
|
if (this.map.getCellKind(gridIndex) !== MapCellKind.Put) return PlaceRejectReason.WrongCellKind;
|
|
if (this.map.hasTile(gridIndex)) return PlaceRejectReason.CellOccupied;
|
|
this.map.placeTile(gridIndex, { ...tile });
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.tilePlaced(gridIndex, tile.weapon2Id));
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 妙手神偷:偷取字块落入己方背包空槽。
|
|
* @returns null 表示成功
|
|
*/
|
|
grantStolenTileInBagSlot(slotIndex: number, tile: MapTileRecord): PlaceRejectReason | null {
|
|
if (!this._prepActive) return PlaceRejectReason.NotPrepPhase;
|
|
if (slotIndex < 0 || slotIndex >= this.bag.slotCount) return PlaceRejectReason.InvalidMerge;
|
|
if (this.bag.getSlot(slotIndex)) return PlaceRejectReason.BagFull;
|
|
this.bag.setSlot(slotIndex, this._bagSlotFromMapTile(tile));
|
|
this.emit(MergeEventFactory.bagRefreshed(false));
|
|
return null;
|
|
}
|
|
|
|
/** 校验封神榜升级目标(地图组合字武将 / 普通 type1 单字) */
|
|
private _evaluateBattleBuffUnitLevelUp(gridIndex: number, levelDelta: number): PlaceRejectReason | null {
|
|
if (levelDelta <= 0) {
|
|
console.log('[MergeSession] buff 升级 levelDelta 无效', levelDelta);
|
|
return PlaceRejectReason.InvalidMerge;
|
|
}
|
|
|
|
const target = this._resolveBattleBuffMapLevelTarget(gridIndex);
|
|
if ('reason' in target) return target.reason;
|
|
|
|
if (target.level >= target.maxLevel) return PlaceRejectReason.MaxLevel;
|
|
if (target.level + levelDelta <= target.level) return PlaceRejectReason.MaxLevel;
|
|
return null;
|
|
}
|
|
|
|
/** 校验封神榜升级目标(背包普通 type1 单字) */
|
|
private _evaluateBattleBuffUnitLevelUpBag(slotIndex: number, levelDelta: number): PlaceRejectReason | null {
|
|
if (levelDelta <= 0) {
|
|
console.log('[MergeSession] buff 升级 levelDelta 无效', levelDelta);
|
|
return PlaceRejectReason.InvalidMerge;
|
|
}
|
|
const target = this._resolveBattleBuffBagSingleTextTarget(slotIndex);
|
|
if ('reason' in target) return target.reason;
|
|
if (target.level >= target.maxLevel) return PlaceRejectReason.MaxLevel;
|
|
if (target.level + levelDelta <= target.level) return PlaceRejectReason.MaxLevel;
|
|
return null;
|
|
}
|
|
|
|
/** 授仙箓目标校验(地图五行字块) */
|
|
private _evaluateBattleBuffRandomLevel(gridIndex: number): PlaceRejectReason | null {
|
|
const target = this._resolveBattleBuffMapLevelTarget(gridIndex);
|
|
if ('reason' in target) return target.reason;
|
|
if (target.level >= target.maxLevel) return PlaceRejectReason.MaxLevel;
|
|
return null;
|
|
}
|
|
|
|
/** 授仙箓目标校验(背包普通 type1 单字) */
|
|
private _evaluateBattleBuffRandomLevelBag(slotIndex: number): PlaceRejectReason | null {
|
|
const target = this._resolveBattleBuffBagSingleTextTarget(slotIndex);
|
|
if ('reason' in target) return target.reason;
|
|
if (target.level <= 1 && target.level >= target.maxLevel) {
|
|
return PlaceRejectReason.MaxLevel;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** 封神榜 / 授仙箓地图目标:五行字块 */
|
|
private _resolveBattleBuffMapLevelTarget(gridIndex: number):
|
|
| { weapon1Id: number; level: number; maxLevel: number }
|
|
| { reason: PlaceRejectReason } {
|
|
const tile = this.map.getTile(gridIndex);
|
|
if (!tile) return { reason: PlaceRejectReason.InvalidMerge };
|
|
|
|
const level = tile.level ?? 1;
|
|
const compose = this._findCompositionAt(gridIndex);
|
|
const heroId = compose?.weapon1Id ?? this._resolveElementHeroId(tile.weapon2Id, level);
|
|
if (heroId == null || !(heroId > 0)) {
|
|
return { reason: PlaceRejectReason.InvalidMerge };
|
|
}
|
|
|
|
return {
|
|
weapon1Id: heroId,
|
|
level,
|
|
maxLevel: this._config.getMaxWeaponLevel(heroId),
|
|
};
|
|
}
|
|
|
|
/** 背包五行字 buff 目标(封神榜 / 授仙箓) */
|
|
private _resolveBattleBuffBagSingleTextTarget(slotIndex: number):
|
|
| { level: number; maxLevel: number; weapon1Id: number; slot: BagSlotRecord }
|
|
| { reason: PlaceRejectReason } {
|
|
const slot = this.bag.getSlot(slotIndex);
|
|
if (!slot) return { reason: PlaceRejectReason.InvalidMerge };
|
|
|
|
const level = slot.level ?? 1;
|
|
const heroId = this._resolveElementHeroId(slot.weapon2Id, level);
|
|
if (heroId == null || !(heroId > 0)) {
|
|
return { reason: PlaceRejectReason.InvalidMerge };
|
|
}
|
|
|
|
return {
|
|
level,
|
|
maxLevel: this._config.getMaxWeaponLevel(heroId),
|
|
weapon1Id: heroId,
|
|
slot,
|
|
};
|
|
}
|
|
|
|
/** 五行字块:写地图等级 */
|
|
private _commitBattleBuffMapSingleTextLevelDelta(
|
|
gridIndex: number,
|
|
weapon1Id: number,
|
|
levelDelta: number,
|
|
): PlaceRejectReason | null {
|
|
const tile = this.map.getTile(gridIndex);
|
|
if (!tile) return PlaceRejectReason.InvalidMerge;
|
|
|
|
const currentLevel = tile.level ?? 1;
|
|
const max = this._config.getMaxWeaponLevel(weapon1Id);
|
|
const newLevel = Math.max(1, Math.min(max, currentLevel + levelDelta));
|
|
if (newLevel === currentLevel) return PlaceRejectReason.MaxLevel;
|
|
|
|
this.map.placeTile(gridIndex, { ...tile, weapon1Id, level: newLevel });
|
|
const singleCompose = this._findCompositionAt(gridIndex);
|
|
if (singleCompose) {
|
|
this._setCompositionLevel(singleCompose, newLevel);
|
|
}
|
|
this.emit(MergeEventFactory.mapUpgraded(gridIndex, weapon1Id, newLevel));
|
|
return null;
|
|
}
|
|
|
|
/** 文昌笔:地图单字 / 组合字 / 单格武将目标校验 */
|
|
private _evaluateBattleBuffRewriteGlyphMap(gridIndex: number): PlaceRejectReason | null {
|
|
const target = this._resolveBattleBuffRewriteGlyphMapTarget(gridIndex);
|
|
if ('reason' in target) return target.reason;
|
|
return null;
|
|
}
|
|
|
|
/** 文昌笔:背包单字 / 组合字 / 单格武将目标校验 */
|
|
private _evaluateBattleBuffRewriteGlyphBag(slotIndex: number): PlaceRejectReason | null {
|
|
const target = this._resolveBattleBuffRewriteBagTarget(slotIndex);
|
|
if ('reason' in target) return target.reason;
|
|
return null;
|
|
}
|
|
|
|
/** 文昌笔地图目标:五行字块 */
|
|
private _resolveBattleBuffRewriteGlyphMapTarget(gridIndex: number):
|
|
| { tile: MapTileRecord; weapon2Id: number; level: number }
|
|
| { reason: PlaceRejectReason } {
|
|
const tile = this.map.getTile(gridIndex);
|
|
if (!tile) return { reason: PlaceRejectReason.InvalidMerge };
|
|
|
|
if (!isElementGlyphType(this._config.getWeapon2Type(tile.weapon2Id))) {
|
|
return { reason: PlaceRejectReason.InvalidMerge };
|
|
}
|
|
|
|
return {
|
|
tile,
|
|
weapon2Id: tile.weapon2Id,
|
|
level: tile.level ?? 1,
|
|
};
|
|
}
|
|
|
|
/** 文昌笔背包目标:五行字块 */
|
|
private _resolveBattleBuffRewriteBagTarget(slotIndex: number):
|
|
| { slot: BagSlotRecord; weapon2Id: number }
|
|
| { reason: PlaceRejectReason } {
|
|
const slot = this.bag.getSlot(slotIndex);
|
|
if (!slot) return { reason: PlaceRejectReason.InvalidMerge };
|
|
|
|
if (!isElementGlyphType(this._config.getWeapon2Type(slot.weapon2Id))) {
|
|
return { reason: PlaceRejectReason.InvalidMerge };
|
|
}
|
|
|
|
return { slot, weapon2Id: slot.weapon2Id };
|
|
}
|
|
|
|
/** 改字后地图 tile(保留 source/等级/绑定;单格武将写入 weapon1Id) */
|
|
private _buildBattleBuffRewriteGlyphMapTile(
|
|
prev: MapTileRecord,
|
|
newWeapon2Id: number,
|
|
level: number,
|
|
): MapTileRecord {
|
|
const next: MapTileRecord = {
|
|
weapon2Id: newWeapon2Id,
|
|
level,
|
|
source: prev.source,
|
|
};
|
|
if (prev.ruined) next.ruined = true;
|
|
if (prev.heroBind) next.heroBind = this._cloneHeroBind(prev.heroBind);
|
|
const textType = Math.floor(this._config.getWeapon2Type(newWeapon2Id));
|
|
const heroId = this._config.getElementHeroId(textType, level);
|
|
if (heroId != null && heroId > 0) {
|
|
next.weapon1Id = heroId;
|
|
}
|
|
return next;
|
|
}
|
|
|
|
/** 改字后背包槽(保留等级/绑定;写入 Hero.id) */
|
|
private _buildBattleBuffRewriteGlyphBagSlot(
|
|
prev: BagSlotRecord,
|
|
newWeapon2Id: number,
|
|
level: number,
|
|
): BagSlotRecord {
|
|
const next: BagSlotRecord = {
|
|
weapon2Id: newWeapon2Id,
|
|
level,
|
|
};
|
|
if (prev.ruined) next.ruined = true;
|
|
if (prev.heroBind) next.heroBind = this._cloneHeroBind(prev.heroBind);
|
|
const textType = Math.floor(this._config.getWeapon2Type(newWeapon2Id));
|
|
const heroId = this._config.getElementHeroId(textType, level);
|
|
if (heroId != null && heroId > 0) {
|
|
next.weapon1Id = heroId;
|
|
}
|
|
return next;
|
|
}
|
|
|
|
/** 授仙箓掷骰结果;背包路径 allowLevelDown=false 时不降级 */
|
|
private _rollRandomLevelDelta(
|
|
currentLevel: number,
|
|
maxLevel: number,
|
|
levelUpProbsWan: readonly number[],
|
|
rollWan: number,
|
|
allowLevelDown: boolean,
|
|
): number {
|
|
const level = Math.max(1, Math.floor(currentLevel) || 1);
|
|
const max = Math.max(1, Math.floor(maxLevel) || 1);
|
|
const roll = Math.max(0, Math.min(9999, Math.floor(rollWan) || 0));
|
|
if (level < max) {
|
|
const prob = levelUpProbsWan[level - 1] ?? 0;
|
|
if (roll < prob) return 1;
|
|
}
|
|
if (allowLevelDown && level > 1) return -1;
|
|
return 0;
|
|
}
|
|
|
|
private _findCompositionAt(gridIndex: number): ComposedUnit | null {
|
|
return this._compositions.find((c) => c.gridIndices.includes(gridIndex)) ?? null;
|
|
}
|
|
|
|
private _composeKey(c: ComposedUnit): string {
|
|
return `${c.weapon1Id}_${c.gridIndices.join(',')}`;
|
|
}
|
|
|
|
private _getCompositionLevel(c: ComposedUnit): number {
|
|
return this._compositionLevels.get(this._composeKey(c)) ?? c.level;
|
|
}
|
|
|
|
private _setCompositionLevel(c: ComposedUnit, level: number): void {
|
|
this._compositionLevels.set(this._composeKey(c), level);
|
|
c.level = level;
|
|
}
|
|
|
|
private _inferCompositionLevel(c: ComposedUnit): number {
|
|
const cap = this._config.getMaxWeaponLevel(c.weapon1Id);
|
|
const lv = this._maxTileLevelInCompose(c);
|
|
return lv > cap ? cap : lv;
|
|
}
|
|
|
|
/**
|
|
* 把绑定快照写到组合内每个字上(组合英雄与基础单字)。
|
|
* @param compose 合成单位
|
|
* @param bind 英雄状态
|
|
*/
|
|
writeCompositionHeroBind(compose: ComposedUnit, bind: GlyphHeroBind): void {
|
|
if (compose.gridIndices.length <= 0) return;
|
|
const cap = this._config.getMaxWeaponLevel(compose.weapon1Id);
|
|
const level = Math.max(1, Math.min(cap, bind.level));
|
|
this._stampHeroBindToTiles(compose, { ...this._cloneHeroBind(bind), level }, level);
|
|
}
|
|
|
|
/**
|
|
* 读取组合内等级最高的字绑定。
|
|
* @param compose 合成单位
|
|
*/
|
|
readCompositionHeroBind(compose: ComposedUnit): GlyphHeroBind | null {
|
|
const win = this._pickWinningHeroBind(compose);
|
|
return win ? this._cloneHeroBind(win) : null;
|
|
}
|
|
|
|
/**
|
|
* 把绑定写到组合内每个字,并统一等级。
|
|
* @param compose 组合
|
|
* @param bind 绑定;空则只写等级
|
|
* @param level 统一等级
|
|
*/
|
|
private _stampHeroBindToTiles(compose: ComposedUnit, bind: GlyphHeroBind | null, level: number): void {
|
|
for (let i = 0; i < compose.gridIndices.length; i++) {
|
|
const idx = compose.gridIndices[i];
|
|
const tile = this.map.getTile(idx);
|
|
if (!tile) continue;
|
|
const rec: MapTileRecord = { ...tile, level };
|
|
if (bind) rec.heroBind = this._cloneHeroBind(bind);
|
|
this.map.placeTile(idx, rec);
|
|
}
|
|
}
|
|
|
|
/** 两个字里绑定英雄等级更高的一方;同级比经验 */
|
|
private _pickWinningHeroBind(c: ComposedUnit): GlyphHeroBind | null {
|
|
let best: GlyphHeroBind | null = null;
|
|
for (let i = 0; i < c.gridIndices.length; i++) {
|
|
const bind = this.map.getTile(c.gridIndices[i])?.heroBind;
|
|
if (!bind) continue;
|
|
if (!best || bind.level > best.level || (bind.level === best.level && bind.exp > best.exp)) {
|
|
best = bind;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
/** 拷贝绑定快照 */
|
|
private _cloneHeroBind(bind: GlyphHeroBind): GlyphHeroBind {
|
|
const aspd: GlyphHeroBuff[] = [];
|
|
for (let i = 0; i < bind.aspd.length; i++) aspd.push({ ...bind.aspd[i] });
|
|
const range: GlyphHeroBuff[] = [];
|
|
for (let i = 0; i < bind.range.length; i++) range.push({ ...bind.range[i] });
|
|
return {
|
|
weapon1Id: bind.weapon1Id,
|
|
level: bind.level,
|
|
exp: bind.exp,
|
|
aspd,
|
|
range,
|
|
};
|
|
}
|
|
|
|
private emit(event: MergeEvent): void {
|
|
if (this._emitSuppressed) return;
|
|
const specific = this._listeners.get(event.type);
|
|
if (specific) membershipForEach(specific, fn => fn(event));
|
|
const all = this._listeners.get(MERGE_EVENT_ALL);
|
|
if (all) membershipForEach(all, fn => fn(event));
|
|
if (event.type === MergeEventType.PlaceRejected) return;
|
|
}
|
|
|
|
reject(reason: PlaceRejectReason): void {
|
|
this.emit(MergeEventFactory.placeRejected(reason));
|
|
}
|
|
|
|
/** 校验地图回收目标(五行字块) */
|
|
private _evaluateBattleBuffRecycleMap(gridIndex: number): PlaceRejectReason | null {
|
|
const tile = this.map.getTile(gridIndex);
|
|
if (!tile) return PlaceRejectReason.InvalidMerge;
|
|
|
|
if (!isElementGlyphType(this._config.getWeapon2Type(tile.weapon2Id))) {
|
|
return PlaceRejectReason.InvalidMerge;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** 校验妙手神偷:Put 区五行字块 */
|
|
private _evaluateStealGlyphMap(gridIndex: number): PlaceRejectReason | null {
|
|
const tile = this.map.getTile(gridIndex);
|
|
if (!tile) return PlaceRejectReason.InvalidMerge;
|
|
|
|
if (this.map.getCellKind(gridIndex) !== MapCellKind.Put) {
|
|
return PlaceRejectReason.WrongCellKind;
|
|
}
|
|
|
|
if (!isElementGlyphType(this._config.getWeapon2Type(tile.weapon2Id))) {
|
|
return PlaceRejectReason.InvalidMerge;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** 校验背包回收目标(五行字块) */
|
|
private _evaluateBattleBuffRecycleBag(slotIndex: number): PlaceRejectReason | null {
|
|
const slot = this.bag.getSlot(slotIndex);
|
|
if (!slot) return PlaceRejectReason.InvalidMerge;
|
|
|
|
if (!isElementGlyphType(this._config.getWeapon2Type(slot.weapon2Id))) {
|
|
return PlaceRejectReason.InvalidMerge;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 技能封地:吞掉单格字块并使格不可再放置(组合武将与组合字免疫)。
|
|
* @returns 是否成功改变地块
|
|
*/
|
|
skillSwallowDeployTile(gridIndex: number): boolean {
|
|
return this.skillSealPutToDig(gridIndex);
|
|
}
|
|
|
|
/**
|
|
* 千里眼:可放置格改为可开挖;有单字则移除(组合武将与组合字免疫)。
|
|
* @returns 是否成功改格
|
|
*/
|
|
skillSealPutToDig(gridIndex: number): boolean {
|
|
const kind = this.map.getCellKind(gridIndex);
|
|
if (kind !== MapCellKind.Put) {
|
|
console.log('[MergeSession] 封格失败:不是可放置格', gridIndex, kind);
|
|
return false;
|
|
}
|
|
if (this.map.hasTile(gridIndex)) {
|
|
this.map.removeTile(gridIndex);
|
|
}
|
|
this.map.setPrepType3(gridIndex, false);
|
|
this.map.setCellKind(gridIndex, MapCellKind.Dig);
|
|
this._recompose();
|
|
this.emit(MergeEventFactory.composeChanged(this._compositions));
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 调整指定格合成体等级(仅单格;delta 可负)。
|
|
* @param allowFloorAtOne true 时低于 1 钳到 1;false 时拒绝
|
|
* @returns 是否实际变更
|
|
*/
|
|
adjustCompositionLevelAtGrid(gridIndex: number, delta: number, allowFloorAtOne = true): boolean {
|
|
const comp = this._findCompositionAt(gridIndex);
|
|
if (!comp) return false;
|
|
const cur = this._getCompositionLevel(comp);
|
|
const next = cur + delta;
|
|
if (next < 1 && !allowFloorAtOne) return false;
|
|
return this.setCompositionLevelAtGrid(gridIndex, next);
|
|
}
|
|
|
|
/**
|
|
* 将指定格合成体设为绝对等级(仅单格,钳制在 1~上限)。
|
|
* @returns 是否实际变更
|
|
*/
|
|
setCompositionLevelAtGrid(gridIndex: number, level: number): boolean {
|
|
const comp = this._findCompositionAt(gridIndex);
|
|
if (!comp || comp.gridIndices.length > 1) return false;
|
|
const idx = comp.gridIndices[0];
|
|
const tile = this.map.getTile(idx);
|
|
if (!tile) return false;
|
|
const cur = this._getCompositionLevel(comp);
|
|
const max = this._config.getMaxWeaponLevel(comp.weapon1Id);
|
|
const newLevel = Math.max(1, Math.min(max, Math.floor(Number(level) || 1)));
|
|
if (newLevel === cur) return false;
|
|
this.map.placeTile(idx, { ...tile, level: newLevel });
|
|
this._setCompositionLevel(comp, newLevel);
|
|
comp.level = newLevel;
|
|
this.emit(MergeEventFactory.mapUpgraded(idx, comp.weapon1Id, newLevel));
|
|
return true;
|
|
}
|
|
|
|
/** 战斗快照:恢复背包/地图/组合态(不触发拖放校验) */
|
|
restoreBoardState(
|
|
data: {
|
|
prepActive: boolean;
|
|
bagSlots: ReadonlyArray<BagSlotRecord | null>;
|
|
mapCells: ReadonlyArray<{ gridIndex: number; cell: import('../board/MapTileBoard').MapCellSnapshot }>;
|
|
mapTiles: ReadonlyArray<{ gridIndex: number; tile: MapTileRecord }>;
|
|
compositions: readonly ComposedUnit[];
|
|
},
|
|
opts?: { silent?: boolean },
|
|
): void {
|
|
this._prepActive = data.prepActive;
|
|
for (let i = 0; i < this.bag.slotCount; i++) {
|
|
const slot = data.bagSlots[i] ?? null;
|
|
this.bag.setSlot(i, slot ? { ...slot } : null);
|
|
}
|
|
this.map.restoreBoard(data.mapCells, data.mapTiles);
|
|
this._compositions = data.compositions.map(c => ({
|
|
weapon1Id: c.weapon1Id,
|
|
level: c.level,
|
|
gridIndices: [...c.gridIndices],
|
|
direction: c.direction,
|
|
}));
|
|
this._compositionLevels.clear();
|
|
for (const c of this._compositions) {
|
|
this._compositionLevels.set(this._composeKey(c), c.level);
|
|
}
|
|
if (!opts?.silent) {
|
|
this.emit(MergeEventFactory.bagRefreshed(false));
|
|
this.emit(MergeEventFactory.composeChanged(this._compositions));
|
|
}
|
|
}
|
|
}
|
|
|