Boss单位在渲染排序时未获得足够高的优先级,导致可能被其他单位遮挡。 现在为Boss单位添加专门的渲染优先级字段(bossPriority),并在排序时作为第一排序条件。 同时为Boss的spawnOrder添加偏移量,确保同优先级内Boss保持正确的生成顺序。
458 lines
18 KiB
TypeScript
458 lines
18 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, 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 allyOverlapSpacingX = 14;
|
|
private readonly displacementReleaseX = 10;
|
|
private readonly attackReadyLockX = 10;
|
|
private readonly attackPassThresholdX = 60;
|
|
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; bossPriority: number; frontScore: number; spawnOrder: number; eid: number }[] = [];
|
|
private renderEntryCount = 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,
|
|
}
|
|
};
|
|
|
|
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.in_stun || model.in_frost) {
|
|
this.clearCombatTarget(model);
|
|
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);
|
|
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) {
|
|
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;
|
|
move.targetX = enemyX - move.direction * maxRange;
|
|
|
|
if (dist <= maxRange) {
|
|
view.status_change("idle");
|
|
model.is_atking = true;
|
|
} else {
|
|
const speed = model.speed / 3;
|
|
this.moveEntity(view, move.direction, speed, move.targetX);
|
|
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 selfMove = self.get(MoveComp);
|
|
const selfPriority = selfAttrs ? this.getCombatPriority(selfAttrs) : 0;
|
|
let clampedX = proposedX;
|
|
ecs.query(this.getHeroMoveMatcher()).forEach(e => {
|
|
if (e === self) return;
|
|
const attrs = e.get(HeroAttrsComp);
|
|
const view = e.get(HeroViewComp);
|
|
const allyMove = e.get(MoveComp);
|
|
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;
|
|
const spacing = this.resolveAllySpacing(selfAttrs, selfMove, currentX, direction, allyMove, x, allyPriority, selfPriority);
|
|
if (direction > 0 && x > currentX) {
|
|
clampedX = Math.min(clampedX, x - spacing);
|
|
}
|
|
if (direction < 0 && x < currentX) {
|
|
clampedX = Math.max(clampedX, x + spacing);
|
|
}
|
|
});
|
|
return clampedX;
|
|
}
|
|
|
|
private resolveAllySpacing(
|
|
selfAttrs: HeroAttrsComp | null,
|
|
selfMove: MoveComp | null,
|
|
currentX: number,
|
|
direction: number,
|
|
allyMove: MoveComp | null,
|
|
allyX: number,
|
|
allyPriority: number,
|
|
selfPriority: number
|
|
): number {
|
|
if (!selfAttrs || !selfMove || !allyMove) return this.allySpacingX;
|
|
if ((selfAttrs.type as HType) !== HType.Melee) return this.allySpacingX;
|
|
if (allyPriority !== selfPriority) return this.allySpacingX;
|
|
const selfTargetX = selfMove.targetX;
|
|
const allyTargetX = allyMove.targetX;
|
|
const selfHasTarget = Math.abs(selfTargetX) > 0.01;
|
|
const allyHasTarget = Math.abs(allyTargetX) > 0.01;
|
|
if (!selfHasTarget || !allyHasTarget) return this.allySpacingX;
|
|
const selfDistToAttack = Math.abs(selfTargetX - currentX);
|
|
const canAttackNow = selfAttrs.enemy_in_cast_range || selfDistToAttack <= this.attackReadyLockX;
|
|
if (canAttackNow) return this.allySpacingX;
|
|
const targetTooFar = selfDistToAttack >= this.attackPassThresholdX;
|
|
if (!targetTooFar) return this.allySpacingX;
|
|
const allyDisplaced = Math.abs(allyX - allyTargetX) >= this.displacementReleaseX;
|
|
const selfNeedAdvance = direction > 0 ? selfTargetX > currentX + 2 : selfTargetX < currentX - 2;
|
|
if (allyDisplaced && selfNeedAdvance) return this.allyOverlapSpacingX;
|
|
return this.allySpacingX;
|
|
}
|
|
|
|
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, 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;
|
|
|
|
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);
|
|
});
|
|
}
|
|
}
|