You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
AdLike2048/assets/script/battle/ai/decision/AiBoardProbe.ts

730 lines
30 KiB

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<ITableHero>(TableNames.Hero, weapon1Id);
if (hero) {
const skillId = heroNormalSkillId(hero, 1);
const skill = skillId > 0
? gg.data.table.getTableData<ITableSkill>(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;
}
}