import type { AbilityDef, BuffDef, LevelDef, UnitDef, WaveDef, } from 'db://assets/mx/module/battle/config/BattleConfigTypes'; import type { IConfigProvider } from 'db://assets/mx/module/battle/config/IConfigProvider'; import { InMemoryConfigProvider } from 'db://assets/mx/module/battle/config/InMemoryConfigProvider'; import { EntityTag } from 'db://assets/mx/module/battle/ecs/EntityTag'; import { TableNames } from '../../game/ConfigTableData'; import { createMembershipSet, membershipAdd, membershipFromArray, membershipDedupeArray, membershipToArray, mapValuesToArray, type MembershipSet, type ReadonlyMembershipSet } from '../../../mx/tools/MembershipSet'; import { calcChapterMonsterHp, makeMonsterUnitDefId, parseLegacyMonsterId, parseMonsterUnitDefId } from './MonsterHpCalculator'; import { readMonsterConfigFromLevel, type ProjectLevelMonsterConfig } from './LevelMonsterConfig'; import { ChapterLevelBuilder } from './ChapterLevelBuilder'; import { finitePositiveHp } from '../../../mx/tools/FiniteHp'; import { Weapon1TowerBuilder } from './weapon1'; import { BaseAtkAbilityBuilder } from './baseAtk/BaseAtkAbilityBuilder'; import { buildBasicDebuffDefs } from './DebuffBuffBuilder'; /** * 项目配表 → MX 通用战斗配置 * 数据源:Weapon1 / Monster / Chapter(由 ChapterLevelBuilder 动态生成 Level) */ export class TableBattleConfigProvider implements IConfigProvider { private _store = new InMemoryConfigProvider(); private _weapon1ToUnit = new Map(); private _levels = new Map(); private _loaded = false; /** 本局怪物血量公式参数(开战 registerLevel 写入) */ private _monsterConfig: ProjectLevelMonsterConfig | undefined; /** 加载 Weapon1、Monster 等静态表(由 gg.battle.init 调用) */ loadStaticTables(): void { if (this._loaded) return; this._loaded = true; this._store.units.clear(); this._store.abilities.clear(); this._weapon1ToUnit.clear(); this.loadWeapon1Table(); BaseAtkAbilityBuilder.registerAll({ registerAbility: (id, def) => this._store.abilities.set(id, def), }); for (const buff of buildBasicDebuffDefs()) { this._store.buffs.set(buff.id, buff); } } /** 每次开战刷新 Weapon1 单位映射 */ refreshWeaponUnits(): void { const list = gg.data.table.getTableList(TableNames.Hero) ?? []; const bundle = Weapon1TowerBuilder.buildAll(list); this._weapon1ToUnit = bundle.weapon1ToUnit; for (const [id, unit] of bundle.units) { this._store.units.set(id, unit); } } /** 开战重刷 Skill→Ability(覆盖热更前残留的错误 targeting) */ refreshAbilities(): void { BaseAtkAbilityBuilder.registerAll({ registerAbility: (id, def) => this._store.abilities.set(id, def), }); } /** 注册章节关卡(每次开战前调用) */ registerLevel(level: LevelDef): void { this._levels.set(level.id, level); this._store.levels.set(level.id, level); this._monsterConfig = readMonsterConfigFromLevel(level.extra); if (!this._monsterConfig) { console.log('[TableBattleConfigProvider] Level 无 monsterConfig,怪物血量无法按公式计算', level.id); } this.ensureMonstersForLevel(level); this._sessionMonsterUnits.set(level.id, this._collectMonsterUnitIds(level)); } /** 退战斗释放本局 Level 与动态注册的怪物 Unit */ releaseBattleSession(levelId: string): void { this._levels.delete(levelId); this._store.levels.delete(levelId); const unitIds = this._sessionMonsterUnits.get(levelId); if (!unitIds) return; const weaponUnitIds = membershipFromArray(mapValuesToArray(this._weapon1ToUnit)); for (const unitId of membershipToArray(unitIds)) { if (weaponUnitIds.has(unitId)) continue; this._store.units.delete(unitId); } this._sessionMonsterUnits.delete(levelId); if (this._levels.size === 0) this._monsterConfig = undefined; } private _sessionMonsterUnits = new Map>(); private _collectMonsterUnitIds(level: LevelDef): MembershipSet { const ids = createMembershipSet(); for (const wave of level.waves) { if (wave.ports?.length) { for (const port of wave.ports) { for (const seg of port.segments) { membershipAdd(ids, seg.unitDefId); } } continue; } for (const spawn of wave.spawns ?? []) { membershipAdd(ids, spawn.unitDefId); } } return ids; } getUnit(id: string): UnitDef | undefined { const unit = this._store.getUnit(id); if (unit) this._applyFormulaHp(id, unit); return unit; } getAbility(id: string) { return this._store.getAbility(id); } getBuff(id: string): BuffDef | undefined { return this._store.getBuff(id); } getLevel(id: string): LevelDef | undefined { return this._levels.get(id) ?? this._store.getLevel(id); } getWave(levelId: string, waveIndex: number): WaveDef | undefined { return this._store.getWave(levelId, waveIndex); } getUnitIdByWeapon1(weapon1Id: number): string | undefined { return this._weapon1ToUnit.get(weapon1Id); } registerAbility(id: string, def: AbilityDef): void { this._store.abilities.set(id, def); } registerBuff(id: string, def: BuffDef): void { this._store.buffs.set(id, def); } private loadWeapon1Table(): void { const list = gg.data.table.getTableList(TableNames.Hero) ?? []; const bundle = Weapon1TowerBuilder.buildAll(list); this._weapon1ToUnit = bundle.weapon1ToUnit; for (const [id, unit] of bundle.units) { this._store.units.set(id, unit); } } private ensureMonstersForLevel(level: LevelDef): void { for (const wave of level.waves) { if (wave.ports?.length) { for (const port of wave.ports) { for (const seg of port.segments) { this.ensureMonsterUnit(seg.unitDefId, seg.maxHp); } } continue; } for (const spawn of wave.spawns ?? []) { this.ensureMonsterUnit(spawn.unitDefId); } } } /** 机制怪等动态单位:确保 monster_{id} 已注册并可覆盖 maxHp */ ensureMonsterUnitById(monsterId: number, maxHpOverride?: number): string { const unitDefId = `monster_${monsterId}`; this.ensureMonsterUnit(unitDefId, maxHpOverride); return unitDefId; } /** 与波次相同 unitDefId(含 port/难度行)注册并可选覆盖 maxHp */ ensureMonsterUnitDef(unitDefId: string, maxHpOverride?: number): string { this.ensureMonsterUnit(unitDefId, maxHpOverride); return unitDefId; } /** * 出怪时按当前波次公式注册怪物(技能召唤与波次怪同一套)。 * @param monsterId Monster 表 id * @param port 出怪口 * @param difficultyRow Monster1 行(波次序号) */ ensureSpawnMonsterDef(monsterId: number, port: number, difficultyRow: number): string { const row = Math.max(1, Math.floor(Number(difficultyRow) || 1)); const unitDefId = makeMonsterUnitDefId(monsterId, port, row); const cfg = this._monsterConfig; if (!cfg) { console.log('[TableBattleConfigProvider] 出怪缺 monsterConfig', monsterId, port, row); this.ensureMonsterUnit(unitDefId); return unitDefId; } this.ensureMonsterUnit(unitDefId, calcChapterMonsterHp(monsterId, port, row, cfg)); return unitDefId; } private ensureMonsterUnit(unitDefId: string, maxHpOverride?: number): void { const parsed = parseMonsterUnitDefId(unitDefId); const m = parsed?.monsterId ?? parseLegacyMonsterId(unitDefId); if (m <= 0) return; const row = gg.data.table.getTableData(TableNames.Monster, m); if (!row) return; const maxHp = this._resolveSpawnHp(unitDefId, row, maxHpOverride); const existing = this._store.units.get(unitDefId); if (existing) { existing.components.health = { maxHp }; return; } const tags = [EntityTag.Enemy, `monster_${m}`]; // Monster.type: 1小怪 2BOSS 3召唤 if (row.type === 2) tags.push(EntityTag.Boss); else if (row.type === 3) tags.push(EntityTag.Elite); const prefabName = ChapterLevelBuilder.resolveMonsterPrefabFromRow(row); const atkRange = Math.max(0, Math.floor(Number(row.atkdistance) || 0)); if (!(atkRange > 0)) { console.log('[TableBattleConfigProvider] Monster.atkdistance 无效', m, row.atkdistance); } const unit: UnitDef = { id: unitDefId, name: prefabName ?? `monster_${m}`, team: 'enemy', components: { health: { maxHp }, combat: { attack: (row as ITableMonster & { attack?: number }).attack ?? 1, attackSpeed: (row as ITableMonster & { attack_speed?: number }).attack_speed ?? 1, range: atkRange, critRate: 0, critDamage: 1, }, movement: { speed: row.speed ?? 25 }, tags, }, presentation: { prefab: prefabName ?? undefined }, }; this._store.units.set(unitDefId, unit); this._sessionMonsterUnits.forEach((ids) => membershipAdd(ids, unitDefId)); } /** * 出怪取 UnitDef 时按公式刷新血量(不以开局缓存为准)。 * @param unitDefId 单位 id * @param unit 已注册定义 */ private _applyFormulaHp(unitDefId: string, unit: UnitDef): void { const parsed = parseMonsterUnitDefId(unitDefId); const cfg = this._monsterConfig; if (!parsed || !cfg) return; unit.components.health = { maxHp: calcChapterMonsterHp(parsed.monsterId, parsed.port, parsed.difficultyRow, cfg), }; } /** * 解析出怪血量:覆盖值 > 波次公式 > 表 hp。 * @param unitDefId 单位 id * @param row Monster 行 * @param maxHpOverride 显式覆盖 */ private _resolveSpawnHp(unitDefId: string, row: ITableMonster, maxHpOverride?: number): number { if (maxHpOverride != null) return finitePositiveHp(maxHpOverride, 1); const parsed = parseMonsterUnitDefId(unitDefId); const cfg = this._monsterConfig; if (parsed && cfg) { return calcChapterMonsterHp(parsed.monsterId, parsed.port, parsed.difficultyRow, cfg); } if (parsed && !cfg) { console.log('[TableBattleConfigProvider] 出怪缺 monsterConfig,回退 Monster.hp', unitDefId); } return finitePositiveHp(row.hp, 1); } }