feat: 完成肉鸽回合制游戏核心玩法迭代

新增连杀系统、金币飞行特效、波次HUD、屏幕震动等表现功能,重构怪物刷出逻辑与难度动态调节,调整回合回血比例,优化游戏节奏与体验。

主要变更:
1.  调整回合回血比例从0.5到0.4,优化前期节奏
2.  新增连杀计数与奖励系统,支持5/10/20连杀触发对应表现
3.  实现怪物死亡掉落金币的抛物线飞行特效
4.  增加波次进度HUD,显示剩余怪物与全局回合进度
5.  新增屏幕震动工具与战斗横幅统一展示系统
6.  重构怪物刷出逻辑,支持按回合类型调整刷怪间隔,加入清场加速机制
7.  优化动态难度调节算法,增加滞回与指数平滑,避免难度突变
8.  新增Boss预警、登场事件与回合清屏事件,完善事件总线
9.  调整怪物数量阈值与回合倒计时档位,适配新的节奏设计
This commit is contained in:
pan
2026-08-11 19:00:23 +08:00
parent d57f4c14e0
commit 409113e269
12 changed files with 904 additions and 80 deletions

View File

@@ -9,18 +9,19 @@
* 4. MonSkillSet - 怪物技能池atking / atked / dead 等全触发类型)
* 5. RogueSpawningEngine - 生成引擎(按英雄强度反推怪物强度)
*
* 核心公式:
* 核心公式(最终强度 = heroPower × typeRatio × power_adjust × DDA × hp/ap_mul
* heroPower = Σ calcHeroPower(HeroInfo[uuid], lv) (场上存活英雄)
* targetPower = heroPower × 回合类型系数 × wave.power_adjust × DynamicTuner.factor
* scale = targetPower ÷ Σ 怪物基础强度
* 每只怪: hp ×= scale, ap ×= scale
* hpScale = targetPower × wave.hp_mul ÷ Σ 怪物基础强度
* apScale = targetPower × wave.ap_mul ÷ Σ 怪物基础强度
* 每只怪: hp ×= hpScale, ap ×= apScale
*
* 回合节奏:
* - 最大 20 回合,第 20 回合通关
* - 每回合 30 秒,固定分 3 批,每 10 秒释放一批
* - 普通回合 18~36 只,放松回合 × 1.5 = 27~54 只
* - wave % 5 === 0 → 压力回合(必带 Boss强度高、数量少
* - wave % 5 === 1 → 放松回合(数量 × 1.5,强度低,爽快清屏)
* - wave % 5 === 4 → 放松回合(数量 × 1.5,强度低,爽快清屏,大战前夜收割补给
*/
import { HeroInfo, MonType, MonTypeName, calcHeroPower, TriggerGrouped, LvReviveEntry, heroInfo } from "../common/config/heroSet";
@@ -35,7 +36,7 @@ import { HeroAttrsComp } from "../hero/HeroAttrsComp";
export enum WaveType {
Normal = 0, // 普通回合
Pressure = 1, // 压力回合wave % 5 === 0必带 Boss
Relax = 2, // 放松回合wave % 5 === 1,量大强度低)
Relax = 2, // 放松回合wave % 5 === 4,量大强度低,大战前夜的收割补给
}
/** 回合类型名称 */
@@ -70,6 +71,28 @@ export const BATCH_INTERVAL = WAVE_DURATION / BATCH_COUNT;
/** 每回合怪物硬上限(放松回合 36 × 1.5 = 54 */
export const MAX_MONSTERS = 54;
/** Boss 护卫队数量(与 Boss 同批压轴进场,占用回合总名额) */
export const BOSS_GUARD_COUNT = 3;
/** 批次怪物数量占比(铺垫 → 加压 → 高潮,第三批另有收尾小队加压) */
export const BATCH_RATIO: number[] = [0.25, 0.35, 0.40];
/** 收尾高潮批额外补入的最强小队数量(非放松回合生效) */
export const FINALE_SQUAD_COUNT = 2;
/** 按回合类型的逐个刷怪间隔(秒):放松回合快速倾泻造潮水感,压力回合稍慢便于聚焦 */
export const SPAWN_INTERVAL_BY_TYPE: Record<WaveType, number> = {
[WaveType.Normal]: 0.18,
[WaveType.Pressure]: 0.25,
[WaveType.Relax]: 0.12,
};
/** 回合战斗超时超过后强制结束回合残留怪销毁并扣留存分DynamicTuner 因 clearTime 过大自动放水) */
export const WAVE_TIMEOUT = 75;
/** Boss 回合超时Boss 压轴进场(第 20 秒),击杀耗时更长,放宽兜底阈值 */
export const WAVE_TIMEOUT_BOSS = 90;
/**
* 获取指定回合的回合类型
* @param wave 回合1 起)
@@ -77,7 +100,7 @@ export const MAX_MONSTERS = 54;
*/
export function getWaveType(wave: number): WaveType {
if (wave % 5 === 0) return WaveType.Pressure;
if (wave % 5 === 1) return WaveType.Relax;
if (wave % 5 === 4) return WaveType.Relax; // 大战前夜的收割补给:清杂攒金币备战 Boss
return WaveType.Normal;
}
@@ -88,42 +111,66 @@ export function getWaveType(wave: number): WaveType {
* 与硬编码系数并存,用于根据战况实时微调难度。
*
* 用法示例MissionComp 每回合结束时调用):
* DynamicTuner.adjust(clearTime, heroDeathCount);
* DynamicTuner.adjust(effectiveClearTime, heroDeathCount);
*
* 调节规则(内部硬编码
* - 清场时间 < 20s 且无英雄死亡 → factor += 0.05(加压)
* - 清场时间 > 28s 或有英雄死亡 → factor -= 0.05(放水)
* - factor 范围钳制 [0.5, 2.0]
* 设计原则(真隐形 DDA
* - 连续映射desired = 1 + (0.8 - clearTime/WAVE_DURATION) × K清场越快要价越高非离散跳变
* - 滞回:连续同方向判定满 HYSTERESIS 回合才生效,偶发超神/崩盘不立即拉阀门
* - 指数靠拢:每回合向 desired 移动 50%,避免突变被玩家察觉
* - 总幅度钳制 [0.7, 1.3]±30%),防止橡皮筋效应
* - 英雄死亡直接锚定 desired=0.8(温和放水),不与慢清场放水叠加
*/
export const DynamicTuner = {
/** 当前难度系数(默认 1.0>1 加压,<1 放水) */
factor: 1.0,
/** 系数下限(最多放水到 50% */
MIN_FACTOR: 0.5,
/** 系数上限(最多加压到 200% */
MAX_FACTOR: 2.0,
/** 单次调节步长 */
STEP: 0.05,
/** 系数下限(最多放水到 70% */
MIN_FACTOR: 0.7,
/** 系数上限(最多加压到 130% */
MAX_FACTOR: 1.3,
/** 连续映射增益clearTime 每偏离基准 100% 时长factor 偏移 K */
K: 0.5,
/** 滞回:连续同方向判定满 N 回合才生效 */
HYSTERESIS: 2,
/** 连续方向计数(>0 加压倾向,<0 放水倾向) */
streak: 0,
/**
* 根据上一回合战况自动调节难度
* @param clearTime 清场耗时(秒)
* @param clearTime 清场耗时(秒,已含清场加速提前量的还原口径
* @param heroDeathCount 英雄死亡数
* @returns 本回合是否实际调整了 factor
*/
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);
adjust(clearTime: number, heroDeathCount: number): boolean {
// 1) 连续映射期望系数:基准 0.8×时长不动,更快加压、更慢放水;死亡锚定 0.8
let desired: number;
if (heroDeathCount > 0) {
desired = 1 + (0.8 - 1.2) * this.K; // = 0.8
} else {
desired = 1 + (0.8 - clearTime / WAVE_DURATION) * this.K;
}
desired = Math.min(this.MAX_FACTOR, Math.max(this.MIN_FACTOR, desired));
// 2) 滞回:连续同方向满 HYSTERESIS 回合才向 desired 靠拢
const dir = Math.sign(desired - 1);
if (dir === 0) {
this.streak = 0;
return false;
}
this.streak = (Math.sign(this.streak) === dir) ? this.streak + dir : dir;
if (Math.abs(this.streak) < this.HYSTERESIS) return false;
// 3) 指数靠拢:每回合向 desired 移动 50%,避免跳变
const old = this.factor;
this.factor = old + (desired - old) * 0.5;
this.factor = Math.min(this.MAX_FACTOR, Math.max(this.MIN_FACTOR, this.factor));
return this.factor !== old;
},
/** 重置调节器(每局开始时调用) */
reset(): void {
this.factor = 1.0;
this.streak = 0;
},
};
@@ -195,6 +242,8 @@ export const SquadLibrary: Record<string, SquadConfig> = {
long_line: { id: "long_line", name: "远程线列组", weight: 7, slots: [{ type: MonType.Long, count: 2 }, { type: MonType.Support, count: 1 }] },
heavy_shield: { id: "heavy_shield", name: "重盾堡垒组", weight: 5, slots: [{ type: MonType.Heavy, count: 1 }, { type: MonType.Melee, count: 2 }] },
summoner_cult: { id: "summoner_cult", name: "召唤教派组", weight: 4, slots: [{ type: MonType.Summoner, count: 1 }, { type: MonType.Long, count: 2 }] },
/** Boss 护卫队weight=0 不进随机池,仅供引擎在 Boss 回合直接引用,与 Boss 同批压轴进场) */
boss_guard: { id: "boss_guard", name: "Boss 护卫队", weight: 0, slots: [{ type: MonType.Heavy, count: 1 }, { type: MonType.Melee, count: 2 }] },
};
// ======================== 5. 怪物技能池 ========================
@@ -309,9 +358,9 @@ export interface WaveConfig {
base_count: number;
/** 可选小队 id 池,引擎按权重抽取拼装到 base_count */
squad_pool: string[];
/** HP 强化倍率(硬编码,逐回合递进 */
/** HP 成长乘区(并入强度缩放分子,终值 = heroPower × 系数 × hp_mul ÷ Σ基础强度 */
hp_mul: number;
/** AP 强化倍率(硬编码,逐回合递进 */
/** AP 成长乘区(同 hp_mul独立控制怪物肉度与输出的成长比例 */
ap_mul: number;
/** 强度微调(放水 / 加压,默认 1.0 */
power_adjust?: number;
@@ -328,7 +377,7 @@ export interface WaveConfig {
*
* 心流循环5 回合一循环):
* wave % 5 === 0 → 压力回合(必带 Boss强度高、数量少
* wave % 5 === 1 → 放松回合(数量 × 1.5,强度低)
* wave % 5 === 4 → 放松回合(数量 × 1.5,强度低,大战前夜收割补给
* 其余 → 普通回合(标准强度)
*
* 强度递进hp_mul / ap_mul 每 5 回合一档,压力回合额外提升。
@@ -338,34 +387,35 @@ export const WaveConfigs: Record<number, WaveConfig> = {
1: { base_count: 18, squad_pool: ["melee_grunt"], hp_mul: 1.00, ap_mul: 1.00 },
2: { base_count: 21, squad_pool: ["melee_grunt", "mixed_balanced"], hp_mul: 1.00, ap_mul: 1.00 },
3: { base_count: 24, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.05, ap_mul: 1.00 },
// 放松回合量大好清Boss 前收割补给
4: { base_count: 27, squad_pool: ["mixed_balanced", "long_line", "heavy_shield"], hp_mul: 1.10, ap_mul: 1.05 },
// 压力回合:第一 Boss
5: { base_count: 21, 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: 30, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.10, ap_mul: 1.05 },
7: { base_count: 30, squad_pool: ["melee_grunt", "heavy_shield", "long_line"], hp_mul: 1.15, ap_mul: 1.10, skill_pool: ["tough"] },
8: { base_count: 33, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.20, ap_mul: 1.15, skill_pool: ["berserk"] },
// 放松回合量大好清Boss 前收割补给
9: { base_count: 33, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["berserk", "tough"] },
// 压力回合:第二 Boss
10: { base_count: 24, 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: 33, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["leech"] },
12: { base_count: 33, 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: 36, squad_pool: ["assassin_squad", "summoner_cult", "mixed_balanced"], hp_mul: 1.35, ap_mul: 1.30, skill_pool: ["berserk", "leech"] },
// 放松回合量大好清Boss 前收割补给
14: { base_count: 36, 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: 27, 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"] },
// ===== 第四循环:终极阶段 =====
// 放松回合
// ===== 第四循环:终极阶段17~19 power_adjust 逐步爬坡,为最终 Boss 蓄势) =====
16: { base_count: 36, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], hp_mul: 1.45, ap_mul: 1.40, skill_pool: ["leech", "warcry"] },
17: { base_count: 36, squad_pool: ["melee_grunt", "summoner_cult", "mixed_balanced"], hp_mul: 1.50, ap_mul: 1.45, skill_pool: ["berserk", "legacy"] },
18: { base_count: 36, squad_pool: ["assassin_squad", "long_line", "heavy_shield"], hp_mul: 1.55, ap_mul: 1.50, skill_pool: ["berserk", "tough", "leech"] },
19: { base_count: 36, squad_pool: ["mixed_balanced", "summoner_cult", "melee_grunt"], hp_mul: 1.60, ap_mul: 1.55, skill_pool: ["berserk", "tough", "legacy", "warcry"] },
17: { base_count: 36, squad_pool: ["melee_grunt", "summoner_cult", "mixed_balanced"], hp_mul: 1.50, ap_mul: 1.45, skill_pool: ["berserk", "legacy"], power_adjust: 1.05 },
18: { base_count: 36, squad_pool: ["assassin_squad", "long_line", "heavy_shield"], hp_mul: 1.55, ap_mul: 1.50, skill_pool: ["berserk", "tough", "leech"], power_adjust: 1.10 },
// 放松回合:量大好清,最终 Boss 前收割补给
19: { base_count: 36, squad_pool: ["mixed_balanced", "summoner_cult", "melee_grunt"], hp_mul: 1.60, ap_mul: 1.55, skill_pool: ["berserk", "tough", "legacy", "warcry"], power_adjust: 1.20 },
// 压力回合:最终 Boss
20: { base_count: 30, 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"] },
};
@@ -405,6 +455,9 @@ export function validateRogueConfig(): string[] {
if (cfg.base_count < 1 || cfg.base_count > MAX_MONSTERS) {
errors.push(`Wave ${wave} base_count=${cfg.base_count} 越界 (1~${MAX_MONSTERS})`);
}
if (cfg.power_adjust !== undefined && (cfg.power_adjust < 0.8 || cfg.power_adjust > 1.3)) {
errors.push(`Wave ${wave} power_adjust=${cfg.power_adjust} 越界 (0.8~1.3)`);
}
}
// 2. 校验 SquadLibrary 中所有 type 在 MonList 中有怪
@@ -429,6 +482,8 @@ export interface GeneratedMonster {
hp: number;
ap: number;
isBoss: boolean;
/** 是否为 Boss 护卫队成员(与 Boss 同批压轴进场,供 UI/统计识别) */
isBossGuard?: boolean;
spawnIndex: number;
/** 本怪所属批次0~2由 MissionMonComp 按 BATCH_INTERVAL 释放) */
batch: number;
@@ -499,42 +554,59 @@ export class RogueSpawningEngine {
const powerAdjust = cfg.power_adjust ?? 1.0;
const targetPower = heroPower * typeRatio * powerAdjust * DynamicTuner.factor;
// 2. 确定怪物总数(放松回合 × 1.5
// 2. 确定怪物总数(放松回合 × 1.5;普通/压力回合预留收尾高潮批名额,防 slice 截掉
let totalCount = cfg.base_count;
if (waveType === WaveType.Relax) {
totalCount = Math.round(totalCount * RELAX_COUNT_MUL);
} else {
totalCount += this.estimateFinaleCount(cfg.squad_pool);
}
totalCount = Math.min(totalCount, MAX_MONSTERS);
// 3. Boss 位(压力回合必带 Boss,占 1 个名额)
const monsters: GeneratedMonster[] = [];
// 3. Boss 位(压力回合必带 Boss):先记录延后挂载,使其压轴进场而非第 0 秒开场
let boss: GeneratedMonster | null = null;
let remaining = totalCount;
if (cfg.boss_wave) {
monsters.push(this.makeBoss(wave, cfg));
remaining -= 1;
boss = this.makeBoss(wave, cfg);
remaining -= 1 + BOSS_GUARD_COUNT; // Boss 1 只 + 护卫队名额
}
// 4. 小队拼装:按权重从 squad_pool 抽小队,累计到 remaining
const squadMonsters = this.assembleSquads(cfg.squad_pool, remaining, wave);
monsters.push(...squadMonsters);
const monsters: GeneratedMonster[] = this.assembleSquads(cfg.squad_pool, remaining, wave);
// 5. 应用硬编码 HP/AP 倍率
for (const m of monsters) {
m.hp = Math.max(1, Math.round(m.hp * cfg.hp_mul));
m.ap = Math.max(1, Math.round(m.ap * cfg.ap_mul));
// 4.5 收尾高潮批:非放松回合额外补入最强小队,与 Boss 一样压轴(先补入再统一缩放,保证强度自洽)
const finaleSquad = waveType !== WaveType.Relax ? this.pickStrongestSquad(cfg.squad_pool) : null;
if (finaleSquad) {
for (let s = 0; s < FINALE_SQUAD_COUNT; s++) {
for (const slot of finaleSquad.slots) {
for (let c = 0; c < slot.count; c++) {
const m = this.makeMonster(slot.type, wave, 0);
m.batch = BATCH_COUNT - 1; // 标记收尾批,第 8 步不再覆盖
monsters.push(m);
}
}
}
}
// 6. 按英雄强度反推缩放系数
// 4.6 Boss 与护卫队压队尾,使其在批次分配后落入最后一批(回合内高潮点)
if (boss) {
monsters.push(...this.makeBossGuards(wave));
monsters.push(boss);
}
// 5. 按英雄强度反推缩放系数hp_mul/ap_mul 并入目标强度乘区,而非预先乘到怪物上,
// 保证配置表语义单一:最终强度 = heroPower × typeRatio × power_adjust × DDA × hp/ap_mul
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;
const hpScale = (targetPower * cfg.hp_mul) / totalBasePower;
const apScale = (targetPower * cfg.ap_mul) / 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));
m.hp = Math.max(1, Math.round(m.hp * hpScale));
m.ap = Math.max(1, Math.round(m.ap * apScale));
}
}
@@ -547,9 +619,15 @@ export class RogueSpawningEngine {
}
}
// 8. 分配批次0~2均匀分布
// 8. 分配批次:按 BATCH_RATIO 递增加权(铺垫 → 加压 → 高潮),收尾小队/Boss/护卫保持最后一批
this.assignBatches(monsters);
if (boss) {
const lastBatch = BATCH_COUNT - 1;
for (const m of monsters) {
if (m.isBoss || m.isBossGuard) m.batch = lastBatch;
}
}
for (let i = 0; i < monsters.length; i++) {
monsters[i].batch = i % BATCH_COUNT;
monsters[i].spawnIndex = i;
}
@@ -656,8 +734,8 @@ export class RogueSpawningEngine {
return result;
}
/** 生成 Boss首位 */
private makeBoss(wave: number, cfg: WaveConfig): GeneratedMonster {
/** 生成 Boss压轴位batch/spawnIndex 为占位值,由 generateWave 统一分配并强制最后一批 */
private makeBoss(wave: number, _cfg: WaveConfig): GeneratedMonster {
const isMeleeBoss = Math.random() < 0.5;
const type = isMeleeBoss ? MonType.MeleeBoss : MonType.LongBoss;
@@ -681,13 +759,82 @@ export class RogueSpawningEngine {
hp: Math.round(baseHp * bossBonusHpMul),
ap: baseAp,
isBoss: true,
spawnIndex: 0,
batch: 0, // Boss 固定第一批
spawnIndex: 0, // 占位值,由 generateWave 第 8 步统一分配
batch: 0, // 占位值generateWave 会强制 Boss 进入最后一批
};
}
/** 生成普通怪物 */
private makeMonster(type: MonType, wave: number, spawnIndex: number): GeneratedMonster {
/** 生成 Boss 护卫队(复用 boss_guard 小队模板),与 Boss 同批压轴进场 */
private makeBossGuards(wave: number): GeneratedMonster[] {
const squad = SquadLibrary["boss_guard"];
const guards: GeneratedMonster[] = [];
for (const slot of squad.slots) {
for (let i = 0; i < slot.count; i++) {
const g = this.makeMonster(slot.type, wave, 0);
g.isBossGuard = true;
guards.push(g);
}
}
return guards;
}
/**
* 批次分配:按 BATCH_RATIO 递增加权切分(铺垫 → 加压 → 高潮)。
* 已预标记 batch 的怪(收尾小队 / Boss / 护卫)不参与切分,保持最后一批。
*/
private assignBatches(monsters: GeneratedMonster[]): void {
const normal = monsters.filter(m => m.batch !== BATCH_COUNT - 1 && !m.isBoss && !m.isBossGuard);
const n = normal.length;
if (n === 0) return;
let cursor = 0;
for (let b = 0; b < BATCH_COUNT - 1; b++) {
const quota = Math.round(n * BATCH_RATIO[b]);
for (let k = 0; k < quota && cursor < n; k++, cursor++) {
normal[cursor].batch = b;
}
}
// 剩余全部进入高潮批
for (; cursor < n; cursor++) {
normal[cursor].batch = BATCH_COUNT - 1;
}
}
/** 预估收尾高潮批额外补入的怪物数量(用于 totalCount 预留名额) */
private estimateFinaleCount(pool: string[]): number {
const squad = this.pickStrongestSquad(pool);
if (!squad) return 0;
let per = 0;
for (const slot of squad.slots) per += slot.count;
return per * FINALE_SQUAD_COUNT;
}
/**
* 识别小队池中最强小队:按槽位 MonType 基础强度calcHeroPower 1 级样本)× 数量加权求和。
* 注意不能用 squad.weight——它是"出现频率"语义而非强度。
*/
private pickStrongestSquad(pool: string[]): SquadConfig | null {
let best: SquadConfig | null = null;
let bestScore = -1;
for (const id of pool) {
const sq = SquadLibrary[id];
if (!sq) continue;
let score = 0;
for (const slot of sq.slots) {
const uuids = MonList[slot.type];
const sample = uuids && uuids.length ? HeroInfo[uuids[0]] : null;
score += (sample ? calcHeroPower(sample, 1) : 100) * slot.count;
}
if (score > bestScore) {
bestScore = score;
best = sq;
}
}
return best;
}
/** 生成普通怪物wave 保留参数位,供后续按回合差异化基础属性扩展) */
private makeMonster(type: MonType, _wave: number, spawnIndex: number): GeneratedMonster {
let uuids = MonList[type];
if (!uuids || uuids.length === 0) {
// 兜底 Melee