refactor(rogue): 重构肉鸽刷怪系统,新增动态难度与分批刷怪
1. 重构RogueConfig:替换旧强化系统为动态难度调节器,调整波次配置为20波通关,新增技能池与阵营适配 2. 优化刷怪逻辑:实现每波3批10秒间隔的分批刷怪,调整怪物上限为18只 3. 新增驻场技能支持:添加resolveFieldByLv处理等级化驻场技能,扩展FieldSkillHelper支持多阵营统计 4. 调整关卡判定:将最大波次从15改为20,新增英雄死亡计数与难度动态调节 5. 优化代码结构:移除废弃字段,统一技能注入方式,修复槽位复用逻辑
This commit is contained in:
@@ -15,13 +15,23 @@ import { EquipBoxComp } from "../map/EquipBoxComp";
|
||||
export class FieldSkillHelper {
|
||||
/** 获取指定驻场技能类型的总加成值(计算存活的友方英雄 + 场上的驻场技能卡) */
|
||||
public static getFieldSkillTotalValue(type: FieldSkillType): number {
|
||||
return FieldSkillHelper.getFieldSkillTotalValueForFac(type, FacSet.HERO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定阵营的驻场技能总加成值
|
||||
* @param type 驻场技能类型
|
||||
* @param fac 阵营(FacSet.HERO / FacSet.MON),默认 HERO
|
||||
* @returns 总加成值
|
||||
*/
|
||||
public static getFieldSkillTotalValueForFac(type: FieldSkillType, fac: number = FacSet.HERO): number {
|
||||
let total = 0;
|
||||
|
||||
// 1. 统计英雄带来的驻场技能加成
|
||||
// 1. 统计英雄/怪物带来的驻场技能加成
|
||||
// 读 model.runtime_field(已按当前等级 resolve 的 uuid 列表)
|
||||
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
|
||||
const model = entity.get(HeroAttrsComp);
|
||||
if (!model || model.is_dead || model.fac !== FacSet.HERO) return;
|
||||
if (!model || model.is_dead || model.fac !== fac) return;
|
||||
const fields = model.runtime_field;
|
||||
if (fields) {
|
||||
for (const skillUuid of fields) {
|
||||
@@ -33,31 +43,33 @@ export class FieldSkillHelper {
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 统计技能盒子(技能卡)带来的驻场技能加成
|
||||
ecs.query(ecs.allOf(SkillBoxComp)).forEach((entity: ecs.Entity) => {
|
||||
const skillBox = entity.get(SkillBoxComp);
|
||||
if (!skillBox || !skillBox.field || skillBox.field.length === 0) return;
|
||||
// 2. 统计技能盒子(技能卡)带来的驻场技能加成(仅英雄阵营)
|
||||
if (fac === FacSet.HERO) {
|
||||
ecs.query(ecs.allOf(SkillBoxComp)).forEach((entity: ecs.Entity) => {
|
||||
const skillBox = entity.get(SkillBoxComp);
|
||||
if (!skillBox || !skillBox.field || skillBox.field.length === 0) return;
|
||||
|
||||
for (const skillUuid of skillBox.field) {
|
||||
const skillConfig = FieldSkillSet[skillUuid];
|
||||
if (skillConfig && skillConfig.type === type) {
|
||||
total += skillConfig.value;
|
||||
for (const skillUuid of skillBox.field) {
|
||||
const skillConfig = FieldSkillSet[skillUuid];
|
||||
if (skillConfig && skillConfig.type === type) {
|
||||
total += skillConfig.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 3. 统计装备盒(装备卡)带来的驻场技能加成
|
||||
ecs.query(ecs.allOf(EquipBoxComp)).forEach((entity: ecs.Entity) => {
|
||||
const equipBox = entity.get(EquipBoxComp);
|
||||
if (!equipBox || !equipBox.field || equipBox.field.length === 0) return;
|
||||
// 3. 统计装备盒(装备卡)带来的驻场技能加成(仅英雄阵营)
|
||||
ecs.query(ecs.allOf(EquipBoxComp)).forEach((entity: ecs.Entity) => {
|
||||
const equipBox = entity.get(EquipBoxComp);
|
||||
if (!equipBox || !equipBox.field || equipBox.field.length === 0) return;
|
||||
|
||||
for (const skillUuid of equipBox.field) {
|
||||
const skillConfig = FieldSkillSet[skillUuid];
|
||||
if (skillConfig && skillConfig.type === type) {
|
||||
total += skillConfig.value;
|
||||
for (const skillUuid of equipBox.field) {
|
||||
const skillConfig = FieldSkillSet[skillUuid];
|
||||
if (skillConfig && skillConfig.type === type) {
|
||||
total += skillConfig.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
@@ -225,6 +225,8 @@ export class Monster extends ecs.Entity {
|
||||
if (testSkills.dead !== undefined) model.dead = testSkills.dead;
|
||||
if (testSkills.fstart !== undefined) model.fstart = testSkills.fstart;
|
||||
if (testSkills.fend !== undefined) model.fend = testSkills.fend;
|
||||
if (testSkills.call !== undefined) model.call = testSkills.call;
|
||||
if (testSkills.revive !== undefined) model.revive = testSkills.revive;
|
||||
}
|
||||
|
||||
// 按怪物等级 resolve 触发技能/复活到运行时缓存(怪物无 lv 成长,恒用 mon_lv)
|
||||
|
||||
@@ -48,7 +48,7 @@ import { Tooltip } from "../skill/Tooltip";
|
||||
import { Timer } from "db://oops-framework/core/common/timer/Timer";
|
||||
import { FieldSkillType } from "../common/config/SkillSet";
|
||||
import { FieldSkillHelper } from "../hero/FieldSkillHelper";
|
||||
import { spawningEngine } from "./RogueConfig";
|
||||
import { spawningEngine, MAX_WAVE, DynamicTuner } from "./RogueConfig";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/** 任务(关卡)生命周期阶段 */
|
||||
@@ -524,20 +524,28 @@ export class MissionComp extends CCComp {
|
||||
|
||||
let allAlive = true;
|
||||
let hasHero = false;
|
||||
let heroDeathCount = 0;
|
||||
ecs.query(this.heroAttrsMatcher).forEach(entity => {
|
||||
const attrs = entity.get(HeroAttrsComp);
|
||||
if (attrs && attrs.fac === FacSet.HERO) {
|
||||
hasHero = true;
|
||||
if (attrs.is_dead) allAlive = false;
|
||||
if (attrs.is_dead) {
|
||||
allAlive = false;
|
||||
heroDeathCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 【动态难度调节】根据本波战况自动放水 / 加压
|
||||
DynamicTuner.adjust(this.clearTime, heroDeathCount);
|
||||
mLogger.log(this.debugMode, 'MissionComp', `[DynamicTuner] wave=${this.currentWave} clearTime=${this.clearTime.toFixed(1)}s deaths=${heroDeathCount} factor=${DynamicTuner.factor.toFixed(2)}`);
|
||||
// 【评分系统 - 战绩分】记录全员存活的胜利回合数(额外加分)
|
||||
if (hasHero && allAlive) {
|
||||
smc.vmdata.scores.wave_all_alive_count++;
|
||||
}
|
||||
|
||||
// 【评分系统 - 战绩分】判断是否通过最后一关(第15回合)
|
||||
if (this.currentWave === 15) {
|
||||
// 【评分系统 - 战绩分】判断是否通过最后一关(第20回合)
|
||||
if (this.currentWave === MAX_WAVE) {
|
||||
smc.vmdata.scores.passed_wave_20 = true;
|
||||
}
|
||||
}
|
||||
@@ -889,8 +897,8 @@ export class MissionComp extends CCComp {
|
||||
// 怪物全灭检测:如果战斗阶段场上没有任何活着的怪物,且待刷新的怪物队列也为空,直接结束战斗进入下一波的准备阶段
|
||||
const pendingCount = smc.vmdata.mission_data.pending_mon_num || 0;
|
||||
if (monsterCount === 0 && pendingCount === 0 && smc.mission.play && !smc.mission.pause && this.currentPhase === MissionPhase.Battle) {
|
||||
if (this.currentWave >= 15) {
|
||||
// 15 波通关
|
||||
if (this.currentWave >= MAX_WAVE) {
|
||||
// 20 波通关
|
||||
this.open_Victory(null, false);
|
||||
} else {
|
||||
oops.message.dispatchEvent("TimeUpAdvanceWave");
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*
|
||||
* 职责:
|
||||
* 1. 管理每一波怪物的生成计划:根据 RogueConfig 生成怪物。
|
||||
* 2. 自动推进波次:在准备阶段结束时(PhasePrepareEnd)把怪物转入逐个刷出队列。
|
||||
* 2. 自动推进波次:在准备阶段结束时(PhasePrepareEnd)启动分批释放。
|
||||
* 3. 分批刷怪:每波固定 3 批,每 10 秒释放一批,批内按 MON_SPAWN_INTERVAL 逐个刷出。
|
||||
*
|
||||
* 关键设计:
|
||||
* - 所有怪物统一从右侧 X=400 出生点逐个刷出(MON_SPAWN_INTERVAL 节奏控制)。
|
||||
@@ -21,7 +22,7 @@ import { Monster } from "../hero/Mon";
|
||||
import { smc } from "../common/SingletonModuleComp";
|
||||
import { GameEvent } from "../common/config/GameEvent";
|
||||
import { BoxSet, FacSet } from "../common/config/GameSet";
|
||||
import { spawningEngine, GeneratedMonster, TestModeConfig } from "./RogueConfig";
|
||||
import { spawningEngine, GeneratedMonster, TestModeConfig, MAX_MONSTERS, BATCH_COUNT, BATCH_INTERVAL } from "./RogueConfig";
|
||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||
import { MonMoveComp } from "../hero/MonMoveComp";
|
||||
|
||||
@@ -32,8 +33,6 @@ const { ccclass, property } = _decorator;
|
||||
export class MissionMonCompComp extends CCComp {
|
||||
// ======================== 常量 ========================
|
||||
|
||||
/** 怪物最多 12 个 */
|
||||
private static readonly MAX_MONSTERS = 12;
|
||||
/** 怪物出生掉落高度 */
|
||||
private static readonly MON_DROP_HEIGHT = 0;
|
||||
/** 怪物统一从右侧 X=400 出生点逐个刷出,随后向左推进 */
|
||||
@@ -42,9 +41,10 @@ export class MissionMonCompComp extends CCComp {
|
||||
private static readonly MON_SPAWN_INTERVAL = 0.3;
|
||||
|
||||
/**
|
||||
* 12 个怪物占位点:所有槽位统一以右侧出生点 (X=400) 作为坐标,
|
||||
* 怪物占位点:所有槽位统一以右侧出生点 (X=400) 作为坐标,
|
||||
* 实际阵型由 MonMoveComp 在战斗中向左推进时自然拉开。
|
||||
* 槽位索引仍保留 3 行 × 4 列结构,供 monGrid 寻路与 SCastSystem 索敌使用。
|
||||
* 超出 12 个槽位的怪物(放松波)按 spawnIndex % 12 复用槽位。
|
||||
*/
|
||||
public static readonly MON_POSITIONS: Vec3[] = [
|
||||
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 0: Col1-Top
|
||||
@@ -65,7 +65,7 @@ export class MissionMonCompComp extends CCComp {
|
||||
|
||||
@property({ tooltip: "是否启用调试日志" })
|
||||
private debugMode: boolean = false;
|
||||
|
||||
|
||||
// ======================== 运行时状态 ========================
|
||||
|
||||
/** 全局生成顺序计数器(用于渲染层级排序) */
|
||||
@@ -76,9 +76,15 @@ export class MissionMonCompComp extends CCComp {
|
||||
private waveTargetCount: number = 0;
|
||||
/** 当前波已生成的怪物数量 */
|
||||
private waveSpawnedCount: number = 0;
|
||||
/** 等待生成的怪物队列(波次总池) */
|
||||
private pendingMonsters: GeneratedMonster[] = [];
|
||||
/** 逐个刷怪队列:从 pendingMonsters 转入,按节奏在 update 中释放 */
|
||||
/** 等待生成的怪物队列(按批次分组,batch 0~2) */
|
||||
private pendingBatches: GeneratedMonster[][] = [[], [], []];
|
||||
/** 当前正在释放的批次索引 */
|
||||
private currentBatch: number = 0;
|
||||
/** 批次释放计时器(秒) */
|
||||
private batchTimer: number = 0;
|
||||
/** 是否正在分批释放中 */
|
||||
private isReleasing: boolean = false;
|
||||
/** 逐个刷怪队列:从当前批次转入,按节奏在 update 中释放 */
|
||||
private spawnQueue: GeneratedMonster[] = [];
|
||||
/** 逐个刷怪累计计时(秒) */
|
||||
private spawnTimer: number = 0;
|
||||
@@ -92,8 +98,21 @@ export class MissionMonCompComp extends CCComp {
|
||||
}
|
||||
|
||||
protected update(dt: number): void {
|
||||
// pending_mon_num 同时统计未刷出的总池与正在释放的队列,确保 UI 与全灭判定一致
|
||||
smc.vmdata.mission_data.pending_mon_num = this.pendingMonsters.length + this.spawnQueue.length;
|
||||
// 统计待刷出的怪物总数(未释放的批次 + 正在释放的队列)
|
||||
let pendingCount = this.spawnQueue.length;
|
||||
for (let i = this.currentBatch; i < BATCH_COUNT; i++) {
|
||||
pendingCount += this.pendingBatches[i].length;
|
||||
}
|
||||
smc.vmdata.mission_data.pending_mon_num = pendingCount;
|
||||
|
||||
// 分批释放:按 BATCH_INTERVAL 节奏推进批次
|
||||
if (this.isReleasing) {
|
||||
this.batchTimer += dt;
|
||||
if (this.batchTimer >= BATCH_INTERVAL && this.currentBatch < BATCH_COUNT - 1) {
|
||||
this.batchTimer = 0;
|
||||
this.advanceBatch();
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个刷怪:按 MON_SPAWN_INTERVAL 节奏从队列释放
|
||||
if (this.spawnQueue.length > 0) {
|
||||
@@ -101,7 +120,7 @@ export class MissionMonCompComp extends CCComp {
|
||||
if (this.spawnTimer >= MissionMonCompComp.MON_SPAWN_INTERVAL) {
|
||||
this.spawnTimer = 0;
|
||||
const monData = this.spawnQueue.shift()!;
|
||||
const targetPosIndex = this.waveSpawnedCount % MissionMonCompComp.MAX_MONSTERS;
|
||||
const targetPosIndex = this.waveSpawnedCount % MissionMonCompComp.MON_POSITIONS.length;
|
||||
this.addMonsterAtGrid(targetPosIndex, monData, this.currentWave);
|
||||
this.waveSpawnedCount++;
|
||||
}
|
||||
@@ -111,10 +130,16 @@ export class MissionMonCompComp extends CCComp {
|
||||
start() {}
|
||||
|
||||
private setupWaveData(monsters: GeneratedMonster[]) {
|
||||
this.pendingMonsters = monsters.slice(0, MissionMonCompComp.MAX_MONSTERS);
|
||||
smc.vmdata.mission_data.pending_mon_num = this.pendingMonsters.length;
|
||||
this.waveTargetCount = this.pendingMonsters.length;
|
||||
|
||||
// 按批次分组
|
||||
this.pendingBatches = [[], [], []];
|
||||
for (const m of monsters) {
|
||||
const batch = Math.min(m.batch, BATCH_COUNT - 1);
|
||||
this.pendingBatches[batch].push(m);
|
||||
}
|
||||
|
||||
this.waveTargetCount = monsters.length;
|
||||
smc.vmdata.mission_data.pending_mon_num = this.waveTargetCount;
|
||||
|
||||
let hasBoss = monsters.some(m => m.isBoss);
|
||||
|
||||
mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] 波次 ${this.currentWave} 生成怪物总数: ${this.waveTargetCount}`);
|
||||
@@ -136,10 +161,13 @@ export class MissionMonCompComp extends CCComp {
|
||||
this.currentWave = 1;
|
||||
this.waveTargetCount = 0;
|
||||
this.waveSpawnedCount = 0;
|
||||
this.pendingMonsters = [];
|
||||
this.pendingBatches = [[], [], []];
|
||||
this.currentBatch = 0;
|
||||
this.batchTimer = 0;
|
||||
this.isReleasing = false;
|
||||
this.spawnQueue = [];
|
||||
this.spawnTimer = 0;
|
||||
|
||||
|
||||
// 预生成第一波数据以获取数量和 Boss 信息
|
||||
const monsters = spawningEngine.generateWave(this.currentWave);
|
||||
this.setupWaveData(monsters);
|
||||
@@ -159,7 +187,7 @@ export class MissionMonCompComp extends CCComp {
|
||||
private onTimeUpAdvanceWave() {
|
||||
this.currentWave += 1;
|
||||
smc.vmdata.mission_data.level = this.currentWave;
|
||||
|
||||
|
||||
const monsters = spawningEngine.generateWave(this.currentWave);
|
||||
this.setupWaveData(monsters);
|
||||
}
|
||||
@@ -167,17 +195,53 @@ export class MissionMonCompComp extends CCComp {
|
||||
private onPhasePrepareEnd() {
|
||||
this.resetSlotSpawnData();
|
||||
|
||||
// 准备结束阶段:把本波待刷怪物转入逐个释放队列,
|
||||
// 实际生成时机由 update() 按 MON_SPAWN_INTERVAL 节奏触发,
|
||||
// 让怪物从 X=400 出生点排成纵队依次向左推进。
|
||||
if (this.pendingMonsters.length > 0) {
|
||||
const count = Math.min(this.pendingMonsters.length, MissionMonCompComp.MAX_MONSTERS);
|
||||
for (let i = 0; i < count; i++) {
|
||||
this.spawnQueue.push(this.pendingMonsters.shift()!);
|
||||
}
|
||||
// 让首个怪物在下一帧立即刷出,避免额外延迟
|
||||
this.spawnTimer = MissionMonCompComp.MON_SPAWN_INTERVAL;
|
||||
// 准备结束阶段:启动分批释放,
|
||||
// 第一批立即转入 spawnQueue,后续批次由 update 按 BATCH_INTERVAL 推进。
|
||||
this.startBatchRelease();
|
||||
}
|
||||
|
||||
// ======================== 分批释放 ========================
|
||||
|
||||
/** 启动分批释放:立即释放第一批,启动批次计时器 */
|
||||
private startBatchRelease() {
|
||||
this.currentBatch = 0;
|
||||
this.batchTimer = 0;
|
||||
this.isReleasing = true;
|
||||
this.releaseCurrentBatch();
|
||||
}
|
||||
|
||||
/** 推进到下一批次 */
|
||||
private advanceBatch() {
|
||||
if (this.currentBatch >= BATCH_COUNT - 1) {
|
||||
this.isReleasing = false;
|
||||
return;
|
||||
}
|
||||
this.currentBatch++;
|
||||
this.releaseCurrentBatch();
|
||||
}
|
||||
|
||||
/** 将当前批次的怪物转入 spawnQueue,等待逐个刷出 */
|
||||
private releaseCurrentBatch() {
|
||||
const batch = this.pendingBatches[this.currentBatch];
|
||||
if (batch.length === 0) {
|
||||
// 当前批次为空,尝试推进到下一批
|
||||
if (this.currentBatch < BATCH_COUNT - 1) {
|
||||
this.currentBatch++;
|
||||
this.releaseCurrentBatch();
|
||||
} else {
|
||||
this.isReleasing = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 将批次怪物转入逐个刷怪队列
|
||||
for (const m of batch) {
|
||||
this.spawnQueue.push(m);
|
||||
}
|
||||
batch.length = 0;
|
||||
|
||||
// 让首个怪物在下一帧立即刷出,避免额外延迟
|
||||
this.spawnTimer = MissionMonCompComp.MON_SPAWN_INTERVAL;
|
||||
}
|
||||
|
||||
// ======================== 槽位管理 ========================
|
||||
@@ -197,6 +261,9 @@ export class MissionMonCompComp extends CCComp {
|
||||
// 同步丢弃上一波未释放的队列,避免与新一波混合
|
||||
this.spawnQueue = [];
|
||||
this.spawnTimer = 0;
|
||||
this.isReleasing = false;
|
||||
this.currentBatch = 0;
|
||||
this.batchTimer = 0;
|
||||
}
|
||||
|
||||
// ======================== 怪物生成 ========================
|
||||
@@ -211,19 +278,20 @@ export class MissionMonCompComp extends CCComp {
|
||||
) {
|
||||
let mon = ecs.getEntity<Monster>(Monster);
|
||||
let scale = -1;
|
||||
|
||||
|
||||
const basePos = MissionMonCompComp.MON_POSITIONS[posIndex % MissionMonCompComp.MON_POSITIONS.length];
|
||||
const spawnX = basePos.x;
|
||||
const landingY = basePos.y + (monData.isBoss ? 6 : 0);
|
||||
const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0);
|
||||
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
|
||||
|
||||
if (monData.testSkills) {
|
||||
(mon as any)._testSkills = monData.testSkills;
|
||||
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
|
||||
|
||||
// 技能套装注入:通过 _testSkills 通道传递给 Mon.load()
|
||||
if (monData.skills) {
|
||||
(mon as any)._testSkills = monData.skills;
|
||||
}
|
||||
|
||||
mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monLv, posIndex);
|
||||
|
||||
|
||||
const move = mon.get(MonMoveComp);
|
||||
if (move) {
|
||||
move.spawnOrder = this.globalSpawnOrder;
|
||||
|
||||
@@ -1,68 +1,132 @@
|
||||
/**
|
||||
* @file RogueConfig.ts
|
||||
* @description 肉鸽刷怪系统(基于硬编码规则 + 模块化随机)
|
||||
* @description 肉鸽刷怪系统(基于英雄强度的动态难度 + 心流循环)
|
||||
*
|
||||
* 设计层次:
|
||||
* 1. MonsterElite - 个体强化(5 种)
|
||||
* 2. WaveEnchant - 波次级强化(环境修饰)
|
||||
* 3. SquadConfig - 小队模板(3-4 只怪的组合单元)
|
||||
* 4. WaveConfig - 每波硬编码(基础数 + 小队池 + 强化池)
|
||||
* 5. RogueSpawningEngine - 生成引擎(按规则组合上述配置)
|
||||
* 1. WaveType - 波型(普通 / 压力 / 放松),5 波一个心流循环
|
||||
* 2. WaveConfig - 每波硬编码(数量 + 小队池 + HP/AP 倍率 + 强度微调)
|
||||
* 3. DynamicTuner - 动态难度调节器(系统根据战况放水 / 加压)
|
||||
* 4. MonSkillSet - 怪物技能池(atking / atked / dead 等全触发类型)
|
||||
* 5. RogueSpawningEngine - 生成引擎(按英雄强度反推怪物强度)
|
||||
*
|
||||
* 详细设计见 docs/superpowers/specs/2026-07-05-rogue-config-refactor-design.md
|
||||
* 核心公式:
|
||||
* heroPower = Σ calcHeroPower(HeroInfo[uuid], lv) (场上存活英雄)
|
||||
* targetPower = heroPower × 波型系数 × wave.power_adjust × DynamicTuner.factor
|
||||
* scale = targetPower ÷ Σ 怪物基础强度
|
||||
* 每只怪: hp ×= scale, ap ×= scale
|
||||
*
|
||||
* 波次节奏:
|
||||
* - 最大 20 波,第 20 波通关
|
||||
* - 每波 30 秒,固定分 3 批,每 10 秒释放一批
|
||||
* - wave % 5 === 0 → 压力波(必带 Boss,强度高、数量少)
|
||||
* - wave % 5 === 1 → 放松波(数量 × 1.5,强度低,爽快清屏)
|
||||
*/
|
||||
|
||||
import { HeroInfo, MonType, MonTypeName } from "../common/config/heroSet";
|
||||
import { HeroInfo, MonType, MonTypeName, calcHeroPower, TriggerGrouped, LvReviveEntry, heroInfo } from "../common/config/heroSet";
|
||||
import { SkillOverrides } from "../common/config/SkillSet";
|
||||
import { FacSet } from "../common/config/GameSet";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||
|
||||
// ======================== 1. 枚举与基础类型 ========================
|
||||
// ======================== 1. 波型与节奏常量 ========================
|
||||
|
||||
/**
|
||||
* 个体强化类型(精选 5 种)
|
||||
* 设计权衡:HeroAttrsComp 暂无 atk_cd / regen 字段,因此 Swift/Regen 简化为数值差异化。
|
||||
* 待 HeroAttrsComp 升级后可启用 cd_mul / regen 扩展点。
|
||||
*/
|
||||
export enum MonsterElite {
|
||||
Elite = 0, // 精英:全面强化
|
||||
Berserk = 1, // 狂暴:高攻低血
|
||||
Shield = 2, // 护盾:坦克
|
||||
Regen = 3, // 再生:高血偏弱攻
|
||||
Swift = 4, // 疾风:平衡偏输出
|
||||
/** 波型枚举 */
|
||||
export enum WaveType {
|
||||
Normal = 0, // 普通波
|
||||
Pressure = 1, // 压力波(wave % 5 === 0,必带 Boss)
|
||||
Relax = 2, // 放松波(wave % 5 === 1,量大强度低)
|
||||
}
|
||||
|
||||
/** 个体强化效果定义 */
|
||||
export const MonsterEliteSet: Record<MonsterElite, {
|
||||
name: string;
|
||||
hp_mul: number;
|
||||
ap_mul: number;
|
||||
}> = {
|
||||
[MonsterElite.Elite]: { name: "精英", hp_mul: 1.5, ap_mul: 1.30 },
|
||||
[MonsterElite.Berserk]: { name: "狂暴", hp_mul: 0.9, ap_mul: 1.60 },
|
||||
[MonsterElite.Shield]: { name: "护盾", hp_mul: 1.8, ap_mul: 0.80 },
|
||||
[MonsterElite.Regen]: { name: "再生", hp_mul: 1.3, ap_mul: 0.95 },
|
||||
[MonsterElite.Swift]: { name: "疾风", hp_mul: 1.0, ap_mul: 1.15 },
|
||||
/** 波型名称 */
|
||||
export const WaveTypeName: Record<WaveType, string> = {
|
||||
[WaveType.Normal]: "普通",
|
||||
[WaveType.Pressure]: "压力",
|
||||
[WaveType.Relax]: "放松",
|
||||
};
|
||||
|
||||
/** 波型强度系数(硬编码) */
|
||||
export const WAVE_TYPE_POWER_RATIO: Record<WaveType, number> = {
|
||||
[WaveType.Normal]: 0.9, // 普通波:标准强度
|
||||
[WaveType.Pressure]: 1.2, // 压力波:强度高、数量少
|
||||
[WaveType.Relax]: 0.6, // 放松波:量大、强度低
|
||||
};
|
||||
|
||||
/** 放松波数量倍率(相对普通波) */
|
||||
export const RELAX_COUNT_MUL = 1.5;
|
||||
|
||||
/** 最大波次(第 20 波通关) */
|
||||
export const MAX_WAVE = 20;
|
||||
|
||||
/** 每波时长(秒) */
|
||||
export const WAVE_DURATION = 30;
|
||||
|
||||
/** 每波分批次数 */
|
||||
export const BATCH_COUNT = 3;
|
||||
|
||||
/** 每批间隔(秒):30 秒 / 3 批 = 10 秒 */
|
||||
export const BATCH_INTERVAL = WAVE_DURATION / BATCH_COUNT;
|
||||
|
||||
/** 每波怪物硬上限(放松波 12 × 1.5 = 18) */
|
||||
export const MAX_MONSTERS = 18;
|
||||
|
||||
/**
|
||||
* 旧版词缀类型枚举(已废弃)
|
||||
* @deprecated 已被 MonsterElite 取代,仅保留导出避免破坏外部引用。
|
||||
* 新代码请使用 MonsterElite。
|
||||
* 获取指定波次的波型
|
||||
* @param wave 波次(1 起)
|
||||
* @returns WaveType
|
||||
*/
|
||||
export enum AffixType {
|
||||
Elite = 0,
|
||||
Berserk = 1,
|
||||
Shield = 2,
|
||||
Regen = 3,
|
||||
Swift = 4,
|
||||
Giant = 5,
|
||||
Chain = 6,
|
||||
SummonerA = 7,
|
||||
CritRes = 8,
|
||||
FreezeRes = 9,
|
||||
KnockbackRes = 10,
|
||||
export function getWaveType(wave: number): WaveType {
|
||||
if (wave % 5 === 0) return WaveType.Pressure;
|
||||
if (wave % 5 === 1) return WaveType.Relax;
|
||||
return WaveType.Normal;
|
||||
}
|
||||
|
||||
// ======================== 2. 怪物 UUID 池 ========================
|
||||
// ======================== 2. 动态难度调节器 ========================
|
||||
|
||||
/**
|
||||
* 动态难度调节器(系统控制放水 / 加压)
|
||||
* 与硬编码系数并存,用于根据战况实时微调难度。
|
||||
*
|
||||
* 用法示例(MissionComp 每波结束时调用):
|
||||
* DynamicTuner.adjust(clearTime, heroDeathCount);
|
||||
*
|
||||
* 调节规则(内部硬编码):
|
||||
* - 清场时间 < 20s 且无英雄死亡 → factor += 0.05(加压)
|
||||
* - 清场时间 > 28s 或有英雄死亡 → factor -= 0.05(放水)
|
||||
* - factor 范围钳制 [0.5, 2.0]
|
||||
*/
|
||||
export const DynamicTuner = {
|
||||
/** 当前难度系数(默认 1.0,>1 加压,<1 放水) */
|
||||
factor: 1.0,
|
||||
|
||||
/** 系数下限(最多放水到 50%) */
|
||||
MIN_FACTOR: 0.5,
|
||||
/** 系数上限(最多加压到 200%) */
|
||||
MAX_FACTOR: 2.0,
|
||||
/** 单次调节步长 */
|
||||
STEP: 0.05,
|
||||
|
||||
/**
|
||||
* 根据上一波战况自动调节难度
|
||||
* @param clearTime 清场耗时(秒)
|
||||
* @param heroDeathCount 英雄死亡数
|
||||
*/
|
||||
adjust(clearTime: number, heroDeathCount: number): void {
|
||||
if (heroDeathCount > 0 || clearTime > WAVE_DURATION * 0.95) {
|
||||
// 有英雄死亡或清场过慢 → 放水
|
||||
this.factor = Math.max(this.MIN_FACTOR, this.factor - this.STEP);
|
||||
} else if (clearTime < WAVE_DURATION * 0.65 && heroDeathCount === 0) {
|
||||
// 清场过快且无死亡 → 加压
|
||||
this.factor = Math.min(this.MAX_FACTOR, this.factor + this.STEP);
|
||||
}
|
||||
},
|
||||
|
||||
/** 重置调节器(每局开始时调用) */
|
||||
reset(): void {
|
||||
this.factor = 1.0;
|
||||
},
|
||||
};
|
||||
|
||||
// ======================== 3. 怪物 UUID 池 ========================
|
||||
|
||||
/** 按 MonType 分组的怪物 uuid 池,动态从 HeroInfo 提取,避免硬编码 */
|
||||
export const MonList: Record<number, number[]> = {};
|
||||
@@ -76,16 +140,12 @@ for (const key in HeroInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 2.5 怪物金币掉落配置 ========================
|
||||
// ======================== 3.5 怪物金币掉落配置 ========================
|
||||
|
||||
/**
|
||||
* 怪物金币掉落配置(按 MonType 分类)
|
||||
* - base: 普通怪物死亡时掉落的固定金币数
|
||||
* - boss: Boss 怪物死亡时掉落的固定金币数(仅对 Boss 类型生效)
|
||||
*
|
||||
* 设计说明:
|
||||
* 金币不再按波次固定发放,改为每只怪物死亡时掉落。
|
||||
* Boss 提供高额固定金币奖励,作为战斗收益的核心来源。
|
||||
*/
|
||||
export const MonsterGoldSet: Record<number, { base: number; boss: number }> = {
|
||||
[MonType.Melee]: { base: 1, boss: 0 },
|
||||
@@ -110,32 +170,6 @@ export function getMonsterGoldDrop(monType: number, isBoss: boolean): number {
|
||||
return Math.max(0, Math.floor(isBoss ? cfg.boss : cfg.base));
|
||||
}
|
||||
|
||||
// ======================== 3. 波次级强化库 ========================
|
||||
|
||||
/**
|
||||
* 波次级强化(环境修饰符)
|
||||
* 作用于整波怪物,与 MonsterElite(个体)解耦。
|
||||
*/
|
||||
export interface WaveEnchant {
|
||||
id: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
hp_mul?: number; // 全员 HP 乘子
|
||||
ap_mul?: number; // 全员 AP 乘子
|
||||
cd_mul?: number; // 全员攻击间隔乘子(后续扩展点)
|
||||
elite_rate_mul?: number; // 该波个体强化出现概率乘子
|
||||
weight: number; // 在强化池中的抽取权重
|
||||
}
|
||||
|
||||
/** 波次强化库(硬编码) */
|
||||
export const WaveEnchantLibrary: Record<string, WaveEnchant> = {
|
||||
frenzy: { id: "frenzy", name: "狂热浪潮", desc: "全员攻击力+25%", ap_mul: 1.25, weight: 10 },
|
||||
ironhide: { id: "ironhide", name: "铁皮大军", desc: "全员生命+40%", hp_mul: 1.40, weight: 10 },
|
||||
swift: { id: "swift", name: "疾风突袭", desc: "全员攻速+20%", cd_mul: 0.80, weight: 8 },
|
||||
nightmare: { id: "nightmare", name: "梦魇来袭", desc: "个体强化率×2,HP+15%", elite_rate_mul: 2.0, hp_mul: 1.15, weight: 5 },
|
||||
fortress: { id: "fortress", name: "钢铁堡垒", desc: "全员 HP+60% AP-15%", hp_mul: 1.60, ap_mul: 0.85, weight: 6 },
|
||||
};
|
||||
|
||||
// ======================== 4. 小队模板库 ========================
|
||||
|
||||
/** 小队内单种怪物的槽位定义 */
|
||||
@@ -162,90 +196,183 @@ export const SquadLibrary: Record<string, SquadConfig> = {
|
||||
summoner_cult: { id: "summoner_cult", name: "召唤教派组", weight: 4, slots: [{ type: MonType.Summoner, count: 1 }, { type: MonType.Long, count: 2 }] },
|
||||
};
|
||||
|
||||
// ======================== 5. 波次配置表 ========================
|
||||
// ======================== 5. 怪物技能池 ========================
|
||||
|
||||
/**
|
||||
* 怪物技能套装(一套完整的触发技能配置)
|
||||
* 覆盖触发时机:call / atking / atked / dead / fstart / fend / revive
|
||||
*/
|
||||
export interface MonSkillSet {
|
||||
id: string;
|
||||
name: string;
|
||||
/** 普攻技能覆盖(可选,不填则使用怪物默认普攻) */
|
||||
skill?: { s_uuid: number; cd?: number; overrides?: SkillOverrides };
|
||||
/** 召唤触发 */
|
||||
call?: TriggerGrouped;
|
||||
/** 攻击触发 */
|
||||
atking?: TriggerGrouped;
|
||||
/** 受击触发 */
|
||||
atked?: TriggerGrouped;
|
||||
/** 死亡触发 */
|
||||
dead?: TriggerGrouped;
|
||||
/** 战斗开始触发 */
|
||||
fstart?: TriggerGrouped;
|
||||
/** 战斗结束触发 */
|
||||
fend?: TriggerGrouped;
|
||||
/** 复活 */
|
||||
revive?: LvReviveEntry[];
|
||||
/** 在技能池中的抽取权重 */
|
||||
weight: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 怪物技能池(硬编码)
|
||||
* 压力波 / Boss 可配置专属技能,普通波随机挂载增加变数。
|
||||
*
|
||||
* 技能 uuid 引用 SkillSet 中的 6000~6500 段触发技能。
|
||||
*/
|
||||
export const MonSkillPool: Record<string, MonSkillSet> = {
|
||||
/** 狂暴:攻击触发自身攻击提升 */
|
||||
berserk: {
|
||||
id: "berserk", name: "狂暴", weight: 10,
|
||||
atking: {
|
||||
6401: [{ lv: 1, t_num: 5, overrides: { ap: 1 } }],
|
||||
},
|
||||
},
|
||||
/** 坚韧:受击获得护盾 */
|
||||
tough: {
|
||||
id: "tough", name: "坚韧", weight: 8,
|
||||
atked: {
|
||||
6301: [{ lv: 1, t_num: 3, overrides: { ap: 2 } }],
|
||||
},
|
||||
},
|
||||
/** 遗志:死亡时全队攻击提升 */
|
||||
legacy: {
|
||||
id: "legacy", name: "遗志", weight: 6,
|
||||
dead: {
|
||||
6401: [{ lv: 1, t_num: 1, overrides: { ap: 3 } }],
|
||||
},
|
||||
},
|
||||
/** 战吼:战斗开始时全队攻击提升 */
|
||||
warcry: {
|
||||
id: "warcry", name: "战吼", weight: 5,
|
||||
fstart: {
|
||||
6401: [{ lv: 1, t_num: 1, overrides: { ap: 2 } }],
|
||||
},
|
||||
},
|
||||
/** 吸血:攻击恢复生命 */
|
||||
leech: {
|
||||
id: "leech", name: "吸血", weight: 7,
|
||||
atking: {
|
||||
6302: [{ lv: 1, t_num: 4, overrides: { ap: 150 } }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/** Boss 专属技能池(压力波 Boss 随机挂载) */
|
||||
export const BossSkillPool: Record<string, MonSkillSet> = {
|
||||
/** 狂暴领主:攻击触发全队攻击提升 */
|
||||
boss_rage: {
|
||||
id: "boss_rage", name: "狂暴领主", weight: 10,
|
||||
atking: {
|
||||
6401: [{ lv: 1, t_num: 3, overrides: { ap: 3 } }],
|
||||
},
|
||||
dead: {
|
||||
6401: [{ lv: 1, t_num: 1, overrides: { ap: 5 } }],
|
||||
},
|
||||
},
|
||||
/** 铁壁领主:受击获得高额护盾 */
|
||||
boss_iron: {
|
||||
id: "boss_iron", name: "铁壁领主", weight: 8,
|
||||
atked: {
|
||||
6301: [{ lv: 1, t_num: 2, overrides: { ap: 5 } }],
|
||||
},
|
||||
},
|
||||
/** 毁灭领主:战斗开始时全队攻击大幅提升 */
|
||||
boss_doom: {
|
||||
id: "boss_doom", name: "毁灭领主", weight: 6,
|
||||
fstart: {
|
||||
6401: [{ lv: 1, t_num: 1, overrides: { ap: 8 } }],
|
||||
},
|
||||
atking: {
|
||||
6401: [{ lv: 1, t_num: 5, overrides: { ap: 2 } }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ======================== 6. 波次配置表 ========================
|
||||
|
||||
/** 单波次完整配置 */
|
||||
export interface WaveConfig {
|
||||
/** 基础怪物数(1~12) */
|
||||
/** 基础怪物总数(普通波 12 为上限,放松波自动 × 1.5) */
|
||||
base_count: number;
|
||||
/** 可选小队 id 池,引擎按权重抽取拼装到 base_count */
|
||||
squad_pool: string[];
|
||||
/** 可选波次强化 id 池,按权重抽 0~2 个(可空表示无强化) */
|
||||
enchant_pool?: string[];
|
||||
/** 是否 Boss 波(首位放 Boss) */
|
||||
/** HP 强化倍率(硬编码,逐波递进) */
|
||||
hp_mul: number;
|
||||
/** AP 强化倍率(硬编码,逐波递进) */
|
||||
ap_mul: number;
|
||||
/** 强度微调(放水 / 加压,默认 1.0) */
|
||||
power_adjust?: number;
|
||||
/** 是否 Boss 波(压力波必为 true) */
|
||||
boss_wave?: boolean;
|
||||
/** 该波个体强化基础概率(默认按波次递增) */
|
||||
elite_base_rate?: number;
|
||||
/** 普通怪技能池 id(可选,随机挂载) */
|
||||
skill_pool?: string[];
|
||||
/** Boss 技能池 id(Boss 波专用,随机挂载) */
|
||||
boss_skill_pool?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 波次配置表(硬编码 wave 1~30)
|
||||
* 节奏曲线:教学期(1-4) → 第一Boss(5) → 引入强化(6-9) → 第二Boss(10) →
|
||||
* 组合多样化(11-14) → 第三Boss(15) → 高压阶段(16-24) →
|
||||
* 第四Boss(25) → 终极阶段(26-29) → 最终Boss(30)
|
||||
* 波次配置表(硬编码 wave 1~20)
|
||||
*
|
||||
* 心流循环(5 波一循环):
|
||||
* wave % 5 === 0 → 压力波(必带 Boss,强度高、数量少)
|
||||
* wave % 5 === 1 → 放松波(数量 × 1.5,强度低)
|
||||
* 其余 → 普通波(标准强度)
|
||||
*
|
||||
* 强度递进:hp_mul / ap_mul 每 5 波一档,压力波额外提升。
|
||||
*/
|
||||
export const WaveConfigs: Record<number, WaveConfig> = {
|
||||
// ===== 教学期 =====
|
||||
1: { base_count: 3, squad_pool: ["melee_grunt"] },
|
||||
2: { base_count: 4, squad_pool: ["melee_grunt", "mixed_balanced"] },
|
||||
3: { base_count: 5, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"] },
|
||||
4: { base_count: 6, squad_pool: ["mixed_balanced", "long_line", "heavy_shield"] },
|
||||
// ===== 第一 Boss =====
|
||||
5: { base_count: 7, squad_pool: ["melee_grunt", "mixed_balanced", "heavy_shield"], boss_wave: true },
|
||||
// ===== 引入波次强化 =====
|
||||
6: { base_count: 7, squad_pool: ["assassin_squad", "long_line", "mixed_balanced"], enchant_pool: ["frenzy"], elite_base_rate: 0.10 },
|
||||
7: { base_count: 8, squad_pool: ["melee_grunt", "heavy_shield", "long_line"], enchant_pool: ["ironhide"], elite_base_rate: 0.12 },
|
||||
8: { base_count: 8, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], enchant_pool: ["frenzy", "swift"], elite_base_rate: 0.14 },
|
||||
9: { base_count: 9, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], enchant_pool: ["ironhide", "frenzy"], elite_base_rate: 0.16 },
|
||||
// ===== 第二 Boss =====
|
||||
10: { base_count: 10, squad_pool: ["melee_grunt", "assassin_squad", "mixed_balanced"], enchant_pool: ["frenzy", "ironhide"], boss_wave: true, elite_base_rate: 0.18 },
|
||||
// ===== 组合多样化 =====
|
||||
11: { base_count: 10, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"], enchant_pool: ["swift", "frenzy"], elite_base_rate: 0.18 },
|
||||
12: { base_count: 10, squad_pool: ["heavy_shield", "melee_grunt", "mixed_balanced", "long_line"], enchant_pool: ["ironhide"], elite_base_rate: 0.20 },
|
||||
13: { base_count: 11, squad_pool: ["assassin_squad", "summoner_cult", "mixed_balanced"], enchant_pool: ["frenzy", "nightmare"], elite_base_rate: 0.22 },
|
||||
14: { base_count: 11, squad_pool: ["heavy_shield", "long_line", "melee_grunt"], enchant_pool: ["ironhide", "swift"], elite_base_rate: 0.24 },
|
||||
// ===== 第三 Boss(中期高潮) =====
|
||||
15: { base_count: 11, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], enchant_pool: ["nightmare", "fortress"], boss_wave: true, elite_base_rate: 0.25 },
|
||||
// ===== 高压阶段 =====
|
||||
16: { base_count: 11, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], enchant_pool: ["frenzy", "swift"], elite_base_rate: 0.26 },
|
||||
17: { base_count: 11, squad_pool: ["melee_grunt", "summoner_cult", "mixed_balanced"], enchant_pool: ["ironhide", "frenzy"], elite_base_rate: 0.27 },
|
||||
18: { base_count: 12, squad_pool: ["assassin_squad", "long_line", "heavy_shield"], enchant_pool: ["nightmare", "swift"], elite_base_rate: 0.28 },
|
||||
19: { base_count: 12, squad_pool: ["mixed_balanced", "summoner_cult", "melee_grunt"], enchant_pool: ["fortress"], elite_base_rate: 0.28 },
|
||||
20: { base_count: 12, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], enchant_pool: ["frenzy", "ironhide"], boss_wave: true, elite_base_rate: 0.30 },
|
||||
21: { base_count: 12, squad_pool: ["melee_grunt", "assassin_squad", "summoner_cult"], enchant_pool: ["swift", "nightmare"], elite_base_rate: 0.30 },
|
||||
22: { base_count: 12, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], enchant_pool: ["ironhide", "fortress"], elite_base_rate: 0.30 },
|
||||
23: { base_count: 12, squad_pool: ["assassin_squad", "summoner_cult", "melee_grunt"], enchant_pool: ["frenzy", "nightmare"], elite_base_rate: 0.32 },
|
||||
24: { base_count: 12, squad_pool: ["mixed_balanced", "heavy_shield", "long_line"], enchant_pool: ["fortress", "swift"], elite_base_rate: 0.32 },
|
||||
// ===== 第四 Boss =====
|
||||
25: { base_count: 12, squad_pool: ["assassin_squad", "summoner_cult", "heavy_shield"], enchant_pool: ["nightmare", "fortress"], boss_wave: true, elite_base_rate: 0.35 },
|
||||
// ===== 终极阶段 =====
|
||||
26: { base_count: 12, squad_pool: ["melee_grunt", "assassin_squad", "long_line", "summoner_cult"], enchant_pool: ["frenzy", "nightmare"], elite_base_rate: 0.35 },
|
||||
27: { base_count: 12, squad_pool: ["heavy_shield", "mixed_balanced", "summoner_cult"], enchant_pool: ["ironhide", "fortress"], elite_base_rate: 0.38 },
|
||||
28: { base_count: 12, squad_pool: ["assassin_squad", "long_line", "melee_grunt"], enchant_pool: ["swift", "nightmare"], elite_base_rate: 0.40 },
|
||||
29: { base_count: 12, squad_pool: ["mixed_balanced", "heavy_shield", "summoner_cult"], enchant_pool: ["frenzy", "ironhide", "fortress"], elite_base_rate: 0.42 },
|
||||
// ===== 最终 Boss =====
|
||||
30: { base_count: 12, squad_pool: ["assassin_squad", "heavy_shield", "summoner_cult", "mixed_balanced"], enchant_pool: ["frenzy", "ironhide", "swift", "nightmare", "fortress"], boss_wave: true, elite_base_rate: 0.45 },
|
||||
// ===== 第一循环:教学期 =====
|
||||
1: { base_count: 6, squad_pool: ["melee_grunt"], hp_mul: 1.00, ap_mul: 1.00 },
|
||||
2: { base_count: 7, squad_pool: ["melee_grunt", "mixed_balanced"], hp_mul: 1.00, ap_mul: 1.00 },
|
||||
3: { base_count: 8, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.05, ap_mul: 1.00 },
|
||||
4: { base_count: 9, squad_pool: ["mixed_balanced", "long_line", "heavy_shield"], hp_mul: 1.10, ap_mul: 1.05 },
|
||||
// 压力波:第一 Boss
|
||||
5: { base_count: 7, squad_pool: ["melee_grunt", "mixed_balanced", "heavy_shield"], hp_mul: 1.15, ap_mul: 1.10, boss_wave: true, boss_skill_pool: ["boss_rage"] },
|
||||
|
||||
// ===== 第二循环:引入技能怪 =====
|
||||
// 放松波:量大好清
|
||||
6: { base_count: 10, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.10, ap_mul: 1.05 },
|
||||
7: { base_count: 10, squad_pool: ["melee_grunt", "heavy_shield", "long_line"], hp_mul: 1.15, ap_mul: 1.10, skill_pool: ["tough"] },
|
||||
8: { base_count: 11, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.20, ap_mul: 1.15, skill_pool: ["berserk"] },
|
||||
9: { base_count: 11, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["berserk", "tough"] },
|
||||
// 压力波:第二 Boss
|
||||
10: { base_count: 8, squad_pool: ["melee_grunt", "assassin_squad", "mixed_balanced"], hp_mul: 1.30, ap_mul: 1.25, boss_wave: true, boss_skill_pool: ["boss_rage", "boss_iron"] },
|
||||
|
||||
// ===== 第三循环:组合多样化 =====
|
||||
// 放松波
|
||||
11: { base_count: 11, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["leech"] },
|
||||
12: { base_count: 11, squad_pool: ["heavy_shield", "melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.30, ap_mul: 1.25, skill_pool: ["tough", "warcry"] },
|
||||
13: { base_count: 12, squad_pool: ["assassin_squad", "summoner_cult", "mixed_balanced"], hp_mul: 1.35, ap_mul: 1.30, skill_pool: ["berserk", "leech"] },
|
||||
14: { base_count: 12, squad_pool: ["heavy_shield", "long_line", "melee_grunt"], hp_mul: 1.40, ap_mul: 1.35, skill_pool: ["berserk", "tough", "legacy"] },
|
||||
// 压力波:第三 Boss(中期高潮)
|
||||
15: { base_count: 9, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.50, ap_mul: 1.40, boss_wave: true, boss_skill_pool: ["boss_rage", "boss_iron", "boss_doom"] },
|
||||
|
||||
// ===== 第四循环:终极阶段 =====
|
||||
// 放松波
|
||||
16: { base_count: 12, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], hp_mul: 1.45, ap_mul: 1.40, skill_pool: ["leech", "warcry"] },
|
||||
17: { base_count: 12, squad_pool: ["melee_grunt", "summoner_cult", "mixed_balanced"], hp_mul: 1.50, ap_mul: 1.45, skill_pool: ["berserk", "legacy"] },
|
||||
18: { base_count: 12, squad_pool: ["assassin_squad", "long_line", "heavy_shield"], hp_mul: 1.55, ap_mul: 1.50, skill_pool: ["berserk", "tough", "leech"] },
|
||||
19: { base_count: 12, squad_pool: ["mixed_balanced", "summoner_cult", "melee_grunt"], hp_mul: 1.60, ap_mul: 1.55, skill_pool: ["berserk", "tough", "legacy", "warcry"] },
|
||||
// 压力波:最终 Boss
|
||||
20: { base_count: 10, squad_pool: ["assassin_squad", "heavy_shield", "summoner_cult", "mixed_balanced"], hp_mul: 1.80, ap_mul: 1.70, boss_wave: true, boss_skill_pool: ["boss_doom", "boss_rage"] },
|
||||
};
|
||||
|
||||
// ======================== 6. 全局难度缩放 ========================
|
||||
|
||||
/**
|
||||
* 根据波次返回全局 HP/AP 乘子,每 5 波递进一档
|
||||
* @param wave 当前波次
|
||||
* @returns hp_mul / ap_mul
|
||||
*/
|
||||
export function getGlobalScale(wave: number): { hp_mul: number; ap_mul: number } {
|
||||
const step = Math.floor((wave - 1) / 5);
|
||||
return {
|
||||
hp_mul: 1 + 0.15 * step, // wave 1=1.0, 6=1.15, 11=1.30, 16=1.45, 21=1.60, 26=1.75
|
||||
ap_mul: 1 + 0.08 * step, // wave 1=1.0, 6=1.08, 11=1.16, 16=1.24, 21=1.32, 26=1.40
|
||||
};
|
||||
}
|
||||
|
||||
// ======================== 7. 配置校验 ========================
|
||||
|
||||
/**
|
||||
* 校验 WaveConfigs / SquadLibrary / WaveEnchantLibrary 引用一致性
|
||||
* 校验 WaveConfigs / SquadLibrary / MonSkillPool 引用一致性
|
||||
* 建议在游戏启动时调用一次,便于发现配置错误
|
||||
* @returns 错误信息数组,空数组表示校验通过
|
||||
*/
|
||||
@@ -260,15 +387,22 @@ export function validateRogueConfig(): string[] {
|
||||
errors.push(`Wave ${wave} 引用了不存在的小队: ${squadId}`);
|
||||
}
|
||||
}
|
||||
if (cfg.enchant_pool) {
|
||||
for (const enchId of cfg.enchant_pool) {
|
||||
if (!WaveEnchantLibrary[enchId]) {
|
||||
errors.push(`Wave ${wave} 引用了不存在的强化: ${enchId}`);
|
||||
if (cfg.skill_pool) {
|
||||
for (const skillId of cfg.skill_pool) {
|
||||
if (!MonSkillPool[skillId]) {
|
||||
errors.push(`Wave ${wave} 引用了不存在的技能: ${skillId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cfg.base_count < 1 || cfg.base_count > 12) {
|
||||
errors.push(`Wave ${wave} base_count=${cfg.base_count} 越界 (1~12)`);
|
||||
if (cfg.boss_skill_pool) {
|
||||
for (const skillId of cfg.boss_skill_pool) {
|
||||
if (!BossSkillPool[skillId]) {
|
||||
errors.push(`Wave ${wave} 引用了不存在的 Boss 技能: ${skillId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cfg.base_count < 1 || cfg.base_count > MAX_MONSTERS) {
|
||||
errors.push(`Wave ${wave} base_count=${cfg.base_count} 越界 (1~${MAX_MONSTERS})`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,23 +429,22 @@ export interface GeneratedMonster {
|
||||
ap: number;
|
||||
isBoss: boolean;
|
||||
spawnIndex: number;
|
||||
/** 本怪所属批次(0~2,由 MissionMonComp 按 BATCH_INTERVAL 释放) */
|
||||
batch: number;
|
||||
|
||||
/** @deprecated 已被 elite 取代,始终返回 [],仅向后兼容 */
|
||||
affixes: AffixType[];
|
||||
|
||||
/** 个体强化(无则 undefined) */
|
||||
elite?: MonsterElite;
|
||||
/** 该波触发的波次强化 id 列表 */
|
||||
wave_enchants: string[];
|
||||
|
||||
/** 测试模式专用技能覆盖(按 s_uuid 分组 + lv 数组,与 heroInfo 结构一致) */
|
||||
testSkills?: {
|
||||
skill?: { s_uuid: number; cd?: number; overrides?: any };
|
||||
atking?: Record<number, { lv: number; t_num: number; overrides?: any }[]>;
|
||||
atked?: Record<number, { lv: number; t_num: number; overrides?: any }[]>;
|
||||
dead?: Record<number, { lv: number; t_num: number; overrides?: any }[]>;
|
||||
fstart?: Record<number, { lv: number; t_num: number; overrides?: any }[]>;
|
||||
fend?: Record<number, { lv: number; t_num: number; overrides?: any }[]>;
|
||||
/**
|
||||
* 怪物技能套装(覆盖全部触发时机)
|
||||
* 注入方式与 _testSkills 相同,在 Mon.load() 中写入 HeroAttrsComp
|
||||
*/
|
||||
skills?: {
|
||||
skill?: { s_uuid: number; cd?: number; overrides?: SkillOverrides };
|
||||
call?: TriggerGrouped;
|
||||
atking?: TriggerGrouped;
|
||||
atked?: TriggerGrouped;
|
||||
dead?: TriggerGrouped;
|
||||
fstart?: TriggerGrouped;
|
||||
fend?: TriggerGrouped;
|
||||
revive?: LvReviveEntry[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -324,35 +457,28 @@ export const TestModeConfig = {
|
||||
growthRatePerWave: 0.2,
|
||||
monType: MonType.Melee,
|
||||
monUuid: 6001,
|
||||
/** @deprecated 已被 testElite 取代 */
|
||||
affixes: [] as AffixType[],
|
||||
spawnCount: 1,
|
||||
|
||||
/** 测试个体强化 */
|
||||
testElite: undefined as MonsterElite | undefined,
|
||||
/** 测试波次强化 id */
|
||||
testWaveEnchant: undefined as string | undefined,
|
||||
|
||||
skill: undefined as { s_uuid: number; cd?: number; overrides?: any } | undefined,
|
||||
atking: undefined as Record<number, { lv: number; t_num: number; overrides?: any }[]> | undefined,
|
||||
atked: undefined as Record<number, { lv: number; t_num: number; overrides?: any }[]> | undefined,
|
||||
dead: undefined as Record<number, { lv: number; t_num: number; overrides?: any }[]> | undefined,
|
||||
fstart: undefined as Record<number, { lv: number; t_num: number; overrides?: any }[]> | undefined,
|
||||
fend: undefined as Record<number, { lv: number; t_num: number; overrides?: any }[]> | undefined,
|
||||
skill: undefined as { s_uuid: number; cd?: number; overrides?: SkillOverrides } | undefined,
|
||||
atking: undefined as TriggerGrouped | undefined,
|
||||
atked: undefined as TriggerGrouped | undefined,
|
||||
dead: undefined as TriggerGrouped | undefined,
|
||||
fstart: undefined as TriggerGrouped | undefined,
|
||||
fend: undefined as TriggerGrouped | undefined,
|
||||
};
|
||||
|
||||
// ======================== 10. 生成引擎 ========================
|
||||
|
||||
/**
|
||||
* 肉鸽刷怪生成引擎
|
||||
* 按硬编码 WaveConfig 规则,组合小队模板与双层强化系统生成怪物列表
|
||||
* 按英雄强度反推怪物强度,结合波型系数与动态调节器生成怪物列表
|
||||
*/
|
||||
export class RogueSpawningEngine {
|
||||
|
||||
/**
|
||||
* 生成指定波次的怪物列表
|
||||
* @param waveNumber 波次(1 起,>30 时复用 wave 30 配置)
|
||||
* @returns 怪物列表,长度 ≤ 12
|
||||
* @param waveNumber 波次(1 起,>MAX_WAVE 时复用 wave MAX_WAVE 配置)
|
||||
* @returns 怪物列表,长度 ≤ MAX_MONSTERS
|
||||
*/
|
||||
generateWave(waveNumber: number): GeneratedMonster[] {
|
||||
if (waveNumber < 1) return [];
|
||||
@@ -362,97 +488,120 @@ export class RogueSpawningEngine {
|
||||
return this.generateTestWave(waveNumber);
|
||||
}
|
||||
|
||||
// 1. 取硬编码 WaveConfig(>30 时复用 wave 30 配置)
|
||||
const cfg = WaveConfigs[Math.min(waveNumber, 30)];
|
||||
const wave = Math.min(waveNumber, MAX_WAVE);
|
||||
const cfg = WaveConfigs[wave];
|
||||
const waveType = getWaveType(wave);
|
||||
const typeRatio = WAVE_TYPE_POWER_RATIO[waveType];
|
||||
|
||||
// 2. 抽取波次级强化(按权重从 enchant_pool 抽 0~2 个)
|
||||
const enchants = this.pickEnchants(cfg.enchant_pool);
|
||||
// 1. 计算目标强度
|
||||
const heroPower = this.getCurrentHeroPower();
|
||||
const powerAdjust = cfg.power_adjust ?? 1.0;
|
||||
const targetPower = heroPower * typeRatio * powerAdjust * DynamicTuner.factor;
|
||||
|
||||
// 3. 计算波次最终属性乘子
|
||||
const globalScale = getGlobalScale(waveNumber);
|
||||
const enchantHpMul = enchants.reduce((m, e) => m * (e.hp_mul ?? 1), 1);
|
||||
const enchantApMul = enchants.reduce((m, e) => m * (e.ap_mul ?? 1), 1);
|
||||
const eliteRateMul = enchants.reduce((m, e) => m * (e.elite_rate_mul ?? 1), 1);
|
||||
// 2. 确定怪物总数(放松波 × 1.5)
|
||||
let totalCount = cfg.base_count;
|
||||
if (waveType === WaveType.Relax) {
|
||||
totalCount = Math.round(totalCount * RELAX_COUNT_MUL);
|
||||
}
|
||||
totalCount = Math.min(totalCount, MAX_MONSTERS);
|
||||
|
||||
// 4. Boss 位(Boss 波首位占 1 个)
|
||||
// 3. Boss 位(压力波必带 Boss,占 1 个名额)
|
||||
const monsters: GeneratedMonster[] = [];
|
||||
let remaining = cfg.base_count;
|
||||
let remaining = totalCount;
|
||||
if (cfg.boss_wave) {
|
||||
monsters.push(this.makeBoss(waveNumber));
|
||||
monsters.push(this.makeBoss(wave, cfg));
|
||||
remaining -= 1;
|
||||
}
|
||||
|
||||
// 5. 小队拼装:按权重从 squad_pool 抽小队,累计到 remaining
|
||||
const squadMonsters = this.assembleSquads(cfg.squad_pool, remaining, waveNumber);
|
||||
// 4. 小队拼装:按权重从 squad_pool 抽小队,累计到 remaining
|
||||
const squadMonsters = this.assembleSquads(cfg.squad_pool, remaining, wave);
|
||||
monsters.push(...squadMonsters);
|
||||
|
||||
// 6. 应用全局 & 波次 Enchant 乘子
|
||||
const waveEnchantIds = enchants.map(e => e.id);
|
||||
// 5. 应用硬编码 HP/AP 倍率
|
||||
for (const m of monsters) {
|
||||
m.hp = Math.max(1, Math.round(m.hp * globalScale.hp_mul * enchantHpMul));
|
||||
m.ap = Math.max(1, Math.round(m.ap * globalScale.ap_mul * enchantApMul));
|
||||
m.wave_enchants = waveEnchantIds.slice();
|
||||
m.hp = Math.max(1, Math.round(m.hp * cfg.hp_mul));
|
||||
m.ap = Math.max(1, Math.round(m.ap * cfg.ap_mul));
|
||||
}
|
||||
|
||||
// 7. 应用个体 Elite(非 Boss 怪,每只最多 1 个)
|
||||
const eliteBaseRate = cfg.elite_base_rate ?? Math.min(0.05 + waveNumber * 0.01, 0.30);
|
||||
for (const m of monsters) {
|
||||
if (!m.isBoss && Math.random() < eliteBaseRate * eliteRateMul) {
|
||||
const elite = this.pickElite();
|
||||
this.applyElite(m, elite);
|
||||
// 6. 按英雄强度反推缩放系数
|
||||
const totalBasePower = monsters.reduce((sum, m) => {
|
||||
const info = HeroInfo[m.uuid];
|
||||
return sum + (info ? calcHeroPower(info, 1) : m.hp + m.ap);
|
||||
}, 0);
|
||||
|
||||
if (totalBasePower > 0 && targetPower > 0) {
|
||||
const scale = targetPower / totalBasePower;
|
||||
for (const m of monsters) {
|
||||
m.hp = Math.max(1, Math.round(m.hp * scale));
|
||||
m.ap = Math.max(1, Math.round(m.ap * scale));
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 硬上限保护(理论不会触发,但防止配置错误)
|
||||
return monsters.slice(0, 12);
|
||||
// 7. 挂载技能(普通怪随机技能池,Boss 专属技能池)
|
||||
for (const m of monsters) {
|
||||
if (m.isBoss && cfg.boss_skill_pool) {
|
||||
m.skills = this.pickSkillSet(cfg.boss_skill_pool, BossSkillPool);
|
||||
} else if (!m.isBoss && cfg.skill_pool) {
|
||||
m.skills = this.pickSkillSet(cfg.skill_pool, MonSkillPool);
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 分配批次(0~2,均匀分布)
|
||||
for (let i = 0; i < monsters.length; i++) {
|
||||
monsters[i].batch = i % BATCH_COUNT;
|
||||
monsters[i].spawnIndex = i;
|
||||
}
|
||||
|
||||
// 9. 硬上限保护
|
||||
return monsters.slice(0, MAX_MONSTERS);
|
||||
}
|
||||
|
||||
/** 重置引擎状态(保留接口兼容,当前无内部状态) */
|
||||
/** 重置引擎状态(每局开始时调用) */
|
||||
reset(): void {
|
||||
// 无可变状态
|
||||
DynamicTuner.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定波次的怪物总数
|
||||
* @param waveNumber 目标波数
|
||||
* @returns 预计生成的怪物总数(base_count,受 12 上限约束)
|
||||
* @returns 预计生成的怪物总数
|
||||
*/
|
||||
getWaveMonsterCount(waveNumber: number): number {
|
||||
if (waveNumber < 1) return 0;
|
||||
if (TestModeConfig.enable) {
|
||||
return Math.max(1, TestModeConfig.spawnCount || 1);
|
||||
}
|
||||
const cfg = WaveConfigs[Math.min(waveNumber, 30)];
|
||||
return Math.min(cfg.base_count, 12);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取波次槽位配置(向后兼容接口)
|
||||
* 内部从 generateWave 反推,仅用于历史调用方
|
||||
* @param waveNumber 目标波数
|
||||
*/
|
||||
getWaveSlotConfig(waveNumber: number): IWaveSlot[] {
|
||||
const generated = this.generateWave(waveNumber);
|
||||
const slotMap = new Map<number, { count: number; affixes: AffixType[] }>();
|
||||
|
||||
for (const m of generated) {
|
||||
const existing = slotMap.get(m.type);
|
||||
if (existing) {
|
||||
existing.count++;
|
||||
} else {
|
||||
slotMap.set(m.type, { count: 1, affixes: [] });
|
||||
}
|
||||
const wave = Math.min(waveNumber, MAX_WAVE);
|
||||
const cfg = WaveConfigs[wave];
|
||||
const waveType = getWaveType(wave);
|
||||
let count = cfg.base_count;
|
||||
if (waveType === WaveType.Relax) {
|
||||
count = Math.round(count * RELAX_COUNT_MUL);
|
||||
}
|
||||
|
||||
return Array.from(slotMap.entries()).map(([type, data]) => ({
|
||||
type,
|
||||
count: data.count,
|
||||
...(data.affixes.length > 0 ? { affixes: data.affixes } : {}),
|
||||
}));
|
||||
return Math.min(count, MAX_MONSTERS);
|
||||
}
|
||||
|
||||
// ======================== 私有生成子算法 ========================
|
||||
|
||||
/**
|
||||
* 计算场上存活英雄的总强度
|
||||
* 通过 ECS 查询所有 HeroAttrsComp,累加 calcHeroPower
|
||||
*/
|
||||
private getCurrentHeroPower(): number {
|
||||
let total = 0;
|
||||
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
|
||||
const attrs = entity.get(HeroAttrsComp);
|
||||
if (!attrs || attrs.is_dead || attrs.fac !== FacSet.HERO) return;
|
||||
const info = HeroInfo[attrs.hero_uuid];
|
||||
if (!info) return;
|
||||
// 使用英雄当前等级(升级卡驱动),保证强度评估与实战一致
|
||||
const lv = Math.max(1, attrs.lv || 1);
|
||||
total += calcHeroPower(info, lv);
|
||||
});
|
||||
// 兜底:无英雄时返回基准强度,避免除零
|
||||
return Math.max(total, 100);
|
||||
}
|
||||
|
||||
/** 测试模式:完全绕过引擎逻辑 */
|
||||
private generateTestWave(waveNumber: number): GeneratedMonster[] {
|
||||
const growth = 1 + (waveNumber - 1) * TestModeConfig.growthRatePerWave;
|
||||
@@ -465,12 +614,10 @@ export class RogueSpawningEngine {
|
||||
type: TestModeConfig.monType,
|
||||
hp: Math.round(TestModeConfig.baseHp * growth),
|
||||
ap: Math.round(TestModeConfig.baseAp * growth),
|
||||
affixes: [...TestModeConfig.affixes],
|
||||
isBoss: false,
|
||||
spawnIndex: i,
|
||||
elite: TestModeConfig.testElite,
|
||||
wave_enchants: TestModeConfig.testWaveEnchant ? [TestModeConfig.testWaveEnchant] : [],
|
||||
testSkills: {
|
||||
batch: i % BATCH_COUNT,
|
||||
skills: {
|
||||
skill: TestModeConfig.skill,
|
||||
atking: TestModeConfig.atking,
|
||||
atked: TestModeConfig.atked,
|
||||
@@ -509,7 +656,7 @@ export class RogueSpawningEngine {
|
||||
}
|
||||
|
||||
/** 生成 Boss(首位) */
|
||||
private makeBoss(wave: number): GeneratedMonster {
|
||||
private makeBoss(wave: number, cfg: WaveConfig): GeneratedMonster {
|
||||
const isMeleeBoss = Math.random() < 0.5;
|
||||
const type = isMeleeBoss ? MonType.MeleeBoss : MonType.LongBoss;
|
||||
|
||||
@@ -532,10 +679,9 @@ export class RogueSpawningEngine {
|
||||
type,
|
||||
hp: Math.round(baseHp * bossBonusHpMul),
|
||||
ap: baseAp,
|
||||
affixes: [], // 兼容字段
|
||||
isBoss: true,
|
||||
spawnIndex: 0,
|
||||
wave_enchants: [], // 后续统一填充
|
||||
batch: 0, // Boss 固定第一批
|
||||
};
|
||||
}
|
||||
|
||||
@@ -557,52 +703,28 @@ export class RogueSpawningEngine {
|
||||
type,
|
||||
hp: baseHp,
|
||||
ap: baseAp,
|
||||
affixes: [], // 兼容字段
|
||||
isBoss: false,
|
||||
spawnIndex,
|
||||
wave_enchants: [], // 后续统一填充
|
||||
batch: 0, // 后续统一分配
|
||||
};
|
||||
}
|
||||
|
||||
/** 抽取波次级强化(按权重抽 0~2 个,不重复) */
|
||||
private pickEnchants(pool?: string[]): WaveEnchant[] {
|
||||
if (!pool || pool.length === 0) return [];
|
||||
const enchants: WaveEnchant[] = [];
|
||||
|
||||
// 第一个 Enchant: 70% 概率抽 1 个
|
||||
if (Math.random() < 0.7) {
|
||||
const first = this.pickWeightedEnchant(pool);
|
||||
if (first) enchants.push(first);
|
||||
}
|
||||
|
||||
// 第二个 Enchant: 30% 概率再抽 1 个(不重复)
|
||||
if (Math.random() < 0.3 && pool.length > 1) {
|
||||
const remaining = pool.filter(id => !enchants.some(e => e.id === id));
|
||||
const second = this.pickWeightedEnchant(remaining);
|
||||
if (second) enchants.push(second);
|
||||
}
|
||||
|
||||
return enchants;
|
||||
}
|
||||
|
||||
/** 抽取个体强化(等概率随机 1 种) */
|
||||
private pickElite(): MonsterElite {
|
||||
const candidates: MonsterElite[] = [
|
||||
MonsterElite.Elite,
|
||||
MonsterElite.Berserk,
|
||||
MonsterElite.Shield,
|
||||
MonsterElite.Swift,
|
||||
MonsterElite.Regen,
|
||||
];
|
||||
return candidates[Math.floor(Math.random() * candidates.length)];
|
||||
}
|
||||
|
||||
/** 应用个体强化到怪物 */
|
||||
private applyElite(m: GeneratedMonster, elite: MonsterElite): void {
|
||||
const def = MonsterEliteSet[elite];
|
||||
m.hp = Math.max(1, Math.round(m.hp * def.hp_mul));
|
||||
m.ap = Math.max(1, Math.round(m.ap * def.ap_mul));
|
||||
m.elite = elite;
|
||||
/** 从技能池中按权重抽取一套技能 */
|
||||
private pickSkillSet(pool: string[], library: Record<string, MonSkillSet>): GeneratedMonster["skills"] | undefined {
|
||||
const valid = pool.map(id => library[id]).filter(s => s);
|
||||
if (valid.length === 0) return undefined;
|
||||
const picked = this.pickWeighted(valid);
|
||||
if (!picked) return undefined;
|
||||
return {
|
||||
skill: picked.skill,
|
||||
call: picked.call,
|
||||
atking: picked.atking,
|
||||
atked: picked.atked,
|
||||
dead: picked.dead,
|
||||
fstart: picked.fstart,
|
||||
fend: picked.fend,
|
||||
revive: picked.revive,
|
||||
};
|
||||
}
|
||||
|
||||
/** 按权重从小队 id 池抽 1 个小队 */
|
||||
@@ -612,13 +734,6 @@ export class RogueSpawningEngine {
|
||||
return this.pickWeighted(valid);
|
||||
}
|
||||
|
||||
/** 按权重从强化 id 池抽 1 个强化 */
|
||||
private pickWeightedEnchant(pool: string[]): WaveEnchant | null {
|
||||
const valid = pool.map(id => WaveEnchantLibrary[id]).filter(e => e);
|
||||
if (valid.length === 0) return null;
|
||||
return this.pickWeighted(valid);
|
||||
}
|
||||
|
||||
/** 通用加权随机抽取 */
|
||||
private pickWeighted<T extends { weight: number }>(items: T[]): T | null {
|
||||
if (items.length === 0) return null;
|
||||
@@ -641,8 +756,6 @@ export const spawningEngine = new RogueSpawningEngine();
|
||||
export interface IWaveSlot {
|
||||
type: number;
|
||||
count: number;
|
||||
/** @deprecated 已废弃,新接口不再使用 */
|
||||
affixes?: AffixType[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -653,9 +766,16 @@ export function getWaveMonsterCount(waveNumber: number): number {
|
||||
return spawningEngine.getWaveMonsterCount(waveNumber);
|
||||
}
|
||||
|
||||
/** 获取波次槽位配置(向后兼容) */
|
||||
/** 获取波次槽位配置(向后兼容,从 generateWave 反推) */
|
||||
export function getWaveSlotConfig(waveNumber: number): IWaveSlot[] {
|
||||
return spawningEngine.getWaveSlotConfig(waveNumber);
|
||||
const generated = spawningEngine.generateWave(waveNumber);
|
||||
const slotMap = new Map<number, number>();
|
||||
|
||||
for (const m of generated) {
|
||||
slotMap.set(m.type, (slotMap.get(m.type) || 0) + 1);
|
||||
}
|
||||
|
||||
return Array.from(slotMap.entries()).map(([type, count]) => ({ type, count }));
|
||||
}
|
||||
|
||||
export const DefaultWaveSlot: IWaveSlot[] = [
|
||||
@@ -671,7 +791,7 @@ export const WaveSlotConfig: { [wave: number]: IWaveSlot[] } = new Proxy(
|
||||
get(_target, prop: string) {
|
||||
const wave = parseInt(prop, 10);
|
||||
if (!isNaN(wave) && wave >= 1) {
|
||||
return spawningEngine.getWaveSlotConfig(wave);
|
||||
return getWaveSlotConfig(wave);
|
||||
}
|
||||
if (prop === "toJSON") return () => ({});
|
||||
return undefined;
|
||||
|
||||
Reference in New Issue
Block a user