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, resolveFormationTargetX } from "../common/config/heroSet"; import { Node } from "cc"; @ecs.register('MoveComp') export class MoveComp extends ecs.Comp { /** 朝向:1=向右,-1=向左 */ direction: number = 1; /** 当前移动目标 X(战斗/回位都会更新) */ targetX: number = 0; /** 是否允许移动(出生落地前会短暂关闭) */ moving: boolean = true; /** 预留:目标 Y(当前逻辑主要使用 baseY 锁定地平线) */ targetY: number = 0; /** 角色所属车道的地平线 Y,移动时强制贴地 */ baseY: number = 0; /** 预留:车道索引 */ lane: number = 0; /** 出生序,用于同条件渲染排序稳定 */ spawnOrder: number = 0; reset() { this.direction = 1; this.targetX = 0; this.moving = true; this.targetY = 0; this.baseY = 0; this.lane = 0; this.spawnOrder = 0; } } interface MoveFacConfig { /** 阵营可前进边界(靠近敌方一侧) */ moveFrontX: number; /** 阵营可后退边界(靠近己方一侧) */ moveBackX: number; /** 被逼退时前侧安全边界 */ retreatFrontX: number; /** 被逼退时后侧安全边界 */ retreatBackX: number; } @ecs.register('MoveSystem') export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate { /** 近战判定射程(来自 heroSet) */ private readonly meleeAttackRange = HeroDisVal[HType.Melee]; /** 近战贴脸最小距离,避免完全重叠 */ private readonly meleeMinEnemyDistanceX = 60; /** 同优先级近战允许“超车”时,至少要快这么多 */ private readonly meleeOvertakeSpeedGap = 20; /** 常规同阵营横向最小间距 */ private readonly allySpacingX = 60; /** 允许临时压缩站位时的最小间距 */ private readonly allyOverlapSpacingX = 14; /** 友军偏离其目标点超过该值,可放宽让路 */ private readonly displacementReleaseX = 10; /** 即将进入攻击位的锁定阈值 */ private readonly attackReadyLockX = 10; /** 目标距离足够远才触发“借道前压” */ private readonly attackPassThresholdX = 60; /** 纵向判定为同排的最大 Y 差 */ private readonly minSpacingY = 30; /** 渲染层级重排节流,避免每帧排序 */ private readonly renderSortInterval = 0.05; private lastRenderSortAt = 0; private heroMoveMatcher: 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; /** * 阵营可移动边界配置。 * HERO 在左侧出生并向右推进;MON 在右侧出生并向左推进。 */ private readonly facConfigs: Record = { [FacSet.HERO]: { moveFrontX: 320, moveBackX: -320, retreatFrontX: 300, retreatBackX: -300, }, [FacSet.MON]: { moveFrontX: -320, moveBackX: 320, retreatFrontX: -300, retreatBackX: 300, } }; private getHeroMoveMatcher(): ecs.IMatcher { if (!this.heroMoveMatcher) { this.heroMoveMatcher = ecs.allOf(HeroAttrsComp, HeroViewComp, MoveComp); } return this.heroMoveMatcher; } 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 (model.fac !== FacSet.HERO && model.fac !== FacSet.MON) return; if (!move.moving) return; /** 关卡阶段性冻结怪物行为 */ if (model.fac === FacSet.MON && 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); } this.updateRenderOrder(view); const nearestEnemy = this.findNearestEnemy(e); if (nearestEnemy) { /** 有敌人:进入战斗位移逻辑 */ this.processCombatLogic(e, move, view, model, nearestEnemy); this.syncCombatTarget(model, view, nearestEnemy); } else { /** 无敌人:清目标并回归编队站位 */ this.clearCombatTarget(model); move.targetY = 0; this.processReturnFormation(e, move, view, model); model.is_atking = false; } } 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; if (rangeType === HType.Long) { const [, maxRange] = this.resolveCombatRange(model, 360, 720); return dist <= maxRange; } const [, maxRange] = this.resolveCombatRange(model, 120, 360); return dist <= maxRange; } private processCombatLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) { const rangeType = model.type as HType.Melee | HType.Mid | HType.Long; switch (rangeType) { case HType.Melee: this.processMeleeLogic(e, move, view, model, enemy); break; case HType.Mid: this.processMidLogic(e, move, view, model, enemy); break; case HType.Long: this.processLongLogic(e, move, view, model, enemy); break; default: this.processMidLogic(e, move, view, model, enemy); // 默认中程 break; } } private processMeleeLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) { this.processRangedFormationCombat(move, view, model); } private processMidLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) { this.processRangedFormationCombat(move, view, model); } private processLongLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) { this.processRangedFormationCombat(move, view, model); } private processRangedFormationCombat(move: MoveComp, view: HeroViewComp, model: HeroAttrsComp) { const currentX = view.node.position.x; /** 中/远程不追人:强制回到自己职业站位点输出 */ const targetX = this.getFixedFormationX(model); move.targetX = targetX; const needMoveToFormation = Math.abs(currentX - targetX) > 5; if (needMoveToFormation) { const dir = targetX > currentX ? 1 : -1; move.direction = dir; const speed = model.speed / 3; this.moveEntity(view, dir, speed); model.is_atking = true; } else { view.status_change("idle"); model.is_atking = true; } } private performRetreat(view: HeroViewComp, move: MoveComp, model: HeroAttrsComp, currentX: number) { const cfg = this.facConfigs[model.fac] || this.facConfigs[FacSet.HERO]; const retreatMinX = Math.min(cfg.retreatBackX, cfg.retreatFrontX); const retreatMaxX = Math.max(cfg.retreatBackX, cfg.retreatFrontX); const safeRetreatX = currentX - move.direction * 50; if (safeRetreatX >= retreatMinX && safeRetreatX <= retreatMaxX) { const retreatSpeed = (model.speed / 3) * 0.8; this.moveEntity(view, -move.direction, retreatSpeed); model.is_atking = false; } else { view.status_change("idle"); model.is_atking = true; } } private processReturnFormation(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp) { const currentX = view.node.position.x; /** 脱战时所有类型都回职业站位点,保证阵型稳定 */ const targetX = this.getFixedFormationX(model); move.targetX = targetX; if (Math.abs(currentX - targetX) > 5) { const dir = targetX > currentX ? 1 : -1; const speed = model.speed / 3; move.direction = dir; this.moveEntity(view, dir, speed); const newX = view.node.position.x; if ((dir === 1 && newX > targetX) || (dir === -1 && newX < targetX)) { if (!this.hasAnyActorTooClose(e, targetX, view.node.position.y)) { view.node.setPosition(targetX, view.node.position.y, 0); } } } else { view.status_change("idle"); } } private getFixedFormationX(model: HeroAttrsComp): number { /** * 站位点来自 heroSet.resolveFormationTargetX: * - 近战 HType.Melee: FormationPointX=0 * - 中程 HType.Mid: FormationPointX=100 * - 远程 HType.Long: FormationPointX=180 * 阵营镜像规则: * - HERO(FacSet.HERO=0):乘 -1 => 近战0 / 中程-100 / 远程-180 * - MON(FacSet.MON=1):乘 +1 => 近战0 / 中程100 / 远程180 */ return resolveFormationTargetX(model.fac, model.type as HType); } private moveEntity(view: HeroViewComp, direction: number, speed: number, stopAtX?: number) { const model = view.ent.get(HeroAttrsComp); const move = view.ent.get(MoveComp); if (!model || !move) return; /** 按阵营边界裁剪,防止跑出战场可移动区域 */ const cfg = this.facConfigs[model.fac] || this.facConfigs[FacSet.HERO]; const moveMinX = Math.min(cfg.moveBackX, cfg.moveFrontX); const moveMaxX = Math.max(cfg.moveBackX, cfg.moveFrontX); const currentX = view.node.position.x; const delta = speed * this.dt * direction; let newX = view.node.position.x + delta; if (currentX < moveMinX && direction < 0) { view.status_change("idle"); return; } if (currentX > moveMaxX && direction > 0) { view.status_change("idle"); return; } newX = Math.max(moveMinX, Math.min(moveMaxX, newX)); if (stopAtX !== undefined) { /** 指定停止点时,限制不越过 stopAtX */ newX = direction > 0 ? Math.min(newX, stopAtX) : Math.max(newX, stopAtX); } /** 结合同排友军占位,做“让位/防重叠”裁剪 */ newX = this.clampXByAllies(view.ent, model.fac, move.baseY, currentX, newX, direction); if (direction > 0) { newX = Math.max(currentX, newX); } else { newX = Math.min(currentX, newX); } if (Math.abs(newX - currentX) < 0.01) { view.status_change("idle"); return; } view.node.setPosition(newX, move.baseY, 0); view.status_change("move"); } private clampXByAllies(self: ecs.Entity, fac: number, baseY: number, currentX: number, proposedX: number, direction: number): number { const selfAttrs = self.get(HeroAttrsComp); const selfMove = self.get(MoveComp); const selfPriority = selfAttrs ? this.getCombatPriority(selfAttrs) : 0; let clampedX = proposedX; ecs.query(this.getHeroMoveMatcher()).forEach(e => { if (e === self) return; const attrs = e.get(HeroAttrsComp); const view = e.get(HeroViewComp); const allyMove = e.get(MoveComp); /** 只处理同阵营且同排(Y 接近)的友军碰撞约束 */ if (!attrs || !view?.node || attrs.is_dead) return; if (attrs.fac !== fac) return; if (Math.abs(view.node.position.y - baseY) >= this.minSpacingY) return; const allyPriority = this.getCombatPriority(attrs); if (allyPriority < selfPriority) return; const x = view.node.position.x; /** 近战同优先级在满足条件时可超车,不做阻挡 */ if (this.shouldAllowMeleeOvertake(selfAttrs, selfMove, attrs, allyMove, currentX, x, direction, allyPriority, selfPriority)) return; const spacing = this.resolveAllySpacing(selfAttrs, selfMove, currentX, direction, allyMove, x, allyPriority, selfPriority); if (direction > 0 && x > currentX) { clampedX = Math.min(clampedX, x - spacing); } if (direction < 0 && x < currentX) { clampedX = Math.max(clampedX, x + spacing); } }); return clampedX; } private shouldAllowMeleeOvertake( selfAttrs: HeroAttrsComp | null, selfMove: MoveComp | null, allyAttrs: HeroAttrsComp, allyMove: MoveComp | null, currentX: number, allyX: number, direction: number, allyPriority: number, selfPriority: number ): boolean { if (!selfAttrs || !selfMove || !allyMove) return false; /** 仅近战对近战、且同优先级才进入超车判定 */ if ((selfAttrs.type as HType) !== HType.Melee || (allyAttrs.type as HType) !== HType.Melee) return false; if (allyPriority !== selfPriority) return false; /** 我方更快,且双方都在前压且友军尚未到可攻击位,允许穿插 */ if (selfAttrs.speed <= allyAttrs.speed + this.meleeOvertakeSpeedGap) return false; if (direction > 0 && allyX <= currentX) return false; if (direction < 0 && allyX >= currentX) return false; const selfTargetX = selfMove.targetX; const allyTargetX = allyMove.targetX; if (Math.abs(selfTargetX) <= 0.01 || Math.abs(allyTargetX) <= 0.01) return false; const selfNeedAdvance = direction > 0 ? selfTargetX > currentX + 2 : selfTargetX < currentX - 2; if (!selfNeedAdvance) return false; const allyCanAttackNow = allyAttrs.enemy_in_cast_range || Math.abs(allyTargetX - allyX) <= this.attackReadyLockX; if (allyCanAttackNow) return false; const allyStillAdvancing = direction > 0 ? allyTargetX > allyX + 2 : allyTargetX < allyX - 2; if (!allyStillAdvancing) return false; return true; } private resolveAllySpacing( selfAttrs: HeroAttrsComp | null, selfMove: MoveComp | null, currentX: number, direction: number, allyMove: MoveComp | null, allyX: number, allyPriority: number, selfPriority: number ): number { /** 默认保持标准间距,仅在“需要抢位输出”时放宽 */ if (!selfAttrs || !selfMove || !allyMove) return this.allySpacingX; if ((selfAttrs.type as HType) !== HType.Melee) return this.allySpacingX; if (allyPriority !== selfPriority) return this.allySpacingX; const selfTargetX = selfMove.targetX; const allyTargetX = allyMove.targetX; const selfHasTarget = Math.abs(selfTargetX) > 0.01; const allyHasTarget = Math.abs(allyTargetX) > 0.01; if (!selfHasTarget || !allyHasTarget) return this.allySpacingX; const selfDistToAttack = Math.abs(selfTargetX - currentX); const canAttackNow = selfAttrs.enemy_in_cast_range || selfDistToAttack <= this.attackReadyLockX; if (canAttackNow) return this.allySpacingX; const targetTooFar = selfDistToAttack >= this.attackPassThresholdX; if (!targetTooFar) return this.allySpacingX; const allyDisplaced = Math.abs(allyX - allyTargetX) >= this.displacementReleaseX; const selfNeedAdvance = direction > 0 ? selfTargetX > currentX + 2 : selfTargetX < currentX - 2; if (allyDisplaced && selfNeedAdvance) return this.allyOverlapSpacingX; return this.allySpacingX; } private getCombatPriority(model: HeroAttrsComp): number { /** 数值越大越靠前:近战 > 中程 > 远程 */ const rangeType = model.type as HType.Melee | HType.Mid | HType.Long; if (rangeType === HType.Melee) return 3; if (rangeType === HType.Mid) return 2; return 1; } private hasAnyActorTooClose(self: ecs.Entity, x: number, y: number): boolean { const myAttrs = self.get(HeroAttrsComp); if (!myAttrs) return false; return ecs.query(this.getHeroViewMatcher()).some(e => { if (e === self) return false; const attrs = e.get(HeroAttrsComp); if (!attrs || attrs.is_dead) return false; if (attrs.fac !== myAttrs.fac) return false; const view = e.get(HeroViewComp); if (!view || !view.node) return false; return Math.abs(view.node.position.x - x) < this.allySpacingX && Math.abs(view.node.position.y - y) < this.minSpacingY; }); } 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 轴距离) */ 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; } private updateRenderOrder(view: HeroViewComp) { const scene = smc.map?.MapView?.scene; const actorRoot = scene?.entityLayer?.node?.getChildByName("HERO"); if (!actorRoot) return; if (view.node.parent !== actorRoot) { view.node.parent = actorRoot; } const now = Date.now() / 1000; if (now - this.lastRenderSortAt < this.renderSortInterval) return; this.lastRenderSortAt = now; this.renderEntryCount = 0; ecs.query(this.getHeroMoveMatcher()).forEach(e => { const attrs = e.get(HeroAttrsComp); const actorView = e.get(HeroViewComp); const actorMove = e.get(MoveComp); if (!attrs || !actorView?.node || !actorMove || attrs.is_dead) return; if (attrs.fac !== FacSet.HERO && attrs.fac !== FacSet.MON) return; if (actorView.node.parent !== actorRoot) { actorView.node.parent = actorRoot; } 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 }; this.renderEntries.push(entry); } entry.node = actorView.node; entry.bossPriority = attrs.is_boss ? 1 : 0; entry.frontScore = frontScore; entry.spawnOrder = actorMove.spawnOrder; entry.eid = e.eid; this.renderEntryCount += 1; }); this.renderEntries.length = this.renderEntryCount; /** 定时重排同层节点:Boss 优先级 -> 前后位置 -> 出生序 -> eid */ this.renderEntries.sort((a, b) => { if (a.bossPriority !== b.bossPriority) return a.bossPriority - b.bossPriority; 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; }); this.renderEntries.forEach((item, index) => { item.node.setSiblingIndex(index); }); } }