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.
141 lines
4.9 KiB
141 lines
4.9 KiB
|
3 weeks ago
|
import type { EntityId } from 'db://assets/mx/module/ecs';
|
||
|
|
import {
|
||
|
|
BattleTeamId,
|
||
|
|
EntityTag,
|
||
|
|
MovementComp,
|
||
|
|
TagComp,
|
||
|
|
TeamComp,
|
||
|
|
UnitMetaComp,
|
||
|
|
isYanSoulClaimed,
|
||
|
|
type BattleSession,
|
||
|
|
type Cell,
|
||
|
|
} from 'db://assets/mx/module/battle';
|
||
|
|
import type { MapGrid } from '../map/legacy/MapGrid';
|
||
|
|
import { MECHANISM_PARATROOP_TAG, MECHANISM_UNIT_TAG } from './MechanismConstants';
|
||
|
|
import { mapKeysToArray } from '../../../mx/tools/MembershipSet';
|
||
|
|
|
||
|
|
export interface MapRowRange {
|
||
|
|
startRow: number;
|
||
|
|
endRow: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 取地图中间 rowRatio 比例的可刷怪行(默认 2/3) */
|
||
|
|
export function calcMiddleRowRange(totalRows: number, rowRatio = 2 / 3): MapRowRange {
|
||
|
|
const middleCount = Math.floor(totalRows * rowRatio);
|
||
|
|
const excludeTotal = totalRows - middleCount;
|
||
|
|
const topExclude = Math.floor(excludeTotal / 2);
|
||
|
|
const bottomExclude = excludeTotal - topExclude;
|
||
|
|
return {
|
||
|
|
startRow: topExclude,
|
||
|
|
endRow: totalRows - bottomExclude - 1,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function isRowInRange(row: number, range: MapRowRange): boolean {
|
||
|
|
return row >= range.startRow && row <= range.endRow;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 场上普通怪物按 UnitMeta.spawnPoint 统计;飞禽机制怪不参与计数 */
|
||
|
|
function isMonsterForSpawnAreaCount(
|
||
|
|
world: BattleSession['world'],
|
||
|
|
id: EntityId,
|
||
|
|
): boolean {
|
||
|
|
const team = world.getComponent(id, TeamComp);
|
||
|
|
const tags = world.getComponent(id, TagComp);
|
||
|
|
const isEnemy = (team && BattleTeamId.isEnemyTeam(team.team))
|
||
|
|
|| tags?.tags.includes(EntityTag.Enemy);
|
||
|
|
if (!isEnemy) return false;
|
||
|
|
if (tags?.tags.includes(MECHANISM_UNIT_TAG)) return false;
|
||
|
|
if (tags?.tags.includes(MECHANISM_PARATROOP_TAG)) return false;
|
||
|
|
const meta = world.getComponent(id, UnitMetaComp);
|
||
|
|
if (!meta?.unitDefId) return false;
|
||
|
|
const move = world.getComponent(id, MovementComp);
|
||
|
|
if (!move || move.imprisoned || isYanSoulClaimed(move)) return false;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 场上怪物按出怪口统计,选怪物最多的区域(port 1-based);
|
||
|
|
* 全空或并列时取 port 更小者。
|
||
|
|
*/
|
||
|
|
export function pickBusiestSpawnPort(session: BattleSession): number {
|
||
|
|
const counts = new Map<number, number>();
|
||
|
|
for (const port of session.topology.spawnPorts.keys()) {
|
||
|
|
counts.set(port, 0);
|
||
|
|
}
|
||
|
|
const world = session.world;
|
||
|
|
world.forEachAlive((id) => {
|
||
|
|
if (!isMonsterForSpawnAreaCount(world, id)) return;
|
||
|
|
const meta = world.getComponent(id, UnitMetaComp);
|
||
|
|
const port = meta?.spawnPoint && meta.spawnPoint > 0 ? meta.spawnPoint : 1;
|
||
|
|
counts.set(port, (counts.get(port) ?? 0) + 1);
|
||
|
|
});
|
||
|
|
|
||
|
|
let bestPort = 0;
|
||
|
|
let bestCount = -1;
|
||
|
|
const ports = counts.size > 0
|
||
|
|
? mapKeysToArray(counts)
|
||
|
|
: mapKeysToArray(session.topology.spawnPorts);
|
||
|
|
ports.sort((a, b) => a - b);
|
||
|
|
for (const port of ports) {
|
||
|
|
const c = counts.get(port) ?? 0;
|
||
|
|
if (c > bestCount) {
|
||
|
|
bestCount = c;
|
||
|
|
bestPort = port;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (bestPort > 0) return bestPort;
|
||
|
|
if (session.topology.getSpawnCell(1)) return 1;
|
||
|
|
const fallback = mapKeysToArray(session.topology.spawnPorts).sort((a, b) => a - b);
|
||
|
|
return fallback[0] ?? 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 格子归属出怪口:距各出怪口进路最短者(与怪物 spawnPoint 语义一致) */
|
||
|
|
function resolveGridSpawnPort(session: BattleSession, cell: Cell): number {
|
||
|
|
const ports = mapKeysToArray(session.topology.spawnPorts).sort((a, b) => a - b);
|
||
|
|
if (ports.length === 0) return 1;
|
||
|
|
|
||
|
|
let bestPort = ports[0];
|
||
|
|
let bestDist = Number.POSITIVE_INFINITY;
|
||
|
|
for (const port of ports) {
|
||
|
|
const spawn = session.topology.getSpawnCell(port);
|
||
|
|
if (!spawn) {
|
||
|
|
console.error('[MechanismSpawnArea] 出怪口未配置', port);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const path = session.pathfinder.buildRetreatPath(cell, Number.MAX_SAFE_INTEGER, spawn, port);
|
||
|
|
const dist = path.length > 0 ? path.length - 1 : Number.POSITIVE_INFINITY;
|
||
|
|
if (dist < bestDist) {
|
||
|
|
bestDist = dist;
|
||
|
|
bestPort = port;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return bestPort;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 机制怪落点:先找怪物最多的出怪口区域,再在该区域内选格。
|
||
|
|
* 区域内无可用格时回退全量候选(避免机制无法触发)。
|
||
|
|
*/
|
||
|
|
export function filterGridsByBusiestMonsterArea(
|
||
|
|
session: BattleSession,
|
||
|
|
grids: readonly MapGrid[],
|
||
|
|
): MapGrid[] {
|
||
|
|
if (grids.length === 0) return [];
|
||
|
|
const busyPort = pickBusiestSpawnPort(session);
|
||
|
|
session.pathfinder.rebuild(session.topology);
|
||
|
|
const inArea = grids.filter(
|
||
|
|
(g) => resolveGridSpawnPort(session, { row: g.Row, col: g.Col }) === busyPort,
|
||
|
|
);
|
||
|
|
return inArea.length > 0 ? inArea : grids.slice();
|
||
|
|
}
|
||
|
|
|
||
|
|
export function shuffleMapGrids(grids: MapGrid[]): MapGrid[] {
|
||
|
|
const shuffled = grids.slice();
|
||
|
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
||
|
|
const j = Math.floor(Math.random() * (i + 1));
|
||
|
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||
|
|
}
|
||
|
|
return shuffled;
|
||
|
|
}
|