From a905db8e48fba0f377a7a3d3578eed67d00b25d4 Mon Sep 17 00:00:00 2001 From: pan Date: Wed, 12 Aug 2026 17:56:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=A7=E5=B9=85=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=88=98=E6=96=97=E4=BD=93=E9=AA=8C=E4=B8=8E=E7=BC=96=E9=98=9F?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 扩展英雄/怪物网格站位到10个槽位 2. 重构默认攻击距离计算逻辑,按职业动态适配 3. 重做英雄站位与移动系统,新增同阵营间距约束 4. 调整rogue模式怪物数量与强度配置,提升战斗爽感 5. 重构刷怪逻辑为匀速曲线+欠账补刷,优化刷怪节奏 --- .../script/game/common/SingletonModuleComp.ts | 2 +- assets/script/game/hero/Hero.ts | 5 +- assets/script/game/hero/Mon.ts | 5 +- assets/script/game/hero/MoveComp.ts | 62 ++++- assets/script/game/map/MissionHeroComp.ts | 31 +-- assets/script/game/map/MissionMonComp.ts | 219 ++++++++---------- assets/script/game/map/RogueConfig.ts | 62 ++--- 7 files changed, 212 insertions(+), 174 deletions(-) diff --git a/assets/script/game/common/SingletonModuleComp.ts b/assets/script/game/common/SingletonModuleComp.ts index 245c089f..0be20c3d 100644 --- a/assets/script/game/common/SingletonModuleComp.ts +++ b/assets/script/game/common/SingletonModuleComp.ts @@ -75,7 +75,7 @@ export class SingletonModuleComp extends ecs.Comp { in_select: false, in_fight: false, stop_mon_action: false, - heroGrid: [-1, -1, -1, -1, -1, -1], + heroGrid: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1], monGrid: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], /** 本局真实死亡的英雄快照列表,供后续复活机制消费;整局结束(MissionEnd)时清空 */ dead_heroes: [] as DeadHeroRecord[], diff --git a/assets/script/game/hero/Hero.ts b/assets/script/game/hero/Hero.ts index 40b66a0b..add96f00 100644 --- a/assets/script/game/hero/Hero.ts +++ b/assets/script/game/hero/Hero.ts @@ -7,7 +7,7 @@ import { HeroViewComp } from "./HeroViewComp"; import { BoxSet, FacSet, FightSet, IndexSet } from "../common/config/GameSet"; import { HeroInfo, HeroPos, resolveFormationTargetX, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv, calcGrowBonus } from "../common/config/heroSet"; import { GameEvent } from "../common/config/GameEvent"; -import { SkillTriggerType } from "../common/config/heroSet"; +import { SkillTriggerType, HType } from "../common/config/heroSet"; import { SkillTriggerHelper } from "./SkillTriggerHelper"; import { Attrs } from "../common/config/HeroAttrs"; import { MoveComp } from "./MoveComp"; @@ -122,7 +122,8 @@ export class Hero extends ecs.Entity { model.grow_type = hero.grow_type; model.role = hero.role; model.fac = FacSet.HERO; - model.dis = hero.dis ?? 720; + // 攻击距离:配置优先;未配置按攻击定位给默认值(近战180 / 远程600) + model.dis = hero.dis ?? (hero.type === HType.Melee ? 180 : 600); model.posIndex = posIndex; if (posIndex >= 0) { smc.mission.heroGrid[posIndex] = this.eid; diff --git a/assets/script/game/hero/Mon.ts b/assets/script/game/hero/Mon.ts index 38ebc243..963b511e 100644 --- a/assets/script/game/hero/Mon.ts +++ b/assets/script/game/hero/Mon.ts @@ -9,7 +9,7 @@ import { HeroViewComp } from "./HeroViewComp"; import { MoveComp } from "./MoveComp"; import { MonMoveComp } from "./MonMoveComp"; import { GameEvent } from "../common/config/GameEvent"; -import { SkillTriggerType } from "../common/config/heroSet"; +import { SkillTriggerType, HType } from "../common/config/heroSet"; import { SkillTriggerHelper } from "./SkillTriggerHelper"; /** 怪物实体:负责怪物对象池复用、属性初始化、入场动画与回收 */ @ecs.register(`Monster`) @@ -175,7 +175,8 @@ export class Monster extends ecs.Entity { model.speed = hero.speed ?? 800; model.type = hero.type; model.fac = FacSet.MON; - model.dis = hero.dis ?? 720; + // 攻击距离:配置优先;未配置按攻击定位给默认值(近战180 / 远程600) + model.dis = hero.dis ?? (hero.type === HType.Melee ? 180 : 600); model.posIndex = laneIndex; if (laneIndex >= 0) { smc.mission.monGrid[laneIndex] = this.eid; diff --git a/assets/script/game/hero/MoveComp.ts b/assets/script/game/hero/MoveComp.ts index 3da8ab4b..5f1a863f 100644 --- a/assets/script/game/hero/MoveComp.ts +++ b/assets/script/game/hero/MoveComp.ts @@ -223,8 +223,10 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate // 未贴近:向怪物方向推进(攻击范围内也继续移动),于接触距离处停下 const dir = enemyX > selfX ? 1 : -1; move.direction = dir; - // 终止点取接触距离与推进上限的较小者,防止越过 300 继续追击 - const stopX = Math.min(enemyX - dir * MoveSystem.MELEE_ENGAGE_X, MoveSystem.HERO_ADVANCE_LIMIT_X); + // 终止点取接触距离与推进上限的较小者,防止越过 150 继续追击 + let stopX = Math.min(enemyX - dir * MoveSystem.MELEE_ENGAGE_X, MoveSystem.HERO_ADVANCE_LIMIT_X); + // 同阵营最小间距约束:防止近战推进时穿越前方队友 + stopX = this.clampAllySpacing(e, view, move, model, stopX); const speed = model.speed / 3; this.moveEntity(view, dir, speed, stopX); model.is_atking = false; @@ -292,7 +294,9 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate /** 近战贴近怪物的固定接触距离:小于攻击距离 dis,使英雄进攻击范围后仍继续贴近到此间距才停 */ private static readonly MELEE_ENGAGE_X = 60; /** 英雄向敌人推进的最远终止点 X,防止近战追敌时无限右移脱离战场 */ - private static readonly HERO_ADVANCE_LIMIT_X = 300; + private static readonly HERO_ADVANCE_LIMIT_X = 100; + /** 同阵营英雄间最小间距:小于该值时低优先级一方需让位 */ + private static readonly ALLY_MIN_GAP_X = 30; /** * 计算某个槽位的目标 X。 @@ -328,7 +332,57 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate const dir = targetX > currentX ? 1 : -1; move.direction = dir; const speed = model.speed / 3; - this.moveEntity(view, dir, speed, targetX); + // 同阵营最小间距约束:防止编队挤压时穿越前方队友 + const clampedTargetX = this.clampAllySpacing(view.ent, view, move, model, targetX); + this.moveEntity(view, dir, speed, clampedTargetX); + } + + /** + * 同阵营横向最小间距约束。 + * Why: 编队/追击时英雄可能挤到一起,低优先级一方需要保持在高优先级身后 ALLY_MIN_GAP_X 处, + * 避免视觉重叠与站位混乱。 + * 优先级:编队优先级高者在前;同级时召唤早(spawnOrder 小)者在前。 + * @returns 裁剪后的目标 X(保证不越过身前队友) + */ + private clampAllySpacing(self: ecs.Entity, view: HeroViewComp, selfMove: MoveComp, model: HeroAttrsComp, targetX: number): number { + const selfX = view.node.position.x; + const selfY = view.node.position.y; + let clamped = targetX; + + ecs.query(this.getHeroMoveMatcher()).forEach(e => { + if (e === self) return; + const attrs = e.get(HeroAttrsComp); + const v = e.get(HeroViewComp); + const mv = e.get(MoveComp); + if (!attrs || !v?.node || !mv) return; + if (attrs.is_dead || attrs.is_reviving) return; + if (attrs.fac !== model.fac) return; + // 只约束同排(Y 差在阈值内)的队友 + if (Math.abs(v.node.position.y - selfY) > this.minSpacingY) return; + + const otherX = v.node.position.x; + // 判断对方是否在自己前方(以目标方向为前) + const isAhead = clamped > selfX ? otherX > selfX : otherX < selfX; + if (!isAhead) return; + + // 判定谁该在前:编队优先级高者在前;同级时 spawnOrder 小者在前 + const myPriority = this.getFormationPriority(model); + const otherPriority = this.getFormationPriority(attrs); + let selfIsFront = false; + if (myPriority !== otherPriority) { + selfIsFront = myPriority > otherPriority; + } else { + selfIsFront = selfMove.spawnOrder <= mv.spawnOrder; + } + + if (!selfIsFront) { + // 自己靠后:不能越过 otherX - ALLY_MIN_GAP_X(朝右)或 otherX + ALLY_MIN_GAP_X(朝左) + const limitX = clamped > selfX ? otherX - MoveSystem.ALLY_MIN_GAP_X : otherX + MoveSystem.ALLY_MIN_GAP_X; + clamped = clamped > selfX ? Math.min(clamped, limitX) : Math.max(clamped, limitX); + } + }); + + return clamped; } private moveEntity(view: HeroViewComp, direction: number, speed: number, stopAtX?: number) { diff --git a/assets/script/game/map/MissionHeroComp.ts b/assets/script/game/map/MissionHeroComp.ts index c65478fe..af8111c2 100644 --- a/assets/script/game/map/MissionHeroComp.ts +++ b/assets/script/game/map/MissionHeroComp.ts @@ -53,14 +53,18 @@ const { ccclass } = _decorator; export class MissionHeroComp extends CCComp { // ======================== 常量 ======================== - /** 硬编码的6个英雄占位点 */ + /** 硬编码的英雄占位点(数量对齐 FightSet.HERO_MAX_NUM,落点 X 统一,英雄落地后由 MoveComp 自行移位到阵型目标) */ public static readonly HERO_POSITIONS: Vec3[] = [ - v3(-220, BoxSet.GAME_LINE, 0), // index 0 (node_index 1): Top Front 第二 - v3(-140, BoxSet.GAME_LINE, 0), // index 1 (node_index 2): Mid Front 第一 - v3(-300, BoxSet.GAME_LINE, 0), // index 2 (node_index 3): Bot Front 第三 - // v3(-280, BoxSet.GAME_LINE + 100, 0), // index 3 (node_index 4): Top Back - // v3(-280, BoxSet.GAME_LINE, 0), // index 4 (node_index 5): Mid Back - // v3(-280, BoxSet.GAME_LINE - 100, 0), // index 5 (node_index 6): Bot Back + v3(-300, BoxSet.GAME_LINE, 0), // index 0 + v3(-300, BoxSet.GAME_LINE, 0), // index 1 + v3(-300, BoxSet.GAME_LINE, 0), // index 2 + v3(-300, BoxSet.GAME_LINE, 0), // index 3 + v3(-300, BoxSet.GAME_LINE, 0), // index 4 + v3(-300, BoxSet.GAME_LINE, 0), // index 5 + v3(-300, BoxSet.GAME_LINE, 0), // index 6 + v3(-300, BoxSet.GAME_LINE, 0), // index 7 + v3(-300, BoxSet.GAME_LINE, 0), // index 8 + v3(-300, BoxSet.GAME_LINE, 0), // index 9 ]; /** 英雄出生时的掉落高度(从空中落到地面的像素差) */ @@ -211,16 +215,16 @@ export class MissionHeroComp extends CCComp { if (m && m.posIndex >= 0) occupied.add(m.posIndex); } - // 优先中前(1) -> 上前(0) -> 下前(2) -> 中后(4) -> 上后(3) -> 下后(5) - const slotPriority = [1, 0, 2, 4, 3, 5]; + // 按索引顺序填充空位(0..9),全部落点相同,英雄落地后自行移位 + const slotPriority = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; for (const idx of slotPriority) { - if (!occupied.has(idx)) { + if (!occupied.has(idx) && MissionHeroComp.HERO_POSITIONS[idx]) { return idx; } } - // 溢出:默认中前 - return 1; + // 溢出兜底:复用 index 0(坐标有效,避免越界崩溃) + return 0; } /** @@ -252,7 +256,8 @@ export class MissionHeroComp extends CCComp { let hero = ecs.getEntity(Hero); let scale = 1 const finalPosIndex = posIndex >= 0 ? posIndex : this.pickPositionIndexForHero(); - const landingPos = MissionHeroComp.HERO_POSITIONS[finalPosIndex]; + // 兜底:索引越界或点位缺失时复用 index 0,保证 landingPos 一定有效 + const landingPos = MissionHeroComp.HERO_POSITIONS[finalPosIndex] ?? MissionHeroComp.HERO_POSITIONS[0]; let spawnPos: Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0); hero.load(spawnPos, scale, uuid, landingPos.y, hero_lv, card_lv, finalPosIndex); diff --git a/assets/script/game/map/MissionMonComp.ts b/assets/script/game/map/MissionMonComp.ts index 77fa5fc6..879cd16a 100644 --- a/assets/script/game/map/MissionMonComp.ts +++ b/assets/script/game/map/MissionMonComp.ts @@ -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; diff --git a/assets/script/game/map/RogueConfig.ts b/assets/script/game/map/RogueConfig.ts index 8805eb9e..a60ea163 100644 --- a/assets/script/game/map/RogueConfig.ts +++ b/assets/script/game/map/RogueConfig.ts @@ -23,7 +23,7 @@ * 回合节奏: * - 最大 20 回合,第 20 回合通关 * - 每回合 30 秒,固定分 3 批,每 10 秒释放一批 - * - 普通回合 18~36 只,放松回合 × 1.5 = 27~54 只 + * - 普通回合 36~72 只,放松回合 × 1.5 = 54~108 只(全局 MON_COUNT_MUL = 2) * - wave % 5 === 0 → 压力回合(必带 Boss,强度高、数量少) * - wave % 5 === 4 → 放松回合(数量 × 1.5,强度低,爽快清屏,大战前夜收割补给) */ @@ -60,6 +60,12 @@ export const WAVE_TYPE_POWER_RATIO: Record = { /** 放松回合数量倍率(相对普通回合) */ export const RELAX_COUNT_MUL = 1.5; +/** 全局怪物数量倍率(在 base_count 基础上整体翻倍,量大管饱) */ +export const MON_COUNT_MUL = 2; + +/** 全局怪物强度系数(2/3 = 强度削弱三分之一,同时作用于 hp 缩放与 ap 挂钩公式) */ +export const MON_POWER_MUL = 2 / 3; + /** 最大回合数(第 20 回合通关) */ export const MAX_WAVE = 20; @@ -72,8 +78,8 @@ export const BATCH_COUNT = 3; /** 每批间隔(秒):30 秒 / 3 批 = 10 秒 */ export const BATCH_INTERVAL = WAVE_DURATION / BATCH_COUNT; -/** 每回合怪物硬上限(放松回合 36 × 1.5 = 54) */ -export const MAX_MONSTERS = 54; +/** 每回合怪物硬上限(放松回合 72 × 1.5 = 108) */ +export const MAX_MONSTERS = 108; /** Boss 护卫队数量(与 Boss 同批压轴进场,占用回合总名额) */ export const BOSS_GUARD_COUNT = 3; @@ -371,7 +377,7 @@ export const BossSkillPool: Record = { /** 单回合完整配置 */ export interface WaveConfig { - /** 基础怪物总数(普通回合 36 为上限,放松回合自动 × 1.5 = 54) */ + /** 基础怪物总数(普通回合 72 为上限,放松回合自动 × 1.5 = 108,均已含全局 MON_COUNT_MUL = 2) */ base_count: number; /** 可选小队 id 池,引擎按权重抽取拼装到 base_count */ squad_pool: string[]; @@ -401,40 +407,40 @@ export interface WaveConfig { */ export const WaveConfigs: Record = { // ===== 第一循环:教学期 ===== - 1: { base_count: 18, squad_pool: ["melee_grunt"], hp_mul: 1.00, ap_mul: 1.00 }, - 2: { base_count: 21, squad_pool: ["melee_grunt", "mixed_balanced"], hp_mul: 1.00, ap_mul: 1.00 }, - 3: { base_count: 24, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.05, ap_mul: 1.00 }, + 1: { base_count: 36, squad_pool: ["melee_grunt"], hp_mul: 1.00, ap_mul: 1.00 }, + 2: { base_count: 42, squad_pool: ["melee_grunt", "mixed_balanced"], hp_mul: 1.00, ap_mul: 1.00 }, + 3: { base_count: 48, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.05, ap_mul: 1.00 }, // 放松回合:量大好清,Boss 前收割补给 - 4: { base_count: 27, squad_pool: ["mixed_balanced", "long_line", "heavy_shield"], hp_mul: 1.10, ap_mul: 1.05 }, + 4: { base_count: 54, squad_pool: ["mixed_balanced", "long_line", "heavy_shield"], hp_mul: 1.10, ap_mul: 1.05 }, // 压力回合:第一 Boss - 5: { base_count: 21, squad_pool: ["melee_grunt", "mixed_balanced", "heavy_shield"], hp_mul: 1.15, ap_mul: 1.10, boss_wave: true, boss_skill_pool: ["boss_rage"] }, + 5: { base_count: 42, squad_pool: ["melee_grunt", "mixed_balanced", "heavy_shield"], hp_mul: 1.15, ap_mul: 1.10, boss_wave: true, boss_skill_pool: ["boss_rage"] }, // ===== 第二循环:引入技能怪 ===== - 6: { base_count: 30, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.10, ap_mul: 1.05 }, - 7: { base_count: 30, squad_pool: ["melee_grunt", "heavy_shield", "long_line"], hp_mul: 1.15, ap_mul: 1.10, skill_pool: ["tough"] }, - 8: { base_count: 33, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.20, ap_mul: 1.15, skill_pool: ["berserk"] }, + 6: { base_count: 60, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.10, ap_mul: 1.05 }, + 7: { base_count: 60, squad_pool: ["melee_grunt", "heavy_shield", "long_line"], hp_mul: 1.15, ap_mul: 1.10, skill_pool: ["tough"] }, + 8: { base_count: 66, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.20, ap_mul: 1.15, skill_pool: ["berserk"] }, // 放松回合:量大好清,Boss 前收割补给 - 9: { base_count: 33, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["berserk", "tough"] }, + 9: { base_count: 66, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["berserk", "tough"] }, // 压力回合:第二 Boss - 10: { base_count: 24, squad_pool: ["melee_grunt", "assassin_squad", "mixed_balanced"], hp_mul: 1.30, ap_mul: 1.25, boss_wave: true, boss_skill_pool: ["boss_rage", "boss_iron"] }, + 10: { base_count: 48, squad_pool: ["melee_grunt", "assassin_squad", "mixed_balanced"], hp_mul: 1.30, ap_mul: 1.25, boss_wave: true, boss_skill_pool: ["boss_rage", "boss_iron"] }, // ===== 第三循环:组合多样化 ===== - 11: { base_count: 33, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["leech"] }, - 12: { base_count: 33, squad_pool: ["heavy_shield", "melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.30, ap_mul: 1.25, skill_pool: ["tough", "warcry"] }, - 13: { base_count: 36, squad_pool: ["assassin_squad", "summoner_cult", "mixed_balanced"], hp_mul: 1.35, ap_mul: 1.30, skill_pool: ["berserk", "leech"] }, + 11: { base_count: 66, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["leech"] }, + 12: { base_count: 66, squad_pool: ["heavy_shield", "melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.30, ap_mul: 1.25, skill_pool: ["tough", "warcry"] }, + 13: { base_count: 72, squad_pool: ["assassin_squad", "summoner_cult", "mixed_balanced"], hp_mul: 1.35, ap_mul: 1.30, skill_pool: ["berserk", "leech"] }, // 放松回合:量大好清,Boss 前收割补给 - 14: { base_count: 36, squad_pool: ["heavy_shield", "long_line", "melee_grunt"], hp_mul: 1.40, ap_mul: 1.35, skill_pool: ["berserk", "tough", "legacy"] }, + 14: { base_count: 72, squad_pool: ["heavy_shield", "long_line", "melee_grunt"], hp_mul: 1.40, ap_mul: 1.35, skill_pool: ["berserk", "tough", "legacy"] }, // 压力回合:第三 Boss(中期高潮) - 15: { base_count: 27, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.50, ap_mul: 1.40, boss_wave: true, boss_skill_pool: ["boss_rage", "boss_iron", "boss_doom"] }, + 15: { base_count: 54, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.50, ap_mul: 1.40, boss_wave: true, boss_skill_pool: ["boss_rage", "boss_iron", "boss_doom"] }, // ===== 第四循环:终极阶段(17~19 power_adjust 逐步爬坡,为最终 Boss 蓄势) ===== - 16: { base_count: 36, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], hp_mul: 1.45, ap_mul: 1.40, skill_pool: ["leech", "warcry"] }, - 17: { base_count: 36, squad_pool: ["melee_grunt", "summoner_cult", "mixed_balanced"], hp_mul: 1.50, ap_mul: 1.45, skill_pool: ["berserk", "legacy"], power_adjust: 1.05 }, - 18: { base_count: 36, squad_pool: ["assassin_squad", "long_line", "heavy_shield"], hp_mul: 1.55, ap_mul: 1.50, skill_pool: ["berserk", "tough", "leech"], power_adjust: 1.10 }, + 16: { base_count: 72, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], hp_mul: 1.45, ap_mul: 1.40, skill_pool: ["leech", "warcry"] }, + 17: { base_count: 72, squad_pool: ["melee_grunt", "summoner_cult", "mixed_balanced"], hp_mul: 1.50, ap_mul: 1.45, skill_pool: ["berserk", "legacy"], power_adjust: 1.05 }, + 18: { base_count: 72, squad_pool: ["assassin_squad", "long_line", "heavy_shield"], hp_mul: 1.55, ap_mul: 1.50, skill_pool: ["berserk", "tough", "leech"], power_adjust: 1.10 }, // 放松回合:量大好清,最终 Boss 前收割补给 - 19: { base_count: 36, squad_pool: ["mixed_balanced", "summoner_cult", "melee_grunt"], hp_mul: 1.60, ap_mul: 1.55, skill_pool: ["berserk", "tough", "legacy", "warcry"], power_adjust: 1.20 }, + 19: { base_count: 72, squad_pool: ["mixed_balanced", "summoner_cult", "melee_grunt"], hp_mul: 1.60, ap_mul: 1.55, skill_pool: ["berserk", "tough", "legacy", "warcry"], power_adjust: 1.20 }, // 压力回合:最终 Boss - 20: { base_count: 30, squad_pool: ["assassin_squad", "heavy_shield", "summoner_cult", "mixed_balanced"], hp_mul: 1.80, ap_mul: 1.70, boss_wave: true, boss_skill_pool: ["boss_doom", "boss_rage"] }, + 20: { base_count: 60, squad_pool: ["assassin_squad", "heavy_shield", "summoner_cult", "mixed_balanced"], hp_mul: 1.80, ap_mul: 1.70, boss_wave: true, boss_skill_pool: ["boss_doom", "boss_rage"] }, }; // ======================== 7. 配置校验 ======================== @@ -566,10 +572,10 @@ export class RogueSpawningEngine { const waveType = getWaveType(wave); const typeRatio = WAVE_TYPE_POWER_RATIO[waveType]; - // 1. 计算目标强度 + // 1. 计算目标强度(MON_POWER_MUL = 2/3:数量翻倍后单怪强度整体削弱三分之一,避免总压力膨胀) const heroPower = this.getCurrentHeroPower(); const powerAdjust = cfg.power_adjust ?? 1.0; - const targetPower = heroPower * typeRatio * powerAdjust * DynamicTuner.factor; + const targetPower = heroPower * typeRatio * powerAdjust * DynamicTuner.factor * MON_POWER_MUL; // 2. 确定怪物总数(放松回合 × 1.5;普通/压力回合预留收尾高潮批名额,防 slice 截掉) let totalCount = cfg.base_count; @@ -626,8 +632,8 @@ export class RogueSpawningEngine { const apGrowth = getApWaveGrowth(wave); for (const m of monsters) { m.hp = Math.max(1, Math.round(m.hp * hpScale)); - // ap = 玩家平均攻击 × 回合递进 × 基础系数 × 类型系数(Boss/护卫/精英在上游已上浮 base_ap) - m.ap = Math.max(1, Math.round(heroAvgAp * apGrowth * AP_RELATIVE * this.getApTypeFactor(m))); + // ap = 玩家平均攻击 × 回合递进 × 基础系数 × 类型系数 × 全局强度系数(削弱三分之一) + m.ap = Math.max(1, Math.round(heroAvgAp * apGrowth * AP_RELATIVE * MON_POWER_MUL * this.getApTypeFactor(m))); } }