From 038abdc5d2ef9822a19974760f9a5e3bb88df51e Mon Sep 17 00:00:00 2001 From: pan Date: Thu, 13 Aug 2026 09:20:27 +0800 Subject: [PATCH] =?UTF-8?q?refactor(monster):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E7=BB=84=E4=BB=B6=E4=BD=93=E7=B3=BB=EF=BC=8C?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E8=8B=B1=E9=9B=84=E4=B8=8E=E6=80=AA=E7=89=A9?= =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 删除废弃的 MonMoveComp,将功能合并到 MoveComp 中 2. 移除 MissionComp 中冗余的怪物最大数量配置计算 3. 优化英雄站位分配逻辑,近战优先靠前站位 4. 统一移动系统的边界约束与渲染排序逻辑 5. 清理 Hero.ts 中未使用的导入项 --- assets/script/game/hero/Hero.ts | 2 +- assets/script/game/hero/Mon.ts | 7 +- assets/script/game/hero/MonMoveComp.ts | 201 --------- assets/script/game/hero/MonMoveComp.ts.meta | 9 - assets/script/game/hero/MoveComp.ts | 452 ++++---------------- assets/script/game/map/MissionComp.ts | 1 - assets/script/game/map/MissionHeroComp.ts | 37 +- assets/script/game/map/MissionMonComp.ts | 6 +- 8 files changed, 102 insertions(+), 613 deletions(-) delete mode 100644 assets/script/game/hero/MonMoveComp.ts delete mode 100644 assets/script/game/hero/MonMoveComp.ts.meta diff --git a/assets/script/game/hero/Hero.ts b/assets/script/game/hero/Hero.ts index add96f00..e7eb3bcd 100644 --- a/assets/script/game/hero/Hero.ts +++ b/assets/script/game/hero/Hero.ts @@ -5,7 +5,7 @@ import { smc } from "../common/SingletonModuleComp"; import { HeroAttrsComp } from "./HeroAttrsComp"; import { HeroViewComp } from "./HeroViewComp"; import { BoxSet, FacSet, FightSet, IndexSet } from "../common/config/GameSet"; -import { HeroInfo, HeroPos, resolveFormationTargetX, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv, calcGrowBonus } from "../common/config/heroSet"; +import { HeroInfo, resolveFormationTargetX, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv, calcGrowBonus } from "../common/config/heroSet"; import { GameEvent } from "../common/config/GameEvent"; import { SkillTriggerType, HType } from "../common/config/heroSet"; import { SkillTriggerHelper } from "./SkillTriggerHelper"; diff --git a/assets/script/game/hero/Mon.ts b/assets/script/game/hero/Mon.ts index 963b511e..6821159b 100644 --- a/assets/script/game/hero/Mon.ts +++ b/assets/script/game/hero/Mon.ts @@ -7,7 +7,6 @@ import { HeroInfo, resolveTriggerByLv, resolveReviveByLv } from "../common/confi import { HeroAttrsComp } from "./HeroAttrsComp"; import { HeroViewComp } from "./HeroViewComp"; import { MoveComp } from "./MoveComp"; -import { MonMoveComp } from "./MonMoveComp"; import { GameEvent } from "../common/config/GameEvent"; import { SkillTriggerType, HType } from "../common/config/heroSet"; import { SkillTriggerHelper } from "./SkillTriggerHelper"; @@ -19,7 +18,7 @@ export class Monster extends ecs.Entity { /** 怪物表现组件引用 */ HeroView!: HeroViewComp; /** 怪物移动组件引用 */ - MonMove!: MonMoveComp; + MonMove!: MoveComp; /** 调试开关,控制生命周期日志输出 */ private debugMode: boolean = false; @@ -98,7 +97,7 @@ export class Monster extends ecs.Entity { /** 注册实体必需组件:移动 + 属性 */ protected init() { this.addComponents( - MonMoveComp, + MoveComp, HeroAttrsComp, ); } @@ -249,7 +248,7 @@ export class Monster extends ecs.Entity { // 广播怪物加载事件,供刷怪与战斗系统联动 oops.message.dispatchEvent("monster_load", this) // 初始化移动参数:方向、目标 X、站位基准 Y - const move = this.get(MonMoveComp); + const move = this.get(MoveComp); move.reset(); move.direction = -1; move.targetX = pos.x; diff --git a/assets/script/game/hero/MonMoveComp.ts b/assets/script/game/hero/MonMoveComp.ts deleted file mode 100644 index 0affa74d..00000000 --- a/assets/script/game/hero/MonMoveComp.ts +++ /dev/null @@ -1,201 +0,0 @@ -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 { 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 combatHaltDistance = 80; - /** 渲染层级重排节流,避免每帧排序 */ - 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() || model.isStun()) { - 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 - - // 仅在战斗中才处理索敌;非战斗阶段也允许向左推进,让怪物自然逼近英雄阵地 - const nearestEnemy = smc.mission.in_fight ? this.findNearestEnemy(e) : null; - if (nearestEnemy) { - /** 有敌人:边移动边攻击 */ - this.processCombatLogic(e, move, view, model, nearestEnemy); - this.syncCombatTarget(model, view, nearestEnemy); - } else { - /** 无敌人:清目标并继续向左推进 */ - this.clearCombatTarget(model); - model.is_atking = false; - this.moveLeft(view, move, model); - } - } - - 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: MonMoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) { - const selfX = view.node.position.x; - const enemyX = enemy.node.position.x; - // 停船判定使用固定阈值(80),不依赖技能攻击范围 - const inRange = Math.abs(selfX - enemyX) <= this.combatHaltDistance; - - if (inRange) { - // 接触到英雄方:停止移动,进入攻击状态,由 SCastSystem 负责技能释放 - model.is_atking = true; - const dir = enemyX > selfX ? 1 : -1; - view.scale = dir; - if (view.status === "move") { - view.status_change("idle"); - } - } else { - // 未接触:继续向左推进 - model.is_atking = false; - this.moveLeft(view, move, model); - } - } - - /** - * 持续向左推进:怪物不再固定站位,而是以 speed/3 的速度向左移动, - * 直到抵达左边界(基地位置)后转入待机,由 SCastSystem 继续负责技能释放。 - */ - private moveLeft(view: HeroViewComp, move: MonMoveComp, model: HeroAttrsComp) { - const currentX = view.node.position.x; - if (currentX <= BoxSet.LETF_END) { - if (view.status !== "atk") { - view.status_change("idle"); - } - return; - } - const dir = -1; - move.direction = dir; - const speed = model.speed / 3; - const delta = speed * this.dt * dir; - const newX = Math.max(BoxSet.LETF_END, currentX + delta); - if (Math.abs(newX - currentX) >= 0.01) { - view.node.setPosition(newX, view.node.position.y, 0); - view.status_change("move"); - } - } - - 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; - } -} \ No newline at end of file diff --git a/assets/script/game/hero/MonMoveComp.ts.meta b/assets/script/game/hero/MonMoveComp.ts.meta deleted file mode 100644 index bc9a1549..00000000 --- a/assets/script/game/hero/MonMoveComp.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "a3113d69-645e-4a4b-bf68-a434fcbd6d8d", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/assets/script/game/hero/MoveComp.ts b/assets/script/game/hero/MoveComp.ts index 199177af..3da80f12 100644 --- a/assets/script/game/hero/MoveComp.ts +++ b/assets/script/game/hero/MoveComp.ts @@ -1,26 +1,31 @@ +/** + * @file MoveComp.ts + * @description 统一移动组件与系统(英雄 + 怪物) + * + * 核心规则: + * 1. 准备阶段:双方原地待命,不推进不索敌。 + * 2. 战斗阶段:双方向最近敌人推进,进入攻击范围(model.dis)后停止移动并攻击。 + * 3. 场上无敌人:停止移动(战斗结束,由 MissionComp 进入结算)。 + * 4. 击退/位移不会把单位推出屏幕:HERO 不越过 BoxSet.LETF_END,MON 不越过 BoxSet.RIGHT_END。 + * 5. 推进方向无上限限制,直到接触敌人或抵达敌方边界。 + */ 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"; +import { Node } from "cc"; @ecs.register('MoveComp') export class MoveComp extends ecs.Comp { - /** 朝向:1=向右,-1=向左 */ + /** 朝向:1=向右(HERO),-1=向左(MON),由系统根据 fac 自动维护 */ direction: number = 1; - /** 当前移动目标 X(战斗/回位都会更新) */ + /** 当前移动目标 X */ targetX: number = 0; /** 是否允许移动(出生落地前会短暂关闭) */ moving: boolean = true; - /** 预留:目标 Y(当前逻辑主要使用 baseY 锁定地平线) */ - targetY: number = 0; - /** 角色所属车道的地平线 Y,移动时强制贴地 */ + /** 站位基准 Y,移动时强制贴地 */ baseY: number = 0; - /** 预留:车道索引 */ - lane: number = 0; /** 出生序,用于同条件渲染排序稳定 */ spawnOrder: number = 0; @@ -28,68 +33,20 @@ export class MoveComp extends ecs.Comp { 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); @@ -109,52 +66,49 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate 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()) { + /** 关卡阶段性冻结怪物行为 */ + if (smc.mission.stop_mon_action && model.fac === FacSet.MON) { + this.clearCombatTarget(model); + view.status_change("idle"); + return; + } + + /** 死亡/复活/控制状态:停止移动与索敌 */ + if (model.is_stop || model.is_dead || model.is_reviving || model.isFrost() || model.isStun()) { 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 { + /** 所有移动都锁定在 baseY,避免出现“漂移” */ + if (view.node.position.y !== move.baseY) { view.node.setPosition(view.node.position.x, move.baseY, 0); } - // 渲染层级重排 - this.updateRenderOrder(); + /** 准备阶段:双方原地待命,不推进不索敌 */ + if (!smc.mission.in_fight) { + this.clearCombatTarget(model); + model.is_atking = false; + if (view.status !== "atk") view.status_change("idle"); + return; + } + /** 战斗阶段:索敌并推进 */ 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; + if (view.status !== "atk") view.status_change("idle"); } - // 如果只在 Y 轴移动,也要播放 move 动画 - if (isChangingLane && view.status !== "move" && view.status !== "atk") { - view.status_change("move"); - } + /** 渲染层级重排(双阵营统一处理) */ + this.updateRenderOrder(); } private clearCombatTarget(model: HeroAttrsComp): void { @@ -182,300 +136,53 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate 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; - } - } - /** - * 近战交战:怪物进入警戒线(ALERT_RANGE_X)内才向右推进迎敌, - * 贴近到固定接触距离后停下攻击;警戒线外则退回后方编队位待命。 - * 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; - } + private processCombatLogic(_e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) { const selfX = view.node.position.x; const enemyX = enemy.node.position.x; const dist = Math.abs(selfX - enemyX); - // 已贴近到固定接触距离:停下攻击 - if (dist <= MoveSystem.MELEE_ENGAGE_X) { + const attackRange = model.dis; + const inRange = dist <= attackRange; + + if (inRange) { + /** 进入攻击范围:停止移动,进入攻击状态,由 SCastSystem 负责技能释放 */ model.is_atking = true; - if (view.status !== "atk") view.status_change("idle"); - return; - } - // 怪物在警戒线之外:不主动迎敌,退回后方编队位待命 - if (dist > MoveSystem.MELEE_ALERT_X) { - this.clearCombatTarget(model); - this.moveToSlot(view, move, model, move.targetX); + const dir = enemyX > selfX ? 1 : -1; + view.scale = dir; + if (view.status === "move") { + view.status_change("idle"); + } + } else { + /** 未接触:向敌人方向推进 */ model.is_atking = false; - return; + const dir = enemyX > selfX ? 1 : -1; + move.direction = dir; + const speed = model.speed / 3; + const delta = speed * this.dt * dir; + let newX = selfX + delta; + + /** 后退边界钳制:防止被击退/位移推出屏幕 */ + newX = this.clampByFacBoundary(newX, model.fac); + + if (Math.abs(newX - selfX) >= 0.01) { + view.node.setPosition(newX, view.node.position.y, 0); + view.status_change("move"); + } } - // 警戒线内未贴近:向怪物方向推进(攻击范围内也继续移动),于接触距离处停下 - const dir = enemyX > selfX ? 1 : -1; - move.direction = dir; - // 终止点取接触距离与推进上限的较小者,防止越过 150 继续追击 - let stopX = Math.min(enemyX - dir * MoveSystem.MELEE_ENGAGE_X, MoveSystem.HERO_ADVANCE_LIMIT_X); - // 同阵营最小间距约束:防止近战推进时穿越前方队友 - stopX = this.clampAllySpacing(e, view, move, model, stopX); - 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; - /** 近战警戒线:怪物进入该距离内才主动向右推进迎敌,否则退回后方编队位待命 */ - private static readonly MELEE_ALERT_X = 200; - /** 英雄向敌人推进的最远终止点 X,防止近战追敌时无限右移脱离战场 */ - private static readonly HERO_ADVANCE_LIMIT_X = 100; - /** 同阵营英雄间最小间距:小于该值时低优先级一方需让位 */ - private static readonly ALLY_MIN_GAP_X = 30; - - /** - * 计算某个槽位的目标 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; - // 同阵营最小间距约束:防止编队挤压时穿越前方队友 - const clampedTargetX = this.clampAllySpacing(view.ent, view, move, model, targetX); - this.moveEntity(view, dir, speed, clampedTargetX); } /** - * 同阵营横向最小间距约束。 - * Why: 编队/追击时英雄可能挤到一起,低优先级一方需要保持在高优先级身后 ALLY_MIN_GAP_X 处, - * 避免视觉重叠与站位混乱。 - * 优先级:编队优先级高者在前;同级时召唤早(spawnOrder 小)者在前。 - * @returns 裁剪后的目标 X(保证不越过身前队友) + * 按阵营裁剪 X 坐标,防止被击退/位移推出屏幕。 + * HERO 后退不越过 BoxSet.LETF_END;MON 后退不越过 BoxSet.RIGHT_END。 */ - private clampAllySpacing(self: ecs.Entity, view: HeroViewComp, selfMove: MoveComp, model: HeroAttrsComp, targetX: number): number { - const selfX = view.node.position.x; - const selfY = view.node.position.y; - let clamped = targetX; - - ecs.query(this.getHeroMoveMatcher()).forEach(e => { - if (e === self) return; - const attrs = e.get(HeroAttrsComp); - const v = e.get(HeroViewComp); - const mv = e.get(MoveComp); - if (!attrs || !v?.node || !mv) return; - if (attrs.is_dead || attrs.is_reviving) return; - if (attrs.fac !== model.fac) return; - // 只约束同排(Y 差在阈值内)的队友 - if (Math.abs(v.node.position.y - selfY) > this.minSpacingY) return; - - const otherX = v.node.position.x; - // 判断对方是否在自己前方(以目标方向为前) - const isAhead = clamped > selfX ? otherX > selfX : otherX < selfX; - if (!isAhead) return; - - // 判定谁该在前:编队优先级高者在前;同级时 spawnOrder 小者在前 - const myPriority = this.getFormationPriority(model); - const otherPriority = this.getFormationPriority(attrs); - let selfIsFront = false; - if (myPriority !== otherPriority) { - selfIsFront = myPriority > otherPriority; - } else { - selfIsFront = selfMove.spawnOrder <= mv.spawnOrder; - } - - if (!selfIsFront) { - // 自己靠后:不能越过 otherX - ALLY_MIN_GAP_X(朝右)或 otherX + ALLY_MIN_GAP_X(朝左) - const limitX = clamped > selfX ? otherX - MoveSystem.ALLY_MIN_GAP_X : otherX + MoveSystem.ALLY_MIN_GAP_X; - clamped = clamped > selfX ? Math.min(clamped, limitX) : Math.max(clamped, limitX); - } - }); - - return clamped; - } - - 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; + private clampByFacBoundary(x: number, fac: number): number { + if (fac === FacSet.HERO) { + return Math.max(BoxSet.LETF_END, x); } - 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]; + return Math.min(BoxSet.RIGHT_END, x); } private findNearestEnemy(entity: ecs.Entity): HeroViewComp | null { @@ -527,20 +234,11 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate 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; - } - } + /** 统一从 MoveComp 获取 spawnOrder */ + const move = e.get(MoveComp); + const spawnOrder = move?.spawnOrder ?? 0; // 按 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; diff --git a/assets/script/game/map/MissionComp.ts b/assets/script/game/map/MissionComp.ts index 4dbf947f..bb8d87d8 100644 --- a/assets/script/game/map/MissionComp.ts +++ b/assets/script/game/map/MissionComp.ts @@ -805,7 +805,6 @@ export class MissionComp extends CCComp { this.clearTime = 0 smc.vmdata.mission_data.mon_num = 0 smc.vmdata.mission_data.level = 1 - smc.vmdata.mission_data.mon_max = Math.max(1, Math.floor(this.maxMonsterCount)) this.currentPhase = MissionPhase.None; this.currentWave = 1; this.isBossWave = false; diff --git a/assets/script/game/map/MissionHeroComp.ts b/assets/script/game/map/MissionHeroComp.ts index af8111c2..21bc92a3 100644 --- a/assets/script/game/map/MissionHeroComp.ts +++ b/assets/script/game/map/MissionHeroComp.ts @@ -53,19 +53,20 @@ const { ccclass } = _decorator; export class MissionHeroComp extends CCComp { // ======================== 常量 ======================== - /** 硬编码的英雄占位点(数量对齐 FightSet.HERO_MAX_NUM,落点 X 统一,英雄落地后由 MoveComp 自行移位到阵型目标) */ - public static readonly HERO_POSITIONS: Vec3[] = [ - v3(-300, BoxSet.GAME_LINE, 0), // index 0 - v3(-300, BoxSet.GAME_LINE, 0), // index 1 - v3(-300, BoxSet.GAME_LINE, 0), // index 2 - v3(-300, BoxSet.GAME_LINE, 0), // index 3 - v3(-300, BoxSet.GAME_LINE, 0), // index 4 - v3(-300, BoxSet.GAME_LINE, 0), // index 5 - v3(-300, BoxSet.GAME_LINE, 0), // index 6 - v3(-300, BoxSet.GAME_LINE, 0), // index 7 - v3(-300, BoxSet.GAME_LINE, 0), // index 8 - v3(-300, BoxSet.GAME_LINE, 0), // index 9 - ]; + /** + * 英雄登场占位点: + * - 从 x=-300 开始,每个点位间隔 60(x 向右递增表示更靠前)。 + * - 分配规则:近战优先靠前(靠近 0 的高索引),远程/中程优先靠后(低索引)。 + */ + public static readonly HERO_POSITIONS: Vec3[] = (() => { + const startX = -300; + const step = 60; + const positions: Vec3[] = []; + for (let i = 0; i < FightSet.HERO_MAX_NUM; i++) { + positions.push(v3(startX + i * step, BoxSet.GAME_LINE, 0)); + } + return positions; + })(); /** 英雄出生时的掉落高度(从空中落到地面的像素差) */ private static readonly HERO_DROP_HEIGHT = 260 @@ -203,7 +204,7 @@ export class MissionHeroComp extends CCComp { * 动态分配英雄上场的位置 * @param excludeEids 排除计算的实体ID数组(避免复活时把自己算成占据的位置) */ - private pickPositionIndexForHero(excludeEids: number[] = []): number { + private pickPositionIndexForHero(excludeEids: number[] = [], heroType: HType = HType.Melee): number { const heroes = this.getAllHeroes().filter(h => { const m = h.get(HeroAttrsComp); return m && !m.is_dead && !excludeEids.includes(h.eid); @@ -215,8 +216,10 @@ export class MissionHeroComp extends CCComp { if (m && m.posIndex >= 0) occupied.add(m.posIndex); } - // 按索引顺序填充空位(0..9),全部落点相同,英雄落地后自行移位 - const slotPriority = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + // 近战优先靠前(x 更大,索引更大);远程/中程优先靠后(x 更小,索引更小) + const slotPriority = heroType === HType.Melee + ? [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; for (const idx of slotPriority) { if (!occupied.has(idx) && MissionHeroComp.HERO_POSITIONS[idx]) { return idx; @@ -255,7 +258,7 @@ export class MissionHeroComp extends CCComp { private spawnHeroAt(uuid: number, hero_lv: number, card_lv: number, posIndex: number) { let hero = ecs.getEntity(Hero); let scale = 1 - const finalPosIndex = posIndex >= 0 ? posIndex : this.pickPositionIndexForHero(); + const finalPosIndex = posIndex >= 0 ? posIndex : this.pickPositionIndexForHero([], HeroInfo[uuid]?.type ?? HType.Melee); // 兜底:索引越界或点位缺失时复用 index 0,保证 landingPos 一定有效 const landingPos = MissionHeroComp.HERO_POSITIONS[finalPosIndex] ?? MissionHeroComp.HERO_POSITIONS[0]; let spawnPos: Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0); diff --git a/assets/script/game/map/MissionMonComp.ts b/assets/script/game/map/MissionMonComp.ts index fe588b13..22ccd4a1 100644 --- a/assets/script/game/map/MissionMonComp.ts +++ b/assets/script/game/map/MissionMonComp.ts @@ -8,7 +8,7 @@ * * 关键设计: * - 5 只怪在 X∈[100,320] 内按登场序号均匀铺开,Boss/小怪同时登场,玩家看清阵容后三选一。 - * - 实际阵型与推进由 MonMoveComp 在战斗中向左移动自然形成,不再使用固定网格点。 + * - 实际推进由统一 MoveComp 在战斗中向敌人移动自然形成,不再使用固定网格点。 * - 槽位索引仅用于 monGrid 寻路与 SCastSystem 索敌定位。 */ import { _decorator, v3, Vec3 } from "cc"; @@ -22,7 +22,7 @@ import { GameEvent } from "../common/config/GameEvent"; import { BoxSet } from "../common/config/GameSet"; import { spawningEngine, GeneratedMonster, TestModeConfig, BOSS_COUNT, MINION_COUNT } from "./RogueConfig"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; -import { MonMoveComp } from "../hero/MonMoveComp"; +import { MoveComp } from "../hero/MoveComp"; const { ccclass, property } = _decorator; @@ -158,7 +158,7 @@ export class MissionMonCompComp extends CCComp { oops.message.dispatchEvent(GameEvent.BossSpawn, { pos: spawnPos.clone() }); } - const move = mon.get(MonMoveComp); + const move = mon.get(MoveComp); if (move) { move.spawnOrder = this.globalSpawnOrder; }