4 Commits

Author SHA1 Message Date
panFD
95969781fb feat(map:heroBox): add upgrade cost display and insufficient gold prompt
新增英雄升级费用显示控件,添加金币不足时的小提示和费用标红逻辑,清空旧的升级费用标签引用
2026-08-05 09:21:22 +08:00
panFD
4938e8468e feat(hero): add hero upgrade function
1. 新增英雄升级相关UI控件与事件流程
2. 实现升级消耗计算与统一扣费逻辑
3. 添加升级预览确认弹窗与属性成长展示
4. 开放tryUpgradeHeroByEid与applyHeroLevel为公共方法
2026-08-04 22:18:20 +08:00
panFD
153e0e9a1f feat(map): 调整肉鸽模式怪物数量配置
统一更新全波次基础怪物上限,将普通波基数提升至36,放松波达到54,同时同步更新所有波次的怪物数量配置,优化游戏战斗体验
2026-08-04 21:01:50 +08:00
panFD
988c669bba refactor(rogue): 重构肉鸽刷怪系统,新增动态难度与分批刷怪
1. 重构RogueConfig:替换旧强化系统为动态难度调节器,调整波次配置为20波通关,新增技能池与阵营适配
2. 优化刷怪逻辑:实现每波3批10秒间隔的分批刷怪,调整怪物上限为18只
3. 新增驻场技能支持:添加resolveFieldByLv处理等级化驻场技能,扩展FieldSkillHelper支持多阵营统计
4. 调整关卡判定:将最大波次从15改为20,新增英雄死亡计数与难度动态调节
5. 优化代码结构:移除废弃字段,统一技能注入方式,修复槽位复用逻辑
2026-08-04 20:53:59 +08:00
10 changed files with 12470 additions and 11379 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -68,6 +68,8 @@ export enum GameEvent {
MonDead = "MonDead",
HeroDead = "HeroDead",
HeroSell = "HeroSell",
/** 英雄面板升级按钮点击事件payload: { eid },由 MissionCardComp 执行升级) */
HeroUpgrade = "HeroUpgrade",
GOLD_UPDATE = "GOLD_UPDATE",
DIAMOND_UPDATE = "DIAMOND_UPDATE",
MEAT_UPDATE = "MEAT_UPDATE",

View File

@@ -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,7 +43,8 @@ export class FieldSkillHelper {
}
});
// 2. 统计技能盒子(技能卡)带来的驻场技能加成
// 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;
@@ -46,7 +57,7 @@ export class FieldSkillHelper {
}
});
// 3. 统计装备盒(装备卡)带来的驻场技能加成
// 3. 统计装备盒(装备卡)带来的驻场技能加成(仅英雄阵营)
ecs.query(ecs.allOf(EquipBoxComp)).forEach((entity: ecs.Entity) => {
const equipBox = entity.get(EquipBoxComp);
if (!equipBox || !equipBox.field || equipBox.field.length === 0) return;
@@ -58,6 +69,7 @@ export class FieldSkillHelper {
}
}
});
}
return total;
}

View File

@@ -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

View File

