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/config/LevelWaveParser.ts

353 lines
14 KiB

3 weeks ago
import type { WaveDef, WavePortPlan, WaveSpawnSegment } from 'db://assets/mx/module/battle/config/BattleConfigTypes';
2 weeks ago
import { TableNames } from '../../game/ConfigTableData';
import { MonsterDataManager, MonsterType } from '../../manager/MonsterDataManager';
import { LOCAL_ENEMY_SPAWN_PORT, LOCAL_OWN_SPAWN_PORT } from '../map/MapPathQueries';
3 weeks ago
import type { ProjectLevelMonsterConfig } from './LevelMonsterConfig';
import {
calcChapterMonsterHp,
makeMonsterUnitDefId,
} from './MonsterHpCalculator';
2 weeks ago
/** 调试:双方出怪口不出小怪(正式出怪须为 false) */
export const NO_SMALL_MONSTER = false;
2 weeks ago
2 weeks ago
/** 调试:双方出怪口不出 Boss(正式出怪须为 false) */
export const NO_BOSS = false;
2 weeks ago
3 weeks ago
/** 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<number, true> = {};
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<number, true> = {};
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;
}
2 weeks ago
/** 构建 MX WaveDef[]:spawnPoint_1 敌方怪攻本机,spawnPoint_2 我方怪攻 AI */
3 weeks ago
static toWaveDefs(
levelRow: ITableLevel,
monsterConfig: ProjectLevelMonsterConfig,
spawnInterval: number,
firstWaveDelaySec: number,
subsequentIntervalSec: number,
seed: number,
): WaveDef[] {
const waveSegments = this.buildWaveSegments(levelRow, seed, spawnInterval);
2 weeks ago
const usable = (NO_SMALL_MONSTER || NO_BOSS)
? this._dropDebugWaves(waveSegments)
: waveSegments;
return usable.map((wave, idx) => {
3 weeks ago
const difficultyRow = wave.waveIndex;
const buildPortSegments = (port: number) => wave.segments.map(seg => this._toSpawnSegment(
seg.monsterId,
seg.count,
difficultyRow,
seg.spawnInterval,
port,
monsterConfig,
));
2 weeks ago
/** spawnPoint_1 敌方怪 → 攻本机并走向 wallTarget2;spawnPoint_2 我方怪 → 攻 AI 并走向 wallTarget1 */
3 weeks ago
const ports: WavePortPlan[] = [
2 weeks ago
{ 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) },
3 weeks ago
];
return {
id: `wave_${wave.waveIndex}`,
prepTime: idx === 0 ? firstWaveDelaySec : subsequentIntervalSec,
rewardGold: 0,
ports,
};
});
}
2 weeks ago
/**
* 去掉被调试开关屏蔽的出怪段。整波没有剩余段时丢掉该波。
* 全部被屏蔽时保留第一波空队列,便于进入战斗且不会立刻判胜。
* @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<ITableMonster>(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<ITableMonster>(TableNames.Monster, monsterId);
if (!row) {
console.log('[LevelWaveParser] 不出 Boss:Monster 缺失', monsterId);
return false;
}
return MonsterDataManager.isBossType(Math.floor(Number(row.type)));
}
3 weeks ago
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;
}
}