Files
pixelheros/assets/script/game/hero/MonMoveComp.ts
panw 20e9b1d484 refactor(monster&hero): 重构三路分层逻辑与渲染排序
1. 移除飞行怪特殊判定,统一按Y轴高度处理三路渲染
2. 重命名飞行层相关变量为更准确的路次命名
3. 新增英雄自动分路均衡分配逻辑
4. 调整渲染排序规则,按Y轴高度决定上下层显示顺序
5. 修复怪物入场动画与刷怪分路逻辑
2026-05-12 16:32:25 +08:00

227 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { Node } from "cc";
@ecs.register('MonMoveComp')
export class MonMoveComp extends ecs.Comp {
/** 朝向1=向右,-1=向左 */
direction: number = -1;
/** 当前移动目标 X */
targetX: number = 0;
/** 是否允许移动(出生落地前会短暂关闭) */
moving: boolean = true;
/** 站位基准 Y */
baseY: number = 0;
/** 出生序,用于同条件渲染排序稳定 */
spawnOrder: number = 0;
reset() {
this.direction = -1;
this.targetX = 0;
this.moving = true;
this.baseY = 0;
this.spawnOrder = 0;
}
}
@ecs.register('MonMoveSystem')
export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
/** 近战判定射程 */
private readonly meleeAttackRange = 250;
/** 远程判定射程 */
private readonly longAttackRange = 600;
/** 渲染层级重排节流,避免每帧排序 */
private readonly renderSortInterval = 0.05;
private lastRenderSortAt = 0;
private monMoveMatcher: 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 getMonMoveMatcher(): ecs.IMatcher {
if (!this.monMoveMatcher) {
this.monMoveMatcher = ecs.allOf(HeroAttrsComp, HeroViewComp, MonMoveComp);
}
return this.monMoveMatcher;
}
private getHeroViewMatcher(): ecs.IMatcher {
if (!this.heroViewMatcher) {
this.heroViewMatcher = ecs.allOf(HeroAttrsComp, HeroViewComp);
}
return this.heroViewMatcher;
}
filter(): ecs.IMatcher {
return ecs.allOf(MonMoveComp, HeroViewComp, HeroAttrsComp);
}
update(e: ecs.Entity) {
/** 战斗未开始/暂停时不驱动移动 */
if (!smc.mission.play || smc.mission.pause) return;
const model = e.get(HeroAttrsComp);
const move = e.get(MonMoveComp);
const view = e.get(HeroViewComp);
if (!model || !move || !view || !view.node) return;
if (model.fac !== FacSet.MON) return;
if (!move.moving) return;
/** 关卡阶段性冻结怪物行为 */
if (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);
}
// 渲染层级统交由 MoveSystem 统一处理,避免两个 System 争抢 setSiblingIndex
// 仅在战斗中才处理索敌和移动
if (!smc.mission.in_fight) return;
const nearestEnemy = this.findNearestEnemy(e);
if (nearestEnemy) {
/** 有敌人:进入战斗位移逻辑 */
this.processCombatLogic(e, move, view, model, nearestEnemy);
this.syncCombatTarget(model, view, nearestEnemy);
} else {
/** 无敌人:继续向左推进 */
this.clearCombatTarget(model);
model.is_atking = false;
this.moveEntity(view, -1, model.speed / 3);
}
}
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;
return dist <= this.longAttackRange;
}
private processCombatLogic(e: ecs.Entity, move: MonMoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) {
const selfX = view.node.position.x;
const enemyX = enemy.node.position.x;
const dist = Math.abs(selfX - enemyX);
const inRange = this.isEnemyInAttackRange(model, selfX, enemyX);
// 接触判定距离,只有接触英雄才停止移动
const touchDistance = 50;
const isTouching = dist <= touchDistance;
// 攻击判定
if (inRange) {
model.is_atking = true;
} else {
model.is_atking = false;
}
// 移动判定:只有接触了英雄才停止移动,否则继续向英雄方向移动
if (isTouching) {
view.status_change("idle");
} else {
const dir = enemyX > selfX ? 1 : -1;
this.moveEntity(view, dir, model.speed / 3);
}
}
private moveEntity(view: HeroViewComp, direction: number, speed: number) {
const model = view.ent.get(HeroAttrsComp);
const move = view.ent.get(MonMoveComp);
if (!model || !move) return;
// 简化的边界限制(怪物主要往左走,英雄防线在左侧,-999999 代表左侧尽头)
const moveMinX = -999999;
const moveMaxX = 999999;
const currentX = view.node.position.x;
const delta = speed * this.dt * direction;
let newX = currentX + delta;
newX = Math.max(moveMinX, Math.min(moveMaxX, newX));
if (Math.abs(newX - currentX) < 0.01) {
view.status_change("idle");
return;
}
view.node.setPosition(newX, move.baseY, 0);
move.direction = direction;
// 确保怪物的朝向表现,向左走 scale=-1向右走 scale=1
if (direction < 0) {
view.scale = -1;
} else if (direction > 0) {
view.scale = 1;
}
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;
/** 一次遍历筛出最近敌人(仅比较 x 轴距离,忽略 Y飞行和地面都能互相攻击 */
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;
}
}