refactor: 重构关卡战斗流程与AI逻辑,改为单场Boss战模式
1. 近战AI新增警戒线机制,无威胁时退回编队 2. 替换多回合波次为单场Boss战,简化刷怪与回合逻辑 3. 重构招募系统为开局免费三选一流程 4. 调整英雄与怪物基础属性平衡 5. 精简HUD与配置文件冗余代码
This commit is contained in:
@@ -1,19 +1,15 @@
|
||||
/**
|
||||
* @file MissionMonComp.ts
|
||||
* @description 怪物(Monster)回合刷新管理组件(逻辑层)
|
||||
* @description 怪物(Monster)登场管理组件(逻辑层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 管理每一回合怪物的生成计划:根据 RogueConfig 生成怪物。
|
||||
* 2. 自动推进回合:在准备阶段结束时(PhasePrepareEnd)启动分批释放。
|
||||
* 3. 持续刷怪:回合开始整波进入队列,30 秒内按速度曲线匀速刷出(铺垫 → 加压 → 高潮)。
|
||||
* 1. 预生成战斗怪物:FightReady 时通过 RogueSpawningEngine.generateBossCombo 随机一组 Boss 组合,并立即登场展示(供玩家侦查后再选英雄)。
|
||||
* 2. 战斗开始:PhasePrepareEnd 时恢复怪物行动(in_fight 前怪物仅原地待命,不会推进/索敌)。
|
||||
*
|
||||
* 关键设计:
|
||||
* - 所有怪物统一从右侧 X=400 出生点刷出,速度曲线由 m.batch 驱动(前慢后快,压轴批自动高潮)。
|
||||
* - 每帧欠账式补刷(期望数 − 已刷数),自动补偿帧余量,保证 30 秒预算不流失。
|
||||
* - 场上怪物超阈值(stop_spawn_mon)时暂停推进,恢复后瞬时补齐欠账。
|
||||
* - 5 只怪在 X∈[100,320] 内按登场序号均匀铺开,Boss/小怪同时登场,玩家看清阵容后三选一。
|
||||
* - 实际阵型与推进由 MonMoveComp 在战斗中向左移动自然形成,不再使用固定网格点。
|
||||
* - 槽位索引(3行×4列)仅用于 monGrid 寻路与 SCastSystem 索敌定位。
|
||||
* - 上一回合残留怪在回合结束/开始时统一清理。
|
||||
* - 槽位索引仅用于 monGrid 寻路与 SCastSystem 索敌定位。
|
||||
*/
|
||||
import { _decorator, v3, Vec3 } from "cc";
|
||||
import { mLogger } from "../common/Logger";
|
||||
@@ -23,8 +19,8 @@ import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/O
|
||||
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, WAVE_DURATION } from "./RogueConfig";
|
||||
import { BoxSet } from "../common/config/GameSet";
|
||||
import { spawningEngine, GeneratedMonster, TestModeConfig, BOSS_COUNT, MINION_COUNT } from "./RogueConfig";
|
||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||
import { MonMoveComp } from "../hero/MonMoveComp";
|
||||
|
||||
@@ -37,28 +33,25 @@ export class MissionMonCompComp extends CCComp {
|
||||
|
||||
/** 怪物出生掉落高度 */
|
||||
private static readonly MON_DROP_HEIGHT = 0;
|
||||
/** 怪物统一从右侧 X=400 出生点逐个刷出,随后向左推进 */
|
||||
private static readonly MON_SPAWN_X = 400;
|
||||
/** 怪物登场 X 范围下限(靠近英雄侧) */
|
||||
private static readonly SPAWN_X_MIN = 100;
|
||||
/** 怪物登场 X 范围上限(靠近右侧) */
|
||||
private static readonly SPAWN_X_MAX = 320;
|
||||
|
||||
/**
|
||||
* 怪物占位点:所有槽位统一以右侧出生点 (X=400) 作为坐标,
|
||||
* 实际阵型由 MonMoveComp 在战斗中向左推进时自然拉开。
|
||||
* 槽位索引仍保留 3 行 × 4 列结构,供 monGrid 寻路与 SCastSystem 索敌使用。
|
||||
* 超出 12 个槽位的怪物(放松回合)按 spawnIndex % 12 复用槽位。
|
||||
*/
|
||||
/** 槽位基准 Y(3行×4列结构仅供 monGrid 寻路与 SCastSystem 索敌定位) */
|
||||
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
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 0: Col1-Top
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 1: Col1-Mid
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 2: Col1-Bot
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 3: Col2-Top
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 4: Col2-Mid
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 5: Col2-Bot
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 6: Col3-Top
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 7: Col3-Mid
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 8: Col3-Bot
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 9: Col4-Top
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 10: Col4-Mid
|
||||
v3(0, BoxSet.GAME_LINE, 0), // index 11: Col4-Bot
|
||||
];
|
||||
|
||||
// ======================== 编辑器属性 ========================
|
||||
@@ -70,107 +63,30 @@ export class MissionMonCompComp extends CCComp {
|
||||
|
||||
/** 全局生成顺序计数器(用于渲染层级排序) */
|
||||
private globalSpawnOrder: number = 0;
|
||||
/** 当前回合数 */
|
||||
private currentWave: number = 0;
|
||||
/** 当前回合的目标怪物总数 */
|
||||
/** 战斗场次(本局恒为 1,保留供 HUD/事件协议) */
|
||||
private currentWave: number = 1;
|
||||
/** 本场战斗的怪物总数 */
|
||||
private waveTargetCount: number = 0;
|
||||
/** 当前回合已生成的怪物数量 */
|
||||
/** 已登场的怪物数量 */
|
||||
private waveSpawnedCount: number = 0;
|
||||
/** 待刷队列(回合开始时整波进入,按速度曲线匀速刷出) */
|
||||
/** 待登场队列(FightReady 时整组进入,PhasePrepareEnd 时一次全量刷出) */
|
||||
private spawnQueue: GeneratedMonster[] = [];
|
||||
/** 本回合已推进的战斗时间(秒,暂停刷怪时冻结) */
|
||||
private waveTime: number = 0;
|
||||
/** 各批次在队列中的起始下标(用于速度曲线按批变速) */
|
||||
private batchStarts: number[] = [0, 0, 0];
|
||||
/** 是否正在刷怪释放中 */
|
||||
private isReleasing: boolean = false;
|
||||
/** 当前批已刷出的怪物数(清场加速存活比例分母) */
|
||||
private batchReleasedCount: number = 0;
|
||||
/** 当前批是否已通过清场加速提前推进过(每批只触发一次) */
|
||||
private batchFastForwarded: boolean = false;
|
||||
/** 清场加速检测节流计时器(秒) */
|
||||
private aliveCheckTimer: number = 0;
|
||||
/** 本回合因清场加速累计节省的秒数(供 MissionComp 还原 DDA 判定口径,避免双重惩罚) */
|
||||
public waveEarlySkipTotal: number = 0;
|
||||
/** 清场加速触发阈值:当前批存活比例低于此值时提前推进(0.25 = 清掉 75%) */
|
||||
private static readonly BATCH_EARLY_RATIO = 0.25;
|
||||
/** 清场加速提前量(秒):等效推进时间快进,保底不被瞬间叠爆 */
|
||||
private static readonly BATCH_EARLY_SKIP = 2.0;
|
||||
/** 批次时间边界(秒):batch0 → [0, T1),batch1 → [T1, T2),batch2 → [T2, WAVE_DURATION] */
|
||||
private static readonly BATCH_T1 = WAVE_DURATION * 0.25;
|
||||
private static readonly BATCH_T2 = WAVE_DURATION * 0.60;
|
||||
|
||||
// ======================== 生命周期 ========================
|
||||
|
||||
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 {
|
||||
// 待刷出统计(回合结束检测与 HUD 进度消费,必须在 return 前执行保证语义不受暂停影响)
|
||||
// 待登场统计(战斗结束检测与 HUD 进度消费)
|
||||
smc.vmdata.mission_data.pending_mon_num = this.spawnQueue.length;
|
||||
|
||||
// 场上怪物超阈值时暂停推进:冻结 waveTime,恢复后欠账式瞬时补齐,30 秒预算不流失
|
||||
if (smc.mission.stop_spawn_mon) return;
|
||||
if (!this.isReleasing) return;
|
||||
|
||||
this.waveTime += dt;
|
||||
|
||||
// 清场加速:0.2s 节流检测当前段存活比例,清得快则快进推进时间(压缩垃圾时间)
|
||||
this.aliveCheckTimer += dt;
|
||||
if (this.aliveCheckTimer >= 0.2) {
|
||||
this.aliveCheckTimer = 0;
|
||||
this.checkEarlyAdvance();
|
||||
}
|
||||
|
||||
// 欠账式补刷:期望累计刷出数 − 已刷数,自动补偿帧余量与暂停期间欠账
|
||||
let expected = this.expectedSpawned(this.waveTime);
|
||||
while (this.waveSpawnedCount < expected && this.spawnQueue.length > 0) {
|
||||
const monData = this.spawnQueue.shift()!;
|
||||
const targetPosIndex = this.waveSpawnedCount % MissionMonCompComp.MON_POSITIONS.length;
|
||||
this.addMonsterAtGrid(targetPosIndex, monData, this.currentWave);
|
||||
this.waveSpawnedCount++;
|
||||
}
|
||||
if (this.spawnQueue.length === 0) {
|
||||
this.isReleasing = false;
|
||||
}
|
||||
}
|
||||
|
||||
start() { }
|
||||
|
||||
private setupWaveData(monsters: GeneratedMonster[]) {
|
||||
// 按批次排序后整波入队,记录各批起始下标供速度曲线按批变速(batch 越大越靠后压轴)
|
||||
const sorted = monsters.slice().sort((a, b) => a.batch - b.batch);
|
||||
this.spawnQueue = sorted;
|
||||
const starts = [sorted.length, sorted.length, sorted.length];
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const b = Math.min(sorted[i].batch, 2);
|
||||
if (i < starts[b]) starts[b] = i;
|
||||
}
|
||||
// 空批起始下标前推到下一批起点(防 expectedSpawned 区间错乱)
|
||||
for (let b = 1; b >= 0; b--) {
|
||||
if (starts[b] === sorted.length) starts[b] = starts[b + 1];
|
||||
}
|
||||
this.batchStarts = starts;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 战斗准备:重置所有运行时状态并开始第一回合。
|
||||
* 战斗准备:重置运行时状态并预生成本场 Boss 组合。
|
||||
*/
|
||||
fight_ready() {
|
||||
smc.vmdata.mission_data.mon_num = 0;
|
||||
@@ -180,133 +96,39 @@ export class MissionMonCompComp extends CCComp {
|
||||
this.waveTargetCount = 0;
|
||||
this.waveSpawnedCount = 0;
|
||||
this.spawnQueue = [];
|
||||
this.batchStarts = [0, 0, 0];
|
||||
this.waveTime = 0;
|
||||
this.isReleasing = false;
|
||||
this.batchReleasedCount = 0;
|
||||
this.batchFastForwarded = false;
|
||||
this.aliveCheckTimer = 0;
|
||||
this.waveEarlySkipTotal = 0;
|
||||
|
||||
// 预生成第一回合数据以获取数量和 Boss 信息
|
||||
const monsters = spawningEngine.generateWave(this.currentWave);
|
||||
this.setupWaveData(monsters);
|
||||
// 随机一组 Boss 组合(最终 Boss 战强度)
|
||||
const monsters = spawningEngine.generateBossCombo();
|
||||
this.spawnQueue = monsters;
|
||||
this.waveTargetCount = monsters.length;
|
||||
smc.vmdata.mission_data.pending_mon_num = this.waveTargetCount;
|
||||
|
||||
// 怪物先行登场:玩家侦查 Boss 组合后再做英雄三选一(in_fight 前怪物原地待命,不推进不索敌)
|
||||
const squad = this.spawnQueue;
|
||||
this.spawnQueue = [];
|
||||
smc.vmdata.mission_data.pending_mon_num = 0;
|
||||
for (const monData of squad) {
|
||||
this.addMonsterAtGrid(this.waveSpawnedCount, monData, this.currentWave);
|
||||
this.waveSpawnedCount++;
|
||||
}
|
||||
|
||||
if (TestModeConfig.enable) {
|
||||
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] 测试模式已开启");
|
||||
}
|
||||
mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] Boss 组合生成完成,怪物总数: ${this.waveTargetCount}`);
|
||||
|
||||
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() {
|
||||
// 准备结束:清残留怪与计时,启动持续刷怪(队列已在 setupWaveData 整波入队,不能清)
|
||||
this.resetBattleTimer();
|
||||
this.isReleasing = true;
|
||||
this.waveTime = 0;
|
||||
}
|
||||
|
||||
// ======================== 持续刷怪 ========================
|
||||
|
||||
/**
|
||||
* 速度曲线:按 waveTime 所在时间段返回"到当前时刻应累计刷出的怪物数"。
|
||||
* 三段时间对应三批(铺垫 → 加压 → 高潮),批内匀速;m.batch 只影响排序,不再切断节奏。
|
||||
*/
|
||||
private expectedSpawned(t: number): number {
|
||||
const total = this.waveTargetCount;
|
||||
if (total <= 0) return 0;
|
||||
const [s0, s1] = [this.batchStarts[0], this.batchStarts[1]];
|
||||
const s2 = this.batchStarts[2];
|
||||
const T1 = MissionMonCompComp.BATCH_T1;
|
||||
const T2 = MissionMonCompComp.BATCH_T2;
|
||||
if (t < T1) {
|
||||
const segTotal = s1 - s0;
|
||||
const segTime = T1;
|
||||
return s0 + Math.min(segTotal, Math.floor(t / segTime * segTotal));
|
||||
} else if (t < T2) {
|
||||
const segTotal = s2 - s1;
|
||||
const segTime = T2 - T1;
|
||||
return s1 + Math.min(segTotal, Math.floor((t - T1) / segTime * segTotal));
|
||||
} else {
|
||||
const segTotal = total - s2;
|
||||
const segTime = WAVE_DURATION - T2;
|
||||
return s2 + Math.min(segTotal, Math.floor((t - T2) / segTime * segTotal));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清场加速检测:当前时间段已刷完且存活比例 ≤ BATCH_EARLY_RATIO 时,快进推进时间。
|
||||
* 只奖励"清得快"的 build(压缩垃圾时间),弱 build 清不完则不触发、不叠加压力。
|
||||
*/
|
||||
private checkEarlyAdvance() {
|
||||
if (this.batchFastForwarded) return;
|
||||
if (this.spawnQueue.length === 0) return;
|
||||
// 当前时间段的 quota 是否已刷完:用 expectedSpawned 反推
|
||||
const expected = this.expectedSpawned(this.waveTime);
|
||||
if (this.waveSpawnedCount < expected) return; // 本时间段还没放完,不判断
|
||||
if (this.batchReleasedCount <= 0) {
|
||||
this.batchReleasedCount = Math.max(1, expected);
|
||||
}
|
||||
|
||||
let alive = 0;
|
||||
ecs.query(ecs.allOf(HeroAttrsComp)).forEach(e => {
|
||||
const a = e.get(HeroAttrsComp);
|
||||
if (a && a.fac === FacSet.MON && !a.is_dead) alive++;
|
||||
oops.message.dispatchEvent(GameEvent.NewWave, {
|
||||
wave: this.currentWave,
|
||||
total: this.waveTargetCount,
|
||||
bossWave: monsters.some(m => m.isBoss),
|
||||
});
|
||||
// alive 含前几批残留,比例 clamp 防低估
|
||||
const aliveRatio = Math.min(1, alive / this.batchReleasedCount);
|
||||
if (aliveRatio <= MissionMonCompComp.BATCH_EARLY_RATIO) {
|
||||
this.batchFastForwarded = true;
|
||||
this.waveTime += MissionMonCompComp.BATCH_EARLY_SKIP;
|
||||
this.waveEarlySkipTotal += MissionMonCompComp.BATCH_EARLY_SKIP;
|
||||
smc.vmdata.mission_data.wave_early_skip = this.waveEarlySkipTotal;
|
||||
mLogger.log(this.debugMode, 'MissionMonComp',
|
||||
`[EarlyAdvance] waveTime=${this.waveTime.toFixed(1)} alive=${alive}/${this.batchReleasedCount},快进 ${MissionMonCompComp.BATCH_EARLY_SKIP}s`);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 槽位管理 ========================
|
||||
|
||||
/**
|
||||
* 清理上一回合残留怪物,并重置战斗计时与清场加速状态。
|
||||
* 注意:spawnQueue / batchStarts / waveTargetCount 由 setupWaveData 在 Prepare 阶段写入,
|
||||
* 本函数不能触碰,否则 PhasePrepareEnd 会清空未刷队列导致回合直接判空。
|
||||
*/
|
||||
private resetBattleTimer() {
|
||||
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.waveTime = 0;
|
||||
this.isReleasing = false;
|
||||
this.batchReleasedCount = 0;
|
||||
this.batchFastForwarded = false;
|
||||
this.aliveCheckTimer = 0;
|
||||
this.waveEarlySkipTotal = 0;
|
||||
smc.vmdata.mission_data.wave_early_skip = 0;
|
||||
}
|
||||
|
||||
// ======================== 怪物生成 ========================
|
||||
|
||||
/**
|
||||
* 在指定位置索引处生成一个怪物
|
||||
* 5 只怪在 X∈[100,320] 内按登场序号均匀铺开,Boss/小怪同时登场直接开打
|
||||
*/
|
||||
private addMonsterAtGrid(
|
||||
posIndex: number,
|
||||
@@ -317,7 +139,9 @@ export class MissionMonCompComp extends CCComp {
|
||||
let scale = -1;
|
||||
|
||||
const basePos = MissionMonCompComp.MON_POSITIONS[posIndex % MissionMonCompComp.MON_POSITIONS.length];
|
||||
const spawnX = basePos.x;
|
||||
const spawnX = MissionMonCompComp.SPAWN_X_MIN
|
||||
+ (MissionMonCompComp.SPAWN_X_MAX - MissionMonCompComp.SPAWN_X_MIN)
|
||||
* (posIndex / Math.max(1, BOSS_COUNT + MINION_COUNT - 1));
|
||||
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;
|
||||
@@ -339,7 +163,7 @@ export class MissionMonCompComp extends CCComp {
|
||||
move.spawnOrder = this.globalSpawnOrder;
|
||||
}
|
||||
|
||||
// 应用新引擎计算好的最终属性
|
||||
// 应用引擎计算好的最终属性
|
||||
const model = mon.get(HeroAttrsComp);
|
||||
if (model) {
|
||||
model.ap = monData.ap;
|
||||
|
||||
Reference in New Issue
Block a user