|
|
|
|
import {
|
|
|
|
|
BattleBagCardKind,
|
|
|
|
|
MapCellKind,
|
|
|
|
|
Weapon2TileType,
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 产卡目标是否为可生效五行数字。
|
|
|
|
|
* @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;
|
|
|
|
|
}
|
|
|
|
|
}
|