From 10f5a9f35d65c25ffa7fa5b1342fa535d09369ce Mon Sep 17 00:00:00 2001 From: pan Date: Thu, 11 Jun 2026 10:47:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(monster):=20=E8=B0=83=E6=95=B4=E6=80=AA?= =?UTF-8?q?=E7=89=A9AI=E5=B9=B6=E9=87=8D=E6=9E=84=E5=88=B7=E6=80=AA?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构怪物移动逻辑,移除移动追击相关代码,改为定点攻击模式,无敌人时原地待机,仅根据攻击范围切换攻击状态与朝向。 重构波次刷怪逻辑,删除分段刷怪阶段处理,改为波次准备结束后批量生成所有怪物。 将原6路刷怪改为3行网格布局,调整怪物出生点的X轴起点与间距。 限制单波最大怪物数量为12,简化刷怪分配逻辑为按生成顺序自动排列行列。 清理冗余的运行时状态变量与废弃函数,优化代码整体结构。 BREAKING CHANGES: 怪物攻击逻辑从移动追击改为定点攻击,移除了MonMoveComp中的moveEntity和resolveCombatRange函数;刷怪系统从6路改为3行网格布局,移除了分段刷怪功能与相关状态变量。 --- assets/script/game/hero/MonMoveComp.ts | 65 ++--------- assets/script/game/map/MissionMonComp.ts | 134 +++++++---------------- 2 files changed, 46 insertions(+), 153 deletions(-) diff --git a/assets/script/game/hero/MonMoveComp.ts b/assets/script/game/hero/MonMoveComp.ts index ceb34ab6..c481f02e 100644 --- a/assets/script/game/hero/MonMoveComp.ts +++ b/assets/script/game/hero/MonMoveComp.ts @@ -86,19 +86,19 @@ export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpda } // 渲染层级统交由 MoveSystem 统一处理,避免两个 System 争抢 setSiblingIndex - // 仅在战斗中才处理索敌和移动 + // 仅在战斗中才处理索敌 if (!smc.mission.in_fight) return; const nearestEnemy = this.findNearestEnemy(e); if (nearestEnemy) { - /** 有敌人:进入战斗位移逻辑 */ + /** 有敌人:进入固定位置攻击逻辑 */ this.processCombatLogic(e, move, view, model, nearestEnemy); this.syncCombatTarget(model, view, nearestEnemy); } else { - /** 无敌人:继续向左推进 */ + /** 无敌人:原地待机 */ this.clearCombatTarget(model); model.is_atking = false; - this.moveEntity(view, -1, model.speed / 3); + view.status_change("idle"); } } @@ -130,69 +130,20 @@ 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 dist = Math.abs(selfX - enemyX); const inRange = this.isEnemyInAttackRange(model, selfX, enemyX); - // 接触判定距离,只有接触英雄才停止移动 - const touchDistance = 120; - const isTouching = dist <= touchDistance; - - // 攻击判定 + // 攻击判定:怪物在固定位置,如果在攻击范围内则攻击,否则原地待机 if (inRange) { model.is_atking = true; + // 确保朝向敌人 + const dir = enemyX > selfX ? 1 : -1; + view.scale = dir; } else { model.is_atking = false; - } - - // 移动判定:只有接触了英雄才停止移动,否则继续向英雄方向移动 - if (isTouching) { view.status_change("idle"); - } else { - const dir = enemyX > selfX ? 1 : -1; - this.moveEntity(view, dir, model.speed / 3); } } - private moveEntity(view: HeroViewComp, direction: number, speed: number) { - const model = view.ent.get(HeroAttrsComp); - const move = view.ent.get(MonMoveComp); - if (!model || !move) return; - - // 简化的边界限制(怪物主要往左走,英雄防线在左侧,-999999 代表左侧尽头) - const moveMinX = -999999; - const moveMaxX = 999999; - - const currentX = view.node.position.x; - const delta = speed * this.dt * direction; - let newX = currentX + delta; - - newX = Math.max(moveMinX, Math.min(moveMaxX, newX)); - if (Math.abs(newX - currentX) < 0.01) { - view.status_change("idle"); - return; - } - - view.node.setPosition(newX, move.baseY, 0); - move.direction = direction; - - // 确保怪物的朝向表现,向左走 scale=-1,向右走 scale=1 - if (direction < 0) { - view.scale = -1; - } else if (direction > 0) { - view.scale = 1; - } - - view.status_change("move"); - } - - private resolveCombatRange(model: HeroAttrsComp, defaultMin: number, defaultMax: number): [number, number] { - const minRange = model.getCachedMinSkillDistance(); - const maxRange = model.getCachedMaxSkillDistance(); - if (maxRange <= 0) return [defaultMin, defaultMax]; - const safeMin = Math.max(0, Math.min(minRange, maxRange - 20)); - return [safeMin, maxRange]; - } - private findNearestEnemy(entity: ecs.Entity): HeroViewComp | null { const currentView = entity.get(HeroViewComp); if (!currentView?.node) return null; diff --git a/assets/script/game/map/MissionMonComp.ts b/assets/script/game/map/MissionMonComp.ts index c266dffa..e84a71a0 100644 --- a/assets/script/game/map/MissionMonComp.ts +++ b/assets/script/game/map/MissionMonComp.ts @@ -51,14 +51,16 @@ const { ccclass, property } = _decorator; export class MissionMonCompComp extends CCComp { // ======================== 常量 ======================== + /** 怪物最多 12 个 */ + private static readonly MAX_MONSTERS = 12; /** 怪物出生点起点 X */ - private static readonly MON_SPAWN_START_X = 460; - /** 怪物出生的 X 间距 */ - private static readonly MON_SPAWN_GAP_X = 50; + private static readonly MON_SPAWN_START_X = 60; + /** 怪物出生的 X 间距 (列距) */ + private static readonly MON_SPAWN_GAP_X = 80; /** 怪物出生掉落高度 */ private static readonly MON_DROP_HEIGHT = 0; - /** 6路高度偏移(在三路的y轴范围内,实现 6路 进军) */ - private static readonly LANE_Y_OFFSETS = [100, 60, 20, -20, -60, -100]; + /** 3行高度偏移 (行距) */ + private static readonly ROW_Y_OFFSETS = [80, 0, -80]; // ======================== 编辑器属性 ======================== @@ -82,8 +84,6 @@ export class MissionMonCompComp extends CCComp { // ======================== 运行时状态 ======================== - /** 记录每条线当前排到的索引 */ - private laneIndices: number[] = [0, 0, 0, 0, 0, 0]; /** 全局生成顺序计数器(用于渲染层级排序) */ private globalSpawnOrder: number = 0; /** 插队刷怪处理计时器 */ @@ -96,18 +96,6 @@ export class MissionMonCompComp extends CCComp { private waveSpawnedCount: number = 0; /** 等待生成的怪物队列(由新肉鸽引擎提供) */ private pendingMonsters: GeneratedMonster[] = []; - /** 增量刷怪计时器 */ - private spawnTimer: number = 0; - /** 分段刷怪阶段 (1, 2, 3) */ - private currentSpawnPhase: number = 1; - /** 下一阶段刷怪的延迟计时器 */ - private phaseDelayTimer: number = 0; - /** 当前阶段目标生成的怪物总数 */ - private phaseTargetCount: number = 0; - /** 当前阶段已生成的怪物数 */ - private phaseSpawnedCount: number = 0; - /** 本波怪物总数量(分段计算基准,避免 shift 后长度变化) */ - private waveTotalForPhase: number = 0; // ======================== 生命周期 ======================== @@ -133,43 +121,6 @@ export class MissionMonCompComp extends CCComp { if(!smc.mission.in_fight) return; this.updateSpecialQueue(dt); - - if(smc.mission.stop_spawn_mon) return; - - // 逐步刷怪逻辑 (分 3 段刷出) - if (this.pendingMonsters.length > 0) { - if (this.phaseSpawnedCount >= this.phaseTargetCount && this.currentSpawnPhase < 3) { - this.phaseDelayTimer -= dt; - if (this.phaseDelayTimer <= 0) { - this.currentSpawnPhase++; - this.phaseSpawnedCount = 0; - this.phaseDelayTimer = 3.0; - const base = this.waveTotalForPhase; - if (this.currentSpawnPhase === 2) { - this.phaseTargetCount = Math.ceil(base / 3); - } else { - this.phaseTargetCount = base - this.phaseTargetCount * 2; - if (this.phaseTargetCount <= 0) this.phaseTargetCount = this.pendingMonsters.length; - } - } - return; - } - - this.spawnTimer += dt; - if (this.spawnTimer > 0.2) { - this.spawnTimer = 0; - for (let i = 0; i < 2; i++) { - if (this.pendingMonsters.length === 0 || this.phaseSpawnedCount >= this.phaseTargetCount) break; - const monData = this.pendingMonsters.shift()!; - const lane = this.pickBalancedLane(); - console.log(`[MissionMonComp] [Phase ${this.currentSpawnPhase}] 准备生成怪物 UUID=${monData.uuid}, 剩余数量=${this.pendingMonsters.length}`); - this.addMonsterAt(lane, this.laneIndices[lane], monData); - this.laneIndices[lane]++; - this.waveSpawnedCount++; - this.phaseSpawnedCount++; - } - } - } } // ======================== 事件处理 ======================== @@ -195,15 +146,9 @@ export class MissionMonCompComp extends CCComp { } private setupWaveData(monsters: GeneratedMonster[]) { - this.pendingMonsters = monsters; + this.pendingMonsters = monsters.slice(0, MissionMonCompComp.MAX_MONSTERS); smc.vmdata.mission_data.pending_mon_num = this.pendingMonsters.length; - this.waveTargetCount = monsters.length; - this.waveTotalForPhase = monsters.length; - - this.currentSpawnPhase = 1; - this.phaseSpawnedCount = 0; - this.phaseTargetCount = Math.ceil(monsters.length / 3); - this.phaseDelayTimer = 3.0; + this.waveTargetCount = this.pendingMonsters.length; let hasBoss = monsters.some(m => m.isBoss); @@ -231,8 +176,6 @@ export class MissionMonCompComp extends CCComp { this.waveSpawnedCount = 0 this.MonQueue = [] this.pendingMonsters = [] - this.spawnTimer = 0 - this.laneIndices = [0, 0, 0, 0, 0, 0]; // 预生成第一波数据以获取数量和 Boss 信息 const monsters = spawningEngine.generateWave(this.currentWave); @@ -243,25 +186,9 @@ export class MissionMonCompComp extends CCComp { // ======================== 插队刷怪 ======================== - /** 选取当前排数最少的路(均衡分配,数量相同时随机选一个) */ - private pickBalancedLane(): number { - let min = this.laneIndices[0]; - let candidates = [0]; - for (let i = 1; i < 6; i++) { - if (this.laneIndices[i] < min) { - min = this.laneIndices[i]; - candidates = [i]; - } else if (this.laneIndices[i] === min) { - candidates.push(i); - } - } - return candidates[Math.floor(Math.random() * candidates.length)]; - } - /** * 处理插队刷怪队列(每 0.15 秒尝试消费一个): - * 1. 根据指定路或均衡分路。 - * 2. 找到后从队列中移除并生成怪物。 + * 1. 找到后从队列中移除并生成怪物。 */ private updateSpecialQueue(dt: number) { if (this.MonQueue.length <= 0) return; @@ -274,7 +201,9 @@ export class MissionMonCompComp extends CCComp { const isBoss = MonList[MonType.MeleeBoss].includes(item.uuid) || MonList[MonType.LongBoss].includes(item.uuid); - const lane = item.flyLane !== undefined && item.flyLane >= 0 && item.flyLane <= 5 ? item.flyLane : this.pickBalancedLane(); + const spawnIndex = this.waveSpawnedCount++; + const row = spawnIndex % 3; + const col = Math.floor(spawnIndex / 3); // 构造一个模拟的 GeneratedMonster 数据传递给 addMonsterAt const base = HeroInfo[item.uuid]; @@ -287,8 +216,7 @@ export class MissionMonCompComp extends CCComp { isBoss: isBoss, spawnIndex: 0 }; - this.addMonsterAt(lane, this.laneIndices[lane], monData, item.level); - this.laneIndices[lane]++; + this.addMonsterAtGrid(row, col, monData, item.level); } // ======================== 波次管理 ======================== @@ -309,6 +237,21 @@ export class MissionMonCompComp extends CCComp { private onPhasePrepareEnd() { this.resetSlotSpawnData(this.currentWave); + + // 准备结束阶段,立即刷出本波所有怪物 + if (this.pendingMonsters.length > 0) { + let count = Math.min(this.pendingMonsters.length, MissionMonCompComp.MAX_MONSTERS); + for (let i = 0; i < count; i++) { + const monData = this.pendingMonsters.shift()!; + const row = this.waveSpawnedCount % 3; + const col = Math.floor(this.waveSpawnedCount / 3); + console.log(`[MissionMonComp] [PhasePrepareEnd] 准备生成怪物 UUID=${monData.uuid}, 当前已生成数量=${this.waveSpawnedCount}`); + this.addMonsterAtGrid(row, col, monData); + this.waveSpawnedCount++; + } + // 生成完毕后清空 pendingMonsters + this.pendingMonsters = []; + } } // ======================== 槽位管理 ======================== @@ -316,7 +259,7 @@ export class MissionMonCompComp extends CCComp { /** * 重新分配本波所有怪物状态: * 1. 清理上一波残留怪物。 - * 2. pendingMonsters 已在 onTimeUpAdvanceWave / fight_ready 中准备好,只需重置 laneIndices 即可。 + * 2. pendingMonsters 已在 onTimeUpAdvanceWave / fight_ready 中准备好。 * * @param wave 当前波数 */ @@ -330,7 +273,6 @@ export class MissionMonCompComp extends CCComp { }); // 2. 重置排号索引 - this.laneIndices = [0, 0, 0, 0, 0, 0]; this.waveSpawnedCount = 0; } @@ -339,14 +281,14 @@ export class MissionMonCompComp extends CCComp { /** * 在指定层级、指定索引处生成一个怪物: * - * @param laneIndex 三路索引 (0 上, 1 中, 2 下) - 已扩展为6路 - * @param monIndex 该路级的第几个怪 (0, 1, 2...) + * @param row 行 (0, 1, 2) + * @param col 列 (0, 1, 2, 3) * @param monData 新引擎生成的怪物数据 (含 uuid, hp, ap, affixes 等) * @param monLv 怪物等级 (仅对旧有的 level 参数做兼容,实际属性由 monData 决定) */ - private addMonsterAt( - laneIndex: number, - monIndex: number, + private addMonsterAtGrid( + row: number, + col: number, monData: GeneratedMonster, monLv: number = 1 ) { @@ -354,13 +296,13 @@ export class MissionMonCompComp extends CCComp { let scale = -1; // 计算坐标 - const spawnX = MissionMonCompComp.MON_SPAWN_START_X + monIndex * MissionMonCompComp.MON_SPAWN_GAP_X; + const spawnX = MissionMonCompComp.MON_SPAWN_START_X + col * MissionMonCompComp.MON_SPAWN_GAP_X; const randomY = Math.random() * 20 - 10; // -10 到 10 的随机Y轴偏移 - const landingY = BoxSet.GAME_LINE + MissionMonCompComp.LANE_Y_OFFSETS[laneIndex] + randomY + (monData.isBoss ? 6 : 0); + const landingY = BoxSet.GAME_LINE + MissionMonCompComp.ROW_Y_OFFSETS[row] + randomY + (monData.isBoss ? 6 : 0); const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0); this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999; - mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monLv, laneIndex); + mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monLv, row); // 设置渲染排序 const move = mon.get(MonMoveComp);