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 { moveMinX: number; moveMaxX: number; retreatMinX: number; retreatMaxX: number; } @ecs.register('MoveSystem') export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate { private readonly facConfigs: Record = { [FacSet.HERO]: { moveMinX: -320, moveMaxX: 320, retreatMinX: -300, retreatMaxX: 300, }, [FacSet.MON]: { moveMinX: -320, moveMaxX: 320, retreatMinX: -300, retreatMaxX: 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.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 safeRetreatX = currentX - move.direction * 50; if (safeRetreatX >= cfg.retreatMinX && safeRetreatX <= cfg.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 currentX = view.node.position.x; const delta = speed * this.dt * direction; let newX = view.node.position.x + delta; if (currentX < cfg.moveMinX && direction < 0) { view.status_change("idle"); return; } if (currentX > cfg.moveMaxX && direction > 0) { view.status_change("idle"); return; } newX = Math.max(cfg.moveMinX, Math.min(cfg.moveMaxX, newX)); const newY = view.node.position.y; view.node.setPosition(newX, newY, 0); view.status_change("move"); } 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; } }