403 lines
16 KiB
TypeScript
403 lines
16 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 { HeroDisVal, HType } from "../common/config/heroSet";
|
||
import { BoxCollider2D, Node } from "cc";
|
||
|
||
@ecs.register('MoveComp')
|
||
export class MoveComp extends ecs.Comp {
|
||
/** 朝向:1=向右,-1=向左 */
|
||
direction: number = 1;
|
||
/** 当前移动目标 X(战斗/回位都会更新) */
|
||
targetX: number = 0;
|
||
/** 是否允许移动(出生落地前会短暂关闭) */
|
||
moving: boolean = true;
|
||
/** 预留:目标 Y(当前逻辑主要使用 baseY 锁定地平线) */
|
||
targetY: number = 0;
|
||
/** 角色所属车道的地平线 Y,移动时强制贴地 */
|
||
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 {
|
||
/** 近战判定射程(来自 heroSet) */
|
||
private readonly meleeAttackRange = HeroDisVal[HType.Melee];
|
||
private readonly heroFrontAnchorX = -30;
|
||
private readonly monFrontAnchorX = 30;
|
||
/** 常规同阵营横向最小间距 */
|
||
private readonly allySpacingX = 60;
|
||
/** 纵向判定为同排的最大 Y 差 */
|
||
private readonly minSpacingY = 30;
|
||
/** 渲染层级重排节流,避免每帧排序 */
|
||
private readonly renderSortInterval = 0.05;
|
||
private lastRenderSortAt = 0;
|
||
private heroMoveMatcher: ecs.IMatcher | null = null;
|
||
private heroViewMatcher: ecs.IMatcher | null = null;
|
||
private readonly renderEntries: { node: Node; bossPriority: number; frontScore: number; spawnOrder: number; eid: number }[] = [];
|
||
private renderEntryCount = 0;
|
||
|
||
/**
|
||
* 阵营可移动边界配置。
|
||
* HERO 在左侧出生并向右推进;MON 在右侧出生并向左推进。
|
||
*/
|
||
private readonly facConfigs: Record<number, MoveFacConfig> = {
|
||
[FacSet.HERO]: {
|
||
moveFrontX: 999999,
|
||
moveBackX: -999999,
|
||
retreatFrontX: 999999,
|
||
retreatBackX: -999999,
|
||
},
|
||
[FacSet.MON]: {
|
||
moveFrontX: -999999,
|
||
moveBackX: 999999,
|
||
retreatFrontX: -999999,
|
||
retreatBackX: 999999,
|
||
}
|
||
};
|
||
|
||
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) {
|
||
this.clearCombatTarget(model);
|
||
view.status_change("idle");
|
||
return;
|
||
}
|
||
if (model.is_stop || model.is_dead || model.is_reviving || model.isFrost()) {
|
||
this.clearCombatTarget(model);
|
||
if (!model.is_reviving) view.status_change("idle");
|
||
return;
|
||
}
|
||
|
||
/** 所有移动都锁定在 baseY,避免出现“漂移” */
|
||
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);
|
||
this.syncCombatTarget(model, view, nearestEnemy);
|
||
} else {
|
||
/** 无敌人:清目标并回归编队站位 */
|
||
this.clearCombatTarget(model);
|
||
move.targetY = 0;
|
||
this.processReturnFormation(e, move, view, model);
|
||
model.is_atking = false;
|
||
}
|
||
}
|
||
|
||
private clearCombatTarget(model: HeroAttrsComp): void {
|
||
model.combat_target_eid = -1;
|
||
model.enemy_in_cast_range = false;
|
||
}
|
||
|
||
private syncCombatTarget(model: HeroAttrsComp, selfView: HeroViewComp, enemyView: HeroViewComp): void {
|
||
if (!enemyView || !enemyView.node || !enemyView.ent) {
|
||
this.clearCombatTarget(model);
|
||
return;
|
||
}
|
||
const enemyAttrs = enemyView.ent.get(HeroAttrsComp);
|
||
if (!enemyAttrs || enemyAttrs.is_dead || enemyAttrs.is_reviving || enemyAttrs.fac === model.fac) {
|
||
this.clearCombatTarget(model);
|
||
return;
|
||
}
|
||
model.combat_target_eid = enemyView.ent.eid;
|
||
model.enemy_in_cast_range = this.isEnemyInAttackRange(model, selfView.node.position.x, enemyView.node.position.x);
|
||
}
|
||
|
||
private isEnemyInAttackRange(model: HeroAttrsComp, selfX: number, enemyX: number): boolean {
|
||
const dist = Math.abs(selfX - enemyX);
|
||
const rangeType = model.type as HType.Melee | HType.Mid | HType.Long;
|
||
if (rangeType === HType.Melee) return dist <= this.meleeAttackRange;
|
||
if (rangeType === HType.Long) {
|
||
const [, maxRange] = this.resolveCombatRange(model, 360, 720);
|
||
return dist <= maxRange;
|
||
}
|
||
const [, maxRange] = this.resolveCombatRange(model, 120, 360);
|
||
return dist <= maxRange;
|
||
}
|
||
|
||
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) {
|
||
this.processFormationCombat(e, move, view, model);
|
||
}
|
||
|
||
private processMidLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) {
|
||
this.processFormationCombat(e, move, view, model);
|
||
}
|
||
|
||
private processLongLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) {
|
||
this.processFormationCombat(e, move, view, model);
|
||
}
|
||
|
||
private processFormationCombat(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp) {
|
||
const targetX = this.getFormationSlotX(e, model, move.baseY);
|
||
move.targetX = targetX;
|
||
this.moveToSlot(view, move, model, targetX);
|
||
model.is_atking = true;
|
||
}
|
||
|
||
private processReturnFormation(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp) {
|
||
const targetX = this.getFormationSlotX(e, model, move.baseY);
|
||
move.targetX = targetX;
|
||
this.moveToSlot(view, move, model, targetX);
|
||
}
|
||
|
||
private getFormationSlotX(self: ecs.Entity, model: HeroAttrsComp, baseY: number): number {
|
||
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 forwardDir = model.fac === FacSet.MON ? -1 : 1;
|
||
const laneAllies: ecs.Entity[] = [];
|
||
ecs.query(this.getHeroMoveMatcher()).forEach(e => {
|
||
if (!this.isFormationParticipant(e, model.fac, baseY)) return;
|
||
laneAllies.push(e);
|
||
});
|
||
laneAllies.sort((a, b) => {
|
||
const attrsA = a.get(HeroAttrsComp);
|
||
const attrsB = b.get(HeroAttrsComp);
|
||
const priorityA = attrsA ? this.getCombatPriority(attrsA) : 0;
|
||
const priorityB = attrsB ? this.getCombatPriority(attrsB) : 0;
|
||
if (priorityA !== priorityB) return priorityB - priorityA;
|
||
const lvA = attrsA?.lv ?? 1;
|
||
const lvB = attrsB?.lv ?? 1;
|
||
if (lvA !== lvB) return lvB - lvA;
|
||
const moveA = a.get(MoveComp);
|
||
const moveB = b.get(MoveComp);
|
||
const orderA = moveA?.spawnOrder ?? 0;
|
||
const orderB = moveB?.spawnOrder ?? 0;
|
||
if (orderA !== orderB) return orderA - orderB;
|
||
return a.eid - b.eid;
|
||
});
|
||
const slotIndex = Math.max(0, laneAllies.findIndex(entity => entity === self));
|
||
const frontAnchorX = model.fac === FacSet.MON ? this.monFrontAnchorX : this.heroFrontAnchorX;
|
||
const targetX = frontAnchorX - forwardDir * slotIndex * this.allySpacingX;
|
||
return Math.max(moveMinX, Math.min(moveMaxX, targetX));
|
||
}
|
||
|
||
private moveToSlot(view: HeroViewComp, move: MoveComp, model: HeroAttrsComp, targetX: number) {
|
||
const currentX = view.node.position.x;
|
||
if (Math.abs(currentX - targetX) <= 2) {
|
||
view.node.setPosition(targetX, move.baseY, 0);
|
||
view.status_change("idle");
|
||
return;
|
||
}
|
||
const dir = targetX > currentX ? 1 : -1;
|
||
move.direction = dir;
|
||
const speed = model.speed / 3;
|
||
this.moveEntity(view, dir, speed, targetX);
|
||
}
|
||
|
||
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) {
|
||
/** 指定停止点时,限制不越过 stopAtX */
|
||
newX = direction > 0 ? Math.min(newX, stopAtX) : Math.max(newX, stopAtX);
|
||
}
|
||
if (Math.abs(newX - currentX) < 0.01) {
|
||
view.status_change("idle");
|
||
return;
|
||
}
|
||
view.node.setPosition(newX, move.baseY, 0);
|
||
view.status_change("move");
|
||
}
|
||
|
||
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 isFormationParticipant(entity: ecs.Entity, fac: number, baseY: number): boolean {
|
||
const attrs = entity.get(HeroAttrsComp);
|
||
const view = entity.get(HeroViewComp);
|
||
const move = entity.get(MoveComp);
|
||
if (!attrs || !view?.node || !move) return false;
|
||
if (attrs.is_dead || attrs.is_reviving) return false;
|
||
if (attrs.fac !== fac) return false;
|
||
if (!move.moving) return false;
|
||
if (Math.abs(view.node.position.y - baseY) >= this.minSpacingY) return false;
|
||
const collider = view.node.getComponent(BoxCollider2D);
|
||
if (collider && !collider.enabled) return false;
|
||
return true;
|
||
}
|
||
|
||
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;
|
||
|
||
/** 一次遍历筛出最近敌人(仅比较 x 轴距离) */
|
||
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) {
|
||
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;
|
||
}
|
||
|
||
const now = Date.now() / 1000;
|
||
if (now - this.lastRenderSortAt < this.renderSortInterval) return;
|
||
this.lastRenderSortAt = now;
|
||
|
||
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, bossPriority: 0, frontScore: 0, spawnOrder: 0, eid: 0 };
|
||
this.renderEntries.push(entry);
|
||
}
|
||
entry.node = actorView.node;
|
||
entry.bossPriority = attrs.is_boss ? 1 : 0;
|
||
entry.frontScore = frontScore;
|
||
entry.spawnOrder = actorMove.spawnOrder;
|
||
entry.eid = e.eid;
|
||
this.renderEntryCount += 1;
|
||
});
|
||
this.renderEntries.length = this.renderEntryCount;
|
||
|
||
/** 定时重排同层节点:Boss 优先级 -> 前后位置 -> 出生序 -> eid */
|
||
this.renderEntries.sort((a, b) => {
|
||
if (a.bossPriority !== b.bossPriority) return a.bossPriority - b.bossPriority;
|
||
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);
|
||
});
|
||
}
|
||
}
|