import { BattleBagCardKind, MapCellKind, Weapon2TileType, isBagToolTextType, isElementGlyphType, type DragSource, type DropTarget, } from 'db://assets/mx/module/merge'; import type { BattleBagSession } from '../../bag/BattleBagSession'; import { WeaponComposeConfigProvider } from '../../bag/WeaponComposeConfigProvider'; import type { MapManager } from '../../map/legacy/MapManager'; import type { MapGrid } from '../../map/legacy/MapGrid'; import { MapDragGuideHelper } from '../../view/MapDragGuideHelper'; import { TextListCellState } from '../../view/TextListLandHost'; import { TableNames } from '../../../game/ConfigTableData'; import { heroNormalSkillId } from '../../config/BattleHeroTable'; import { resolveTowerRangePx } from '../../combat/TowerStatResolver'; import { BattleMapSpec, mapGridToLogicCell } from '../../map/BattleMapSpec'; import { AI_PLACE_CAMP } from '../runtime/AiCampArea'; /** 字块最小字段 */ type TileLike = { weapon2Id: number; weapon1Id?: number; level?: number }; /** * AI 盘面只读探测:解锁 / 五行合成 / 空位 / 阵型。 * 不含旧合将拼装、散字、品质挤占。 */ export class AiBoardProbe { /** * 解析字块对应 Hero id(五行 type+显示等级;蟠桃等仍走单 id)。 * @param tile 背包或地图字 */ resolveWeapon1Id(tile: TileLike): number | null { WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; if (tile.weapon1Id != null && tile.weapon1Id > 0) return tile.weapon1Id; const w2Type = Number(provider.getWeapon2Type(tile.weapon2Id)); if (isElementGlyphType(w2Type)) { return provider.getElementHeroId(w2Type, Math.max(1, Math.floor(tile.level ?? 1))); } return provider.getWeapon1ByGroupSequence([tile.weapon2Id]); } /** * 源落到目标格是否会升级(五行或蟠桃)。 * @param session 会话 * @param gridIndex 目标格 * @param source 源字 * @param fromMapGrid 若源来自地图则填源格 */ canUpgradeDropOnGrid(session: BattleBagSession, gridIndex: number, source: TileLike, fromMapGrid?: number): boolean { WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; if (provider.passiveSpawnKeepsDigCell(source.weapon2Id)) { return this._canKeepDigUpgradeOnGrid(session, gridIndex, source, fromMapGrid); } const w2Type = Number(provider.getWeapon2Type(source.weapon2Id)); if (!isElementGlyphType(w2Type)) return false; if (!session.validator.isAllowedCampForPlace(gridIndex)) return false; if (fromMapGrid != null && fromMapGrid === gridIndex) return false; const tile = session.merge.map.getTile(gridIndex); if (!tile) return false; const dstType = Number(provider.getWeapon2Type(tile.weapon2Id)); if (!isElementGlyphType(dstType) || dstType !== w2Type) return false; const lv = Math.floor(source.level ?? 1); if (lv !== Math.floor(tile.level ?? 1)) return false; return lv < provider.getElementMaxLevel(w2Type); } /** * 两槽是否可在背包内合成升级(五行或蟠桃)。 * @param source 源槽字 * @param target 目标槽字 */ canUpgradeInBag(source: TileLike, target: TileLike): boolean { WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; if (provider.passiveSpawnKeepsDigCell(source.weapon2Id) && provider.passiveSpawnKeepsDigCell(target.weapon2Id)) { if (source.weapon2Id !== target.weapon2Id) return false; const lv = source.level ?? 1; if (lv !== (target.level ?? 1)) return false; return lv < provider.getKeepDigTileMaxLevel(source.weapon2Id); } const srcType = Number(provider.getWeapon2Type(source.weapon2Id)); const dstType = Number(provider.getWeapon2Type(target.weapon2Id)); if (!isElementGlyphType(srcType) || !isElementGlyphType(dstType)) return false; if (srcType !== dstType) return false; const srcLv = Math.floor(source.level ?? 1); const dstLv = Math.floor(target.level ?? 1); if (srcLv !== dstLv) return false; return srcLv < provider.getElementMaxLevel(srcType); } /** * 蟠桃树落到目标格是否会合成升级。 * @param session 会话 * @param gridIndex 目标格 * @param source 源字 * @param fromMapGrid 若源来自地图则填源格 */ private _canKeepDigUpgradeOnGrid(session: BattleBagSession, gridIndex: number, source: TileLike, fromMapGrid?: number): boolean { if (fromMapGrid != null && fromMapGrid === gridIndex) return false; if (!session.validator.isAllowedCampForPlace(gridIndex)) return false; const target = session.merge.map.getTile(gridIndex); if (!target || target.weapon2Id !== source.weapon2Id) return false; const provider = WeaponComposeConfigProvider.instance; if (!provider.passiveSpawnKeepsDigCell(target.weapon2Id)) return false; const lv = source.level ?? 1; if (lv !== (target.level ?? 1)) return false; return lv < provider.getKeepDigTileMaxLevel(source.weapon2Id); } /** * 空 Dig 或空 Put 是否可落蟠桃树。 * @param session 会话 * @param mg 地图格 */ canDropKeepDigOnEmptyGrid(session: BattleBagSession, mg: MapGrid): boolean { if (!mg?.node?.isValid || !mg.node.active) return false; if (!session.validator.isOwnerCampCell(mg.Index)) return false; if (session.merge.map.hasTile(mg.Index)) return false; if (MapDragGuideHelper.hasSpiderWeb(mg)) return false; const kind = session.merge.map.getCellKind(mg.Index); return kind === MapCellKind.Dig || kind === MapCellKind.Put; } /** * 查找一步升级:背包内 → 背包→地图 → 地图→地图。 * @param session 会话 * @param map 地图 */ findUpgradeAction(session: BattleBagSession, map: MapManager): { source: DragSource; target: DropTarget } | null { const slots = session.getBagSlots(); for (let a = 0; a < slots.length; a++) { const src = slots[a]; if (!src) continue; if (!this._canDragBagSlot(session, a)) continue; for (let b = 0; b < slots.length; b++) { if (a === b) continue; const dst = slots[b]; if (!dst) continue; if (!this._canMergeOntoBagSlot(session, b)) continue; if (!this.canUpgradeInBag(src, dst)) continue; return { source: { kind: 'bag', slotIndex: a }, target: { kind: 'bag', slotIndex: b } }; } } const grids = this._listActiveGrids(map); for (let s = 0; s < slots.length; s++) { const slot = slots[s]; if (!slot) continue; if (!this._canDragBagSlot(session, s)) continue; for (let g = 0; g < grids.length; g++) { const mg = grids[g]; if (!this.canUpgradeDropOnGrid(session, mg.Index, slot)) continue; return { source: { kind: 'bag', slotIndex: s }, target: { kind: 'map', gridIndex: mg.Index } }; } } let bestMap: { source: DragSource; target: DropTarget; prefer: number } | null = null; for (let i = 0; i < grids.length; i++) { const from = grids[i]; const tile = session.merge.map.getTile(from.Index); if (!tile) continue; const fromType = Number(WeaponComposeConfigProvider.instance.getWeapon2Type(tile.weapon2Id)); const fromW1 = this.resolveWeapon1Id(tile) ?? 0; const fromForm = this.formationScore(map, from, fromW1); for (let j = 0; j < grids.length; j++) { const to = grids[j]; if (from.Index === to.Index) continue; if (!this.canUpgradeDropOnGrid(session, to.Index, tile, from.Index)) continue; const toTile = session.merge.map.getTile(to.Index); if (!toTile) continue; const toW1 = this.resolveWeapon1Id(toTile) ?? fromW1; const toForm = this.formationScore(map, to, toW1); const prefer = isElementGlyphType(fromType) ? (toForm - fromForm) * 1000 - from.Index : -from.Index * 1000 - to.Index; if (!bestMap || prefer > bestMap.prefer) { bestMap = { source: { kind: 'map', gridIndex: from.Index }, target: { kind: 'map', gridIndex: to.Index }, prefer, }; } } } return bestMap ? { source: bestMap.source, target: bestMap.target } : null; } /** * 倍率卡/属性卡:拖到己方可拖开启格或地图五行数字(优先场上、高等级)。 * @param session 会话 * @param map 地图 */ findBattleCardAction(session: BattleBagSession, map: MapManager): { source: DragSource; target: DropTarget } | null { WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; const slots = session.getBagSlots(); let best: { source: DragSource; target: DropTarget; score: number } | null = null; for (let s = 0; s < slots.length; s++) { const card = slots[s]; if (!card) continue; const kind = Math.floor(Number(card.battleCardKind) || 0); if (kind !== BattleBagCardKind.Multiplier && kind !== BattleBagCardKind.Attr) continue; if (!this._canDragBagSlot(session, s)) continue; for (let t = 0; t < slots.length; t++) { if (t === s) continue; const dst = slots[t]; if (!dst || dst.battleCardKind) continue; const land = session.landStateOf?.(t) ?? TextListCellState.Open; if (land !== TextListCellState.Open) continue; if (!this._isBattleCardDigitTarget(provider, dst.weapon2Id, dst.level ?? 1, kind)) continue; const score = this._scoreBattleCardTarget(provider, dst.weapon2Id, dst.level ?? 1, false, t); if (!best || score > best.score) { best = { source: { kind: 'bag', slotIndex: s }, target: { kind: 'bag', slotIndex: t }, score, }; } } const grids = this._listActiveGrids(map); for (let g = 0; g < grids.length; g++) { const mg = grids[g]; const tile = session.merge.map.getTile(mg.Index); if (!tile || tile.battleCardKind) continue; if (!session.validator.isAllowedCampForPlace(mg.Index)) continue; if (!this._isBattleCardDigitTarget(provider, tile.weapon2Id, tile.level ?? 1, kind)) continue; const score = this._scoreBattleCardTarget(provider, tile.weapon2Id, tile.level ?? 1, true, mg.Index); if (!best || score > best.score) { best = { source: { kind: 'bag', slotIndex: s }, target: { kind: 'map', gridIndex: mg.Index }, score, }; } } } return best ? { source: best.source, target: best.target } : null; } /** * Text 功能符:双倍/四倍/随机打己方数字;衰弱打对方场上高等级数字。 * @param session 会话 * @param map 地图 */ findBagToolAction(session: BattleBagSession, map: MapManager): { source: DragSource; target: DropTarget } | null { WeaponComposeConfigProvider.instance.load(); const provider = WeaponComposeConfigProvider.instance; const slots = session.getBagSlots(); let best: { source: DragSource; target: DropTarget; score: number } | null = null; for (let s = 0; s < slots.length; s++) { const tool = slots[s]; if (!tool || tool.battleCardKind) continue; if (!this._canDragBagSlot(session, s)) continue; const toolType = Math.floor(provider.getWeapon2Type(tool.weapon2Id)); if (!isBagToolTextType(toolType)) continue; if (toolType !== 13) { for (let t = 0; t < slots.length; t++) { if (t === s) continue; const dst = slots[t]; if (!dst || dst.battleCardKind) continue; const land = session.landStateOf?.(t) ?? TextListCellState.Open; if (land !== TextListCellState.Open) continue; if (!this._isBagToolDigitTarget(provider, dst.weapon2Id, dst.level ?? 1, toolType)) continue; const score = this._scoreBattleCardTarget(provider, dst.weapon2Id, dst.level ?? 1, false, t); if (!best || score > best.score) { best = { source: { kind: 'bag', slotIndex: s }, target: { kind: 'bag', slotIndex: t }, score, }; } } } const grids = this._listActiveGrids(map); for (let g = 0; g < grids.length; g++) { const mg = grids[g]; const tile = session.merge.map.getTile(mg.Index); if (!tile || tile.battleCardKind) continue; const ownCamp = session.validator.isAllowedCampForPlace(mg.Index); if (toolType === 13) { if (ownCamp) continue; } else if (!ownCamp) { continue; } if (!this._isBagToolDigitTarget(provider, tile.weapon2Id, tile.level ?? 1, toolType)) continue; const score = this._scoreBattleCardTarget(provider, tile.weapon2Id, tile.level ?? 1, true, mg.Index); if (!best || score > best.score) { best = { source: { kind: 'bag', slotIndex: s }, target: { kind: 'map', gridIndex: mg.Index }, score, }; } } } return best ? { source: best.source, target: best.target } : null; } /** * 功能符目标是否可生效。 * @param provider 配表 * @param weapon2Id 目标字 * @param level 显示等级 * @param toolType Text.type */ private _isBagToolDigitTarget(provider: WeaponComposeConfigProvider, weapon2Id: number, level: number, toolType: number): boolean { const w2Type = Math.floor(provider.getWeapon2Type(weapon2Id)); if (!isElementGlyphType(w2Type)) return false; const lv = Math.max(1, Math.floor(Number(level) || 1)); if (toolType === 13) return lv > 1; if (toolType === 10 || toolType === 11) { return lv < provider.getElementMaxLevel(w2Type); } return true; } /** * 产卡目标是否为可生效五行数字。 * @param provider 配表 * @param weapon2Id 目标字 * @param level 显示等级 * @param kind 卡种类 */ private _isBattleCardDigitTarget(provider: WeaponComposeConfigProvider, weapon2Id: number, level: number, kind: number): boolean { const w2Type = Math.floor(provider.getWeapon2Type(weapon2Id)); if (!isElementGlyphType(w2Type)) return false; const lv = Math.max(1, Math.floor(Number(level) || 1)); if (kind === BattleBagCardKind.Multiplier) { return lv < provider.getElementMaxLevel(w2Type); } return true; } /** * 产卡落点评分:场上优先、等级高优先、下标稳定。 * @param provider 配表 * @param weapon2Id 目标字 * @param level 显示等级 * @param onMap 是否地图 * @param index 槽/格下标 */ private _scoreBattleCardTarget(provider: WeaponComposeConfigProvider, weapon2Id: number, level: number, onMap: boolean, index: number): number { const lv = Math.max(1, Math.floor(Number(level) || 1)); const w2Type = Math.floor(provider.getWeapon2Type(weapon2Id)); const maxLv = isElementGlyphType(w2Type) ? provider.getElementMaxLevel(w2Type) : lv; return (onMap ? 100000 : 0) + lv * 1000 + (maxLv - lv) + (1000 - (index % 1000)); } /** * 空 Put 格是否可落五行字。 * @param session 会话 * @param mg 地图格 * @param w2Type 字类型 */ canDropTextOnEmptyGrid(session: BattleBagSession, mg: MapGrid, w2Type: Weapon2TileType): boolean { if (!isElementGlyphType(Number(w2Type))) return false; if (!mg?.node?.isValid || !mg.node.active) return false; if (!session.validator.isAllowedCampForPlace(mg.Index)) return false; if (session.merge.map.getCellKind(mg.Index) !== MapCellKind.Put) return false; if (session.merge.map.hasTile(mg.Index)) return false; if (MapDragGuideHelper.hasSpiderWeb(mg)) return false; return true; } /** * 背包槽是否可拖出。 * @param session 会话 * @param slotIndex 槽 */ private _canDragBagSlot(session: BattleBagSession, slotIndex: number): boolean { const land = session.landStateOf?.(slotIndex); if (land == null) return true; return land === TextListCellState.Open; } /** * 背包槽是否可被合成落到。 * @param session 会话 * @param slotIndex 槽 */ private _canMergeOntoBagSlot(session: BattleBagSession, slotIndex: number): boolean { const land = session.landStateOf?.(slotIndex); if (land == null) return true; return land === TextListCellState.Open || land === TextListCellState.OpenNoDrag; } /** * AI 阵营空字格数量。 * @param session 会话 * @param map 地图 */ countEmptyTextPut(session: BattleBagSession, map: MapManager): number { let n = 0; const grids = this._listActiveGrids(map); for (let i = 0; i < grids.length; i++) { const mg = grids[i]; if (!session.validator.isAllowedCampForPlace(mg.Index)) continue; if (session.merge.map.getCellKind(mg.Index) !== MapCellKind.Put) continue; if (session.merge.map.hasTile(mg.Index)) continue; if (MapDragGuideHelper.hasSpiderWeb(mg)) continue; n++; } return n; } /** * 本侧 unlockland:有符合条件的字则拖到当前解锁区。 * @param session 会话 * @param map 地图 */ findUnlockAreaAction(session: BattleBagSession, map: MapManager): { source: DragSource; target: DropTarget } | null { const unlockGrids = this._listUnlockGrids(session, map); if (unlockGrids.length <= 0) return null; let best: { source: DragSource; target: DropTarget; level: number } | null = null; const slots = session.getBagSlots(); for (let s = 0; s < slots.length; s++) { const slot = slots[s]; if (!slot || !(slot.weapon2Id > 0)) continue; if (!this._canDragBagSlot(session, s)) continue; const lv = Math.floor(Number(slot.level) || 0); if (!(lv > 0)) continue; for (let g = 0; g < unlockGrids.length; g++) { const grid = unlockGrids[g]; if (!session.canUnlockArea(grid.Index, slot.weapon2Id, lv)) continue; if (!best || lv > best.level) { best = { source: { kind: 'bag', slotIndex: s }, target: { kind: 'map', gridIndex: grid.Index }, level: lv, }; } } } const grids = this._listActiveGrids(map); for (let i = 0; i < grids.length; i++) { const from = grids[i]; if (!session.validator.isAllowedCampForPlace(from.Index)) continue; const tile = session.merge.map.getTile(from.Index); if (!tile || !(tile.weapon2Id > 0)) continue; const lv = Math.floor(Number(tile.level) || 0); if (!(lv > 0)) continue; for (let g = 0; g < unlockGrids.length; g++) { const to = unlockGrids[g]; if (to.Index === from.Index) continue; if (!session.canUnlockArea(to.Index, tile.weapon2Id, lv)) continue; if (!best || lv > best.level) { best = { source: { kind: 'map', gridIndex: from.Index }, target: { kind: 'map', gridIndex: to.Index }, level: lv, }; } } } return best ? { source: best.source, target: best.target } : null; } /** * AI 阵营当前解锁区空格。 * @param session 会话 * @param map 地图 */ private _listUnlockGrids(session: BattleBagSession, map: MapManager): MapGrid[] { const out: MapGrid[] = []; const camp = session.ownerPlayerId + 1; const current = map.areaUnlock.currentId(camp); if (current <= 0) return out; const grids = map.areaUnlock.gridsOf(current, camp); for (let i = 0; i < grids.length; i++) { const mg = grids[i]; if (!mg?.node?.isValid || !mg.node.active) continue; if (!mg.isAreaUnlock) continue; if (session.merge.map.hasTile(mg.Index)) continue; if (session.merge.map.getCellKind(mg.Index) !== MapCellKind.Unlock) { session.merge.map.setCellKind(mg.Index, MapCellKind.Unlock); } out.push(mg); } return out; } /** * 列出 AI 阵营可挖空格。 * @param session 会话 * @param map 地图 */ listEmptyDigGrids(session: BattleBagSession, map: MapManager): MapGrid[] { const out: MapGrid[] = []; const grids = this._listActiveGrids(map); for (let i = 0; i < grids.length; i++) { const mg = grids[i]; if (!session.validator.isAllowedCampForPlace(mg.Index)) continue; if (session.merge.map.getCellKind(mg.Index) !== MapCellKind.Dig) continue; if (mg.isCanPutText) continue; if (mg.isCanWalk) continue; if (session.merge.map.hasTile(mg.Index)) continue; out.push(mg); } return out; } /** * 单位索敌射程像素。 * @param weapon1Id Hero id;<=0 时默认 3 格 */ rangePxForWeapon1(weapon1Id: number): number { if (weapon1Id > 0) { const hero = gg.data.table.getTableData(TableNames.Hero, weapon1Id); if (hero) { const skillId = heroNormalSkillId(hero, 1); const skill = skillId > 0 ? gg.data.table.getTableData(TableNames.Skill, skillId) : null; return resolveTowerRangePx(hero, skill); } } return 3 * BattleMapSpec.CELL_SIZE; } /** * 覆盖分:射程内己方路径格数。 * @param map 地图 * @param anchor 落点格 * @param weapon1Id Hero id */ coverageScore(map: MapManager, anchor: MapGrid, weapon1Id: number): number { const rangePx = this.rangePxForWeapon1(weapon1Id); const ax = anchor.node.worldPosition.x; const ay = anchor.node.worldPosition.y; const range2 = rangePx * rangePx; let score = 0; const grids = this._listActiveGrids(map); for (let i = 0; i < grids.length; i++) { const mg = grids[i]; if ((mg.CampAreaID | 0) !== AI_PLACE_CAMP) continue; if (!mg.isCanWalk) continue; const dx = mg.node.worldPosition.x - ax; const dy = mg.node.worldPosition.y - ay; if (dx * dx + dy * dy <= range2) score++; } return score; } /** * 阵型分:路径出口优先,再覆盖,再短前长后。 * @param map 地图 * @param anchor 落点 * @param weapon1Id Hero id */ formationScore(map: MapManager, anchor: MapGrid, weapon1Id: number): number { const pathDistCells = this.pathDistToWalkPx(map, anchor) / BattleMapSpec.CELL_SIZE; const cover = this.coverageScore(map, anchor, weapon1Id); if (cover <= 0) return -1e9; const rangeCells = Math.max(0.5, this.rangePxForWeapon1(weapon1Id) / BattleMapSpec.CELL_SIZE); const frontBias = 2.5 - rangeCells; const pathFront = this.pathFrontScore(map, anchor, weapon1Id); return pathFront * 100000 + cover * 1000 + frontBias * (-pathDistCells) * 100; } /** * 路径出口分。 * @param map 地图 * @param anchor 落点 * @param weapon1Id Hero id */ pathFrontScore(map: MapManager, anchor: MapGrid, weapon1Id: number): number { const rangePx = this.rangePxForWeapon1(weapon1Id); const ax = anchor.node.worldPosition.x; const ay = anchor.node.worldPosition.y; const range2 = rangePx * rangePx; let best = -1e9; let hit = false; const grids = this._listActiveGrids(map); for (let i = 0; i < grids.length; i++) { const mg = grids[i]; if ((mg.CampAreaID | 0) !== AI_PLACE_CAMP) continue; if (!mg.isCanWalk) continue; const dx = mg.node.worldPosition.x - ax; const dy = mg.node.worldPosition.y - ay; if (dx * dx + dy * dy > range2) continue; hit = true; const front = this._pathFrontOfWalk(map, mg); if (front > best) best = front; } return hit ? best : 0; } /** * 最近己方路径格的出口分。 * @param map 地图 * @param anchor 落点 */ walkPathFrontNear(map: MapManager, anchor: MapGrid): number { const ax = anchor.node.worldPosition.x; const ay = anchor.node.worldPosition.y; let minD2 = Number.POSITIVE_INFINITY; let nearest: MapGrid | null = null; const grids = this._listActiveGrids(map); for (let i = 0; i < grids.length; i++) { const mg = grids[i]; if ((mg.CampAreaID | 0) !== AI_PLACE_CAMP) continue; if (!mg.isCanWalk) continue; const dx = mg.node.worldPosition.x - ax; const dy = mg.node.worldPosition.y - ay; const d2 = dx * dx + dy * dy; if (d2 < minD2) { minD2 = d2; nearest = mg; } } if (!nearest) return 0; return this._pathFrontOfWalk(map, nearest); } /** * 单格路径出口度量。 * @param map 地图 * @param walk 己方可走格 */ private _pathFrontOfWalk(map: MapManager, walk: MapGrid): number { const session = gg.game?.CurentBattle?.session ?? null; if (session?.pathfinder && session.grid) { const cell = mapGridToLogicCell(walk.Row, walk.Col, session.grid.rows, session.grid.cols); if (cell) { const cost = session.pathfinder.getPathCost(cell, AI_PLACE_CAMP); if (Number.isFinite(cost)) return -cost; } } return this._spawnDistCells(map, walk); } /** * 落点到己方路径格的最短距离(像素)。 * @param map 地图 * @param anchor 落点 */ pathDistToWalkPx(map: MapManager, anchor: MapGrid): number { const ax = anchor.node.worldPosition.x; const ay = anchor.node.worldPosition.y; let minD2 = Number.POSITIVE_INFINITY; const grids = this._listActiveGrids(map); for (let i = 0; i < grids.length; i++) { const mg = grids[i]; if ((mg.CampAreaID | 0) !== AI_PLACE_CAMP) continue; if (!mg.isCanWalk) continue; const dx = mg.node.worldPosition.x - ax; const dy = mg.node.worldPosition.y - ay; const d2 = dx * dx + dy * dy; if (d2 < minD2) minD2 = d2; } if (!(minD2 < Number.POSITIVE_INFINITY)) return 1e9; return Math.sqrt(minD2); } /** * 格到己方出怪口的最短距离(格数)。 * @param map 地图 * @param anchor 参考格 */ private _spawnDistCells(map: MapManager, anchor: MapGrid): number { const ax = anchor.node.worldPosition.x; const ay = anchor.node.worldPosition.y; let minD2 = Number.POSITIVE_INFINITY; const parent = map.spawnPointParent; const children = parent?.children; if (children) { for (let i = 0; i < children.length; i++) { const n = children[i]; if (!n?.isValid) continue; const name = n.name ?? ''; if (name.indexOf('spawnPoint_') !== 0) continue; const port = Number(name.substring('spawnPoint_'.length)); if (!Number.isInteger(port) || port < 1) continue; if ((map.spawnPortCampArea.get(port) | 0) !== AI_PLACE_CAMP) continue; const dx = n.worldPosition.x - ax; const dy = n.worldPosition.y - ay; const d2 = dx * dx + dy * dy; if (d2 < minD2) minD2 = d2; } } if (!(minD2 < Number.POSITIVE_INFINITY)) return 1e6; return Math.sqrt(minD2) / BattleMapSpec.CELL_SIZE; } /** * 落子后预估场上组合体数(五行单格英雄)。 * @param session 会话 * @param weapon2Id 待落字;<=0 只返回当前数 */ predictHeroCountAfterPlace(session: BattleBagSession, weapon2Id: number): number { const base = session.compositions.length; if (!(weapon2Id > 0)) return base; WeaponComposeConfigProvider.instance.load(); const w2Type = Number(WeaponComposeConfigProvider.instance.getWeapon2Type(weapon2Id)); if (!isElementGlyphType(w2Type)) return base; const w1 = this.resolveWeapon1Id({ weapon2Id, level: 1 }); return w1 != null && w1 > 0 ? base + 1 : base; } /** * 列出地图有效格。 * @param map 地图 */ private _listActiveGrids(map: MapManager): MapGrid[] { const out: MapGrid[] = []; const grids = map.mapGrids; if (!Array.isArray(grids)) return out; for (let i = 0; i < grids.length; i++) { const mg = grids[i]; if (!mg?.node?.isValid || !mg.node.active) continue; out.push(mg); } return out; } }