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, } 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(); private _listeners = new Map>(); /** 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; } 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 (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(); 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, found: ComposedUnit[]): void { const occupied = createMembershipSet(); 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; 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)); } } }