Files
pixelheros/assets/script/game/map/MissionMonComp.ts
panFD c59ba45b5b refactor: 统一将游戏内的「波次」术语替换为「回合」
本次提交将全项目中所有与「波次」相关的术语、注释、配置描述、UI文本全部替换为「回合」,包括:
1.  技能卡牌、怪物刷新、经济系统的相关注释与变量名
2.  配置表中的波次相关常量与描述文本
3.  UI预制体中的波次显示文本
4.  所有相关的文档注释与接口说明

统一术语后,游戏的关卡流程描述将更符合常规肉鸽游戏的表达习惯,避免与「波次」字面含义混淆,提升代码可读性与团队协作一致性。
2026-08-09 14:51:48 +08:00

312 lines
12 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.
/**
* @file MissionMonComp.ts
* @description 怪物Monster回合刷新管理组件逻辑层
*
* 职责:
* 1. 管理每一回合怪物的生成计划:根据 RogueConfig 生成怪物。
* 2. 自动推进回合在准备阶段结束时PhasePrepareEnd启动分批释放。
* 3. 分批刷怪:每回合固定 3 批,每 10 秒释放一批,批内按 MON_SPAWN_INTERVAL 逐个刷出。
*
* 关键设计:
* - 所有怪物统一从右侧 X=400 出生点逐个刷出MON_SPAWN_INTERVAL 节奏控制)。
* - 实际阵型与推进由 MonMoveComp 在战斗中向左移动自然形成,不再使用固定网格点。
* - 槽位索引3行×4列仅用于 monGrid 寻路与 SCastSystem 索敌定位。
* - 上一回合残留怪在回合结束/开始时统一清理。
*/
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 { smc } from "../common/SingletonModuleComp";
import { GameEvent } from "../common/config/GameEvent";
import { BoxSet, FacSet } from "../common/config/GameSet";
import { spawningEngine, GeneratedMonster, TestModeConfig, MAX_MONSTERS, BATCH_COUNT, BATCH_INTERVAL } from "./RogueConfig";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { MonMoveComp } from "../hero/MonMoveComp";
const { ccclass, property } = _decorator;
@ccclass('MissionMonCompComp')
@ecs.register('MissionMonComp', false)
export class MissionMonCompComp extends CCComp {
// ======================== 常量 ========================
/** 怪物出生掉落高度 */
private static readonly MON_DROP_HEIGHT = 0;
/** 怪物统一从右侧 X=400 出生点逐个刷出,随后向左推进 */
private static readonly MON_SPAWN_X = 400;
/** 逐个刷怪间隔(秒):保证怪物排成纵队,避免堆叠 */
private static readonly MON_SPAWN_INTERVAL = 0.3;
/**
* 怪物占位点:所有槽位统一以右侧出生点 (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
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 1: Col1-Mid
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 2: Col1-Bot
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 3: Col2-Top
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 4: Col2-Mid
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 5: Col2-Bot
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 6: Col3-Top
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 7: Col3-Mid
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 8: Col3-Bot
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 9: Col4-Top
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 10: Col4-Mid
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 11: Col4-Bot
];
// ======================== 编辑器属性 ========================
@property({ tooltip: "是否启用调试日志" })
private debugMode: boolean = false;
// ======================== 运行时状态 ========================
/** 全局生成顺序计数器(用于渲染层级排序) */
private globalSpawnOrder: number = 0;
/** 当前回合数 */
private currentWave: number = 0;
/** 当前回合的目标怪物总数 */
private waveTargetCount: number = 0;
/** 当前回合已生成的怪物数量 */
private waveSpawnedCount: number = 0;
/** 等待生成的怪物队列按批次分组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;
// ======================== 生命周期 ========================
onLoad() {
this.on(GameEvent.FightReady, this.fight_ready, this);
this.on("PhasePrepareEnd", this.onPhasePrepareEnd, this);
this.on("TimeUpAdvanceWave", this.onTimeUpAdvanceWave, this);
}
protected update(dt: number): void {
// 统计待刷出的怪物总数(未释放的批次 + 正在释放的队列)
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) {
this.spawnTimer += dt;
if (this.spawnTimer >= MissionMonCompComp.MON_SPAWN_INTERVAL) {
this.spawnTimer = 0;
const monData = this.spawnQueue.shift()!;
const targetPosIndex = this.waveSpawnedCount % MissionMonCompComp.MON_POSITIONS.length;
this.addMonsterAtGrid(targetPosIndex, monData, this.currentWave);
this.waveSpawnedCount++;
}
}
}
start() {}
private setupWaveData(monsters: GeneratedMonster[]) {
// 按批次分组
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}`);
oops.message.dispatchEvent(GameEvent.NewWave, {
wave: this.currentWave,
total: this.waveTargetCount,
bossWave: hasBoss,
});
}
/**
* 战斗准备:重置所有运行时状态并开始第一回合。
*/
fight_ready() {
smc.vmdata.mission_data.mon_num = 0;
smc.mission.stop_spawn_mon = false;
this.globalSpawnOrder = 0;
this.currentWave = 1;
this.waveTargetCount = 0;
this.waveSpawnedCount = 0;
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);
if (TestModeConfig.enable) {
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] 测试模式已开启");
}
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System");
}
// ======================== 回合管理 ========================
/**
* 开始下一回合:回合数 +1 并预生成数据
*/
private onTimeUpAdvanceWave() {
this.currentWave += 1;
smc.vmdata.mission_data.level = this.currentWave;
const monsters = spawningEngine.generateWave(this.currentWave);
this.setupWaveData(monsters);
}
private onPhasePrepareEnd() {
this.resetSlotSpawnData();
// 准备结束阶段:启动分批释放,
// 第一批立即转入 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;
}
// ======================== 槽位管理 ========================
/**
* 清理上一回合残留怪物,并重置生成计数与逐个刷怪队列
*/
private resetSlotSpawnData() {
ecs.query(ecs.allOf(HeroAttrsComp)).forEach(e => {
const attrs = e.get(HeroAttrsComp);
if (attrs && attrs.fac === FacSet.MON && !attrs.is_dead) {
e.destroy();
}
});
this.waveSpawnedCount = 0;
// 同步丢弃上一回合未释放的队列,避免与新一回合混合
this.spawnQueue = [];
this.spawnTimer = 0;
this.isReleasing = false;
this.currentBatch = 0;
this.batchTimer = 0;
}
// ======================== 怪物生成 ========================
/**
* 在指定位置索引处生成一个怪物
*/
private addMonsterAtGrid(
posIndex: number,
monData: GeneratedMonster,
monLv: number = 1
) {
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;
// 技能套装注入:通过 _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;
}
// 应用新引擎计算好的最终属性
const model = mon.get(HeroAttrsComp);
if (model) {
model.ap = monData.ap;
model.hp_max = monData.hp;
model.hp = model.hp_max;
}
}
/** ECS 组件移除时触发 */
reset() {}
}