From 48e0ab06c58262c591b7cc0eb5474d2fa31b87df Mon Sep 17 00:00:00 2001 From: panFD Date: Mon, 20 Jul 2026 23:20:24 +0800 Subject: [PATCH] =?UTF-8?q?refactor(monster&cast):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=80=AA=E7=89=A9=E7=A7=BB=E5=8A=A8=E4=B8=8EAOE=E6=8A=80?= =?UTF-8?q?=E8=83=BD=E7=9B=AE=E6=A0=87=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 移除固定网格站位,怪物从右侧统一出生并向左推进 2. 修改AOE技能索敌逻辑,直接使用最近敌人真实位置 3. 新增怪物自动推进与战斗停船逻辑,优化战斗体验 --- assets/script/game/hero/MonMoveComp.ts | 53 ++++++++++---- assets/script/game/hero/SCastSystem.ts | 25 ++----- assets/script/game/map/MissionMonComp.ts | 91 +++++++++++++++--------- 3 files changed, 104 insertions(+), 65 deletions(-) diff --git a/assets/script/game/hero/MonMoveComp.ts b/assets/script/game/hero/MonMoveComp.ts index 0f8e6adc..1c9c2be5 100644 --- a/assets/script/game/hero/MonMoveComp.ts +++ b/assets/script/game/hero/MonMoveComp.ts @@ -2,8 +2,7 @@ import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ec import { HeroViewComp } from "./HeroViewComp"; import { HeroAttrsComp } from "./HeroAttrsComp"; import { smc } from "../common/SingletonModuleComp"; -import { FacSet } from "../common/config/GameSet"; -import { HeroDisVal, HType } from "../common/config/heroSet"; +import { BoxSet, FacSet } from "../common/config/GameSet"; import { Node } from "cc"; @ecs.register('MonMoveComp') @@ -30,6 +29,8 @@ export class MonMoveComp extends ecs.Comp { @ecs.register('MonMoveSystem') export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate { + /** 怪物接近英雄后的停船距离:小于该阈值即停下交战 */ + private readonly combatHaltDistance = 80; /** 渲染层级重排节流,避免每帧排序 */ private readonly renderSortInterval = 0.05; private lastRenderSortAt = 0; @@ -81,25 +82,22 @@ export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpda } /** 所有移动都锁定在 baseY,避免出现“漂移” */ - // 注意:不再在 MonMoveComp 强行重置 X 轴坐标,避免与其他表现逻辑冲突 if (view.node.position.y !== move.baseY) { view.node.setPosition(view.node.position.x, move.baseY, 0); } // 渲染层级统交由 MoveSystem 统一处理,避免两个 System 争抢 setSiblingIndex - // 仅在战斗中才处理索敌 - if (!smc.mission.in_fight) return; - - const nearestEnemy = this.findNearestEnemy(e); + // 仅在战斗中才处理索敌;非战斗阶段也允许向左推进,让怪物自然逼近英雄阵地 + const nearestEnemy = smc.mission.in_fight ? this.findNearestEnemy(e) : null; if (nearestEnemy) { - /** 有敌人:进入固定位置攻击逻辑 */ + /** 有敌人:边移动边攻击 */ this.processCombatLogic(e, move, view, model, nearestEnemy); this.syncCombatTarget(model, view, nearestEnemy); } else { - /** 无敌人:原地待机 */ + /** 无敌人:清目标并继续向左推进 */ this.clearCombatTarget(model); model.is_atking = false; - view.status_change("idle"); + this.moveLeft(view, move, model); } } @@ -131,17 +129,44 @@ export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpda private processCombatLogic(e: ecs.Entity, move: MonMoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) { const selfX = view.node.position.x; const enemyX = enemy.node.position.x; - const inRange = this.isEnemyInAttackRange(model, selfX, enemyX); + // 停船判定使用固定阈值(80),不依赖技能攻击范围 + const inRange = Math.abs(selfX - enemyX) <= this.combatHaltDistance; - // 攻击判定:怪物在固定位置,如果在攻击范围内则攻击,否则原地待机 if (inRange) { + // 接触到英雄方:停止移动,进入攻击状态,由 SCastSystem 负责技能释放 model.is_atking = true; - // 确保朝向敌人 const dir = enemyX > selfX ? 1 : -1; view.scale = dir; + if (view.status === "move") { + view.status_change("idle"); + } } else { + // 未接触:继续向左推进 model.is_atking = false; - view.status_change("idle"); + this.moveLeft(view, move, model); + } + } + + /** + * 持续向左推进:怪物不再固定站位,而是以 speed/3 的速度向左移动, + * 直到抵达左边界(基地位置)后转入待机,由 SCastSystem 继续负责技能释放。 + */ + private moveLeft(view: HeroViewComp, move: MonMoveComp, model: HeroAttrsComp) { + const currentX = view.node.position.x; + if (currentX <= BoxSet.LETF_END) { + if (view.status !== "atk") { + view.status_change("idle"); + } + return; + } + const dir = -1; + move.direction = dir; + const speed = model.speed / 3; + const delta = speed * this.dt * dir; + const newX = Math.max(BoxSet.LETF_END, currentX + delta); + if (Math.abs(newX - currentX) >= 0.01) { + view.node.setPosition(newX, view.node.position.y, 0); + view.status_change("move"); } } diff --git a/assets/script/game/hero/SCastSystem.ts b/assets/script/game/hero/SCastSystem.ts index d1ec7d16..347ab648 100644 --- a/assets/script/game/hero/SCastSystem.ts +++ b/assets/script/game/hero/SCastSystem.ts @@ -13,7 +13,6 @@ import { GameEvent } from "../common/config/GameEvent"; import { SkillTriggerType } from "../common/config/heroSet"; import { SkillTriggerHelper } from "./SkillTriggerHelper"; import { MissionEconomy } from "../map/MissionEconomy"; -import { MissionMonCompComp } from "../map/MissionMonComp"; import { MissionHeroComp } from "../map/MissionHeroComp"; /** @@ -695,25 +694,13 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate if (config.DTType === DTType.aoe_grid) { const isHero = heroAttrs.fac === FacSet.HERO; if (isHero) { - // 英雄打怪物,目标为怪物网格的第2列(idx 1)中路(row 1)或第3列(idx 2)中路 - // MissionMonComp.MON_POSITIONS 顺序: - // 列1: 0,1,2 (X=60) - // 列2: 3,4,5 (X=140) -> 第2列中路是 index 4 - // 列3: 6,7,8 (X=220) -> 第3列中路是 index 7 - // 简单起见,可以固定取第2列中路(index 4)或第3列中路(index 7) - // 若最近的敌人在更后排,也可以取那个敌人的列。 - // 需求:三类 需要以中路 第 2、 3列为目标。 - // 我们直接返回第2列中路坐标(X=140, Y=GAME_LINE)或者根据最近敌人判断。 - // 需求说明为第 2 或 第 3 列,这里我们可以根据目标位置,如果目标在第3列及以后,打第3列,否则打第2列。 - let targetCol = 1; // 0-based, so 1 is 2nd col - if (nearestEnemy && nearestEnemy.node) { - // 判断目标所在的列 - const ex = nearestEnemy.node.position.x; - // X=60(列1), X=140(列2), X=220(列3), X=300(列4) - if (ex >= 200) targetCol = 2; // 第3列 + // 怪物已改为从 X=400 出生后动态向左推进,不再有固定列阵型。 + // aoe_grid 直接以最近敌人的真实位置作为目标点, + // 由技能自身的范围半径负责命中附近的怪物集群。 + if (nearestEnemy?.node) { + return nearestEnemy.node.position.clone(); } - const targetIdx = targetCol * 3 + 1; // 1是中路 - return MissionMonCompComp.MON_POSITIONS[targetIdx].clone(); + return null; } else { // 怪物打英雄,英雄网格: // 列1: 0,1,2 (X=-210) diff --git a/assets/script/game/map/MissionMonComp.ts b/assets/script/game/map/MissionMonComp.ts index b059773f..f4972cc1 100644 --- a/assets/script/game/map/MissionMonComp.ts +++ b/assets/script/game/map/MissionMonComp.ts @@ -4,11 +4,12 @@ * * 职责: * 1. 管理每一波怪物的生成计划:根据 RogueConfig 生成怪物。 - * 2. 自动推进波次:在准备阶段结束时(PhasePrepareEnd)统一刷出怪物。 + * 2. 自动推进波次:在准备阶段结束时(PhasePrepareEnd)把怪物转入逐个刷出队列。 * * 关键设计: - * - 采用 12 个硬编码的网格位置点 (MON_POSITIONS,3行x4列) - * - 每次生成最多 12 个怪物,固定在位置点。 + * - 所有怪物统一从右侧 X=400 出生点逐个刷出(MON_SPAWN_INTERVAL 节奏控制)。 + * - 实际阵型与推进由 MonMoveComp 在战斗中向左移动自然形成,不再使用固定网格点。 + * - 槽位索引(3行×4列)仅用于 monGrid 寻路与 SCastSystem 索敌定位。 * - 上一波残留怪在波次结束/开始时统一清理。 */ import { _decorator, v3, Vec3 } from "cc"; @@ -35,25 +36,29 @@ export class MissionMonCompComp extends CCComp { private static readonly MAX_MONSTERS = 12; /** 怪物出生掉落高度 */ private static readonly MON_DROP_HEIGHT = 0; + /** 怪物统一从右侧 X=400 出生点逐个刷出,随后向左推进 */ + private static readonly MON_SPAWN_X = 400; + /** 逐个刷怪间隔(秒):保证怪物排成纵队,避免堆叠 */ + private static readonly MON_SPAWN_INTERVAL = 0.3; - /** 硬编码的 12 个怪物占位点 (3行4列) */ + /** + * 12 个怪物占位点:所有槽位统一以右侧出生点 (X=400) 作为坐标, + * 实际阵型由 MonMoveComp 在战斗中向左推进时自然拉开。 + * 槽位索引仍保留 3 行 × 4 列结构,供 monGrid 寻路与 SCastSystem 索敌使用。 + */ public static readonly MON_POSITIONS: Vec3[] = [ - // 第 1 列 (X=60) - v3(0, BoxSet.GAME_LINE, 0), // index 0: Top - v3(0, BoxSet.GAME_LINE, 0), // index 1: Mid - v3(0, BoxSet.GAME_LINE, 0), // index 2: Bot - // 第 2 列 (X=140) - v3(90, BoxSet.GAME_LINE, 0), // index 3: Top - v3(90, BoxSet.GAME_LINE, 0), // index 4: Mid - v3(90, BoxSet.GAME_LINE, 0), // index 5: Bot - // 第 3 列 (X=220) - v3(180, BoxSet.GAME_LINE, 0), // index 6: Top - v3(180, BoxSet.GAME_LINE, 0), // index 7: Mid - v3(180, BoxSet.GAME_LINE, 0), // index 8: Bot - // 第 4 列 (X=300) - v3(270, BoxSet.GAME_LINE, 0), // index 9: Top - v3(270, BoxSet.GAME_LINE, 0), // index 10: Mid - v3(270, BoxSet.GAME_LINE, 0), // index 11: Bot + 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 ]; // ======================== 编辑器属性 ======================== @@ -71,8 +76,12 @@ export class MissionMonCompComp extends CCComp { private waveTargetCount: number = 0; /** 当前波已生成的怪物数量 */ private waveSpawnedCount: number = 0; - /** 等待生成的怪物队列 */ + /** 等待生成的怪物队列(波次总池) */ private pendingMonsters: GeneratedMonster[] = []; + /** 逐个刷怪队列:从 pendingMonsters 转入,按节奏在 update 中释放 */ + private spawnQueue: GeneratedMonster[] = []; + /** 逐个刷怪累计计时(秒) */ + private spawnTimer: number = 0; // ======================== 生命周期 ======================== @@ -83,7 +92,20 @@ export class MissionMonCompComp extends CCComp { } protected update(dt: number): void { - smc.vmdata.mission_data.pending_mon_num = this.pendingMonsters.length; + // pending_mon_num 同时统计未刷出的总池与正在释放的队列,确保 UI 与全灭判定一致 + smc.vmdata.mission_data.pending_mon_num = this.pendingMonsters.length + this.spawnQueue.length; + + // 逐个刷怪:按 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.MAX_MONSTERS; + this.addMonsterAtGrid(targetPosIndex, monData, this.currentWave); + this.waveSpawnedCount++; + } + } } start() {} @@ -115,6 +137,8 @@ export class MissionMonCompComp extends CCComp { this.waveTargetCount = 0; this.waveSpawnedCount = 0; this.pendingMonsters = []; + this.spawnQueue = []; + this.spawnTimer = 0; // 预生成第一波数据以获取数量和 Boss 信息 const monsters = spawningEngine.generateWave(this.currentWave); @@ -142,24 +166,24 @@ export class MissionMonCompComp extends CCComp { private onPhasePrepareEnd() { this.resetSlotSpawnData(); - - // 准备结束阶段,立即刷出本波所有怪物 + + // 准备结束阶段:把本波待刷怪物转入逐个释放队列, + // 实际生成时机由 update() 按 MON_SPAWN_INTERVAL 节奏触发, + // 让怪物从 X=400 出生点排成纵队依次向左推进。 if (this.pendingMonsters.length > 0) { - let count = Math.min(this.pendingMonsters.length, MissionMonCompComp.MAX_MONSTERS); + const count = Math.min(this.pendingMonsters.length, MissionMonCompComp.MAX_MONSTERS); for (let i = 0; i < count; i++) { - const monData = this.pendingMonsters.shift()!; - const targetPosIndex = this.waveSpawnedCount % MissionMonCompComp.MAX_MONSTERS; - this.addMonsterAtGrid(targetPosIndex, monData, this.currentWave); - this.waveSpawnedCount++; + this.spawnQueue.push(this.pendingMonsters.shift()!); } - this.pendingMonsters = []; + // 让首个怪物在下一帧立即刷出,避免额外延迟 + this.spawnTimer = MissionMonCompComp.MON_SPAWN_INTERVAL; } } // ======================== 槽位管理 ======================== /** - * 清理上一波残留怪物,并重置生成计数 + * 清理上一波残留怪物,并重置生成计数与逐个刷怪队列 */ private resetSlotSpawnData() { ecs.query(ecs.allOf(HeroAttrsComp)).forEach(e => { @@ -168,8 +192,11 @@ export class MissionMonCompComp extends CCComp { e.destroy(); } }); - + this.waveSpawnedCount = 0; + // 同步丢弃上一波未释放的队列,避免与新一波混合 + this.spawnQueue = []; + this.spawnTimer = 0; } // ======================== 怪物生成 ========================