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/mechanism/MechanismSpawnArea.ts

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;
}