feat(monster&spawn): 新增飞行怪物支持,重构怪物移动与刷怪系统

抽离MonMoveComp拆分怪物移动逻辑,让MoveComp仅负责英雄移动
新增Fly和FlyBoss怪物类型,配置三层飞行轨道支持空中怪物
重写波次刷怪逻辑,移除固定5槽限制,按轨道自由排布怪物
将怪物生成上限与恢复阈值从5/3调整为50/30
优化渲染排序逻辑,为飞行怪添加持续浮动动画
移除跨波怪物属性继承,波次切换时自动清理残留怪物
This commit is contained in:
walkpan
2026-05-12 12:23:37 +08:00
parent 92db480baf
commit e7075004fe
7 changed files with 390 additions and 264 deletions

View File

@@ -0,0 +1,229 @@
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
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 { Node } from "cc";
@ecs.register('MonMoveComp')
export class MonMoveComp extends ecs.Comp {
/** 朝向1=向右,-1=向左 */
direction: number = -1;
/** 当前移动目标 X */
targetX: number = 0;
/** 是否允许移动(出生落地前会短暂关闭) */
moving: boolean = true;
/** 站位基准 Y */
baseY: number = 0;
/** 出生序,用于同条件渲染排序稳定 */
spawnOrder: number = 0;
reset() {
this.direction = -1;
this.targetX = 0;
this.moving = true;
this.baseY = 0;
this.spawnOrder = 0;
}
}
@ecs.register('MonMoveSystem')
export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
/** 近战判定射程 */
private readonly meleeAttackRange = 250;
/** 远程判定射程 */
private readonly longAttackRange = 600;
/** 渲染层级重排节流,避免每帧排序 */
private readonly renderSortInterval = 0.05;
private lastRenderSortAt = 0;
private monMoveMatcher: ecs.IMatcher | null = null;
private heroViewMatcher: ecs.IMatcher | null = null;
private readonly renderEntries: { node: Node; bossPriority: number; frontScore: number; spawnOrder: number; eid: number }[] = [];
private renderEntryCount = 0;
private getMonMoveMatcher(): ecs.IMatcher {
if (!this.monMoveMatcher) {
this.monMoveMatcher = ecs.allOf(HeroAttrsComp, HeroViewComp, MonMoveComp);
}
return this.monMoveMatcher;
}
private getHeroViewMatcher(): ecs.IMatcher {
if (!this.heroViewMatcher) {
this.heroViewMatcher = ecs.allOf(HeroAttrsComp, HeroViewComp);
}
return this.heroViewMatcher;
}
filter(): ecs.IMatcher {
return ecs.allOf(MonMoveComp, HeroViewComp, HeroAttrsComp);
}
update(e: ecs.Entity) {
/** 战斗未开始/暂停时不驱动移动 */
if (!smc.mission.play || smc.mission.pause) return;
const model = e.get(HeroAttrsComp);
const move = e.get(MonMoveComp);
const view = e.get(HeroViewComp);
if (!model || !move || !view || !view.node) return;
if (model.fac !== FacSet.MON) return;
if (!move.moving) return;
/** 关卡阶段性冻结怪物行为 */
if (smc.mission.stop_mon_action) {
this.clearCombatTarget(model);
view.status_change("idle");
return;
}
if (model.is_stop || model.is_dead || model.is_reviving || model.isFrost()) {
this.clearCombatTarget(model);
if (!model.is_reviving) view.status_change("idle");
return;
}
/** 所有移动都锁定在 baseY避免出现“漂移” */
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);
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);
}
}
private clearCombatTarget(model: HeroAttrsComp): void {
model.combat_target_eid = -1;
model.enemy_in_cast_range = false;
}
private syncCombatTarget(model: HeroAttrsComp, selfView: HeroViewComp, enemyView: HeroViewComp): void {
if (!enemyView || !enemyView.node || !enemyView.ent) {
this.clearCombatTarget(model);
return;
}
const enemyAttrs = enemyView.ent.get(HeroAttrsComp);
if (!enemyAttrs || enemyAttrs.is_dead || enemyAttrs.is_reviving || enemyAttrs.fac === model.fac) {
this.clearCombatTarget(model);
return;
}
model.combat_target_eid = enemyView.ent.eid;
model.enemy_in_cast_range = this.isEnemyInAttackRange(model, selfView.node.position.x, enemyView.node.position.x);
}
private isEnemyInAttackRange(model: HeroAttrsComp, selfX: number, enemyX: number): boolean {
const dist = Math.abs(selfX - enemyX);
const rangeType = model.type as HType.Melee | HType.Mid | HType.Long;
if (rangeType === HType.Melee) return dist <= this.meleeAttackRange;
return dist <= this.longAttackRange;
}
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 isFly = move.baseY > 50;
const touchDistance = 50;
const isTouching = !isFly && (dist <= touchDistance);
// 攻击判定
if (inRange) {
model.is_atking = true;
} 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;
const currentPos = currentView.node.position;
const myFac = entity.get(HeroAttrsComp).fac;
let nearest: HeroViewComp | null = null;
let minDis = Infinity;
/** 一次遍历筛出最近敌人(仅比较 x 轴距离,忽略 Y飞行和地面都能互相攻击 */
ecs.query(this.getHeroViewMatcher()).forEach(e => {
const m = e.get(HeroAttrsComp);
if (m.fac !== myFac && !m.is_dead) {
const v = e.get(HeroViewComp);
if (v?.node) {
const d = Math.abs(currentPos.x - v.node.position.x);
if (d < minDis) {
minDis = d;
nearest = v;
}
}
}
});
return nearest;
}
}