feat(关卡): 实现基于波次的怪物生成系统

- 将时间轴刷怪改为波次制,每波生成固定数量普通怪
- 每若干波生成一个Boss,Boss波次可配置
- 在界面时间显示前添加当前波次信息
- 添加新波次开始时的事件通知机制
- 调整卡片预制件的Y坐标以适应新布局
This commit is contained in:
walkpan
2026-03-26 21:19:12 +08:00
parent 3963a8f3ba
commit 4fdb424bc4
5 changed files with 2696 additions and 2114 deletions

View File

@@ -2294,7 +2294,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": -108.057,
"y": -136.25,
"z": 0
},
"_lrot": {
@@ -2592,7 +2592,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": -108.057,
"y": -136.25,
"z": 0
},
"_lrot": {

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,8 @@ export class MissionCardComp extends CCComp {
private readonly buttonClickScale: number = 1.06;
/** 四个插卡槽位固定顺序分发1~4 */
@property(Node)
cards_node:Node = null!
@property(Node)
card1:Node = null!
@property(Node)
card2:Node = null!

View File

@@ -60,6 +60,7 @@ export class MissionComp extends CCComp {
private heapTrendTimer: number = 0;
private heapTrendBaseMB: number = -1;
private monsterCountSyncTimer: number = 0;
private currentWave: number = 0;
private readonly heroViewMatcher = ecs.allOf(HeroViewComp);
private readonly skillViewMatcher = ecs.allOf(SkillView);
private readonly heroAttrsMatcher = ecs.allOf(HeroAttrsComp);
@@ -72,6 +73,7 @@ export class MissionComp extends CCComp {
// this.on(GameEvent.HeroDead,this.do_hero_dead,this)
// this.on(GameEvent.FightEnd,this.fight_end,this)
this.on(GameEvent.MissionEnd,this.mission_end,this)
this.on(GameEvent.NewWave,this.onNewWave,this)
this.on(GameEvent.DO_AD_BACK,this.do_ad,this)
this.removeMemoryPanel()
}
@@ -94,7 +96,8 @@ export class MissionComp extends CCComp {
this.lastTimeSecond = remainSecond;
let m = Math.floor(remainSecond / 60);
let s = remainSecond % 60;
let str = `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
const wave = Math.max(1, this.currentWave || smc.vmdata.mission_data.level || 1);
let str = `W${wave.toString().padStart(2, '0')} ${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
if(str != this.lastTimeStr){
this.time_node.getChildByName("time").getComponent(Label).string = str;
this.lastTimeStr = str;
@@ -125,10 +128,9 @@ export class MissionComp extends CCComp {
this.unscheduleAllCallbacks();
// 确保清理上一局的残留实体
this.cleanComponents();
oops.message.dispatchEvent(GameEvent.FightReady)
this.node.active=true
this.data_init()
oops.message.dispatchEvent(GameEvent.FightReady)
let loading=this.node.parent.getChildByName("loading")
loading.active=true
this.scheduleOnce(()=>{
@@ -197,6 +199,7 @@ export class MissionComp extends CCComp {
smc.vmdata.mission_data.mon_num=0
smc.vmdata.mission_data.level=0
smc.vmdata.mission_data.mon_max = Math.max(1, Math.floor(this.maxMonsterCount))
this.currentWave = 0;
this.FightTime=FightSet.FiIGHT_TIME
this.rewards=[] // 改为数组,用于存储掉落物品列表
this.revive_times = 1; // 每次任务开始重置复活次数
@@ -221,6 +224,15 @@ export class MissionComp extends CCComp {
// mLogger.log(this.debugMode, 'MissionComp', "局内数据初始化",smc.vmdata.mission_data)
}
private onNewWave(event: string, data: any) {
const wave = Number(data?.wave ?? 0);
if (wave <= 0) return;
this.currentWave = wave;
smc.vmdata.mission_data.level = wave;
this.lastTimeSecond = -1;
this.update_time();
}
private getMonsterThresholds(): { max: number; resume: number } {
const max = Math.max(1, Math.floor(this.maxMonsterCount));
const resume = Math.min(max - 1, Math.max(0, Math.floor(this.resumeMonsterCount)));

View File

@@ -2,12 +2,13 @@ import { _decorator, v3, Vec3 } from "cc";
import { mLogger } from "../common/Logger";
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 "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { Monster } from "../hero/Mon";
import { HeroInfo, MonStart } from "../common/config/heroSet";
import { smc } from "../common/SingletonModuleComp";
import { GameEvent } from "../common/config/GameEvent";
import {BoxSet } from "../common/config/GameSet";
import { BossList, BossSpawnTimeline, MonList, MonType, SpawnBaseCd, SpawnMinCd, SpawnPowerBias, SpawnStageReduce, StageBossGrow, StageDuration, StageGrow, UpType } from "./RogueConfig";
import { BossList, MonList, MonType, SpawnPowerBias, StageBossGrow, StageGrow, UpType } from "./RogueConfig";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { MoveComp } from "../hero/MoveComp";
const { ccclass, property } = _decorator;
@@ -20,6 +21,14 @@ export class MissionMonCompComp extends CCComp {
private static readonly MON_DROP_HEIGHT = 300;
@property({ tooltip: "是否启用调试日志" })
private debugMode: boolean = false;
@property({ tooltip: "每波基础普通怪数量" })
private baseMonstersPerWave: number = 5;
@property({ tooltip: "每波额外增加普通怪数量" })
private waveMonsterGrowth: number = 1;
@property({ tooltip: "多少波刷新一次 Boss" })
private bossWaveInterval: number = 5;
@property({ tooltip: "同一波内刷怪间隔(秒)" })
private waveSpawnCd: number = 0.35;
// 刷怪队列(用于插队生成:比如运营活动怪、技能召唤怪、剧情强制怪)
// 约定:队列里的怪会优先于常规刷新处理
@@ -31,18 +40,15 @@ export class MissionMonCompComp extends CCComp {
private spawnCount: number = 0; // 总生成计数,用于控制横向分布位置
/** 全局生成顺序计数器,用于层级管理(预留) */
private globalSpawnOrder: number = 0;
/** 局内战斗运行时间(秒) */
private gameTime: number = 0;
/** 普通怪刷新计时器 */
private waveTimer: number = 0;
/** 插队刷怪处理计时器 */
private queueTimer: number = 0;
private nextBossSpawnIndex: number = 0;
private waveSpawnTimer: number = 0;
private currentWave: number = 0;
private waveTargetCount: number = 0;
private waveSpawnedCount: number = 0;
private bossSpawnedInWave: boolean = false;
onLoad(){
// 关卡准备、切换波次时重置刷怪状态
this.on(GameEvent.FightReady,this.fight_ready,this)
this.on(GameEvent.NewWave,this.fight_ready,this)
// 支持外部模块主动推入特殊怪(优先级高于常规刷新)
this.on("SpawnSpecialMonster", this.onSpawnSpecialMonster, this);
}
@@ -65,34 +71,30 @@ export class MissionMonCompComp extends CCComp {
}
fight_ready(){
// 仅重置“本组件”刷怪状态,不处理其他系统状态
smc.vmdata.mission_data.mon_num=0
smc.mission.stop_spawn_mon = false
this.globalSpawnOrder = 0
this.gameTime = 0
this.waveTimer = 0
this.queueTimer = 0
this.nextBossSpawnIndex = 0
this.waveSpawnTimer = 0
this.currentWave = 0
this.waveTargetCount = 0
this.waveSpawnedCount = 0
this.bossSpawnedInWave = false
this.MonQueue = []
this.spawnCount = 0
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System (15-min Cycle)");
this.startNextWave()
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System");
}
protected update(dt: number): void {
// 统一战斗状态门禁:未开战/暂停/角色死亡阶段均不刷怪
if(!smc.mission.play) return
if(smc.mission.pause) return
if(smc.mission.stop_mon_action) return;
if(!smc.mission.in_fight) return;
// 计时推进:所有“按时间驱动”的曲线都依赖 gameTime
this.gameTime += dt;
this.updateBossSpawn();
this.tryAdvanceWave();
if(smc.mission.stop_spawn_mon) return;
this.updateSpecialQueue(dt);
this.updateNormalSpawn(dt);
this.updateWaveSpawn(dt);
}
private updateSpecialQueue(dt: number) {
@@ -109,43 +111,55 @@ export class MissionMonCompComp extends CCComp {
this.spawnCount += 1;
}
private updateNormalSpawn(dt: number) {
this.waveTimer += dt;
// 普通怪刷新间隔会随 stage 增长而缩短
const spawnCd = this.getCurrentSpawnCd();
if (this.waveTimer < spawnCd) return;
this.waveTimer = 0;
// 先随机怪物类型,再在该类型池内随机具体 uuid
private updateWaveSpawn(dt: number) {
this.waveSpawnTimer += dt;
if (this.waveSpawnTimer < this.waveSpawnCd) return;
this.waveSpawnTimer = 0;
if (this.waveSpawnedCount >= this.waveTargetCount) {
if (this.isBossWave() && !this.bossSpawnedInWave) {
const bossUuid = this.getRandomBossUuid();
const bossUpType = this.getRandomUpType();
this.addMonster(bossUuid, this.spawnCount, true, bossUpType);
this.spawnCount += 1;
this.bossSpawnedInWave = true;
}
return;
}
const uuid = this.getRandomNormalMonsterUuid();
// 每只怪独立抽取成长方案,使同时间段怪群有轻微离散性
const upType = this.getRandomUpType();
this.addMonster(uuid, this.spawnCount, false, upType);
this.waveSpawnedCount += 1;
this.spawnCount += 1;
}
private updateBossSpawn() {
while (
this.nextBossSpawnIndex < BossSpawnTimeline.length &&
this.gameTime >= BossSpawnTimeline[this.nextBossSpawnIndex]
) {
const uuid = this.getRandomBossUuid();
const upType = this.getRandomUpType();
this.addMonster(uuid, this.spawnCount, true, upType);
this.spawnCount += 1;
this.nextBossSpawnIndex += 1;
}
private startNextWave() {
this.currentWave += 1;
smc.vmdata.mission_data.level = this.currentWave;
this.waveTargetCount = Math.max(1, this.baseMonstersPerWave + (this.currentWave - 1) * this.waveMonsterGrowth);
this.waveSpawnedCount = 0;
this.bossSpawnedInWave = false;
this.waveSpawnTimer = this.waveSpawnCd;
oops.message.dispatchEvent(GameEvent.NewWave, {
wave: this.currentWave,
total: this.waveTargetCount,
bossWave: this.isBossWave(),
});
}
private tryAdvanceWave() {
if (this.waveSpawnedCount < this.waveTargetCount) return;
if (this.isBossWave() && !this.bossSpawnedInWave) return;
if (smc.vmdata.mission_data.mon_num > 0) return;
this.startNextWave();
}
private isBossWave() {
const interval = Math.max(1, Math.floor(this.bossWaveInterval));
return this.currentWave > 0 && this.currentWave % interval === 0;
}
private getCurrentStage(): number {
// 每 30 秒提升 1 阶(由配置 StageDuration 控制)
return Math.floor(this.gameTime / StageDuration);
}
private getCurrentSpawnCd(): number {
const stage = this.getCurrentStage();
const cd = SpawnBaseCd - stage * SpawnStageReduce;
// 保护下限,避免后期刷新间隔过小打爆性能
return Math.max(SpawnMinCd, cd);
return Math.max(0, this.currentWave - 1);
}
private getRandomUpType(): UpType {