@@ -12,7 +12,7 @@
* - HeroInfoheroSet—— 英雄静态配置
*/
import { mLogger } from "../common/Logger";
import { _decorator, Node, Sprite, Label, RichText, resources, AnimationClip, SpriteFrame, NodeEventType, Tween, tween, Vec3 } from "cc";
import { _decorator, Node, Sprite, Label, RichText, resources, AnimationClip, SpriteFrame, NodeEventType, Tween, tween, Vec3, Color } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { oops } from "db://oops-framework/core/Oops";
@@ -20,8 +20,8 @@ import { GameEvent } from "../common/config/GameEvent";
import { HeroInfo } from "../common/config/heroSet";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { Hero } from "../hero/Hero";
import { FacSet, getLvColor } from "../common/config/GameSet";
import { buildSkillDescRich } from "../common/config/HeroSkillDesc";
import { FacSet, FightSet, getLvColor } from "../common/config/GameSet";
import { buildSkillDesc, buildSkillDescRich, ISkillDescSource } from "../common/config/HeroSkillDesc";
import { MissionEconomy } from "./MissionEconomy";
import { UIID } from "../common/config/GameUIConfig";
@@ -37,6 +37,9 @@ const { ccclass, property } = _decorator;
export class HeroBoxComp extends CCComp {
private debugMode: boolean = false;
/** 金币不足时升级费用标红色(暗红,区别于正常白色) */
private static readonly INSUFFICIENT_COLOR: Color = new Color(231, 76, 60);
// ======================== 编辑器绑定节点 ========================
/** 英雄图标节点 */
@@ -60,10 +63,18 @@ export class HeroBoxComp extends CCComp {
@property(Node)
private sell_btn: Node = null;
/** 升级按钮(绑定英雄时激活,点击弹出升级预览确认框) */
@property(Node)
private upgrade_btn: Node = null;
/** 出售价格标签(按英雄等级显示当前卖价,含驻场加成) */
@property(Label)
private sell_price_label: Label = null;
/** 升级费用标签(按当前等级显示升级消耗金币,满级时隐藏) */
@property(Label)
private upgrade_price_label: Label = null;
/** 技能描述富文本(多行,展示当前等级触发技能,数值高亮) */
@property(RichText)
private skill_label: RichText = null;
@@ -167,6 +178,8 @@ export class HeroBoxComp extends CCComp {
this.noHero?.on(NodeEventType.TOUCH_END, this.onNoHeroClick, this);
// 出售按钮点击 → 出售当前绑定英雄
this.sell_btn?.on(NodeEventType.TOUCH_END, this.onSellHeroClick, this);
// 升级按钮点击 → 弹出升级预览确认框
this.upgrade_btn?.on(NodeEventType.TOUCH_END, this.onUpgradeHeroClick, this);
}
onDestroy() {
@@ -181,6 +194,101 @@ export class HeroBoxComp extends CCComp {
if (this.sell_btn && this.sell_btn.isValid) {
this.sell_btn.off(NodeEventType.TOUCH_END, this.onSellHeroClick, this);
}
if (this.upgrade_btn && this.upgrade_btn.isValid) {
this.upgrade_btn.off(NodeEventType.TOUCH_END, this.onUpgradeHeroClick, this);
}
}
/**
* 升级按钮点击:构建升级后新能力预览,弹出公共确认框。
* Why: 升级消耗金币不可逆,需展示收益并二次确认。
*/
private onUpgradeHeroClick() {
if (!this.eid || !this.model) return;
const heroLv = Math.max(1, Math.floor(this.model.lv ?? 1));
// 已满级:气泡提示,复用英雄栏 smalltip 挂载点
if (heroLv >= FightSet.HERO_MAX_LV) {
oops.message.dispatchEvent(GameEvent.ShowSmallTip, { type: "hero_full", text: "已达最高等级" });
return;
}
oops.audio.playEffect("music/button");
const heroName = this.model.hero_name ?? "";
const nextLv = heroLv + 1;
const cost = MissionEconomy.getUpgradeCost(heroLv);
// 金币不足smalltip 气泡提示(挂载金币节点),不弹确认框
if (MissionEconomy.getCoin() < cost) {
oops.message.dispatchEvent(GameEvent.ShowSmallTip, { type: "buy_coin", text: "金币不足" });
return;
}
const preview = this.buildUpgradePreview(nextLv);
// oops.gui 公共确认窗口(自定义 CommonPrompt内容支持富文本高亮
oops.gui.open(UIID.Window, {
title: `升级英雄`,
content: `<color=#C0392B>${heroName}</color> Lv.${heroLv} → <color=#1E8449>Lv.${nextLv}</color>\n消耗 <color=#E67E22>${cost}</color> 金币\n\n<b>升级后获得:</b>\n${preview}`,
okWord: "升级",
cancelWord: "取消",
needCancel: true,
okFunc: () => this.executeUpgradeHero()
});
}
/**
* 执行升级:校验状态后派发 HeroUpgrade 事件,由 MissionCardComp 扣费并应用等级。
* Why: 扣费/属性重算统一走 MissionCardComp与升级卡共用 tryUpgradeHeroByEid
* 本组件仅负责 UI 交互与预览,保证单一数据源。
*/
private executeUpgradeHero() {
// 确认窗口打开期间英雄可能已死亡/被移除,需二次校验
if (!this.eid || !this.model) return;
oops.message.dispatchEvent(GameEvent.HeroUpgrade, { eid: this.eid });
}
/**
* 构建升级后新能力预览富文本。
* 数据源取 HeroInfo 静态配置lv 置为目标等级),由描述标准层统一渲染,
* 与面板技能描述同一套文案规则,杜绝数值漂移。
*
* @param nextLv 目标等级
* @returns 多行富文本:属性成长 + 下一档触发技能/光环/复活/加成
*/
private buildUpgradePreview(nextLv: number): string {
const hero = HeroInfo[this.model?.hero_uuid ?? 0];
if (!hero) return "";
// ---- 属性成长(按 HERO_LV_MULTIPLIER 幂成长 + 等级额外加成) ----
const mult = Math.pow(FightSet.HERO_LV_MULTIPLIER, nextLv - 1);
const nextAp = Math.floor(hero.ap * mult + HeroBoxComp.sumBonus(hero.ap_bonus, nextLv));
const nextHp = Math.floor(hero.hp * mult + HeroBoxComp.sumBonus(hero.hp_bonus, nextLv));
const lines: string[] = [
`攻击 <color=#27AE60>${nextAp}</color> 生命 <color=#27AE60>${nextHp}</color>`
];
// ---- 新能力(触发技能 / 驻场光环 / 复活 / 额外加成,取目标等级生效档) ----
const source: ISkillDescSource = {
lv: nextLv,
call: hero.call,
dead: hero.dead,
fstart: hero.fstart,
fend: hero.fend,
atking: hero.atking,
atked: hero.atked,
field: hero.field,
revive: hero.revive,
// 预览仅展示增量,额外加成由上方属性成长体现,避免重复
};
const skillDesc = buildSkillDesc(source);
if (skillDesc) lines.push(skillDesc);
return lines.join("\n");
}
/** 累加等级额外加成(≤目标等级的所有档位) */
private static sumBonus(entries: { lv: number; value: number }[] | undefined, lv: number): number {
if (!entries) return 0;
let total = 0;
for (const e of entries) { if (e.lv <= lv) total += e.value; }
return total;
}
/**
@@ -320,6 +428,9 @@ export class HeroBoxComp extends CCComp {
if (this.sell_btn && this.sell_btn.isValid) {
this.sell_btn.active = true;
}
if (this.upgrade_btn && this.upgrade_btn.isValid) {
this.upgrade_btn.active = true;
}
this.refresh();
}
@@ -337,9 +448,15 @@ export class HeroBoxComp extends CCComp {
if (this.sell_btn && this.sell_btn.isValid) {
this.sell_btn.active = false;
}
if (this.upgrade_btn && this.upgrade_btn.isValid) {
this.upgrade_btn.active = false;
}
if (this.sell_price_label && this.sell_price_label.isValid) {
this.sell_price_label.string = "";
}
if (this.upgrade_price_label && this.upgrade_price_label.isValid) {
this.upgrade_price_label.string = "";
}
if (this.name_label && this.name_label.isValid) {
this.name_label.string = "";
@@ -398,6 +515,19 @@ export class HeroBoxComp extends CCComp {
this.sell_price_label.string = `${MissionEconomy.getSellGold(lv)}`;
}
// ---- 升级费用(满级隐藏;金币不足时标红提示) ----
if (this.upgrade_price_label && this.upgrade_price_label.isValid) {
const lv = Math.max(1, Math.floor(this.model.lv ?? 1));
if (lv >= FightSet.HERO_MAX_LV) {
this.upgrade_price_label.string = "";
} else {
const cost = MissionEconomy.getUpgradeCost(lv);
this.upgrade_price_label.string = `${cost}`;
// 金币不足标红,足够恢复默认色,给玩家直观的可升级反馈
this.upgrade_price_label.color = MissionEconomy.getCoin() >= cost ? Color.WHITE : HeroBoxComp.INSUFFICIENT_COLOR;
}
}
// ---- AP / HP ----
const finalAp = Math.max(0, Math.floor(this.model.getFinalAp()));
const baseAp = Math.max(0, Math.floor(this.model.base_ap ?? 0));

View File

@@ -371,6 +371,7 @@ export class MissionCardComp extends CCComp {
oops.message.on(GameEvent.ShowSmallTip, this.onShowSmallTip, this);
oops.message.on(GameEvent.CardUsed, this.onCardUsed, this);
oops.message.on(GameEvent.HeroBoxEmptyClick, this.onHeroBoxEmptyClick, this);
oops.message.on(GameEvent.HeroUpgrade, this.onHeroUpgrade, this);
/** 按钮触控事件:抽卡 */
this.cards_chou?.on(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
@@ -614,6 +615,7 @@ export class MissionCardComp extends CCComp {
oops.message.off(GameEvent.ShowSmallTip, this.onShowSmallTip, this);
oops.message.off(GameEvent.CardUsed, this.onCardUsed, this);
oops.message.off(GameEvent.HeroBoxEmptyClick, this.onHeroBoxEmptyClick, this);
oops.message.off(GameEvent.HeroUpgrade, this.onHeroUpgrade, this);
if (this.cards_chou && this.cards_chou.isValid) {
this.cards_chou.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
this.cards_chou.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
@@ -694,6 +696,28 @@ export class MissionCardComp extends CCComp {
this.refreshHeroBoxSlots();
}
/**
* 英雄面板升级请求回调(由 HeroBoxComp 确认弹窗后派发)。
* 扣费成功后按 eid 精确升级;金币不足或不可升级时 toast 提示。
*/
private onHeroUpgrade(event: string, args: any) {
const payload = args ?? event;
const eid: number = payload?.eid ?? 0;
if (!eid) return;
const actor = this.queryAliveHeroActors().find(item => item.eid === eid);
if (!actor) return;
if (actor.model.lv >= FightSet.HERO_MAX_LV) {
oops.gui.toast(`已达最高等级`);
return;
}
// 统一经济管理入口:按当前等级扣费
if (!MissionEconomy.executeUpgradeCost(actor.model.lv)) {
oops.gui.toast(`金币不足`);
return;
}
this.tryUpgradeHeroByEid(eid);
}
/** 英雄升级事件回调:刷新英雄信息盒显示 */
private onHeroLvUp() {
this.refreshHeroBoxSlots();
@@ -1818,7 +1842,7 @@ export class MissionCardComp extends CCComp {
* @param heroEid 要升级的英雄实体 eid
* @returns true = 升级成功
*/
private tryUpgradeHeroByEid(heroEid: number): boolean {
public tryUpgradeHeroByEid(heroEid: number): boolean {
if (!heroEid) return false;
const actor = this.queryAliveHeroActors().find(item => item.eid === heroEid);
if (!actor) return false;
@@ -1835,7 +1859,7 @@ export class MissionCardComp extends CCComp {
* 应用英雄等级变化(属性按 HERO_LV_MULTIPLIER 重新计算)。
* 沿用旧版本 SkillLvUp 的技能等级映射规则。
*/
private applyHeroLevel(model: HeroAttrsComp, targetLv: number) {
public applyHeroLevel(model: HeroAttrsComp, targetLv: number) {
const hero = HeroInfo[model.hero_uuid];
if (!hero) return;
const nextLv = Math.max(1, Math.min(FightSet.HERO_MAX_LV, Math.floor(targetLv)));

View File

@@ -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");

View File

@@ -109,6 +109,24 @@ export class MissionEconomy {
return success;
}
/**
* 计算英雄升级金币消耗按当前等级线性缩放Lv×5
* @param heroLevel 当前英雄等级(升级前)
*/
static getUpgradeCost(heroLevel: number = 1): number {
const lv = Math.max(1, Math.floor(heroLevel));
return lv * 5;
}
/**
* 执行英雄升级扣费
* @param heroLevel 当前英雄等级(升级前)
* @returns true = 扣费成功(金币足够)
*/
static executeUpgradeCost(heroLevel: number = 1): boolean {
return this.spendCoin(this.getUpgradeCost(heroLevel));
}
/**
* 计算英雄出售金币(按英雄等级缩放)
*/

View File

@@ -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
@@ -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,9 +130,15 @@ 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);
@@ -136,7 +161,10 @@ 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;
@@ -167,18 +195,54 @@ 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()!);
// 准备结束阶段:启动分批释放
// 第一批立即转入 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;
}
// ======================== 怪物生成 ========================
@@ -218,8 +285,9 @@ export class MissionMonCompComp extends CCComp {
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;
// 技能套装注入:通过 _testSkills 通道传递给 Mon.load()
if (monData.skills) {
(mon as any)._testSkills = monData.skills;
}
mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monLv, posIndex);

View File

@@ -1,68 +1,133 @@
/**
* @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 秒释放一批
* - 普通波 18~36 只,放松波 × 1.5 = 27~54 只
* - 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;
/** 每波怪物硬上限(放松波 36 × 1.5 = 54 */
export const MAX_MONSTERS = 54;
/**
* 旧版词缀类型枚举(已废弃)
* @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 +141,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 +171,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: "个体强化率×2HP+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 +197,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 */
/** 基础怪物数(普通波 36 为上限,放松波自动 × 1.5 = 54 */
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 技能池 idBoss 波专用,随机挂载) */
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: 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 },
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"] },
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"] },
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"] },
// ===== 第四循环:终极阶段 =====
// 放松波
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"] },
// 压力波:最终 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"] },
};
// ======================== 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 +388,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 +430,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 +458,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 +489,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);
// 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) {
if (!m.isBoss && Math.random() < eliteBaseRate * eliteRateMul) {
const elite = this.pickElite();
this.applyElite(m, elite);
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);
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);
}
/**
* 获取波次槽位配置(向后兼容接口)
* 内部从 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: [] });
}
}
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 +615,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 +657,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 +680,9 @@ export class RogueSpawningEngine {
type,
hp: Math.round(baseHp * bossBonusHpMul),
ap: baseAp,
affixes: [], // 兼容字段
isBoss: true,
spawnIndex: 0,
wave_enchants: [], // 后续统一填充
batch: 0, // Boss 固定第一批
};
}
@@ -557,52 +704,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 +735,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 +757,6 @@ export const spawningEngine = new RogueSpawningEngine();
export interface IWaveSlot {
type: number;
count: number;
/** @deprecated 已废弃,新接口不再使用 */
affixes?: AffixType[];
}
/**
@@ -653,9 +767,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 +792,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;