|
|
|
|
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 { BattleBuffCarryStore } from '../../manager/battleBuff/BattleBuffCarryStore';
|
|
|
|
|
import { resolveCarriedBossSpawnBonusWan } from '../../manager/battleBuff/BattleBuffPassiveSkillUtil';
|
|
|
|
|
import { LOCAL_ENEMY_SPAWN_PORT, LOCAL_OWN_SPAWN_PORT } from '../map/MapPathQueries';
|
|
|
|
|
import type { ProjectLevelMonsterConfig } from './LevelMonsterConfig';
|
|
|
|
|
import {
|
|
|
|
|
calcChapterMonsterHp,
|
|
|
|
|
makeMonsterUnitDefId,
|
|
|
|
|
} from './MonsterHpCalculator';
|
|
|
|
|
|
|
|
|
|
/** 调试:双方出怪口不出小怪(正式出怪须为 false) */
|
|
|
|
|
export const NO_SMALL_MONSTER = false;
|
|
|
|
|
|
|
|
|
|
/** 调试:双方出怪口不出 Boss(正式出怪须为 false) */
|
|
|
|
|
export const NO_BOSS = false;
|
|
|
|
|
|
|
|
|
|
/** 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,得到每波顺序段。
|
|
|
|
|
* @param bossProMulWan 灾厄盒等加成万分比(双方取较大值后传入);有效阈值=bosspro×(1+mul/10000)
|
|
|
|
|
*/
|
|
|
|
|
static buildWaveSegments(
|
|
|
|
|
levelRow: ITableLevel,
|
|
|
|
|
seed: number,
|
|
|
|
|
fallbackSpawnInterval: number,
|
|
|
|
|
bossProMulWan = 0,
|
|
|
|
|
): LevelWaveSegments[] {
|
|
|
|
|
const monsterRows = this.parseMonsterRows(levelRow.monster);
|
|
|
|
|
const bossEntries = this.parseBossEntries(levelRow.boss);
|
|
|
|
|
const basePro = Number(levelRow.bosspro) || 0;
|
|
|
|
|
const mul = Math.max(0, Math.floor(Number(bossProMulWan) || 0));
|
|
|
|
|
const effectivePro = Math.floor(basePro * (1 + mul / 10000));
|
|
|
|
|
const bossSpawns = this.resolveBossSpawns(bossEntries, effectivePro, 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 构建 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,
|
|
|
|
|
LevelWaveParser._resolveBossProMulWan(),
|
|
|
|
|
);
|
|
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 灾厄盒:本机 + AI 携带加成万分比取较大值(建波时 AI 可能尚未挂载)。
|
|
|
|
|
*/
|
|
|
|
|
private static _resolveBossProMulWan(): number {
|
|
|
|
|
const localWan = resolveCarriedBossSpawnBonusWan(BattleBuffCarryStore.getPassives(0)).bonusWan;
|
|
|
|
|
const aiWan = resolveCarriedBossSpawnBonusWan(BattleBuffCarryStore.getPassives(1)).bonusWan;
|
|
|
|
|
return Math.max(localWan, aiWan);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 去掉被调试开关屏蔽的出怪段。整波没有剩余段时丢掉该波。
|
|
|
|
|
* 全部被屏蔽时保留第一波空队列,便于进入战斗且不会立刻判胜。
|
|
|
|
|
* @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)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|