import { BattleBagCardKind, canDragMapTile, canDropWeapon2OnCell, isBagToolTextType, isElementGlyphType, 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'; import { unlockHeroTypeName } from '../map/legacy/MapAreaUnlock'; /** 地图格在拖拽悬停中的落子语义 */ 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; /** 局内产卡种类(倍率卡/属性卡) */ battleCardKind?: 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;待解锁格须类型(0 任意)与显示等级都达标。 * 已有字块的格不显示 +(升级目标由 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 (isElementGlyphType(w2Type)) { // 可放置优先:双标 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'; if (!source || !session.canUnlockArea(gridIndex, source.weapon2Id, source.level)) { return 'none'; } return 'valid'; } if (mg.isCanWalk || mg.isCanBeDig) 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._canBattleCardDropOnGrid(session, mg.Index, source)) { return 'valid'; } if (source && MapDragGuideHelper._canBagToolDropOnGrid(session, mg.Index, source)) { return 'valid'; } 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._canBattleCardDropOnGrid(session, mg.Index, source)) { return ''; } if (source && MapDragGuideHelper._canBagToolDropOnGrid(session, mg.Index, source)) { 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 (mg.waitingUnlock && isElementGlyphType(w2Type)) { const needType = session.currentUnlockNeedHeroType(mg.Index); const name = unlockHeroTypeName(needType); if (needType > 0 && name && Math.floor(Number(w2Type) || 0) !== needType) { return `需要用${name}属性数字解锁`; } } 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._canBattleCardDropOnGrid(session, gridIndex, source)) { return true; } if (MapDragGuideHelper._canBagToolDropOnGrid(session, gridIndex, source)) { 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 (isElementGlyphType(w2Type) || 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 (isElementGlyphType(w2Type) || 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; } /** * 倍率卡/属性卡可落到己方阵营已占的五行字上(含已激活上阵)。 * @param session 背包会话 * @param gridIndex 地图格 * @param source 拖拽来源 */ private static _canBattleCardDropOnGrid( session: BattleBagSession, gridIndex: number, source: DragTileSourceInfo, ): boolean { const kind = Math.floor(Number(source.battleCardKind) || 0); if (kind !== BattleBagCardKind.Multiplier && kind !== BattleBagCardKind.Attr) return false; const tile = session.merge.map.getTile(gridIndex); if (!tile || tile.battleCardKind) return false; if (!session.validator.isAllowedCampForPlace(gridIndex)) return false; WeaponComposeConfigProvider.instance.load(); const dstType = Math.floor(WeaponComposeConfigProvider.instance.getWeapon2Type(tile.weapon2Id)); return isElementGlyphType(dstType); } /** * Text 功能符可落到合法阵营已占五行字上(衰弱符仅对方)。 * @param session 背包会话 * @param gridIndex 地图格 * @param source 拖拽来源 */ private static _canBagToolDropOnGrid( session: BattleBagSession, gridIndex: number, source: DragTileSourceInfo, ): boolean { if (Math.floor(Number(source.battleCardKind) || 0) > 0) return false; WeaponComposeConfigProvider.instance.load(); const toolType = Math.floor(WeaponComposeConfigProvider.instance.getWeapon2Type(source.weapon2Id)); if (!isBagToolTextType(toolType)) return false; const tile = session.merge.map.getTile(gridIndex); if (!tile || tile.battleCardKind) return false; const ownCamp = session.validator.isAllowedCampForPlace(gridIndex); if (toolType === 13) { if (ownCamp) return false; } else if (!ownCamp) { return false; } const dstType = Math.floor(WeaponComposeConfigProvider.instance.getWeapon2Type(tile.weapon2Id)); if (!isElementGlyphType(dstType)) return false; const lv = Math.max(1, Math.floor(Number(tile.level) || 1)); if (toolType === 13) return lv > 1; if (toolType === 10 || toolType === 11) { return lv < WeaponComposeConfigProvider.instance.getElementMaxLevel(dstType); } 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 (!isElementGlyphType(w2Type)) return false; const tile = session.merge.map.getTile(gridIndex); if (!tile) return false; if (!session.validator.isAllowedCampForPlace(gridIndex)) return false; if (source.fromMapGrid != null && source.fromMapGrid === gridIndex) return false; const srcLevel = Math.floor(source.level ?? 1); const dstLevel = Math.floor(tile.level ?? 1); const dstType = Math.floor(provider.getWeapon2Type(tile.weapon2Id)); if (dstType !== w2Type || srcLevel !== dstLevel) return false; return srcLevel < provider.getElementMaxLevel(w2Type); } /** * 已占可放置格:双方均为 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 (!isElementGlyphType(srcType)) { return false; } const tile = session.merge.map.getTile(gridIndex); if (!tile) return false; if (!session.validator.isAllowedCampForPlace(gridIndex)) return false; if (session.validator.getLiveCellKind(gridIndex) !== MapCellKind.Put) return false; const dstType = Number(provider.getWeapon2Type(tile.weapon2Id)) as Weapon2TileType; if (!isElementGlyphType(dstType)) { return false; } return canDragMapTile(tile, dstType, false); } 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(); // 五行落空 Put 规则与具体 type 无关,取 1 作探测类型 const w2Type = 1 as Weapon2TileType; 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; } }