refactor(monster): 重构移动组件体系,统一英雄与怪物移动逻辑
1. 删除废弃的 MonMoveComp,将功能合并到 MoveComp 中 2. 移除 MissionComp 中冗余的怪物最大数量配置计算 3. 优化英雄站位分配逻辑,近战优先靠前站位 4. 统一移动系统的边界约束与渲染排序逻辑 5. 清理 Hero.ts 中未使用的导入项
This commit is contained in:
@@ -5,7 +5,7 @@ import { smc } from "../common/SingletonModuleComp";
|
|||||||
import { HeroAttrsComp } from "./HeroAttrsComp";
|
import { HeroAttrsComp } from "./HeroAttrsComp";
|
||||||
import { HeroViewComp } from "./HeroViewComp";
|
import { HeroViewComp } from "./HeroViewComp";
|
||||||
import { BoxSet, FacSet, FightSet, IndexSet } from "../common/config/GameSet";
|
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 { GameEvent } from "../common/config/GameEvent";
|
||||||
import { SkillTriggerType, HType } from "../common/config/heroSet";
|
import { SkillTriggerType, HType } from "../common/config/heroSet";
|
||||||
import { SkillTriggerHelper } from "./SkillTriggerHelper";
|
import { SkillTriggerHelper } from "./SkillTriggerHelper";
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { HeroInfo, resolveTriggerByLv, resolveReviveByLv } from "../common/confi
|
|||||||
import { HeroAttrsComp } from "./HeroAttrsComp";
|
import { HeroAttrsComp } from "./HeroAttrsComp";
|
||||||
import { HeroViewComp } from "./HeroViewComp";
|
import { HeroViewComp } from "./HeroViewComp";
|
||||||
import { MoveComp } from "./MoveComp";
|
import { MoveComp } from "./MoveComp";
|
||||||
import { MonMoveComp } from "./MonMoveComp";
|
|
||||||
import { GameEvent } from "../common/config/GameEvent";
|
import { GameEvent } from "../common/config/GameEvent";
|
||||||
import { SkillTriggerType, HType } from "../common/config/heroSet";
|
import { SkillTriggerType, HType } from "../common/config/heroSet";
|
||||||
import { SkillTriggerHelper } from "./SkillTriggerHelper";
|
import { SkillTriggerHelper } from "./SkillTriggerHelper";
|
||||||
@@ -19,7 +18,7 @@ export class Monster extends ecs.Entity {
|
|||||||
/** 怪物表现组件引用 */
|
/** 怪物表现组件引用 */
|
||||||
HeroView!: HeroViewComp;
|
HeroView!: HeroViewComp;
|
||||||
/** 怪物移动组件引用 */
|
/** 怪物移动组件引用 */
|
||||||
MonMove!: MonMoveComp;
|
MonMove!: MoveComp;
|
||||||
/** 调试开关,控制生命周期日志输出 */
|
/** 调试开关,控制生命周期日志输出 */
|
||||||
private debugMode: boolean = false;
|
private debugMode: boolean = false;
|
||||||
|
|
||||||
@@ -98,7 +97,7 @@ export class Monster extends ecs.Entity {
|
|||||||
/** 注册实体必需组件:移动 + 属性 */
|
/** 注册实体必需组件:移动 + 属性 */
|
||||||
protected init() {
|
protected init() {
|
||||||
this.addComponents<ecs.Comp>(
|
this.addComponents<ecs.Comp>(
|
||||||
MonMoveComp,
|
MoveComp,
|
||||||
HeroAttrsComp,
|
HeroAttrsComp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -249,7 +248,7 @@ export class Monster extends ecs.Entity {
|
|||||||
// 广播怪物加载事件,供刷怪与战斗系统联动
|
// 广播怪物加载事件,供刷怪与战斗系统联动
|
||||||
oops.message.dispatchEvent("monster_load", this)
|
oops.message.dispatchEvent("monster_load", this)
|
||||||
// 初始化移动参数:方向、目标 X、站位基准 Y
|
// 初始化移动参数:方向、目标 X、站位基准 Y
|
||||||
const move = this.get(MonMoveComp);
|
const move = this.get(MoveComp);
|
||||||
move.reset();
|
move.reset();
|
||||||
move.direction = -1;
|
move.direction = -1;
|
||||||
move.targetX = pos.x;
|
move.targetX = pos.x;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"ver": "4.0.24",
|
|
||||||
"importer": "typescript",
|
|
||||||
"imported": true,
|
|
||||||
"uuid": "a3113d69-645e-4a4b-bf68-a434fcbd6d8d",
|
|
||||||
"files": [],
|
|
||||||
"subMetas": {},
|
|
||||||
"userData": {}
|
|
||||||
}
|
|
||||||
@@ -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 { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||||
import { HeroViewComp } from "./HeroViewComp";
|
import { HeroViewComp } from "./HeroViewComp";
|
||||||
import { HeroAttrsComp } from "./HeroAttrsComp";
|
import { HeroAttrsComp } from "./HeroAttrsComp";
|
||||||
import { smc } from "../common/SingletonModuleComp";
|
import { smc } from "../common/SingletonModuleComp";
|
||||||
import { BoxSet, FacSet } from "../common/config/GameSet";
|
import { BoxSet, FacSet } from "../common/config/GameSet";
|
||||||
import { HeroDisVal, HType, HRole } from "../common/config/heroSet";
|
import { Node } from "cc";
|
||||||
import { BoxCollider2D, Node } from "cc";
|
|
||||||
import { MonMoveComp } from "./MonMoveComp";
|
|
||||||
|
|
||||||
@ecs.register('MoveComp')
|
@ecs.register('MoveComp')
|
||||||
export class MoveComp extends ecs.Comp {
|
export class MoveComp extends ecs.Comp {
|
||||||
/** 朝向:1=向右,-1=向左 */
|
/** 朝向:1=向右(HERO),-1=向左(MON),由系统根据 fac 自动维护 */
|
||||||
direction: number = 1;
|
direction: number = 1;
|
||||||
/** 当前移动目标 X(战斗/回位都会更新) */
|
/** 当前移动目标 X */
|
||||||
targetX: number = 0;
|
targetX: number = 0;
|
||||||
/** 是否允许移动(出生落地前会短暂关闭) */
|
/** 是否允许移动(出生落地前会短暂关闭) */
|
||||||
moving: boolean = true;
|
moving: boolean = true;
|
||||||
/** 预留:目标 Y(当前逻辑主要使用 baseY 锁定地平线) */
|
/** 站位基准 Y,移动时强制贴地 */
|
||||||
targetY: number = 0;
|
|
||||||
/** 角色所属车道的地平线 Y,移动时强制贴地 */
|
|
||||||
baseY: number = 0;
|
baseY: number = 0;
|
||||||
/** 预留:车道索引 */
|
|
||||||
lane: number = 0;
|
|
||||||
/** 出生序,用于同条件渲染排序稳定 */
|
/** 出生序,用于同条件渲染排序稳定 */
|
||||||
spawnOrder: number = 0;
|
spawnOrder: number = 0;
|
||||||
|
|
||||||
@@ -28,68 +33,20 @@ export class MoveComp extends ecs.Comp {
|
|||||||
this.direction = 1;
|
this.direction = 1;
|
||||||
this.targetX = 0;
|
this.targetX = 0;
|
||||||
this.moving = true;
|
this.moving = true;
|
||||||
this.targetY = 0;
|
|
||||||
this.baseY = 0;
|
this.baseY = 0;
|
||||||
this.lane = 0;
|
|
||||||
this.spawnOrder = 0;
|
this.spawnOrder = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MoveFacConfig {
|
|
||||||
/** 阵营可前进边界(靠近敌方一侧) */
|
|
||||||
moveFrontX: number;
|
|
||||||
/** 阵营可后退边界(靠近己方一侧) */
|
|
||||||
moveBackX: number;
|
|
||||||
/** 被逼退时前侧安全边界 */
|
|
||||||
retreatFrontX: number;
|
|
||||||
/** 被逼退时后侧安全边界 */
|
|
||||||
retreatBackX: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ecs.register('MoveSystem')
|
@ecs.register('MoveSystem')
|
||||||
export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
|
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 readonly renderSortInterval = 0.05;
|
||||||
private lastRenderSortAt = 0;
|
private lastRenderSortAt = 0;
|
||||||
private heroMoveMatcher: ecs.IMatcher | null = null;
|
|
||||||
private heroViewMatcher: 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 readonly renderEntries: { node: Node; bossPriority: number; frontScore: number; spawnOrder: number; eid: number; laneScore: number }[] = [];
|
||||||
private renderEntryCount = 0;
|
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 {
|
private getHeroViewMatcher(): ecs.IMatcher {
|
||||||
if (!this.heroViewMatcher) {
|
if (!this.heroViewMatcher) {
|
||||||
this.heroViewMatcher = ecs.allOf(HeroAttrsComp, HeroViewComp);
|
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 move = e.get(MoveComp);
|
||||||
const view = e.get(HeroViewComp);
|
const view = e.get(HeroViewComp);
|
||||||
if (!model || !move || !view || !view.node) return;
|
if (!model || !move || !view || !view.node) return;
|
||||||
if (model.fac !== FacSet.HERO) return; // 只处理英雄移动
|
|
||||||
if (!move.moving) 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);
|
this.clearCombatTarget(model);
|
||||||
if (!model.is_reviving) view.status_change("idle");
|
if (!model.is_reviving) view.status_change("idle");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 获取全局排位目标
|
/** 所有移动都锁定在 baseY,避免出现“漂移” */
|
||||||
const slot = this.getGlobalFormationSlot(e, model);
|
if (view.node.position.y !== move.baseY) {
|
||||||
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);
|
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);
|
const nearestEnemy = this.findNearestEnemy(e);
|
||||||
if (nearestEnemy) {
|
if (nearestEnemy) {
|
||||||
/** 有敌人:进入战斗位移逻辑(立即向编队目标移动) */
|
|
||||||
this.processCombatLogic(e, move, view, model, nearestEnemy);
|
this.processCombatLogic(e, move, view, model, nearestEnemy);
|
||||||
this.syncCombatTarget(model, view, nearestEnemy);
|
this.syncCombatTarget(model, view, nearestEnemy);
|
||||||
} else {
|
} else {
|
||||||
/** 无敌人:清目标并回归编队站位 */
|
/** 无敌人:停止移动,等待战斗结束结算 */
|
||||||
this.clearCombatTarget(model);
|
this.clearCombatTarget(model);
|
||||||
this.moveToSlot(view, move, model, move.targetX);
|
|
||||||
model.is_atking = false;
|
model.is_atking = false;
|
||||||
|
if (view.status !== "atk") view.status_change("idle");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果只在 Y 轴移动,也要播放 move 动画
|
/** 渲染层级重排(双阵营统一处理) */
|
||||||
if (isChangingLane && view.status !== "move" && view.status !== "atk") {
|
this.updateRenderOrder();
|
||||||
view.status_change("move");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearCombatTarget(model: HeroAttrsComp): void {
|
private clearCombatTarget(model: HeroAttrsComp): void {
|
||||||
@@ -182,300 +136,53 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
|||||||
return dist <= attackRange;
|
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) {
|
private processCombatLogic(_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 selfX = view.node.position.x;
|
||||||
const enemyX = enemy.node.position.x;
|
const enemyX = enemy.node.position.x;
|
||||||
const dist = Math.abs(selfX - enemyX);
|
const dist = Math.abs(selfX - enemyX);
|
||||||
// 已贴近到固定接触距离:停下攻击
|
const attackRange = model.dis;
|
||||||
if (dist <= MoveSystem.MELEE_ENGAGE_X) {
|
const inRange = dist <= attackRange;
|
||||||
|
|
||||||
|
if (inRange) {
|
||||||
|
/** 进入攻击范围:停止移动,进入攻击状态,由 SCastSystem 负责技能释放 */
|
||||||
model.is_atking = true;
|
model.is_atking = true;
|
||||||
if (view.status !== "atk") view.status_change("idle");
|
const dir = enemyX > selfX ? 1 : -1;
|
||||||
return;
|
view.scale = dir;
|
||||||
}
|
if (view.status === "move") {
|
||||||
// 怪物在警戒线之外:不主动迎敌,退回后方编队位待命
|
view.status_change("idle");
|
||||||
if (dist > MoveSystem.MELEE_ALERT_X) {
|
}
|
||||||
this.clearCombatTarget(model);
|
} else {
|
||||||
this.moveToSlot(view, move, model, move.targetX);
|
/** 未接触:向敌人方向推进 */
|
||||||
model.is_atking = false;
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同阵营横向最小间距约束。
|
* 按阵营裁剪 X 坐标,防止被击退/位移推出屏幕。
|
||||||
* Why: 编队/追击时英雄可能挤到一起,低优先级一方需要保持在高优先级身后 ALLY_MIN_GAP_X 处,
|
* HERO 后退不越过 BoxSet.LETF_END;MON 后退不越过 BoxSet.RIGHT_END。
|
||||||
* 避免视觉重叠与站位混乱。
|
|
||||||
* 优先级:编队优先级高者在前;同级时召唤早(spawnOrder 小)者在前。
|
|
||||||
* @returns 裁剪后的目标 X(保证不越过身前队友)
|
|
||||||
*/
|
*/
|
||||||
private clampAllySpacing(self: ecs.Entity, view: HeroViewComp, selfMove: MoveComp, model: HeroAttrsComp, targetX: number): number {
|
private clampByFacBoundary(x: number, fac: number): number {
|
||||||
const selfX = view.node.position.x;
|
if (fac === FacSet.HERO) {
|
||||||
const selfY = view.node.position.y;
|
return Math.max(BoxSet.LETF_END, x);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
if (currentX > moveMaxX && direction > 0) {
|
return Math.min(BoxSet.RIGHT_END, x);
|
||||||
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 {
|
private findNearestEnemy(entity: ecs.Entity): HeroViewComp | null {
|
||||||
@@ -527,20 +234,11 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
|||||||
actorView.node.parent = actorRoot;
|
actorView.node.parent = actorRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 spawnOrder,由于可能挂载 MoveComp 或 MonMoveComp,我们需要动态获取
|
/** 统一从 MoveComp 获取 spawnOrder */
|
||||||
let spawnOrder = 0;
|
const move = e.get(MoveComp);
|
||||||
const heroMove = e.get(MoveComp);
|
const spawnOrder = move?.spawnOrder ?? 0;
|
||||||
if (heroMove) {
|
|
||||||
spawnOrder = heroMove.spawnOrder;
|
|
||||||
} else {
|
|
||||||
const monMove = e.get(MonMoveComp);
|
|
||||||
if (monMove) {
|
|
||||||
spawnOrder = monMove.spawnOrder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按 Y 轴计算渲染层级权重(Y 越小,说明越靠近屏幕下方,应该渲染在越前面)
|
// 按 Y 轴计算渲染层级权重(Y 越小,说明越靠近屏幕下方,应该渲染在越前面)
|
||||||
// 之前的 isFly 逻辑已被移除,统一按 Y 轴处理三路渲染
|
|
||||||
const laneScore = -actorView.node.position.y;
|
const laneScore = -actorView.node.position.y;
|
||||||
// X 轴权重:站在前排的(交战处)优先渲染
|
// X 轴权重:站在前排的(交战处)优先渲染
|
||||||
const frontScore = attrs.fac === FacSet.HERO ? actorView.node.position.x : -actorView.node.position.x;
|
const frontScore = attrs.fac === FacSet.HERO ? actorView.node.position.x : -actorView.node.position.x;
|
||||||
|
|||||||
@@ -805,7 +805,6 @@ export class MissionComp extends CCComp {
|
|||||||
this.clearTime = 0
|
this.clearTime = 0
|
||||||
smc.vmdata.mission_data.mon_num = 0
|
smc.vmdata.mission_data.mon_num = 0
|
||||||
smc.vmdata.mission_data.level = 1
|
smc.vmdata.mission_data.level = 1
|
||||||
smc.vmdata.mission_data.mon_max = Math.max(1, Math.floor(this.maxMonsterCount))
|
|
||||||
this.currentPhase = MissionPhase.None;
|
this.currentPhase = MissionPhase.None;
|
||||||
this.currentWave = 1;
|
this.currentWave = 1;
|
||||||
this.isBossWave = false;
|
this.isBossWave = false;
|
||||||
|
|||||||
@@ -53,19 +53,20 @@ const { ccclass } = _decorator;
|
|||||||
export class MissionHeroComp extends CCComp {
|
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
|
* - 从 x=-300 开始,每个点位间隔 60(x 向右递增表示更靠前)。
|
||||||
v3(-300, BoxSet.GAME_LINE, 0), // index 1
|
* - 分配规则:近战优先靠前(靠近 0 的高索引),远程/中程优先靠后(低索引)。
|
||||||
v3(-300, BoxSet.GAME_LINE, 0), // index 2
|
*/
|
||||||
v3(-300, BoxSet.GAME_LINE, 0), // index 3
|
public static readonly HERO_POSITIONS: Vec3[] = (() => {
|
||||||
v3(-300, BoxSet.GAME_LINE, 0), // index 4
|
const startX = -300;
|
||||||
v3(-300, BoxSet.GAME_LINE, 0), // index 5
|
const step = 60;
|
||||||
v3(-300, BoxSet.GAME_LINE, 0), // index 6
|
const positions: Vec3[] = [];
|
||||||
v3(-300, BoxSet.GAME_LINE, 0), // index 7
|
for (let i = 0; i < FightSet.HERO_MAX_NUM; i++) {
|
||||||
v3(-300, BoxSet.GAME_LINE, 0), // index 8
|
positions.push(v3(startX + i * step, BoxSet.GAME_LINE, 0));
|
||||||
v3(-300, BoxSet.GAME_LINE, 0), // index 9
|
}
|
||||||
];
|
return positions;
|
||||||
|
})();
|
||||||
|
|
||||||
/** 英雄出生时的掉落高度(从空中落到地面的像素差) */
|
/** 英雄出生时的掉落高度(从空中落到地面的像素差) */
|
||||||
private static readonly HERO_DROP_HEIGHT = 260
|
private static readonly HERO_DROP_HEIGHT = 260
|
||||||
@@ -203,7 +204,7 @@ export class MissionHeroComp extends CCComp {
|
|||||||
* 动态分配英雄上场的位置
|
* 动态分配英雄上场的位置
|
||||||
* @param excludeEids 排除计算的实体ID数组(避免复活时把自己算成占据的位置)
|
* @param excludeEids 排除计算的实体ID数组(避免复活时把自己算成占据的位置)
|
||||||
*/
|
*/
|
||||||
private pickPositionIndexForHero(excludeEids: number[] = []): number {
|
private pickPositionIndexForHero(excludeEids: number[] = [], heroType: HType = HType.Melee): number {
|
||||||
const heroes = this.getAllHeroes().filter(h => {
|
const heroes = this.getAllHeroes().filter(h => {
|
||||||
const m = h.get(HeroAttrsComp);
|
const m = h.get(HeroAttrsComp);
|
||||||
return m && !m.is_dead && !excludeEids.includes(h.eid);
|
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);
|
if (m && m.posIndex >= 0) occupied.add(m.posIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按索引顺序填充空位(0..9),全部落点相同,英雄落地后自行移位
|
// 近战优先靠前(x 更大,索引更大);远程/中程优先靠后(x 更小,索引更小)
|
||||||
const slotPriority = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
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) {
|
for (const idx of slotPriority) {
|
||||||
if (!occupied.has(idx) && MissionHeroComp.HERO_POSITIONS[idx]) {
|
if (!occupied.has(idx) && MissionHeroComp.HERO_POSITIONS[idx]) {
|
||||||
return idx;
|
return idx;
|
||||||
@@ -255,7 +258,7 @@ export class MissionHeroComp extends CCComp {
|
|||||||
private spawnHeroAt(uuid: number, hero_lv: number, card_lv: number, posIndex: number) {
|
private spawnHeroAt(uuid: number, hero_lv: number, card_lv: number, posIndex: number) {
|
||||||
let hero = ecs.getEntity<Hero>(Hero);
|
let hero = ecs.getEntity<Hero>(Hero);
|
||||||
let scale = 1
|
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 一定有效
|
// 兜底:索引越界或点位缺失时复用 index 0,保证 landingPos 一定有效
|
||||||
const landingPos = MissionHeroComp.HERO_POSITIONS[finalPosIndex] ?? MissionHeroComp.HERO_POSITIONS[0];
|
const landingPos = MissionHeroComp.HERO_POSITIONS[finalPosIndex] ?? MissionHeroComp.HERO_POSITIONS[0];
|
||||||
let spawnPos: Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0);
|
let spawnPos: Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
*
|
*
|
||||||
* 关键设计:
|
* 关键设计:
|
||||||
* - 5 只怪在 X∈[100,320] 内按登场序号均匀铺开,Boss/小怪同时登场,玩家看清阵容后三选一。
|
* - 5 只怪在 X∈[100,320] 内按登场序号均匀铺开,Boss/小怪同时登场,玩家看清阵容后三选一。
|
||||||
* - 实际阵型与推进由 MonMoveComp 在战斗中向左移动自然形成,不再使用固定网格点。
|
* - 实际推进由统一 MoveComp 在战斗中向敌人移动自然形成,不再使用固定网格点。
|
||||||
* - 槽位索引仅用于 monGrid 寻路与 SCastSystem 索敌定位。
|
* - 槽位索引仅用于 monGrid 寻路与 SCastSystem 索敌定位。
|
||||||
*/
|
*/
|
||||||
import { _decorator, v3, Vec3 } from "cc";
|
import { _decorator, v3, Vec3 } from "cc";
|
||||||
@@ -22,7 +22,7 @@ import { GameEvent } from "../common/config/GameEvent";
|
|||||||
import { BoxSet } from "../common/config/GameSet";
|
import { BoxSet } from "../common/config/GameSet";
|
||||||
import { spawningEngine, GeneratedMonster, TestModeConfig, BOSS_COUNT, MINION_COUNT } from "./RogueConfig";
|
import { spawningEngine, GeneratedMonster, TestModeConfig, BOSS_COUNT, MINION_COUNT } from "./RogueConfig";
|
||||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||||
import { MonMoveComp } from "../hero/MonMoveComp";
|
import { MoveComp } from "../hero/MoveComp";
|
||||||
|
|
||||||
const { ccclass, property } = _decorator;
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
oops.message.dispatchEvent(GameEvent.BossSpawn, { pos: spawnPos.clone() });
|
oops.message.dispatchEvent(GameEvent.BossSpawn, { pos: spawnPos.clone() });
|
||||||
}
|
}
|
||||||
|
|
||||||
const move = mon.get(MonMoveComp);
|
const move = mon.get(MoveComp);
|
||||||
if (move) {
|
if (move) {
|
||||||
move.spawnOrder = this.globalSpawnOrder;
|
move.spawnOrder = this.globalSpawnOrder;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user