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/TableBattleConfigProvider.ts

288 lines
11 KiB

3 weeks ago
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<number, string>();
private _levels = new Map<string, LevelDef>();
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);
}
this.ensureFallbackEnemy();
}
/** 每次开战刷新 Weapon1 单位映射 */
refreshWeaponUnits(): void {
const list = gg.data.table.getTableList<ITableHero>(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<string, MembershipSet<string>>();
private _collectMonsterUnitIds(level: LevelDef): MembershipSet<string> {
const ids = createMembershipSet<string>();
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<ITableHero>(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<ITableMonster>(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 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: 1,
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 as ITableMonster & { HP?: number }).HP ?? row.hp, 1);
}
private ensureFallbackEnemy(): void {
if (!this._store.units.has('monster_101')) {
this.ensureMonsterUnit('monster_101');
}
}
}