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