Files
pixelheros/assets/script/game/map/MissionMonComp.ts
walkpan 4fdb424bc4 feat(关卡): 实现基于波次的怪物生成系统
- 将时间轴刷怪改为波次制,每波生成固定数量普通怪
- 每若干波生成一个Boss,Boss波次可配置
- 在界面时间显示前添加当前波次信息
- 添加新波次开始时的事件通知机制
- 调整卡片预制件的Y坐标以适应新布局
2026-03-26 21:19:12 +08:00

245 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, MonList, MonType, SpawnPowerBias, StageBossGrow, StageGrow, UpType } from "./RogueConfig";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { MoveComp } from "../hero/MoveComp";
const { ccclass, property } = _decorator;
/** 视图层对象 */
@ccclass('MissionMonCompComp')
@ecs.register('MissionMonComp', false)
export class MissionMonCompComp extends CCComp {
private static readonly BOSS_RENDER_PRIORITY = 1000000;
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;
// 刷怪队列(用于插队生成:比如运营活动怪、技能召唤怪、剧情强制怪)
// 约定:队列里的怪会优先于常规刷新处理
private MonQueue: Array<{
uuid: number,
level: number,
}> = [];
private spawnCount: number = 0; // 总生成计数,用于控制横向分布位置
/** 全局生成顺序计数器,用于层级管理(预留) */
private globalSpawnOrder: number = 0;
/** 插队刷怪处理计时器 */
private queueTimer: 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("SpawnSpecialMonster", this.onSpawnSpecialMonster, this);
}
/**
* 接收特殊刷怪事件并入队
* 事件数据最小结构:{ uuid, level }
*/
private onSpawnSpecialMonster(event: string, args: any) {
if (!args) return;
mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] 收到特殊刷怪指令:`, args);
this.MonQueue.push({
uuid: args.uuid,
level: args.level,
});
// 让队列在下一帧附近尽快消费,提升事件响应感
this.queueTimer = 1.0;
}
start() {
}
fight_ready(){
smc.vmdata.mission_data.mon_num=0
smc.mission.stop_spawn_mon = false
this.globalSpawnOrder = 0
this.queueTimer = 0
this.waveSpawnTimer = 0
this.currentWave = 0
this.waveTargetCount = 0
this.waveSpawnedCount = 0
this.bossSpawnedInWave = false
this.MonQueue = []
this.spawnCount = 0
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;
this.tryAdvanceWave();
if(smc.mission.stop_spawn_mon) return;
this.updateSpecialQueue(dt);
this.updateWaveSpawn(dt);
}
private updateSpecialQueue(dt: number) {
if (this.MonQueue.length <= 0) return;
this.queueTimer += dt;
// 轻微节流,避免同帧内突发大量插队导致瞬间堆怪
if (this.queueTimer < 0.15) return;
this.queueTimer = 0;
const item = this.MonQueue.shift();
if (!item) return;
// 特殊怪同样走随机成长类型,保持局内随机性一致
const upType = this.getRandomUpType();
this.addMonster(item.uuid, this.spawnCount, BossList.includes(item.uuid), upType);
this.spawnCount += 1;
}
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 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 {
return Math.max(0, this.currentWave - 1);
}
private getRandomUpType(): UpType {
// 从 StageGrow 的 key 中采样,保证新增配置无需改逻辑
const keys = Object.keys(StageGrow).map(v => Number(v) as UpType);
const index = Math.floor(Math.random() * keys.length);
return keys[index] ?? UpType.AP1_HP1;
}
private getRandomNormalMonsterUuid(): number {
// MonType 是常量对象,这里通过值采样拿到怪物类型 id
const typeKeys = Object.keys(MonType).map(k => (MonType as any)[k]).filter(v => typeof v === "number");
const randomType = typeKeys[Math.floor(Math.random() * typeKeys.length)] as number;
// 如果某类型配置被清空,回退到 AP 类型,避免空池异常
const pool = MonList[randomType] || MonList[MonType.AP];
const index = Math.floor(Math.random() * pool.length);
return pool[index];
}
private getRandomBossUuid(): number {
// 目前 Boss 池可扩展为多个,先走随机抽取
const index = Math.floor(Math.random() * BossList.length);
return BossList[index];
}
private resolveGrowPair(upType: UpType, isBoss: boolean): [number, number] {
// 普通怪基础成长StageGrow
const grow = StageGrow[upType] || StageGrow[UpType.AP1_HP1];
if (!isBoss) return [grow[0], grow[1]];
// Boss 额外成长StageBossGrow在普通成长上叠加
const bossGrow = StageBossGrow[upType] || StageBossGrow[UpType.AP1_HP1];
return [grow[0] + bossGrow[0], grow[1] + bossGrow[1]];
}
private getSpawnPowerBias(): number {
// 动态难度偏差入口:当前固定读取配置,后续可切到玩家表现驱动
return SpawnPowerBias;
}
private addMonster(
uuid: number = 1001,
i: number = 0,
isBoss: boolean = false,
upType: UpType = UpType.AP1_HP1,
) {
// 创建 ECS 怪物实体
let mon = ecs.getEntity<Monster>(Monster);
let scale = -1;
// 按生成序号做横向错列,减轻重叠感
const landingY = BoxSet.GAME_LINE + (isBoss ? 6 : 0);
const dropOffset = MissionMonCompComp.MON_DROP_HEIGHT + Math.floor(Math.random() * 40);
let pos: Vec3 = v3(MonStart.START_X, landingY + dropOffset, 0);
// 递增全局生成顺序,做溢出保护
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
mon.load(pos, scale, uuid, isBoss, landingY);
const move = mon.get(MoveComp);
if (move) {
move.spawnOrder = isBoss
? MissionMonCompComp.BOSS_RENDER_PRIORITY + this.globalSpawnOrder
: this.globalSpawnOrder;
}
const model = mon.get(HeroAttrsComp);
const base = HeroInfo[uuid];
if (!model || !base) return;
const stage = this.getCurrentStage();
const grow = this.resolveGrowPair(upType, isBoss);
// 偏差值用于整体系数缩放1=不变,>1增强<1减弱
const bias = Math.max(0.1, this.getSpawnPowerBias());
// 最终公式:基础值 + 阶段成长,再乘偏差
model.ap = Math.max(1, Math.floor((base.ap + stage * grow[0]) * bias));
model.hp_max = Math.max(1, Math.floor((base.hp + stage * grow[1]) * bias));
// 满血登场,保证 hp/hp_max 一致
model.hp = model.hp_max;
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() {
// this.node.destroy();
}
}