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 { HType } from "../common/config/heroSet"; import { SkillRange } from "../common/config/SkillSet"; import { Node } from "cc"; @ecs.register('MoveComp') export class MoveComp extends ecs.Comp { direction: number = 1; targetX: number = 0; moving: boolean = true; targetY: number = 0; 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 { private readonly meleeAttackRange = 52; private readonly allySpacingX = 40; private readonly minSpacingY = 30; private readonly renderSortInterval = 0.05; private renderSortElapsed = 0; private readonly facConfigs: Record = { [FacSet.HERO]: { moveFrontX: 320, moveBackX: -320, retreatFrontX: 300, retreatBackX: -300, }, [FacSet.MON]: { moveFrontX: -320, moveBackX: 320, retreatFrontX: -300, retreatBackX: 300, } }; 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) { view.status_change("idle"); return; } if (model.is_stop || model.is_dead || model.is_reviving || model.in_stun || model.in_frost) { if (!model.is_reviving) view.status_change("idle"); return; } 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); } else { move.targetY = 0; this.processReturnFormation(e, move, view, model); model.is_atking = false; } } private processCombatLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) { let rangeType = model.rangeType; if (rangeType === undefined) { if (model.type === HType.warrior || model.type === HType.assassin) { rangeType = SkillRange.Melee; } else if (model.type === HType.remote) { rangeType = SkillRange.Long; } else { rangeType = SkillRange.Mid; } } switch (rangeType) { case SkillRange.Melee: this.processMeleeLogic(e, move, view, model, enemy); break; case SkillRange.Mid: this.processMidLogic(e, move, view, model, enemy); break; case SkillRange.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) { const currentX = view.node.position.x; const enemyX = enemy.node.position.x; const dist = Math.abs(currentX - enemyX); const maxRange = this.meleeAttackRange; move.direction = enemyX > currentX ? 1 : -1; if (dist <= maxRange) { view.status_change("idle"); model.is_atking = true; } else { const speed = model.speed / 3; const stopAtX = enemyX - move.direction * maxRange; this.moveEntity(view, move.direction, speed, stopAtX); model.is_atking = true; } } private processMidLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) { const currentX = view.node.position.x; const enemyX = enemy.node.position.x; const dist = Math.abs(currentX - enemyX); const [, maxRange] = this.resolveCombatRange(model, 120, 360); move.direction = enemyX > currentX ? 1 : -1; if (dist > maxRange) { const speed = model.speed / 3; this.moveEntity(view, move.direction, speed); model.is_atking = true; } else { view.status_change("idle"); model.is_atking = true; } } private processLongLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) { const currentX = view.node.position.x; const enemyX = enemy.node.position.x; const dist = Math.abs(currentX - enemyX); const [, maxRange] = this.resolveCombatRange(model, 360, 720); move.direction = enemyX > currentX ? 1 : -1; if (dist > maxRange) { const speed = model.speed / 3; this.moveEntity(view, move.direction, 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 { let rangeType = model.rangeType; if (rangeType === undefined || rangeType === null) { if (model.type === HType.remote) { rangeType = SkillRange.Long; } else if (model.type === HType.mage || model.type === HType.support) { rangeType = SkillRange.Mid; } else { rangeType = SkillRange.Melee; } } const side = model.fac === FacSet.MON ? 1 : -1; if (rangeType === SkillRange.Long) { return 300 * side; } if (rangeType === SkillRange.Mid) { return 200 * side; } return 0; } 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) { newX = direction > 0 ? Math.min(newX, stopAtX) : Math.max(newX, stopAtX); } newX = this.clampXByAllies(view.ent, model.fac, move.baseY, currentX, newX, direction); 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 { let nearestAheadX = Infinity; let nearestBehindX = -Infinity; ecs.query(ecs.allOf(HeroAttrsComp, HeroViewComp, MoveComp)).forEach(e => { if (e === self) return; const attrs = e.get(HeroAttrsComp); const view = e.get(HeroViewComp); 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 x = view.node.position.x; if (x > currentX && x < nearestAheadX) nearestAheadX = x; if (x < currentX && x > nearestBehindX) nearestBehindX = x; }); if (direction > 0 && nearestAheadX !== Infinity) { return Math.min(proposedX, nearestAheadX - this.allySpacingX); } if (direction < 0 && nearestBehindX !== -Infinity) { return Math.max(proposedX, nearestBehindX + this.allySpacingX); } return proposedX; } private hasAnyActorTooClose(self: ecs.Entity, x: number, y: number): boolean { const myAttrs = self.get(HeroAttrsComp); if (!myAttrs) return false; return ecs.query(ecs.allOf(HeroAttrsComp, HeroViewComp)).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; // 优化查询:一次遍历 ecs.query(ecs.allOf(HeroAttrsComp, HeroViewComp)).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) { this.renderSortElapsed += this.dt; 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; } if (this.renderSortElapsed < this.renderSortInterval) return; this.renderSortElapsed = 0; const renderList: { node: Node; frontScore: number; spawnOrder: number; eid: number }[] = []; ecs.query(ecs.allOf(HeroAttrsComp, HeroViewComp, MoveComp)).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; renderList.push({ node: actorView.node, frontScore, spawnOrder: actorMove.spawnOrder, eid: e.eid }); }); renderList.sort((a, b) => { 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; }); renderList.forEach((item, index) => { item.node.setSiblingIndex(index); }); } }