import type { WaveDef, WavePortPlan, WaveSpawnSegment } from 'db://assets/mx/module/battle/config/BattleConfigTypes'; import { TableNames } from '../../game/ConfigTableData'; import { MonsterDataManager, MonsterType } from '../../manager/MonsterDataManager'; import { LOCAL_ENEMY_SPAWN_PORT, LOCAL_OWN_SPAWN_PORT } from '../map/MapPathQueries'; import type { ProjectLevelMonsterConfig } from './LevelMonsterConfig'; import { calcChapterMonsterHp, makeMonsterUnitDefId, } from './MonsterHpCalculator'; /** 调试:双方出怪口不出小怪 */ export const NO_SMALL_MONSTER = true; /** 调试:双方出怪口不出 Boss */ export const NO_BOSS = true; /** Level.monster 单条:waveIndex,monsterId,count,spawnInterval */ export interface LevelMonsterWaveRow { /** 波次序号(1-based);同时作为 Monster1 行 id */ waveIndex: number; /** Monster 表 id */ monsterId: number; /** 本波小怪数量 */ count: number; /** 本波每只怪出现间隔(秒) */ spawnInterval: number; } /** Level.boss 单条:waveIndex|monsterId */ export interface LevelBossWaveEntry { /** 波次序号(1-based) */ waveIndex: number; /** Boss Monster 表 id */ monsterId: number; } /** 单波内的顺序出怪段(小怪段 + 可选 Boss 段) */ export interface LevelWaveSegments { /** 波次序号 */ waveIndex: number; /** 口内顺序段:先小怪,后 Boss(Boss 与小怪队列出) */ segments: Array<{ monsterId: number; count: number; spawnInterval: number }>; } /** * 解析 Level 表出怪串,生成 MX WaveDef(玩家口1 + AI口2 对称)。 * - monster:`waveIndex,monsterId,count,spawnInterval`,`|` 分隔波次 * - Monster1 行 id = waveIndex(非第 4 列) * - boss:`waveIndex|monsterId`,`,` 分隔;与 monster 同波时 **排队** 在小怪段之后 * - bosspro:Boss 链未激活时在对应波次掷万分比;命中后后续 boss 波 **必出** */ export class LevelWaveParser { /** 解析 Level.monster 四列波次串 */ static parseMonsterRows(monsterStr: string | undefined | null): LevelMonsterWaveRow[] { if (!monsterStr || monsterStr.trim() === '') return []; const out: LevelMonsterWaveRow[] = []; for (const part of monsterStr.split('|')) { const row = this._parseMonsterPart(part); if (row) out.push(row); } return out; } /** 解析 Level.boss 串 */ static parseBossEntries(bossStr: string | undefined | null): LevelBossWaveEntry[] { if (!bossStr || bossStr.trim() === '') return []; const out: LevelBossWaveEntry[] = []; for (const part of bossStr.split(',')) { const trimmed = part.trim(); if (!trimmed) continue; const pipe = trimmed.split('|'); if (pipe.length < 2) continue; const waveIndex = parseInt(pipe[0], 10); const monsterId = parseInt(pipe[1], 10); if (isNaN(waveIndex) || waveIndex <= 0 || isNaN(monsterId) || monsterId <= 0) continue; out.push({ waveIndex, monsterId }); } out.sort((a, b) => a.waveIndex - b.waveIndex); return out; } /** * 按 bosspro 解析本局哪些波次出 Boss。 * @param bossPro 万分比(2500 = 25%) * @param seed 对局种子(同一 seed 结果可复现) */ static resolveBossSpawns( bossEntries: LevelBossWaveEntry[], bossPro: number, seed: number, ): LevelBossWaveEntry[] { const result: LevelBossWaveEntry[] = []; if (bossEntries.length === 0) return result; const threshold = Math.max(0, Math.min(10000, Math.floor(Number(bossPro) || 0))); // 位运算前先归一成无符号整数,防 NaN/字符串种子在不同 JS 引擎里塌成同一序列 const seedU = (Math.floor(Number(seed) || 0)) >>> 0; if (threshold <= 0) { return result; } let chainActive = false; let rollSalt = 0; for (const entry of bossEntries) { if (chainActive) { result.push({ waveIndex: entry.waveIndex, monsterId: entry.monsterId }); continue; } const roll = this._rollUnder10000(seedU, rollSalt); const hit = roll < threshold; rollSalt++; if (hit) { chainActive = true; result.push({ waveIndex: entry.waveIndex, monsterId: entry.monsterId }); } } return result; } /** 合并 monster + boss,得到每波顺序段 */ static buildWaveSegments( levelRow: ITableLevel, seed: number, fallbackSpawnInterval: number, ): LevelWaveSegments[] { const monsterRows = this.parseMonsterRows(levelRow.monster); const bossEntries = this.parseBossEntries(levelRow.boss); const bossSpawns = this.resolveBossSpawns(bossEntries, Number(levelRow.bosspro) || 0, seed); const fallback = fallbackSpawnInterval > 0 && Number.isFinite(fallbackSpawnInterval) ? fallbackSpawnInterval : 1; const waves: LevelWaveSegments[] = []; for (const row of monsterRows) { waves.push({ waveIndex: row.waveIndex, segments: [{ monsterId: row.monsterId, count: row.count, spawnInterval: row.spawnInterval > 0 ? row.spawnInterval : fallback, }], }); } for (let b = 0; b < bossSpawns.length; b++) { const waveIndex = bossSpawns[b].waveIndex; const bossId = bossSpawns[b].monsterId; let wave: LevelWaveSegments | null = null; for (let i = 0; i < waves.length; i++) { if (waves[i].waveIndex === waveIndex) { wave = waves[i]; break; } } if (!wave) { wave = { waveIndex, segments: [] }; waves.push(wave); } const spawnInterval = wave.segments[0]?.spawnInterval ?? fallback; wave.segments.push({ monsterId: bossId, count: 1, spawnInterval, }); } waves.sort((a, b) => a.waveIndex - b.waveIndex); return waves; } /** 关卡 boss 配置中的 Monster id(去重保序,通常为本图 3 个 Boss) */ static collectUniqueBossMonsterIds(bossStr: string | undefined | null): number[] { const out: number[] = []; const seen: Record = {}; for (const entry of this.parseBossEntries(bossStr)) { const id = entry.monsterId; if (id <= 0 || seen[id]) continue; seen[id] = true; out.push(id); } return out; } /** 收集关卡涉及的 Monster id(含 boss 配置,便于预加载) */ static collectMonsterIds(levelRow: ITableLevel): number[] { const ids: number[] = []; const seen: Record = {}; const add = (id: number) => { if (id <= 0 || seen[id]) return; seen[id] = true; ids.push(id); }; for (const row of this.parseMonsterRows(levelRow.monster)) { add(row.monsterId); } for (const entry of this.parseBossEntries(levelRow.boss)) { add(entry.monsterId); } return ids; } /** 构建 MX WaveDef[]:spawnPoint_1 敌方怪攻本机,spawnPoint_2 我方怪攻 AI */ static toWaveDefs( levelRow: ITableLevel, monsterConfig: ProjectLevelMonsterConfig, spawnInterval: number, firstWaveDelaySec: number, subsequentIntervalSec: number, seed: number, ): WaveDef[] { const waveSegments = this.buildWaveSegments(levelRow, seed, spawnInterval); const usable = (NO_SMALL_MONSTER || NO_BOSS) ? this._dropDebugWaves(waveSegments) : waveSegments; return usable.map((wave, idx) => { const difficultyRow = wave.waveIndex; const buildPortSegments = (port: number) => wave.segments.map(seg => this._toSpawnSegment( seg.monsterId, seg.count, difficultyRow, seg.spawnInterval, port, monsterConfig, )); /** spawnPoint_1 敌方怪 → 攻本机并走向 wallTarget2;spawnPoint_2 我方怪 → 攻 AI 并走向 wallTarget1 */ const ports: WavePortPlan[] = [ { port: LOCAL_ENEMY_SPAWN_PORT, playerId: 0, segments: buildPortSegments(LOCAL_ENEMY_SPAWN_PORT) }, { port: LOCAL_OWN_SPAWN_PORT, playerId: 1, segments: buildPortSegments(LOCAL_OWN_SPAWN_PORT) }, ]; return { id: `wave_${wave.waveIndex}`, prepTime: idx === 0 ? firstWaveDelaySec : subsequentIntervalSec, rewardGold: 0, ports, }; }); } /** * 去掉被调试开关屏蔽的出怪段。整波没有剩余段时丢掉该波。 * 全部被屏蔽时保留第一波空队列,便于进入战斗且不会立刻判胜。 * @param waves 原波次 */ private static _dropDebugWaves(waves: LevelWaveSegments[]): LevelWaveSegments[] { const out: LevelWaveSegments[] = []; for (let i = 0; i < waves.length; i++) { const wave = waves[i]; const segments: LevelWaveSegments['segments'] = []; for (let s = 0; s < wave.segments.length; s++) { const seg = wave.segments[s]; if (NO_SMALL_MONSTER && this._isSmallMonster(seg.monsterId)) continue; if (NO_BOSS && this._isBossMonster(seg.monsterId)) continue; segments.push(seg); } if (segments.length === 0) continue; out.push({ waveIndex: wave.waveIndex, segments }); } if (out.length === 0 && waves.length > 0) { out.push({ waveIndex: waves[0].waveIndex, segments: [] }); } return out; } /** * Monster.type=1 为小怪。缺行时保留该段并打日志。 * @param monsterId Monster 表 id */ private static _isSmallMonster(monsterId: number): boolean { const row = gg.data.table.getTableData(TableNames.Monster, monsterId); if (!row) { console.log('[LevelWaveParser] 不出小怪:Monster 缺失', monsterId); return false; } return Math.floor(Number(row.type)) === MonsterType.SmallMonster; } /** * Monster.type 为 Boss(含旧表银/金/橙)。缺行时保留该段并打日志。 * @param monsterId Monster 表 id */ private static _isBossMonster(monsterId: number): boolean { const row = gg.data.table.getTableData(TableNames.Monster, monsterId); if (!row) { console.log('[LevelWaveParser] 不出 Boss:Monster 缺失', monsterId); return false; } return MonsterDataManager.isBossType(Math.floor(Number(row.type))); } private static _parseMonsterPart(part: string): LevelMonsterWaveRow | null { const cols = part.split(',').map(s => s.trim()).filter(Boolean); if (cols.length < 3) return null; const waveIndex = parseInt(cols[0], 10); const monsterId = parseInt(cols[1], 10); const count = parseInt(cols[2], 10); const spawnInterval = cols.length >= 4 ? parseFloat(cols[3]) : NaN; if (isNaN(waveIndex) || waveIndex <= 0) return null; if (isNaN(monsterId) || monsterId <= 0) return null; if (isNaN(count) || count <= 0) return null; if (cols.length >= 4 && !(spawnInterval > 0 && Number.isFinite(spawnInterval))) { console.log('[LevelWaveParser] 无效出怪间隔,波次将用兜底间隔', part); } return { waveIndex, monsterId, count, spawnInterval: spawnInterval > 0 && Number.isFinite(spawnInterval) ? spawnInterval : 0, }; } private static _toSpawnSegment( monsterId: number, count: number, difficultyRow: number, spawnInterval: number, port: number, monsterConfig: ProjectLevelMonsterConfig, ): WaveSpawnSegment { return { unitDefId: makeMonsterUnitDefId(monsterId, port, difficultyRow), count, interval: spawnInterval, difficultyRow, maxHp: calcChapterMonsterHp(monsterId, port, difficultyRow, monsterConfig), }; } /** * 确定性万分比掷点 [0, 10000)。 * 仅用 ES5 位运算 + Math.imul(缺则手写),避免部分小游戏 JS 引擎对大整数/库差异敏感。 */ private static _rollUnder10000(seed: number, salt: number): number { const s0 = (Math.floor(Number(seed) || 0) ^ this._imul(Math.floor(Number(salt) || 0) + 1, 0x9e3779b1)) >>> 0; let s = this._imul(s0 ^ (s0 >>> 16), 2246822507) >>> 0; s = this._imul(s ^ (s >>> 13), 3266489909) >>> 0; const r = (s ^ (s >>> 16)) >>> 0; return r % 10000; } /** 32 位 imul;Math.imul 缺失时用拆半乘法,保证与规范一致 */ private static _imul(a: number, b: number): number { if (typeof Math.imul === 'function') { return Math.imul(a, b); } const ah = (a >>> 16) & 0xffff; const al = a & 0xffff; const bh = (b >>> 16) & 0xffff; const bl = b & 0xffff; return ((al * bl) + (((ah * bl + al * bh) << 16) >>> 0)) | 0; } }