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 { BoxSet, FacSet } from "../common/config/GameSet"; import { HeroDisVal, HType, HRole } from "../common/config/heroSet"; import { BoxCollider2D, Node } from "cc"; import { MonMoveComp } from "./MonMoveComp"; @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 { private readonly heroFrontAnchorX = -200; private readonly monFrontAnchorX = 0; /** 常规同阵营横向最小间距(英雄) */ private readonly heroAllySpacingX = 100; /** 常规同阵营横向最小间距(怪物) */ private readonly monAllySpacingX = 75; /** 纵向判定为同排的最大 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; laneScore: number }[] = []; private renderEntryCount = 0; /** * 阵营可移动边界配置。 * HERO 在左侧出生并向右推进;MON 在右侧出生并向左推进。 */ private readonly facConfigs: Record = { [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) return; // 只处理英雄移动 if (!move.moving) 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; } // 1. 获取全局排位目标 const slot = this.getGlobalFormationSlot(e, model); move.baseY = slot.targetY; move.targetX = slot.targetX; // 2. 平滑 Y 轴换路(不分阶段,立即响应编队变化) let isChangingLane = false; if (Math.abs(view.node.position.y - move.baseY) > 2) { const currentY = view.node.position.y; const deltaY = move.baseY - currentY; const step = 400 * this.dt; // 换路速度 const newY = currentY + Math.sign(deltaY) * Math.min(Math.abs(deltaY), step); view.node.setPosition(view.node.position.x, newY, 0); isChangingLane = true; } else { view.node.setPosition(view.node.position.x, move.baseY, 0); } // 渲染层级重排 this.updateRenderOrder(); const nearestEnemy = this.findNearestEnemy(e); if (nearestEnemy) { /** 有敌人:进入战斗位移逻辑(立即向编队目标移动) */ this.processCombatLogic(e, move, view, model, nearestEnemy); this.syncCombatTarget(model, view, nearestEnemy); } else { /** 无敌人:清目标并回归编队站位 */ this.clearCombatTarget(model); this.moveToSlot(view, move, model, move.targetX); model.is_atking = false; } // 如果只在 Y 轴移动,也要播放 move 动画 if (isChangingLane && view.status !== "move" && view.status !== "atk") { view.status_change("move"); } } 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 attackRange = model.dis; return dist <= attackRange; } 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; } } /** * 近战交战:有怪物时向右推进,贴近到固定接触距离后停下攻击。 * Why: 近战手短需主动逼近;停止判定用固定接触距离(非攻击距离 dis), * 使英雄进入攻击范围后仍继续贴近,直到贴上目标才站稳输出。 * 怪物 y 与英雄同在 GAME_LINE 附近(微调也在攻击范围内),无需管 Y 轴。 */ private processMeleeLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) { if (!enemy?.node) { this.processFormationCombat(e, move, view, model); return; } const selfX = view.node.position.x; const enemyX = enemy.node.position.x; const dist = Math.abs(selfX - enemyX); // 已贴近到固定接触距离:停下攻击 if (dist <= MoveSystem.MELEE_ENGAGE_X) { model.is_atking = true; if (view.status !== "atk") view.status_change("idle"); return; } // 未贴近:向怪物方向推进(攻击范围内也继续移动),于接触距离处停下 const dir = enemyX > selfX ? 1 : -1; move.direction = dir; const stopX = enemyX - dir * MoveSystem.MELEE_ENGAGE_X; const speed = model.speed / 3; this.moveEntity(view, dir, speed, stopX); model.is_atking = false; } 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) { this.moveToSlot(view, move, model, move.targetX); model.is_atking = true; } private getGlobalFormationSlot(self: ecs.Entity, model: HeroAttrsComp): { targetX: number, targetY: number } { const allAllies: ecs.Entity[] = []; ecs.query(this.getHeroMoveMatcher()).forEach(e => { const attrs = e.get(HeroAttrsComp); const view = e.get(HeroViewComp); const move = e.get(MoveComp); if (!attrs || !view?.node || !move) return; if (attrs.is_dead || attrs.is_reviving) return; if (attrs.fac !== model.fac) return; allAllies.push(e); }); allAllies.sort((a, b) => { const attrsA = a.get(HeroAttrsComp); const attrsB = b.get(HeroAttrsComp); const priorityA = attrsA ? this.getFormationPriority(attrsA) : 0; const priorityB = attrsB ? this.getFormationPriority(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; }); // allAllies 按优先级降序(索引 0 = 最高优先级 = 最靠右/最先接敌), // 反转为"从左侧起的排位"后再分配 X:最高优先级 → 最右 const priorityRank = Math.max(0, allAllies.findIndex(entity => entity === self)); const rankFromLeft = allAllies.length - 1 - priorityRank; const targetX = this.calcSlotX(rankFromLeft, allAllies.length); return { targetX, targetY: BoxSet.GAME_LINE }; } // ==================== 编队站位(方案 A:单行横向分布) ==================== /** 编队左端起点(最靠后/最后接敌) */ private static readonly FORMATION_LEFT_X = -320; /** 编队右端终点(最靠前/最先接敌) */ private static readonly FORMATION_RIGHT_X = 100; /** 固定步进间距(人数较少时按此从左侧逐位右排) */ private static readonly FORMATION_STEP_X = 60; /** 近战贴近怪物的固定接触距离:小于攻击距离 dis,使英雄进攻击范围后仍继续贴近到此间距才停 */ private static readonly MELEE_ENGAGE_X = 60; /** * 计算某个槽位的目标 X。 * Why: 单行站位避免横版拥挤——人少时按 60 固定步进从 -320 起步逐位右排(保持松散), * 人多到 60 步进装不下时,改为在 [-320, 100] 区间均分(利用全宽)。 * @param rankFromLeft 从左侧起的排位(0=最靠左/最后,越大越靠右/越先接敌) * @param total 当前存活英雄总数 */ private calcSlotX(rankFromLeft: number, total: number): number { const left = MoveSystem.FORMATION_LEFT_X; const right = MoveSystem.FORMATION_RIGHT_X; const step = MoveSystem.FORMATION_STEP_X; if (total <= 1) return left; // 固定步进能容纳的最大人数:相邻 60,且最右不超过 right const maxStepCount = Math.floor((right - left) / step) + 1; if (total <= maxStepCount) { return left + rankFromLeft * step; } // 超出后均分整段区间(含左右端点) return left + (right - left) * (rankFromLeft / (total - 1)); } private moveToSlot(view: HeroViewComp, move: MoveComp, model: HeroAttrsComp, targetX: number) { const currentX = view.node.position.x; const currentY = view.node.position.y; // 当 X 和 Y 都到达目标时,才算真正到达 if (Math.abs(currentX - targetX) <= 2 && Math.abs(currentY - move.baseY) <= 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 currentY = view.node.position.y; const delta = speed * this.dt * direction; let newX = view.node.position.x + delta; if (currentX < moveMinX && direction < 0) { // X 轴到底了,如果 Y 轴还在换路,继续维持 move 状态 if (Math.abs(currentY - move.baseY) > 2) { view.status_change("move"); } else { view.status_change("idle"); } return; } if (currentX > moveMaxX && direction > 0) { if (Math.abs(currentY - move.baseY) > 2) { view.status_change("move"); } else { 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) { // X轴虽然没变化,但如果Y轴还在移动,依然是move状态 if (Math.abs(currentY - move.baseY) > 2) { view.status_change("move"); } else { view.status_change("idle"); } return; } view.node.setPosition(newX, view.node.position.y, 0); // 注意:这里只更新 X,Y 在外部平滑逻辑更新 view.status_change("move"); } /** * 编队站位优先级(数值越大越靠前/越靠右)。 * Why: 阵容职能决定承伤顺序——死亡触发英雄需最先阵亡以触发遗志, * 其次坦克承伤,再次战士/刺客,远程射手/法师/辅助依次靠后保护。 * 排序档位:死亡触发 > 坦克 > 战士 > 刺客 > 射手 > 法师 > 辅助 */ private getFormationPriority(model: HeroAttrsComp): number { // 死亡触发英雄最高优先:需要先死以发动遗志/献祭 if (model.dead && Object.keys(model.dead).length > 0) return 100; if (model.role !== undefined) { switch (model.role) { case HRole.Tank: return 90; case HRole.Warrior: return 80; case HRole.Assassin: return 70; case HRole.Archer: return 60; case HRole.Mage: return 50; case HRole.Support: return 40; } } // 兜底:未配置 role 时按攻击定位(近战 > 中程 > 远程) const rangeType = model.type as HType.Melee | HType.Mid | HType.Long; if (rangeType === HType.Melee) return 30; if (rangeType === HType.Mid) return 20; return 10; } 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 dx = Math.abs(currentPos.x - v.node.position.x); const dy = Math.abs(currentPos.y - v.node.position.y); const d = dx + dy * 0.1; // Y轴权重较小,仅在 X 相近时起决定作用 if (d < minDis) { minDis = d; nearest = v; } } } }); return nearest; } private updateRenderOrder() { const scene = smc.map?.MapView?.scene; const actorRoot = scene?.entityLayer?.node?.getChildByName("HERO"); if (!actorRoot) return; const now = Date.now() / 1000; if (now - this.lastRenderSortAt < this.renderSortInterval) return; this.lastRenderSortAt = now; this.renderEntryCount = 0; ecs.query(this.getHeroViewMatcher()).forEach(e => { const attrs = e.get(HeroAttrsComp); const actorView = e.get(HeroViewComp); if (!attrs || !actorView?.node || attrs.is_dead) return; if (attrs.fac !== FacSet.HERO && attrs.fac !== FacSet.MON) return; if (actorView.node.parent !== actorRoot) { actorView.node.parent = actorRoot; } // 获取 spawnOrder,由于可能挂载 MoveComp 或 MonMoveComp,我们需要动态获取 let spawnOrder = 0; const heroMove = e.get(MoveComp); if (heroMove) { spawnOrder = heroMove.spawnOrder; } else { const monMove = e.get(MonMoveComp); if (monMove) { spawnOrder = monMove.spawnOrder; } } // 按 Y 轴计算渲染层级权重(Y 越小,说明越靠近屏幕下方,应该渲染在越前面) // 之前的 isFly 逻辑已被移除,统一按 Y 轴处理三路渲染 const laneScore = -actorView.node.position.y; // X 轴权重:站在前排的(交战处)优先渲染 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, laneScore: 0 }; this.renderEntries.push(entry); } entry.node = actorView.node; entry.bossPriority = attrs.is_boss ? 1 : 0; entry.laneScore = laneScore; entry.frontScore = frontScore; entry.spawnOrder = spawnOrder; entry.eid = e.eid; this.renderEntryCount += 1; }); this.renderEntries.length = this.renderEntryCount; /** 定时重排同层节点:Boss 优先级 -> Y 轴路次 -> 前后位置 -> 出生序 -> eid */ this.renderEntries.sort((a, b) => { if (a.bossPriority !== b.bossPriority) return a.bossPriority - b.bossPriority; // Y轴靠下的(laneScore 较大)应该在后面,从而渲染在上面 if (Math.abs(a.laneScore - b.laneScore) > 10) return a.laneScore - b.laneScore; 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); }); } }