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"; @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 ySlots = [ { offset: 0, lineName: "LINE2" }, { offset: 5, lineName: "LINE1" }, { offset: -5, lineName: "LINE3" }, { offset: -10, lineName: "LINE4" }, ]; private readonly samePointXThreshold = 12; private readonly samePointYThreshold = 3; 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; } this.applyLineAndY(e, view, move, view.node.position.x); this.updateRenderOrder(e); 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 [minRange, maxRange] = this.resolveCombatRange(model, 0, 75); move.direction = enemyX > currentX ? 1 : -1; if (dist < minRange) { this.performRetreat(view, move, model, currentX); } else if (dist <= maxRange) { view.status_change("idle"); model.is_atking = true; } else { const speed = model.speed / 3; this.moveEntity(view, move.direction, speed); model.is_atking = false; } } 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 [minRange, maxRange] = this.resolveCombatRange(model, 120, 360); move.direction = enemyX > currentX ? 1 : -1; if (dist < minRange) { // 太近了,后撤 this.performRetreat(view, move, model, currentX); } else if (dist > maxRange) { const speed = model.speed / 3; this.moveEntity(view, move.direction, speed); model.is_atking = false; } 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 [minRange, maxRange] = this.resolveCombatRange(model, 360, 720); move.direction = enemyX > currentX ? 1 : -1; if (dist < minRange) { // 太近了,后撤 (远程单位对距离更敏感) this.performRetreat(view, move, model, currentX); } else if (dist > maxRange) { const speed = model.speed / 3; this.moveEntity(view, move.direction, speed); model.is_atking = false; } 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)) { 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 240 * side; } if (rangeType === SkillRange.Mid) { return 200 * side; } return 0; } private moveEntity(view: HeroViewComp, direction: number, speed: 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)); const newY = this.applyLineAndY(view.ent, view, move, newX); view.node.setPosition(newX, newY, 0); view.status_change("move"); } private applyLineAndY(entity: ecs.Entity, view: HeroViewComp, move: MoveComp, x: number): number { if (!view.node) return 0; const baseY = move.baseY; for (const slot of this.ySlots) { const y = baseY + slot.offset; if (!this.hasTeammateAtPoint(entity, x, y)) { const lineNode = this.getLineNode(slot.lineName); if (lineNode && view.node.parent !== lineNode) { view.node.parent = lineNode; } return y; } } const fallback = this.ySlots[0]; const fallbackNode = this.getLineNode(fallback.lineName); if (fallbackNode && view.node.parent !== fallbackNode) { view.node.parent = fallbackNode; } return baseY + fallback.offset; } private getLineNode(lineName: string) { const scene = smc.map?.MapView?.scene; const layerRoot = scene?.entityLayer?.node; if (!layerRoot) return null; return layerRoot.getChildByName(lineName); } private hasTeammateAtPoint(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.fac !== myAttrs.fac || attrs.is_dead) return false; const view = e.get(HeroViewComp); if (!view || !view.node) return false; return Math.abs(view.node.position.x - x) <= this.samePointXThreshold && Math.abs(view.node.position.y - y) <= this.samePointYThreshold; }); } 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(entity: ecs.Entity) { return; } }