import type { WaveDef, WavePortPlan, WaveSpawnSegment } from 'db://assets/mx/module/battle/config/BattleConfigTypes'; import type { ProjectLevelMonsterConfig } from './LevelMonsterConfig'; import { calcChapterMonsterHp, makeMonsterUnitDefId, } from './MonsterHpCalculator'; /** 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[]:每波双口(port1 玩家 / port2 AI) */ static toWaveDefs( levelRow: ITableLevel, monsterConfig: ProjectLevelMonsterConfig, spawnInterval: number, firstWaveDelaySec: number, subsequentIntervalSec: number, seed: number, ): WaveDef[] { const waveSegments = this.buildWaveSegments(levelRow, seed, spawnInterval); return waveSegments.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, )); /** port1 玩家出怪 → 攻 AI(playerId 1);port2 AI 出怪 → 攻本地(playerId 0) */ const ports: WavePortPlan[] = [ { port: 1, playerId: 1, segments: buildPortSegments(1) }, { port: 2, playerId: 0, segments: buildPortSegments(2) }, ]; return { id: `wave_${wave.waveIndex}`, prepTime: idx === 0 ? firstWaveDelaySec : subsequentIntervalSec, rewardGold: 0, ports, }; }); } 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; } }