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, resolveFormationTargetX } from "../common/config/heroSet"; 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 heroMoveMatcher: ecs.IMatcher | null = null; private heroViewMatcher: ecs.IMatcher | null = null; private readonly renderEntries: { node: Node; frontScore: number; spawnOrder: number; eid: number }[] = []; private renderEntryCount = 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, } }; 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) { 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) { 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) { 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) { this.processRangedFormationCombat(move, view, model, enemy, 120, 360); } private processLongLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) { this.processRangedFormationCombat(move, view, model, enemy, 360, 720); } private processRangedFormationCombat(move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp, defaultMin: number, defaultMax: number) { const currentX = view.node.position.x; const enemyX = enemy.node.position.x; const targetX = this.getFixedFormationX(model); move.targetX = targetX; const [minRange, maxRange] = this.resolveCombatRange(model, defaultMin, defaultMax); const dist = Math.abs(currentX - enemyX); const needMoveToFormation = Math.abs(currentX - targetX) > 5; const shouldKeepDistance = dist < minRange; if (needMoveToFormation || shouldKeepDistance) { 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 { 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) { 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 selfPriority = selfAttrs ? this.getCombatPriority(selfAttrs) : 0; let nearestAheadX = Infinity; let nearestBehindX = -Infinity; ecs.query(this.getHeroMoveMatcher()).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 allyPriority = this.getCombatPriority(attrs); if (allyPriority < selfPriority) 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 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; // 优化查询:一次遍历 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) { 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; 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, frontScore: 0, spawnOrder: 0, eid: 0 }; this.renderEntries.push(entry); } entry.node = actorView.node; entry.frontScore = frontScore; entry.spawnOrder = actorMove.spawnOrder; entry.eid = e.eid; this.renderEntryCount += 1; }); this.renderEntries.length = this.renderEntryCount; this.renderEntries.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; }); this.renderEntries.forEach((item, index) => { item.node.setSiblingIndex(index); }); } }