/** * @file MoveComp.ts * @description 统一移动组件与系统(英雄 + 怪物) * * 核心规则: * 1. 准备阶段:双方原地待命,不推进不索敌。 * 2. 战斗阶段:双方向最近敌人推进,进入攻击范围(model.dis)后停止移动并攻击。 * 3. 场上无敌人:停止移动(战斗结束,由 MissionComp 进入结算)。 * 4. 击退/位移不会把单位推出屏幕:HERO 不越过 BoxSet.LETF_END,MON 不越过 BoxSet.RIGHT_END。 * 5. 推进方向无上限限制,直到接触敌人或抵达敌方边界。 */ 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 { BoxSet, FacSet } from "../common/config/GameSet"; import { Node } from "cc"; @ecs.register('MoveComp') export class MoveComp extends ecs.Comp { /** 朝向:1=向右(HERO),-1=向左(MON),由系统根据 fac 自动维护 */ 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('MoveSystem') export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate { /** 渲染层级重排节流,避免每帧排序 */ private readonly renderSortInterval = 0.05; private lastRenderSortAt = 0; private heroViewMatcher: ecs.IMatcher | null = null; private readonly renderEntries: { node: Node; bossPriority: number; frontScore: number; spawnOrder: number; eid: number; laneScore: number }[] = []; private renderEntryCount = 0; private getHeroViewMatcher(): ecs.IMatcher { if (!this.heroViewMatcher) { this.heroViewMatcher = ecs.allOf(HeroAttrsComp, HeroViewComp); } return this.heroViewMatcher; } filter(): ecs.IMatcher { return ecs.allOf(MoveComp, HeroViewComp, HeroAttrsComp); } update(e: ecs.Entity) { /** 战斗未开始/暂停时不驱动移动 */ if (!smc.mission.play || smc.mission.pause) return; const model = e.get(HeroAttrsComp); const move = e.get(MoveComp); const view = e.get(HeroViewComp); if (!model || !move || !view || !view.node) return; if (!move.moving) return; /** 关卡阶段性冻结怪物行为 */ if (smc.mission.stop_mon_action && model.fac === FacSet.MON) { this.clearCombatTarget(model); view.status_change("idle"); return; } /** 死亡/复活/控制状态:停止移动与索敌 */ if (model.is_stop || model.is_dead || model.is_reviving || model.isFrost() || model.isStun()) { 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); } /** 准备阶段:双方原地待命,不推进不索敌 */ if (!smc.mission.in_fight) { this.clearCombatTarget(model); model.is_atking = false; if (view.status !== "atk") view.status_change("idle"); 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; if (view.status !== "atk") view.status_change("idle"); } /** 渲染层级重排(双阵营统一处理) */ this.updateRenderOrder(); } 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 attackRange = model.dis; return dist <= attackRange; } /** * 战斗位移:向最近敌人推进,进入攻击范围后停止。 * 推进无上限限制,但击退/位移受阵营后退边界保护。 */ private processCombatLogic(_e: ecs.Entity, move: MoveComp, 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 attackRange = model.dis; /** 近战贴身作战:攻击距离 dis(100) 大于技能范围(60),推进到与敌人相隔 60 才停下,保证技能可命中且不过度贴身 */ const stopRange = model.type === 0 ? 60 : attackRange; const inRange = dist <= stopRange; 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; const dir = enemyX > selfX ? 1 : -1; move.direction = dir; const speed = model.speed / 3; const delta = speed * this.dt * dir; let newX = selfX + delta; /** 后退边界钳制:防止被击退/位移推出屏幕 */ newX = this.clampByFacBoundary(newX, model.fac); if (Math.abs(newX - selfX) >= 0.01) { view.node.setPosition(newX, view.node.position.y, 0); view.status_change("move"); } } } /** * 按阵营裁剪 X 坐标,防止被击退/位移推出屏幕。 * HERO 后退不越过 BoxSet.LETF_END;MON 后退不越过 BoxSet.RIGHT_END。 */ private clampByFacBoundary(x: number, fac: number): number { if (fac === FacSet.HERO) { return Math.max(BoxSet.LETF_END, x); } return Math.min(BoxSet.RIGHT_END, x); } 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 dx = Math.abs(currentPos.x - v.node.position.x); const dy = Math.abs(currentPos.y - v.node.position.y); const d = dx + dy * 0.1; // Y轴权重较小,仅在 X 相近时起决定作用 if (d < minDis) { minDis = d; nearest = v; } } } }); return nearest; } private updateRenderOrder() { const scene = smc.map?.MapView?.scene; const layerNode = scene?.entityLayer?.node; if (!layerNode) return; const now = Date.now() / 1000; if (now - this.lastRenderSortAt < this.renderSortInterval) return; this.lastRenderSortAt = now; const heroRoot = layerNode.getChildByName("HERO"); const monRoot = layerNode.getChildByName("MON"); const bossRoot = layerNode.getChildByName("BOSS"); /** 分层重排同层节点:各阵营层内按 Y 轴路次 -> 前后位置 -> 出生序 -> eid 排序 * (Boss 已有独立 BOSS 层固定在最底,无需 bossPriority 参与排序) */ this.renderEntryCount = 0; ecs.query(this.getHeroViewMatcher()).forEach(e => { const attrs = e.get(HeroAttrsComp); const actorView = e.get(HeroViewComp); if (!attrs || !actorView?.node || attrs.is_dead) return; if (attrs.fac !== FacSet.HERO && attrs.fac !== FacSet.MON) return; /** 按阵营/身份归位到对应显示层 */ let targetRoot: Node | null | undefined; if (attrs.fac === FacSet.HERO) { targetRoot = heroRoot; } else { targetRoot = attrs.is_boss ? (bossRoot ?? monRoot ?? heroRoot) : (monRoot ?? heroRoot); } if (!targetRoot) return; if (actorView.node.parent !== targetRoot) { actorView.node.parent = targetRoot; } /** 统一从 MoveComp 获取 spawnOrder */ const move = e.get(MoveComp); const spawnOrder = move?.spawnOrder ?? 0; // 按 Y 轴计算渲染层级权重(Y 越小,说明越靠近屏幕下方,应该渲染在越前面) const laneScore = -actorView.node.position.y; // X 轴权重:站在前排的(交战处)优先渲染 const frontScore = attrs.fac === FacSet.HERO ? actorView.node.position.x : -actorView.node.position.x; const entryIndex = this.renderEntryCount; let entry = this.renderEntries[entryIndex]; if (!entry) { entry = { node: actorView.node, bossPriority: 0, frontScore: 0, spawnOrder: 0, eid: 0, laneScore: 0 }; this.renderEntries.push(entry); } entry.node = actorView.node; entry.laneScore = laneScore; entry.frontScore = frontScore; entry.spawnOrder = spawnOrder; entry.eid = e.eid; this.renderEntryCount += 1; }); this.renderEntries.length = this.renderEntryCount; this.renderEntries.sort((a, b) => { // Y轴靠下的(laneScore 较大)应该在后面,从而渲染在上面 if (Math.abs(a.laneScore - b.laneScore) > 10) return a.laneScore - b.laneScore; if (a.frontScore !== b.frontScore) return a.frontScore - b.frontScore; if (a.spawnOrder !== b.spawnOrder) return a.spawnOrder - b.spawnOrder; return a.eid - b.eid; }); /** 分层应用 siblingIndex:各层内部从 0 递增 */ const layerCounters = new Map(); this.renderEntries.forEach((item) => { const parent = item.node.parent; if (!parent) return; const index = layerCounters.get(parent) ?? 0; item.node.setSiblingIndex(index); layerCounters.set(parent, index + 1); }); } }