feat(monster): 调整怪物AI并重构刷怪系统

重构怪物移动逻辑,移除移动追击相关代码,改为定点攻击模式,无敌人时原地待机,仅根据攻击范围切换攻击状态与朝向。
重构波次刷怪逻辑,删除分段刷怪阶段处理,改为波次准备结束后批量生成所有怪物。
将原6路刷怪改为3行网格布局,调整怪物出生点的X轴起点与间距。
限制单波最大怪物数量为12,简化刷怪分配逻辑为按生成顺序自动排列行列。
清理冗余的运行时状态变量与废弃函数,优化代码整体结构。

BREAKING CHANGES: 怪物攻击逻辑从移动追击改为定点攻击,移除了MonMoveComp中的moveEntity和resolveCombatRange函数;刷怪系统从6路改为3行网格布局,移除了分段刷怪功能与相关状态变量。
This commit is contained in:
pan
2026-06-11 10:47:17 +08:00
parent 8d71cdd050
commit 10f5a9f35d
2 changed files with 46 additions and 153 deletions

View File

@@ -86,19 +86,19 @@ export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
} }
// 渲染层级统交由 MoveSystem 统一处理,避免两个 System 争抢 setSiblingIndex // 渲染层级统交由 MoveSystem 统一处理,避免两个 System 争抢 setSiblingIndex
// 仅在战斗中才处理索敌和移动 // 仅在战斗中才处理索敌
if (!smc.mission.in_fight) return; if (!smc.mission.in_fight) return;
const nearestEnemy = this.findNearestEnemy(e); const nearestEnemy = this.findNearestEnemy(e);
if (nearestEnemy) { if (nearestEnemy) {
/** 有敌人:进入战斗位移逻辑 */ /** 有敌人:进入固定位置攻击逻辑 */
this.processCombatLogic(e, move, view, model, nearestEnemy); this.processCombatLogic(e, move, view, model, nearestEnemy);
this.syncCombatTarget(model, view, nearestEnemy); this.syncCombatTarget(model, view, nearestEnemy);
} else { } else {
/** 无敌人:继续向左推进 */ /** 无敌人:原地待机 */
this.clearCombatTarget(model); this.clearCombatTarget(model);
model.is_atking = false; 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) { private processCombatLogic(e: ecs.Entity, move: MonMoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) {
const selfX = view.node.position.x; const selfX = view.node.position.x;
const enemyX = enemy.node.position.x; const enemyX = enemy.node.position.x;
const dist = Math.abs(selfX - enemyX);
const inRange = this.isEnemyInAttackRange(model, selfX, enemyX); const inRange = this.isEnemyInAttackRange(model, selfX, enemyX);
// 接触判定距离,只有接触英雄才停止移动 // 攻击判定:怪物在固定位置,如果在攻击范围内则攻击,否则原地待机
const touchDistance = 120;
const isTouching = dist <= touchDistance;
// 攻击判定
if (inRange) { if (inRange) {
model.is_atking = true; model.is_atking = true;
// 确保朝向敌人
const dir = enemyX > selfX ? 1 : -1;
view.scale = dir;
} else { } else {
model.is_atking = false; model.is_atking = false;
}
// 移动判定:只有接触了英雄才停止移动,否则继续向英雄方向移动
if (isTouching) {
view.status_change("idle"); 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 { private findNearestEnemy(entity: ecs.Entity): HeroViewComp | null {
const currentView = entity.get(HeroViewComp); const currentView = entity.get(HeroViewComp);
if (!currentView?.node) return null; if (!currentView?.node) return null;

View File

@@ -51,14 +51,16 @@ const { ccclass, property } = _decorator;
export class MissionMonCompComp extends CCComp { export class MissionMonCompComp extends CCComp {
// ======================== 常量 ======================== // ======================== 常量 ========================
/** 怪物最多 12 个 */
private static readonly MAX_MONSTERS = 12;
/** 怪物出生点起点 X */ /** 怪物出生点起点 X */
private static readonly MON_SPAWN_START_X = 460; private static readonly MON_SPAWN_START_X = 60;
/** 怪物出生的 X 间距 */ /** 怪物出生的 X 间距 (列距) */
private static readonly MON_SPAWN_GAP_X = 50; private static readonly MON_SPAWN_GAP_X = 80;
/** 怪物出生掉落高度 */ /** 怪物出生掉落高度 */
private static readonly MON_DROP_HEIGHT = 0; private static readonly MON_DROP_HEIGHT = 0;
/** 6路高度偏移在三路的y轴范围内实现 6路 进军) */ /** 3行高度偏移 (行距) */
private static readonly LANE_Y_OFFSETS = [100, 60, 20, -20, -60, -100]; 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; private globalSpawnOrder: number = 0;
/** 插队刷怪处理计时器 */ /** 插队刷怪处理计时器 */
@@ -96,18 +96,6 @@ export class MissionMonCompComp extends CCComp {
private waveSpawnedCount: number = 0; private waveSpawnedCount: number = 0;
/** 等待生成的怪物队列(由新肉鸽引擎提供) */ /** 等待生成的怪物队列(由新肉鸽引擎提供) */
private pendingMonsters: GeneratedMonster[] = []; 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; if(!smc.mission.in_fight) return;
this.updateSpecialQueue(dt); 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[]) { 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; smc.vmdata.mission_data.pending_mon_num = this.pendingMonsters.length;
this.waveTargetCount = monsters.length; this.waveTargetCount = this.pendingMonsters.length;
this.waveTotalForPhase = monsters.length;
this.currentSpawnPhase = 1;
this.phaseSpawnedCount = 0;
this.phaseTargetCount = Math.ceil(monsters.length / 3);
this.phaseDelayTimer = 3.0;
let hasBoss = monsters.some(m => m.isBoss); let hasBoss = monsters.some(m => m.isBoss);
@@ -231,8 +176,6 @@ export class MissionMonCompComp extends CCComp {
this.waveSpawnedCount = 0 this.waveSpawnedCount = 0
this.MonQueue = [] this.MonQueue = []
this.pendingMonsters = [] this.pendingMonsters = []
this.spawnTimer = 0
this.laneIndices = [0, 0, 0, 0, 0, 0];
// 预生成第一波数据以获取数量和 Boss 信息 // 预生成第一波数据以获取数量和 Boss 信息
const monsters = spawningEngine.generateWave(this.currentWave); 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 秒尝试消费一个): * 处理插队刷怪队列(每 0.15 秒尝试消费一个):
* 1. 根据指定路或均衡分路 * 1. 找到后从队列中移除并生成怪物
* 2. 找到后从队列中移除并生成怪物。
*/ */
private updateSpecialQueue(dt: number) { private updateSpecialQueue(dt: number) {
if (this.MonQueue.length <= 0) return; if (this.MonQueue.length <= 0) return;
@@ -274,7 +201,9 @@ export class MissionMonCompComp extends CCComp {
const isBoss = MonList[MonType.MeleeBoss].includes(item.uuid) || const isBoss = MonList[MonType.MeleeBoss].includes(item.uuid) ||
MonList[MonType.LongBoss].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 // 构造一个模拟的 GeneratedMonster 数据传递给 addMonsterAt
const base = HeroInfo[item.uuid]; const base = HeroInfo[item.uuid];
@@ -287,8 +216,7 @@ export class MissionMonCompComp extends CCComp {
isBoss: isBoss, isBoss: isBoss,
spawnIndex: 0 spawnIndex: 0
}; };
this.addMonsterAt(lane, this.laneIndices[lane], monData, item.level); this.addMonsterAtGrid(row, col, monData, item.level);
this.laneIndices[lane]++;
} }
// ======================== 波次管理 ======================== // ======================== 波次管理 ========================
@@ -309,6 +237,21 @@ export class MissionMonCompComp extends CCComp {
private onPhasePrepareEnd() { private onPhasePrepareEnd() {
this.resetSlotSpawnData(this.currentWave); 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. 清理上一波残留怪物。 * 1. 清理上一波残留怪物。
* 2. pendingMonsters 已在 onTimeUpAdvanceWave / fight_ready 中准备好,只需重置 laneIndices 即可 * 2. pendingMonsters 已在 onTimeUpAdvanceWave / fight_ready 中准备好。
* *
* @param wave 当前波数 * @param wave 当前波数
*/ */
@@ -330,7 +273,6 @@ export class MissionMonCompComp extends CCComp {
}); });
// 2. 重置排号索引 // 2. 重置排号索引
this.laneIndices = [0, 0, 0, 0, 0, 0];
this.waveSpawnedCount = 0; this.waveSpawnedCount = 0;
} }
@@ -339,14 +281,14 @@ export class MissionMonCompComp extends CCComp {
/** /**
* 在指定层级、指定索引处生成一个怪物: * 在指定层级、指定索引处生成一个怪物:
* *
* @param laneIndex 三路索引 (0, 1, 2 下) - 已扩展为6路 * @param row 行 (0, 1, 2)
* @param monIndex 该路级的第几个怪 (0, 1, 2...) * @param col 列 (0, 1, 2, 3)
* @param monData 新引擎生成的怪物数据 (含 uuid, hp, ap, affixes 等) * @param monData 新引擎生成的怪物数据 (含 uuid, hp, ap, affixes 等)
* @param monLv 怪物等级 (仅对旧有的 level 参数做兼容,实际属性由 monData 决定) * @param monLv 怪物等级 (仅对旧有的 level 参数做兼容,实际属性由 monData 决定)
*/ */
private addMonsterAt( private addMonsterAtGrid(
laneIndex: number, row: number,
monIndex: number, col: number,
monData: GeneratedMonster, monData: GeneratedMonster,
monLv: number = 1 monLv: number = 1
) { ) {
@@ -354,13 +296,13 @@ export class MissionMonCompComp extends CCComp {
let scale = -1; 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 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); const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0);
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999; 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); const move = mon.get(MonMoveComp);