import { canDragMapTile, canDropWeapon2OnCell, MapCellKind, PlaceRejectReason, rejectReasonToMessage, Weapon2TileType, TileSource, } from 'db://assets/mx/module/merge'; import type { BattleBagSession } from '../bag/BattleBagSession'; import { WeaponComposeConfigProvider } from '../bag/WeaponComposeConfigProvider'; import type { MapGrid } from '../map/legacy/MapGrid'; import type { MapManager } from '../map/legacy/MapManager'; /** 地图格在拖拽悬停中的落子语义 */ export type MapDragDropState = 'valid' | 'blocked' | 'none'; /** 拖拽跟随字块上的落子提示(textGrid select / redSelect) */ export type DragPlacementHint = 'valid' | 'blocked' | 'off'; /** 拖字来源快照(升级判定用) */ export interface DragTileSourceInfo { /** Weapon2.id */ weapon2Id: number; /** 背包合成后的 Weapon1(单字) */ weapon1Id?: number; /** 字块等级 */ level?: number; /** 字块来源(用于 functiontype=3 的未开挖格规则) */ source?: TileSource; /** 地图拖起原格;同组合内互拖时排除升级提示 */ fromMapGrid?: number; } /** 拖拽期间高亮可落子格(加号仅 valid;type3 blocked 可走格仅红底) */ export class MapDragGuideHelper { /** * @param source 拖字来源快照;须带 source= PassiveFunction3 时未开挖格才有引导 */ static show( map: MapManager, session: BattleBagSession, weapon2Id: number, source?: Omit, ): void { WeaponComposeConfigProvider.instance.load(); const grids = map.mapGrids; if (!Array.isArray(grids)) return; const w2Type = Number( WeaponComposeConfigProvider.instance.getWeapon2Type(weapon2Id), ) as Weapon2TileType; const dragSource: DragTileSourceInfo = { weapon2Id, ...source }; for (const mg of grids) { if (!mg?.node?.isValid || !mg.node.active) continue; mg.showWalkSprite = false; mg.showBlockedWalkSprite = false; mg.isGuideStatus = false; mg.isBlockedGuideStatus = false; const state = MapDragGuideHelper.getMapDragDropState(session, w2Type, mg, dragSource); MapDragGuideHelper._applyMapDragGuideVisual(mg, w2Type, state); } } static liveCellKind(mg: MapGrid): MapCellKind { if (mg.isCanWalk) return MapCellKind.Walkable; // 可放置优先:dig+putText 双标按 Put if (mg.isCanPutText) return MapCellKind.Put; if (mg.isAreaUnlock) return MapCellKind.Unlock; if (mg.isCanBeDig) return MapCellKind.Dig; return MapCellKind.Put; } static hasSpiderWeb(mg: MapGrid): boolean { if (mg.hasSpiderWeb) return true; const silk = mg.node?.getChildByName('spiderSilk'); return !!silk?.isValid && silk.active; } /** * 引导加号语义:普通字仅空 Put 为 valid;蟠桃树空 Dig/Put 为 valid。 * 已有字块的格不显示 +(升级目标由 isBagDropLineTarget / hover 单独放行)。 */ static getMapDragDropState( session: BattleBagSession, w2Type: Weapon2TileType, mg: MapGrid, source?: DragTileSourceInfo, ): MapDragDropState { if (!mg?.node?.isValid) return 'none'; const gridIndex = mg.Index; const hasTile = session.merge.map.hasTile(gridIndex); const canPutGrid = !!mg.isCanPutGrid; if (MapDragGuideHelper.hasSpiderWeb(mg)) return 'blocked'; if (source?.weapon2Id != null && WeaponComposeConfigProvider.instance.passiveSpawnKeepsDigCell(source.weapon2Id)) { if (!session.validator.isAllowedCampForPlace(gridIndex)) return 'blocked'; if (hasTile) return 'none'; if (mg.isCanBeDig && !mg.isCanWalk && mg.unlockAreaId <= 0) return 'valid'; if (mg.isCanPutText && !mg.isCanBeDig && !mg.isCanWalk) return 'valid'; if (mg.isCanWalk) return 'blocked'; return 'none'; } if (source?.source === TileSource.PassiveFunction3) { if (mg.isCanBeDig && !mg.isCanWalk && mg.unlockAreaId <= 0) { if (!session.validator.isAllowedCampForPlace(gridIndex)) return 'blocked'; return hasTile ? 'none' : 'valid'; } return hasTile ? 'none' : 'blocked'; } if (w2Type === Weapon2TileType.Single || w2Type === Weapon2TileType.Combo) { // 可放置优先:双标 put+dig 允许落字 if (mg.isCanPutText && !mg.isCanWalk) { if (!session.validator.isAllowedCampForPlace(gridIndex)) return 'blocked'; if (hasTile) return 'none'; return 'valid'; } if (mg.waitingUnlock && !hasTile) { if (!session.validator.isAllowedCampForPlace(gridIndex)) return 'blocked'; return 'valid'; } if (mg.isCanWalk || mg.isCanBeDig) return 'blocked'; return 'none'; } if (w2Type === Weapon2TileType.Shovel) { // 已可放置的双标不当挖格 if (mg.isCanBeDig && !mg.isCanPutText && !mg.isCanWalk && !hasTile && mg.unlockAreaId <= 0) { if (!session.validator.isAllowedCampForPlace(gridIndex)) return 'blocked'; return 'valid'; } if (mg.isCanWalk || mg.isCanPutText) return 'blocked'; return 'none'; } if (w2Type === Weapon2TileType.Grid) { if (!mg.isCanWalk || hasTile) return 'none'; if (!canPutGrid) return 'blocked'; if (!session.validator.isAllowedCampForPlace(gridIndex)) return 'blocked'; if (session.validator.canPlaceType3Grid(gridIndex)) return 'valid'; return 'blocked'; } return 'none'; } static getMapHoverHint( session: BattleBagSession, w2Type: Weapon2TileType, mg: MapGrid, source?: DragTileSourceInfo, ): DragPlacementHint { const state = MapDragGuideHelper.getMapDragDropState(session, w2Type, mg, source); if (state === 'valid') return 'valid'; if (state === 'blocked') return 'blocked'; if (source && MapDragGuideHelper._canKeepDigMerge(session, mg.Index, source)) { return 'valid'; } if (source && MapDragGuideHelper._canUpgradeDropOnGrid(session, mg.Index, source)) { return 'valid'; } if (source && MapDragGuideHelper._canKeepDigSwap(session, mg.Index, source)) { return 'valid'; } if (source && MapDragGuideHelper._canSwapDropOnGrid(session, mg.Index, source)) { return 'valid'; } return 'off'; } static getMapDropFailMessage( session: BattleBagSession, w2Type: Weapon2TileType, mg: MapGrid, source?: DragTileSourceInfo, ): string { if (MapDragGuideHelper.getMapDragDropState(session, w2Type, mg, source) === 'valid') { return ''; } if (source && MapDragGuideHelper._canKeepDigMerge(session, mg.Index, source)) { return ''; } if (source && MapDragGuideHelper._canUpgradeDropOnGrid(session, mg.Index, source)) { return ''; } if (source && MapDragGuideHelper._canKeepDigSwap(session, mg.Index, source)) { return ''; } if (source && MapDragGuideHelper._canSwapDropOnGrid(session, mg.Index, source)) { return ''; } return MapDragGuideHelper.getBlockedDropMessage(session, w2Type, mg); } static getBlockedDropMessage( session: BattleBagSession, w2Type: Weapon2TileType, mg: MapGrid, ): string { if (!session.validator.isAllowedCampForPlace(mg.Index)) { return rejectReasonToMessage(PlaceRejectReason.WrongCampArea); } if (MapDragGuideHelper.hasSpiderWeb(mg) && w2Type === Weapon2TileType.Grid) { return rejectReasonToMessage(PlaceRejectReason.SpiderWebBlocked); } if (w2Type === Weapon2TileType.Grid) { const cellKind = MapDragGuideHelper.liveCellKind(mg); const hasTile = session.merge.map.hasTile(mg.Index); const canPutGrid = !!mg.isCanPutGrid; if ( cellKind === MapCellKind.Walkable && !hasTile && canPutGrid && !session.validator.canPlaceType3Grid(mg.Index) ) { return rejectReasonToMessage(PlaceRejectReason.PathBlocked); } } return rejectReasonToMessage(PlaceRejectReason.WrongCellKind); } /** * 拉线松手可落点:空可放置格,或已占格上的升级 / 交换。 */ static isBagDropLineTarget( map: MapManager, session: BattleBagSession, source: DragTileSourceInfo, gridIndex: number, ): boolean { const mg = map.getMapGridByIndex(gridIndex); if (!mg?.node?.isValid || !mg.node.active) return false; WeaponComposeConfigProvider.instance.load(); const w2Type = Number( WeaponComposeConfigProvider.instance.getWeapon2Type(source.weapon2Id), ) as Weapon2TileType; if (MapDragGuideHelper.getMapDragDropState(session, w2Type, mg, source) === 'valid') { return true; } if (MapDragGuideHelper._canKeepDigMerge(session, gridIndex, source)) { return true; } if (MapDragGuideHelper._canUpgradeDropOnGrid(session, gridIndex, source)) { return true; } if (MapDragGuideHelper._canKeepDigSwap(session, gridIndex, source)) { return true; } return MapDragGuideHelper._canSwapDropOnGrid(session, gridIndex, source); } static isType3SpiderWebBlockedDrop( map: MapManager, session: BattleBagSession, weapon2Id: number, gridIndex: number, ): boolean { const mg = map.getMapGridByIndex(gridIndex); if (!mg?.node?.isValid || !mg.node.active) return false; WeaponComposeConfigProvider.instance.load(); const w2Type = Number( WeaponComposeConfigProvider.instance.getWeapon2Type(weapon2Id), ) as Weapon2TileType; if (w2Type !== Weapon2TileType.Grid) return false; if (WeaponComposeConfigProvider.instance.passiveSpawnKeepsDigCell(weapon2Id)) return false; if (!MapDragGuideHelper.hasSpiderWeb(mg)) return false; return MapDragGuideHelper.getMapDragDropState(session, w2Type, mg, { weapon2Id }) === 'blocked'; } static isType3PathBlockedDrop( map: MapManager, session: BattleBagSession, weapon2Id: number, gridIndex: number, ): boolean { const mg = map.getMapGridByIndex(gridIndex); if (!mg?.node?.isValid || !mg.node.active) return false; WeaponComposeConfigProvider.instance.load(); const w2Type = Number( WeaponComposeConfigProvider.instance.getWeapon2Type(weapon2Id), ) as Weapon2TileType; if (w2Type !== Weapon2TileType.Grid) return false; if (MapDragGuideHelper.hasSpiderWeb(mg)) return false; const cellKind = MapDragGuideHelper.liveCellKind(mg); const hasTile = session.merge.map.hasTile(gridIndex); const canPutGrid = !!mg.isCanPutGrid; if (!canDropWeapon2OnCell(w2Type, cellKind, hasTile, canPutGrid)) return false; return !session.validator.canPlaceType3Grid(gridIndex); } static clear(map: MapManager | null): void { if (!map) return; const grids = map.mapGrids; if (!Array.isArray(grids)) return; for (const mg of grids) { if (!mg?.node?.isValid) continue; mg.isGuideStatus = false; mg.isBlockedGuideStatus = false; mg.showWalkSprite = false; mg.showBlockedWalkSprite = false; } } /** 来源是否为 functiontype=3 产字 */ private static _isFunction3Source(source?: DragTileSourceInfo): boolean { return source?.source === TileSource.PassiveFunction3; } /** 未开挖格上的 keepDig 文字可与同来源/同 id 文字合成 */ private static _canKeepDigMerge( session: BattleBagSession, gridIndex: number, source: DragTileSourceInfo, ): boolean { const target = session.merge.map.getTile(gridIndex); if (!target || target.weapon2Id !== source.weapon2Id) return false; if ((target.level ?? 1) !== (source.level ?? 1)) return false; WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; const sourceFunction3 = MapDragGuideHelper._isFunction3Source(source); const targetFunction3 = target.source === TileSource.PassiveFunction3; if (sourceFunction3 || targetFunction3) { if (!(sourceFunction3 && targetFunction3)) return false; const weapon1Id = (source.weapon1Id && source.weapon1Id > 0) ? source.weapon1Id : provider.getWeapon1ByGroupSequence([source.weapon2Id]); if (weapon1Id == null) return false; return (source.level ?? 1) < provider.getMaxWeaponLevel(weapon1Id); } if (!provider.passiveSpawnKeepsDigCell(source.weapon2Id)) return false; if (!provider.passiveSpawnKeepsDigCell(target.weapon2Id)) return false; return (source.level ?? 1) < provider.getKeepDigTileMaxLevel(source.weapon2Id); } /** 两棵保持 Dig 的蟠桃树不能合成时允许交换 */ private static _canKeepDigSwap( session: BattleBagSession, gridIndex: number, source: DragTileSourceInfo, ): boolean { WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; if (!provider.passiveSpawnKeepsDigCell(source.weapon2Id)) return false; const target = session.merge.map.getTile(gridIndex); if (!target) return false; if (!provider.passiveSpawnKeepsDigCell(target.weapon2Id)) return false; if (!session.validator.isAllowedCampForPlace(gridIndex)) return false; return true; } private static _canUpgradeDropOnGrid( session: BattleBagSession, gridIndex: number, source: DragTileSourceInfo, ): boolean { WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; const w2Type = Number(provider.getWeapon2Type(source.weapon2Id)) as Weapon2TileType; if (w2Type !== Weapon2TileType.Single && w2Type !== Weapon2TileType.Combo) { return false; } if (!session.merge.map.hasTile(gridIndex)) return false; if (!session.validator.isAllowedCampForPlace(gridIndex)) return false; const level = source.level ?? 1; const compose = session.compositions.find(c => c.gridIndices.includes(gridIndex)); if (compose) { // 同组合内互拖:换位而非升级 if (source.fromMapGrid != null && compose.gridIndices.includes(source.fromMapGrid)) { return false; } const composeLevel = MapDragGuideHelper._composeMaxLevel(session, compose.gridIndices); if (composeLevel >= provider.getMaxWeaponLevel(compose.weapon1Id)) return false; if (provider.isSingleCellWeapon1(compose.weapon1Id)) { if (w2Type !== Weapon2TileType.Single) return false; const srcW1 = (source.weapon1Id && source.weapon1Id > 0) ? source.weapon1Id : provider.getWeapon1ByGroupSequence([source.weapon2Id]); if (srcW1 == null || srcW1 !== compose.weapon1Id) return false; return level === composeLevel; } if (w2Type !== Weapon2TileType.Combo) return false; return provider.getWeapon1Group(compose.weapon1Id).includes(source.weapon2Id); } if (w2Type !== Weapon2TileType.Single) return false; const tile = session.merge.map.getTile(gridIndex); if (!tile) return false; const srcW1 = (source.weapon1Id && source.weapon1Id > 0) ? source.weapon1Id : provider.getWeapon1ByGroupSequence([source.weapon2Id]); const dstW1 = tile.weapon1Id ?? provider.getWeapon1ByGroupSequence([tile.weapon2Id]); if (srcW1 == null || dstW1 == null || srcW1 !== dstW1) return false; if (!provider.isSingleCellWeapon1(srcW1)) return false; if (Number(provider.getWeapon2Type(tile.weapon2Id)) !== Weapon2TileType.Single) return false; if (level !== (tile.level ?? 1)) return false; return level < provider.getMaxWeaponLevel(srcW1); } /** * 已占可放置格:双方均为 type1/2 且目标可拖起时交换(与 MergeSession 一致)。 */ private static _canSwapDropOnGrid( session: BattleBagSession, gridIndex: number, source: DragTileSourceInfo, ): boolean { WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; const srcType = Number(provider.getWeapon2Type(source.weapon2Id)) as Weapon2TileType; if (srcType !== Weapon2TileType.Single && srcType !== Weapon2TileType.Combo) { return false; } const tile = session.merge.map.getTile(gridIndex); if (!tile) return false; if (!session.validator.isAllowedCampForPlace(gridIndex)) return false; if (session.merge.map.getCellKind(gridIndex) !== MapCellKind.Put) return false; const dstType = Number(provider.getWeapon2Type(tile.weapon2Id)) as Weapon2TileType; if (dstType !== Weapon2TileType.Single && dstType !== Weapon2TileType.Combo) { return false; } return canDragMapTile(tile, dstType, false); } private static _composeMaxLevel( session: BattleBagSession, gridIndices: readonly number[], ): number { let max = 1; for (const idx of gridIndices) { const lv = session.merge.map.getTile(idx)?.level ?? 1; if (lv > max) max = lv; } return max; } private static _applyMapDragGuideVisual( mg: MapGrid, w2Type: Weapon2TileType, state: MapDragDropState, ): void { if (state === 'none') return; if (state === 'valid') { if (w2Type === Weapon2TileType.Grid) { mg.showWalkSprite = true; } mg.isGuideStatus = true; return; } if (w2Type === Weapon2TileType.Grid) { mg.showBlockedWalkSprite = true; } } /** * 己方阵营背包中是否存在任一字块可落到地图(空 Put / 升级 / 交换,与拉线落子一致)。 * @param campAreaId CampAreaID(本机 0→1、敌方 1→2) */ static hasLocalCampBagPlacement(session: BattleBagSession, map: MapManager, campAreaId: number): boolean { WeaponComposeConfigProvider.instance.load(); const slots = session.getBagSlots(); const grids = map.mapGrids; if (!Array.isArray(grids)) return false; for (let si = 0; si < slots.length; si++) { const slot = slots[si]; if (!slot) continue; const source: DragTileSourceInfo = { weapon2Id: slot.weapon2Id, weapon1Id: slot.weapon1Id, level: slot.level, }; for (let gi = 0; gi < grids.length; gi++) { const mg = grids[gi]; if (!mg?.node?.isValid || !mg.node.active) continue; if ((mg.CampAreaID | 0) !== campAreaId) continue; if (MapDragGuideHelper.isBagDropLineTarget(map, session, source, mg.Index)) { return true; } } } return false; } /** * 己方阵营仍可将单字/组合字放入的空 Put 格数量(与拖拽 + 号一致)。 * @param campAreaId CampAreaID(本机 0→1、敌方 1→2) */ static countLocalEmptyPutGrids(session: BattleBagSession, map: MapManager, campAreaId: number): number { WeaponComposeConfigProvider.instance.load(); const w2Type = Weapon2TileType.Single; let n = 0; const grids = map.mapGrids; if (!Array.isArray(grids)) return 0; for (const mg of grids) { if (!mg?.node?.isValid || !mg.node.active) continue; if ((mg.CampAreaID | 0) !== campAreaId) continue; if (MapDragGuideHelper.getMapDragDropState(session, w2Type, mg) === 'valid') { n++; } } return n; } /** 己方阵营是否仍有可放置空 Put */ static hasLocalEmptyPutGrid(session: BattleBagSession, map: MapManager, campAreaId: number): boolean { return MapDragGuideHelper.countLocalEmptyPutGrids(session, map, campAreaId) > 0; } }