Files
pixelheros/assets/script/game/hero/MoveComp.ts
panw 4171865efb fix(移动系统): 修正近战攻击距离和友军间距逻辑
- 将 `minSpacingX` 拆分为 `meleeAttackRange` 和 `allySpacingX`,明确区分攻击范围和友军间距
- 在 `moveEntity` 方法中添加 `stopAtX` 参数,确保英雄在攻击范围内停止移动
- 新增 `clampXByAllies` 方法,防止友军单位在移动时相互重叠
- 更新 `hasAnyActorTooClose` 方法使用新的 `allySpacingX` 常量
2026-03-16 14:49:17 +08:00

386 lines
14 KiB
TypeScript

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<number, MoveFacConfig> = {
[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);
});
}
}