feat: 大幅优化战斗体验与编队系统
1. 扩展英雄/怪物网格站位到10个槽位 2. 重构默认攻击距离计算逻辑,按职业动态适配 3. 重做英雄站位与移动系统,新增同阵营间距约束 4. 调整rogue模式怪物数量与强度配置,提升战斗爽感 5. 重构刷怪逻辑为匀速曲线+欠账补刷,优化刷怪节奏
This commit is contained in:
@@ -5,10 +5,12 @@
|
||||
* 职责:
|
||||
* 1. 管理每一回合怪物的生成计划:根据 RogueConfig 生成怪物。
|
||||
* 2. 自动推进回合:在准备阶段结束时(PhasePrepareEnd)启动分批释放。
|
||||
* 3. 分批刷怪:每回合固定 3 批,每 10 秒释放一批,批内按 MON_SPAWN_INTERVAL 逐个刷出。
|
||||
* 3. 持续刷怪:回合开始整波进入队列,30 秒内按速度曲线匀速刷出(铺垫 → 加压 → 高潮)。
|
||||
*
|
||||
* 关键设计:
|
||||
* - 所有怪物统一从右侧 X=400 出生点逐个刷出(MON_SPAWN_INTERVAL 节奏控制)。
|
||||
* - 所有怪物统一从右侧 X=400 出生点刷出,速度曲线由 m.batch 驱动(前慢后快,压轴批自动高潮)。
|
||||
* - 每帧欠账式补刷(期望数 − 已刷数),自动补偿帧余量,保证 30 秒预算不流失。
|
||||
* - 场上怪物超阈值(stop_spawn_mon)时暂停推进,恢复后瞬时补齐欠账。
|
||||
* - 实际阵型与推进由 MonMoveComp 在战斗中向左移动自然形成,不再使用固定网格点。
|
||||
* - 槽位索引(3行×4列)仅用于 monGrid 寻路与 SCastSystem 索敌定位。
|
||||
* - 上一回合残留怪在回合结束/开始时统一清理。
|
||||
@@ -22,7 +24,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, MAX_MONSTERS, BATCH_COUNT, BATCH_INTERVAL, getWaveType, SPAWN_INTERVAL_BY_TYPE } from "./RogueConfig";
|
||||
import { spawningEngine, GeneratedMonster, TestModeConfig, WAVE_DURATION } from "./RogueConfig";
|
||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||
import { MonMoveComp } from "../hero/MonMoveComp";
|
||||
|
||||
@@ -37,8 +39,6 @@ export class MissionMonCompComp extends CCComp {
|
||||
private static readonly MON_DROP_HEIGHT = 0;
|
||||
/** 怪物统一从右侧 X=400 出生点逐个刷出,随后向左推进 */
|
||||
private static readonly MON_SPAWN_X = 400;
|
||||
/** 逐个刷怪默认间隔(秒):保证怪物排成纵队,避免堆叠;运行时按回合类型查 SPAWN_INTERVAL_BY_TYPE 覆盖 */
|
||||
private static readonly MON_SPAWN_INTERVAL = 0.3;
|
||||
|
||||
/**
|
||||
* 怪物占位点:所有槽位统一以右侧出生点 (X=400) 作为坐标,
|
||||
@@ -76,20 +76,14 @@ export class MissionMonCompComp extends CCComp {
|
||||
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;
|
||||
/** 当前回合的逐个刷怪间隔(秒),按回合类型查 SPAWN_INTERVAL_BY_TYPE */
|
||||
private spawnInterval: number = MissionMonCompComp.MON_SPAWN_INTERVAL;
|
||||
/** 本回合已推进的战斗时间(秒,暂停刷怪时冻结) */
|
||||
private waveTime: number = 0;
|
||||
/** 各批次在队列中的起始下标(用于速度曲线按批变速) */
|
||||
private batchStarts: number[] = [0, 0, 0];
|
||||
/** 是否正在刷怪释放中 */
|
||||
private isReleasing: boolean = false;
|
||||
/** 当前批已刷出的怪物数(清场加速存活比例分母) */
|
||||
private batchReleasedCount: number = 0;
|
||||
/** 当前批是否已通过清场加速提前推进过(每批只触发一次) */
|
||||
@@ -100,8 +94,11 @@ export class MissionMonCompComp extends CCComp {
|
||||
public waveEarlySkipTotal: number = 0;
|
||||
/** 清场加速触发阈值:当前批存活比例低于此值时提前推进(0.25 = 清掉 75%) */
|
||||
private static readonly BATCH_EARLY_RATIO = 0.25;
|
||||
/** 清场加速提前量(秒):等效 batchTimer 快进,保底批间隔不被瞬间叠爆 */
|
||||
/** 清场加速提前量(秒):等效推进时间快进,保底不被瞬间叠爆 */
|
||||
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;
|
||||
|
||||
// ======================== 生命周期 ========================
|
||||
|
||||
@@ -112,55 +109,51 @@ export class MissionMonCompComp extends CCComp {
|
||||
}
|
||||
|
||||
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;
|
||||
// 待刷出统计(回合结束检测与 HUD 进度消费,必须在 return 前执行保证语义不受暂停影响)
|
||||
smc.vmdata.mission_data.pending_mon_num = this.spawnQueue.length;
|
||||
|
||||
// 场上怪物超阈值时暂停释放:冻结批次推进与逐个刷出计时(不推进计时器,恢复后从断点平滑续接)
|
||||
// 注意 pending 统计必须在 return 之前执行,保证回合结束检测(pending==0)语义不受暂停影响
|
||||
// 场上怪物超阈值时暂停推进:冻结 waveTime,恢复后欠账式瞬时补齐,30 秒预算不流失
|
||||
if (smc.mission.stop_spawn_mon) return;
|
||||
if (!this.isReleasing) return;
|
||||
|
||||
// 分批释放:按 BATCH_INTERVAL 节奏推进批次
|
||||
if (this.isReleasing) {
|
||||
this.batchTimer += dt;
|
||||
if (this.batchTimer >= BATCH_INTERVAL && this.currentBatch < BATCH_COUNT - 1) {
|
||||
this.batchTimer = 0;
|
||||
this.advanceBatch();
|
||||
}
|
||||
this.waveTime += dt;
|
||||
|
||||
// 清场加速:0.2s 节流检测当前批存活比例,清得快则快进批次计时(学 PvZ 血量阈值提前刷新)
|
||||
this.aliveCheckTimer += dt;
|
||||
if (this.aliveCheckTimer >= 0.2) {
|
||||
this.aliveCheckTimer = 0;
|
||||
this.checkBatchEarlyAdvance();
|
||||
}
|
||||
// 清场加速:0.2s 节流检测当前段存活比例,清得快则快进推进时间(压缩垃圾时间)
|
||||
this.aliveCheckTimer += dt;
|
||||
if (this.aliveCheckTimer >= 0.2) {
|
||||
this.aliveCheckTimer = 0;
|
||||
this.checkEarlyAdvance();
|
||||
}
|
||||
|
||||
// 逐个刷怪:按 spawnInterval 节奏从队列释放
|
||||
if (this.spawnQueue.length > 0) {
|
||||
this.spawnTimer += dt;
|
||||
if (this.spawnTimer >= this.spawnInterval) {
|
||||
this.spawnTimer = 0;
|
||||
const monData = this.spawnQueue.shift()!;
|
||||
const targetPosIndex = this.waveSpawnedCount % MissionMonCompComp.MON_POSITIONS.length;
|
||||
this.addMonsterAtGrid(targetPosIndex, monData, this.currentWave);
|
||||
this.waveSpawnedCount++;
|
||||
}
|
||||
// 欠账式补刷:期望累计刷出数 − 已刷数,自动补偿帧余量与暂停期间欠账
|
||||
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[]) {
|
||||
// 按批次分组
|
||||
this.pendingBatches = [[], [], []];
|
||||
for (const m of monsters) {
|
||||
const batch = Math.min(m.batch, BATCH_COUNT - 1);
|
||||
this.pendingBatches[batch].push(m);
|
||||
// 按批次排序后整波入队,记录各批起始下标供速度曲线按批变速(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;
|
||||
@@ -186,12 +179,10 @@ export class MissionMonCompComp extends CCComp {
|
||||
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;
|
||||
this.batchStarts = [0, 0, 0];
|
||||
this.waveTime = 0;
|
||||
this.isReleasing = false;
|
||||
this.batchReleasedCount = 0;
|
||||
this.batchFastForwarded = false;
|
||||
this.aliveCheckTimer = 0;
|
||||
@@ -222,71 +213,53 @@ export class MissionMonCompComp extends CCComp {
|
||||
}
|
||||
|
||||
private onPhasePrepareEnd() {
|
||||
this.resetSlotSpawnData();
|
||||
|
||||
// 按回合类型确定本回合刷怪间隔(放松回合快速倾泻,压力回合稍慢聚焦)
|
||||
this.spawnInterval = SPAWN_INTERVAL_BY_TYPE[getWaveType(this.currentWave)] ?? MissionMonCompComp.MON_SPAWN_INTERVAL;
|
||||
|
||||
// 准备结束阶段:启动分批释放,
|
||||
// 第一批立即转入 spawnQueue,后续批次由 update 按 BATCH_INTERVAL 推进。
|
||||
this.startBatchRelease();
|
||||
}
|
||||
|
||||
// ======================== 分批释放 ========================
|
||||
|
||||
/** 启动分批释放:立即释放第一批,启动批次计时器 */
|
||||
private startBatchRelease() {
|
||||
this.currentBatch = 0;
|
||||
this.batchTimer = 0;
|
||||
// 准备结束:清残留怪与计时,启动持续刷怪(队列已在 setupWaveData 整波入队,不能清)
|
||||
this.resetBattleTimer();
|
||||
this.isReleasing = true;
|
||||
this.releaseCurrentBatch();
|
||||
this.waveTime = 0;
|
||||
}
|
||||
|
||||
/** 推进到下一批次 */
|
||||
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;
|
||||
/**
|
||||
* 速度曲线:按 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));
|
||||
}
|
||||
|
||||
// 将批次怪物转入逐个刷怪队列
|
||||
for (const m of batch) {
|
||||
this.spawnQueue.push(m);
|
||||
}
|
||||
this.batchReleasedCount = batch.length;
|
||||
this.batchFastForwarded = false;
|
||||
batch.length = 0;
|
||||
|
||||
// 让首个怪物在下一帧立即刷出,避免额外延迟
|
||||
this.spawnTimer = this.spawnInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清场加速检测:当前批已放完且存活比例 ≤ BATCH_EARLY_RATIO 时,快进批次计时器。
|
||||
* 只奖励"清得快"的 build(压缩批间垃圾时间),弱 build 清不完则不触发、不叠加压力。
|
||||
* 清场加速检测:当前时间段已刷完且存活比例 ≤ BATCH_EARLY_RATIO 时,快进推进时间。
|
||||
* 只奖励"清得快"的 build(压缩垃圾时间),弱 build 清不完则不触发、不叠加压力。
|
||||
*/
|
||||
private checkBatchEarlyAdvance() {
|
||||
private checkEarlyAdvance() {
|
||||
if (this.batchFastForwarded) return;
|
||||
if (this.currentBatch >= BATCH_COUNT - 1) return;
|
||||
if (this.spawnQueue.length > 0) return; // 本批还没放完,不判断
|
||||
if (this.batchReleasedCount <= 0) 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 => {
|
||||
@@ -297,20 +270,22 @@ export class MissionMonCompComp extends CCComp {
|
||||
const aliveRatio = Math.min(1, alive / this.batchReleasedCount);
|
||||
if (aliveRatio <= MissionMonCompComp.BATCH_EARLY_RATIO) {
|
||||
this.batchFastForwarded = true;
|
||||
this.batchTimer += MissionMonCompComp.BATCH_EARLY_SKIP;
|
||||
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] batch=${this.currentBatch} alive=${alive}/${this.batchReleasedCount},快进 ${MissionMonCompComp.BATCH_EARLY_SKIP}s`);
|
||||
`[EarlyAdvance] waveTime=${this.waveTime.toFixed(1)} alive=${alive}/${this.batchReleasedCount},快进 ${MissionMonCompComp.BATCH_EARLY_SKIP}s`);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 槽位管理 ========================
|
||||
|
||||
/**
|
||||
* 清理上一回合残留怪物,并重置生成计数与逐个刷怪队列
|
||||
* 清理上一回合残留怪物,并重置战斗计时与清场加速状态。
|
||||
* 注意:spawnQueue / batchStarts / waveTargetCount 由 setupWaveData 在 Prepare 阶段写入,
|
||||
* 本函数不能触碰,否则 PhasePrepareEnd 会清空未刷队列导致回合直接判空。
|
||||
*/
|
||||
private resetSlotSpawnData() {
|
||||
private resetBattleTimer() {
|
||||
ecs.query(ecs.allOf(HeroAttrsComp)).forEach(e => {
|
||||
const attrs = e.get(HeroAttrsComp);
|
||||
if (attrs && attrs.fac === FacSet.MON && !attrs.is_dead) {
|
||||
@@ -319,12 +294,8 @@ export class MissionMonCompComp extends CCComp {
|
||||
});
|
||||
|
||||
this.waveSpawnedCount = 0;
|
||||
// 同步丢弃上一回合未释放的队列,避免与新一回合混合
|
||||
this.spawnQueue = [];
|
||||
this.spawnTimer = 0;
|
||||
this.waveTime = 0;
|
||||
this.isReleasing = false;
|
||||
this.currentBatch = 0;
|
||||
this.batchTimer = 0;
|
||||
this.batchReleasedCount = 0;
|
||||
this.batchFastForwarded = false;
|
||||
this.aliveCheckTimer = 0;
|
||||
|
||||
Reference in New Issue
Block a user