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

362 lines
16 KiB

3 weeks ago
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<Record<number, string>> = {
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<Record<string, string>> = {};
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;
/** 单怪出怪间隔默认秒数(常量表无对应行) */
const DEFAULT_MONSTER_SPAWN_INTERVAL_SEC = 1;
/** 对齐 Const 表 id=8(ConstNumType.WaveInterval),避免从 ConfigProjectData 拉环 */
const CONST_WAVE_INTERVAL_ID = 8;
3 weeks ago
/** 从 Level 表构建 MX LevelDef */
export class ChapterLevelBuilder {
static build(options: LevelDefBuildOptions): LevelDef | null {
resetMonsterHpDebugLog();
const levelRow = gg.data.table.getTableData<ITableLevel>(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 = Math.floor(Number(levelRow.startnumber));
if (!(startGold >= 0) || !Number.isFinite(startGold)) {
console.error('[ChapterLevelBuilder] Level.startnumber 无效', options.levelId, levelRow.startnumber);
return null;
}
3 weeks ago
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 };
}
/**
2 weeks ago
* 地图预制体名。读关卡表当前关卡 Level.map。
* @param levelId 当前关卡 id
3 weeks ago
*/
2 weeks ago
static resolveLevelMapPrefabName(levelId: number): string {
const level = gg.data.table.getTableData<ITableLevel>(TableNames.Level, levelId);
3 weeks ago
const raw = level?.map;
if (typeof raw === 'string') {
const name = raw.trim();
if (name) return name;
}
2 weeks ago
console.warn('[ChapterLevelBuilder] 关卡缺少 Level.map', levelId);
2 weeks ago
return '';
3 weeks ago
}
/** 从 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<ITableLevel>(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<string>();
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<ITableMonster>(TableNames.Monster, monsterId)
?? gg.data.project.getMonsterDataByID(monsterId);
if (!row) return null;
return this.resolveMonsterPrefabFromRow(row);
}
1 week ago
/** Monster 行 → battleGameMonster 预制体文件名(icon → name) */
3 weeks ago
static resolveMonsterPrefabFromRow(row: ITableMonster): string | null {
const icon = typeof row.icon === 'string' ? row.icon.trim() : '';
1 week ago
const raw = icon || row.name;
3 weeks ago
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<string>();
for (const id of this.collectLevelMonsterIds(levelId)) {
const row = gg.data.table.getTableData<ITableMonster>(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<string>(['怪_攻击buff', '怪_加速', '怪物加血', '血量上限', '冰冻']);
3 weeks ago
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<string>, 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<string>();
const baseAtkIds = createMembershipSet<number>();
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);
}
const heroSkillId = Math.floor(Number(w1.skill_id) || 0);
if (heroSkillId > 0) membershipAdd(baseAtkIds, heroSkillId);
3 weeks ago
}
const atkIds = membershipToArray(baseAtkIds);
for (let i = 0; i < atkIds.length; i++) {
const row = gg.data.table.getTableData<ITableSkill>(TableNames.Skill, atkIds[i]);
if (row) this._collectStatusFxFromSkillRow(row, fx);
}
return membershipToArray(fx);
}
private static _collectStatusFxFromAtkHookRaw(raw: string, out: MembershipSet<string>): 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<string>): 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<string>): 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, '怪_晕眩');
}
}
}