import type { LevelDef } from 'db://assets/mx/module/battle/config/BattleConfigTypes'; import { createMembershipSet, membershipAdd, membershipFromArray, membershipDedupeArray, membershipToArray, type MembershipSet, type ReadonlyMembershipSet } from '../../../mx/tools/MembershipSet'; import type { ProjectLevelMonsterConfig } from './LevelMonsterConfig'; import { BattleFlowMode } from 'db://assets/mx/module/battle/core/BattleFlowMode'; import { MonsterType } from '../../manager/MonsterDataManager'; import { TableNames } from '../../game/ConfigTableData'; import { BattleMapSpec, resolveMapSizeFallback } from '../map/BattleMapSpec'; import { probeMapGridSpecFromPrefab, type MapGridSpec } from '../map/MapGridSpecReader'; import { WeaponComposeConfigProvider } from '../bag/WeaponComposeConfigProvider'; import { LevelWaveParser } from './LevelWaveParser'; import { calcSpecialReplicaFinalHpScale, getWeaponParaFromMaxChapter, resolveHpMultiple } from './MonsterHpCalculator'; import { resetMonsterHpDebugLog } from '../debug/MonsterHpDebugLog'; /** atk_after 段 1 类 debuff 类型 → 状态特效预制体 */ const DEBUFF_TYPE_TO_STATUS_FX: Readonly> = { 1: '怪_火', 2: '怪_流血', 3: '怪_冰', 4: '怪_晕眩', 6: '怪_恶鬼', 7: '怪_电', }; /** 通用 debuff 特效(开战预载,避免局内首次 ApplyBuff 卡顿) */ const ALWAYS_BATTLE_DEBUFF_STATUS_FX = Object.values(DEBUFF_TYPE_TO_STATUS_FX); /** Monster 表 icon/name → 预制体文件名(仅表名与 bundle 内文件名不一致时填写) */ const MONSTER_PREFAB_ALIASES: Readonly> = {}; export interface LevelDefBuildOptions { /** 战场 Level 表 id */ levelId: number; /** 段位难度加值(Rank.diffcult) */ rankDifficultyBonus?: number; /** 己方段位 id(查 Level.weapon_para) */ playerRankId?: number; wallHp?: number; spawnInterval?: number; /** @deprecated 仅覆盖后续波间隔;请用 firstWaveDelay / subsequentWaveInterval */ waveInterval?: number; /** 首波开怪前延迟(秒),默认读 Const 波次间隔 para1 第 1 项 */ firstWaveDelay?: number; /** 后续每波开怪前间隔(秒),默认读 Const para1 第 2 项 */ subsequentWaveInterval?: number; flowMode?: BattleFlowMode; /** 对局随机种子(Boss 概率链等) */ seed?: number; /** 地图格子规格(默认从 mapLayer 预制体 MapManager 探测) */ mapGridSpec?: MapGridSpec; } /** Const 波次间隔:首波延迟 + 后续波间隔(秒) */ export interface WaveIntervalConfig { /** 首波开怪前延迟(秒) */ firstWaveDelaySec: number; /** 后续每波开怪前间隔(秒) */ subsequentIntervalSec: number; } /** 双方基地默认血量(后续养成覆盖) */ export const DEFAULT_BATTLE_WALL_HP = 3; /** 开战初始桃子(写死,不读 Level.startnumber) */ const START_PEACH = 1000; /** 单怪出怪间隔默认秒数(常量表无对应行) */ const DEFAULT_MONSTER_SPAWN_INTERVAL_SEC = 1; /** 对齐 Const 表 id=10(ConstNumType.WaveInterval),避免从 ConfigProjectData 拉环 */ const CONST_WAVE_INTERVAL_ID = 10; /** 从 Level 表构建 MX LevelDef */ export class ChapterLevelBuilder { static build(options: LevelDefBuildOptions): LevelDef | null { resetMonsterHpDebugLog(); const levelRow = gg.data.table.getTableData(TableNames.Level, options.levelId); if (!levelRow) { console.error('[ChapterLevelBuilder] level not found', options.levelId); return null; } const difficultyRow = Math.max(1, options.rankDifficultyBonus ?? (Number(levelRow.diffcult) || 1)); const startGold = START_PEACH; const spawnInterval = this.resolveMonsterSpawnIntervalSec(options.spawnInterval); const waveTiming = this.resolveWaveIntervalConfig(); const firstWaveDelay = options.firstWaveDelay ?? waveTiming.firstWaveDelaySec; const subsequentInterval = options.subsequentWaveInterval ?? options.waveInterval ?? waveTiming.subsequentIntervalSec; if (!(firstWaveDelay >= 0 && Number.isFinite(firstWaveDelay))) { console.log('[ChapterLevelBuilder] invalid firstWaveDelay', firstWaveDelay); } if (!(subsequentInterval > 0 && Number.isFinite(subsequentInterval))) { console.log('[ChapterLevelBuilder] invalid subsequentWaveInterval', subsequentInterval); } const monsterConfig: ProjectLevelMonsterConfig = { levelId: options.levelId, difficultyRow, monsterGroup: difficultyRow, hpMultiple: resolveHpMultiple(1), specialReplicaHpScale: calcSpecialReplicaFinalHpScale(), portHpMult: [1, 1, 1], powerPara: 0, powerPara2: 0, weaponPara: getWeaponParaFromMaxChapter(options.playerRankId ?? 0), }; const bossSeed = options.seed != null && Number.isFinite(Number(options.seed)) ? (Math.floor(Number(options.seed)) >>> 0) : ((Math.floor(Number(options.levelId) || 0) * 9973) >>> 0); const waves = LevelWaveParser.toWaveDefs( levelRow, monsterConfig, spawnInterval, Math.max(0, firstWaveDelay), Math.max(0.05, subsequentInterval), bossSeed, ); const flowMode = options.flowMode ?? this.resolveFlowMode(levelRow); const mapPrefabName = this.resolveLevelMapPrefabName(options.levelId); const mapId = this.getLevelMapId(options.levelId); const mapSize = options.mapGridSpec ?? probeMapGridSpecFromPrefab(mapPrefabName) ?? (() => { console.log('[ChapterLevelBuilder] 未能从预制体读取地图规格,使用兜底常量', mapPrefabName, mapId); return resolveMapSizeFallback(mapId); })(); return { id: `level_${options.levelId}_d${difficultyRow}`, flowMode, mapRows: mapSize.rows, mapCols: mapSize.cols, cellSize: mapSize.cellSize, wallHp: options.wallHp ?? DEFAULT_BATTLE_WALL_HP, startGold, extra: { monsterConfig }, waves, }; } /** * 单怪出怪间隔兜底(秒):优先 Level.monster 第 4 列;缺省/无效时用 override 或局内默认。 */ static resolveMonsterSpawnIntervalSec(override?: number): number { if (typeof override === 'number' && override > 0 && Number.isFinite(override)) { return override; } return DEFAULT_MONSTER_SPAWN_INTERVAL_SEC; } /** 从 Const 表 id=10 读取波次间隔:para1「首波延迟,后续间隔」 */ static resolveWaveIntervalConfig(): WaveIntervalConfig { const parts = gg.data.table.getConstArray(CONST_WAVE_INTERVAL_ID); const first = parts[0]; const subsequent = parts[1]; if (parts.length < 2) { console.log( '[ChapterLevelBuilder] Const 波次间隔 para1 须为「首波,后续」', CONST_WAVE_INTERVAL_ID, ); } const firstWaveDelaySec = (typeof first === 'number' && first >= 0 && Number.isFinite(first)) ? first : 10; const subsequentIntervalSec = (typeof subsequent === 'number' && subsequent > 0 && Number.isFinite(subsequent)) ? subsequent : 5; return { firstWaveDelaySec, subsequentIntervalSec }; } /** * 地图预制体名。读关卡表当前关卡 Level.map。 * @param levelId 当前关卡 id */ static resolveLevelMapPrefabName(levelId: number): string { const level = gg.data.table.getTableData(TableNames.Level, levelId); const raw = level?.map; if (typeof raw === 'string') { const name = raw.trim(); if (name) return name; } console.warn('[ChapterLevelBuilder] 关卡缺少 Level.map', levelId); return ''; } /** 从 map 预制体名解析数字索引(mapLayer3→3);供 MAP_SIZE_BY_ID 等 legacy 逻辑 */ static getLevelMapId(levelId: number): number { const name = ChapterLevelBuilder.resolveLevelMapPrefabName(levelId); const matched = /^mapLayer(\d+)$/i.exec(name); if (matched) { const parsed = Number(matched[1]); if (Number.isFinite(parsed) && parsed > 0) return parsed; } const numeric = Number(name); if (Number.isFinite(numeric) && numeric > 0) return numeric; return levelId > 0 ? levelId : 1; } /** @deprecated 使用 getLevelMapId */ static getChapterMapId(levelId: number): number { return this.getLevelMapId(levelId); } /** * 战斗流程:Level.flow_mode;缺省经典备战。 * flow_mode: 0=经典备战,1=连续战斗 */ static resolveFlowMode(chapter: ITableLevel): BattleFlowMode { const rowMode = (chapter as ITableLevel & { flow_mode?: number }).flow_mode; if (rowMode === 1) return BattleFlowMode.Continuous; return BattleFlowMode.ClassicPrep; } /** ???????????? id?????/???????? */ static collectLevelMonsterIds(levelId: number): number[] { const level = gg.data.table.getTableData(TableNames.Level, levelId); if (!level) return []; return LevelWaveParser.collectMonsterIds(level); } /** @deprecated 使用 collectLevelMonsterIds */ static collectChapterMonsterIds(levelId: number): number[] { return this.collectLevelMonsterIds(levelId); } static collectLevelMonsterModels(levelId: number): string[] { const models = createMembershipSet(); for (const id of this.collectLevelMonsterIds(levelId)) { const prefabName = this.resolveMonsterPrefabNameById(id); if (prefabName) membershipAdd(models, prefabName); } return membershipToArray(models); } /** Monster 表 id → 预制体名(icon / model / name + 别名) */ static resolveMonsterPrefabNameById(monsterId: number): string | null { const row = gg.data.table.getTableData(TableNames.Monster, monsterId) ?? gg.data.project.getMonsterDataByID(monsterId); if (!row) return null; return this.resolveMonsterPrefabFromRow(row); } /** Monster 行 → battleGameMonster 预制体文件名 */ static resolveMonsterPrefabFromRow(row: ITableMonster): string | null { const ext = row as ITableMonster & { model?: string }; const icon = typeof row.icon === 'string' ? row.icon.trim() : ''; const model = typeof ext.model === 'string' ? ext.model.trim() : ''; const raw = icon || model || row.name; return this.resolveMonsterPrefabName(raw); } /** 配表 model 或 name → battleGameMonster 预制体文件名 */ static resolveMonsterPrefabName(raw: unknown): string | null { if (typeof raw !== 'string') return null; const name = raw.trim(); if (!name) return null; return MONSTER_PREFAB_ALIASES[name] ?? name; } static collectLevelBloodBarPrefabs(levelId: number): string[] { const prefabs = createMembershipSet(); for (const id of this.collectLevelMonsterIds(levelId)) { const row = gg.data.table.getTableData(TableNames.Monster, id); const type = row?.type ?? gg.data.project.getMonsterDataByID(id)?.type; const prefab = this._bloodBarPrefabForMonsterType(type); if (prefab) membershipAdd(prefabs, prefab); } return membershipToArray(prefabs); } /** * 关卡开战预载的状态特效: * - 通用正面 buff:怪_攻击buff * - 上阵武器 skill / debuff 关联特效 */ static collectLevelStatusFxPrefabs(levelId: number): string[] { const prefabs = membershipFromArray(['怪_攻击buff', '怪_加速', '怪物加血', '血量上限']); for (const fx of ALWAYS_BATTLE_DEBUFF_STATUS_FX) { membershipAdd(prefabs, fx); } for (const fx of this._collectLoadoutStatusFxPrefabs()) { membershipAdd(prefabs, fx); } return membershipToArray(prefabs); } private static _addMonsterModelName(models: MembershipSet, model: unknown): void { const prefabName = ChapterLevelBuilder.resolveMonsterPrefabName(model); if (prefabName) membershipAdd(models, prefabName); } private static _bloodBarPrefabForMonsterType(type: unknown): string | null { const monsterType = Number(type); if (!Number.isFinite(monsterType) || monsterType === MonsterType.TreasureBox) { return null; } if (monsterType === MonsterType.Boss) return 'BOSS血条底框'; if (monsterType === MonsterType.SmallMonster || monsterType === MonsterType.SummonMonster) { return '小怪血条底'; } return 'MidBOSS血条框'; } private static _collectLoadoutStatusFxPrefabs(): string[] { const fx = createMembershipSet(); const baseAtkIds = createMembershipSet(); const equipped = gg.data.project.getEquippedWeaponDataList(); const weapon1Ids = membershipFromArray(equipped.map((w) => Number(w.id))); for (const w1 of equipped) { for (const rawAtkId of w1.atk_id ?? []) { const atkId = Number(rawAtkId); if (Number.isFinite(atkId) && atkId > 0) membershipAdd(baseAtkIds, atkId); } for (const rawSkillId of w1.act_skill ?? []) { const actSkill = Number(rawSkillId); if (Number.isFinite(actSkill) && actSkill > 0) membershipAdd(baseAtkIds, actSkill); } const weaponId = Number(w1.startweapon) || 0; if (weaponId > 0) { const wpn = gg.data.table.getTableData(TableNames.Weapon, weaponId); const wid = Number(wpn?.act_skill) || 0; if (wid > 0) membershipAdd(baseAtkIds, wid); } } const atkIds = membershipToArray(baseAtkIds); for (let i = 0; i < atkIds.length; i++) { const row = gg.data.table.getTableData(TableNames.Skill, atkIds[i]); if (row) this._collectStatusFxFromSkillRow(row, fx); } return membershipToArray(fx); } private static _collectStatusFxFromAtkHookRaw(raw: string, out: MembershipSet): void { if (!raw) return; for (const seg of raw.split(';')) { const part = seg.trim(); if (!part) continue; const nums = part.split(',').map(Number); if (!nums.length || !Number.isFinite(nums[0])) continue; if (nums[0] !== 1 || nums.length < 3) continue; const prefab = DEBUFF_TYPE_TO_STATUS_FX[nums[2]]; if (prefab) membershipAdd(out, prefab); } } private static _collectStatusFxFromSkillAtkBuff(raw: string, out: MembershipSet): void { const s = String(raw ?? '').trim(); if (!s) return; const parts = s.split(',').map(Number); const kind = parts[0] ?? 0; if (kind === 1) membershipAdd(out, '怪_晕眩'); } private static _collectStatusFxFromSkillRow(row: ITableSkill, out: MembershipSet): void { this._collectStatusFxFromSkillAtkBuff(row.atk_buff1 ?? '', out); this._collectStatusFxFromSkillAtkBuff(row.atk_buff2 ?? '', out); const skill = row.skill?.trim(); if (!skill) return; const parts = skill.split(',').map(Number); if ((parts[0] ?? 0) !== 2) return; const stunChance = parts[3] ?? 0; const stunSec = parts[4] ?? 0; if (stunChance > 0 && stunSec > 0) { membershipAdd(out, '怪_晕眩'); } } }