Compare commits
3 Commits
c27c81cfa6
...
a3375ec3b6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3375ec3b6 | ||
|
|
011e7b3dde | ||
|
|
038abdc5d2 |
@@ -85,10 +85,11 @@ export const FormationPointX = {
|
||||
[HType.Long]: 100,
|
||||
} as const;
|
||||
|
||||
/** 各攻击定位的默认攻击距离(像素):近战150 / 中程300 / 远程450;英雄配置了 dis 时优先取配置值 */
|
||||
export const HeroDisVal: Record<HType.Melee | HType.Mid | HType.Long, number> = {
|
||||
[HType.Melee]: 120,
|
||||
[HType.Mid]: 720,
|
||||
[HType.Long]: 720,
|
||||
[HType.Melee]: 150,
|
||||
[HType.Mid]: 300,
|
||||
[HType.Long]: 450,
|
||||
}
|
||||
|
||||
export const resolveFormationTargetX = (fac: FacSet, type: HType): number => {
|
||||
|
||||
@@ -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, HeroDisVal } from "../common/config/heroSet";
|
||||
import { GameEvent } from "../common/config/GameEvent";
|
||||
import { SkillTriggerType, HType } from "../common/config/heroSet";
|
||||
import { SkillTriggerHelper } from "./SkillTriggerHelper";
|
||||
@@ -122,8 +122,8 @@ export class Hero extends ecs.Entity {
|
||||
model.grow_type = hero.grow_type;
|
||||
model.role = hero.role;
|
||||
model.fac = FacSet.HERO;
|
||||
// 攻击距离:配置优先;未配置按攻击定位给默认值(近战180 / 远程600)
|
||||
model.dis = hero.dis ?? (hero.type === HType.Melee ? 180 : 600);
|
||||
// 攻击距离:配置优先;未配置按攻击定位取默认值(近战150 / 中程300 / 远程450)
|
||||
model.dis = hero.dis ?? HeroDisVal[hero.type as HType.Melee | HType.Mid | HType.Long];
|
||||
model.posIndex = posIndex;
|
||||
if (posIndex >= 0) {
|
||||
smc.mission.heroGrid[posIndex] = this.eid;
|
||||
|
||||
@@ -3,11 +3,10 @@ import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/O
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
import { smc } from "../common/SingletonModuleComp";
|
||||
import { BoxSet, FacSet, FightSet, IndexSet } from "../common/config/GameSet";
|
||||
import { HeroInfo, resolveTriggerByLv, resolveReviveByLv } from "../common/config/heroSet";
|
||||
import { HeroInfo, resolveTriggerByLv, resolveReviveByLv, HeroDisVal } from "../common/config/heroSet";
|
||||
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<ecs.Comp>(
|
||||
MonMoveComp,
|
||||
MoveComp,
|
||||
HeroAttrsComp,
|
||||
);
|
||||
}
|
||||
@@ -175,8 +174,8 @@ export class Monster extends ecs.Entity {
|
||||
model.speed = hero.speed ?? 800;
|
||||
model.type = hero.type;
|
||||
model.fac = FacSet.MON;
|
||||
// 攻击距离:配置优先;未配置按攻击定位给默认值(近战180 / 远程600)
|
||||
model.dis = hero.dis ?? (hero.type === HType.Melee ? 180 : 600);
|
||||
// 攻击距离:配置优先;未配置按攻击定位取默认值(近战150 / 中程300 / 远程450)
|
||||
model.dis = hero.dis ?? HeroDisVal[hero.type as HType.Melee | HType.Mid | HType.Long];
|
||||
model.posIndex = laneIndex;
|
||||
if (laneIndex >= 0) {
|
||||
smc.mission.monGrid[laneIndex] = this.eid;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 { 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<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 {
|
||||
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,185 +136,66 @@ 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;
|
||||
}
|
||||
}
|
||||
/** 同阵营横向最小间距,防止停止移动时完全重叠 */
|
||||
private static readonly ALLY_MIN_GAP_X = 10;
|
||||
/** 挤位时的蠕动幅度(像素) */
|
||||
private static readonly JOSTLE_AMPLITUDE = 2;
|
||||
|
||||
/**
|
||||
* 近战交战:怪物进入警戒线(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");
|
||||
}
|
||||
/** 停止时若与队友重叠,则向后微调保持最小间距,并加入蠕动避免死板 */
|
||||
this.adjustSpacingWhenIdle(_e, view, move, model);
|
||||
} 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.adjustSpacingWhileMoving(_e, view, move, model, newX, dir);
|
||||
|
||||
/** 后退边界钳制:防止被击退/位移推出屏幕 */
|
||||
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(保证不越过身前队友)
|
||||
* 推进时的间距避让。
|
||||
* Why: 允许穿越队友,但若与队友完全同点(重叠),后来者稍停一拍,
|
||||
* 避免视觉上的"穿模"过于生硬,营造"挤过去"的自然感。
|
||||
* @returns 调整后的目标 X(被阻挡时返回 selfX,即本帧不移动)
|
||||
*/
|
||||
private clampAllySpacing(self: ecs.Entity, view: HeroViewComp, selfMove: MoveComp, model: HeroAttrsComp, targetX: number): number {
|
||||
private adjustSpacingWhileMoving(self: ecs.Entity, view: HeroViewComp, selfMove: MoveComp, model: HeroAttrsComp, targetX: number, _dir: number): number {
|
||||
const selfX = view.node.position.x;
|
||||
const selfY = view.node.position.y;
|
||||
let clamped = targetX;
|
||||
|
||||
ecs.query(this.getHeroMoveMatcher()).forEach(e => {
|
||||
let blocked = false;
|
||||
ecs.query(this.getHeroViewMatcher()).forEach(e => {
|
||||
if (e === self) return;
|
||||
const attrs = e.get(HeroAttrsComp);
|
||||
const v = e.get(HeroViewComp);
|
||||
@@ -368,114 +203,83 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
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;
|
||||
if (Math.abs(v.node.position.y - selfY) > 30) 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);
|
||||
/** 同点判定:间距小于最小间距的一半视为完全重叠 */
|
||||
if (Math.abs(selfX - otherX) < MoveSystem.ALLY_MIN_GAP_X * 0.5) {
|
||||
/** spawnOrder 大者(后来者)被阻挡 */
|
||||
if (selfMove.spawnOrder > mv.spawnOrder) {
|
||||
blocked = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
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");
|
||||
return blocked ? selfX : targetX;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编队站位优先级(数值越大越靠前/越靠右)。
|
||||
* Why: 阵容职能决定承伤顺序——死亡触发英雄需最先阵亡以触发遗志,
|
||||
* 其次坦克承伤,再次战士/刺客,远程射手/法师/辅助依次靠后保护。
|
||||
* 排序档位:死亡触发 > 坦克 > 战士 > 刺客 > 射手 > 法师 > 辅助
|
||||
* 停止移动时的间距微调 + 蠕动效果。
|
||||
* Why: 允许推进时穿越队友,但进入攻击范围停下后若与队友重叠,
|
||||
* 则向后微调保持 ALLY_MIN_GAP_X 间距,避免视觉完全重叠。
|
||||
* 同时加入基于时间的随机蠕动,营造"挤到前方攻击"的生动感。
|
||||
* Y 轴不变,仅约束 X。
|
||||
* 优先级:spawnOrder 小者(先出生)不动,大者向后让位。
|
||||
*/
|
||||
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;
|
||||
private adjustSpacingWhenIdle(self: ecs.Entity, view: HeroViewComp, selfMove: MoveComp, model: HeroAttrsComp): void {
|
||||
const selfX = view.node.position.x;
|
||||
const selfY = view.node.position.y;
|
||||
let adjustX = selfX;
|
||||
let isCrowded = false;
|
||||
|
||||
ecs.query(this.getHeroViewMatcher()).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) > 30) return;
|
||||
|
||||
const otherX = v.node.position.x;
|
||||
const gap = Math.abs(adjustX - otherX);
|
||||
if (gap >= MoveSystem.ALLY_MIN_GAP_X) return;
|
||||
|
||||
isCrowded = true;
|
||||
/** spawnOrder 小者不动,大者向后让位 */
|
||||
if (selfMove.spawnOrder > mv.spawnOrder) {
|
||||
/** 自己靠后:向远离对方的方向后退到最小间距处 */
|
||||
const dir = adjustX >= otherX ? 1 : -1;
|
||||
adjustX = otherX + dir * MoveSystem.ALLY_MIN_GAP_X;
|
||||
}
|
||||
});
|
||||
|
||||
/** 拥挤时加入蠕动:基于时间+出生序的伪随机偏移,避免多个单位同步抖动 */
|
||||
if (isCrowded) {
|
||||
const time = Date.now() / 1000;
|
||||
const jostlePhase = time * 3 + selfMove.spawnOrder * 0.7; // 不同单位错开相位
|
||||
const jostleOffset = Math.sin(jostlePhase) * MoveSystem.JOSTLE_AMPLITUDE;
|
||||
adjustX += jostleOffset;
|
||||
}
|
||||
|
||||
if (Math.abs(adjustX - selfX) >= 0.01) {
|
||||
/** 后退边界钳制 */
|
||||
adjustX = this.clampByFacBoundary(adjustX, model.fac);
|
||||
view.node.setPosition(adjustX, view.node.position.y, 0);
|
||||
}
|
||||
// 兜底:未配置 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];
|
||||
/**
|
||||
* 按阵营裁剪 X 坐标,防止被击退/位移推出屏幕。
|
||||
* HERO 后退不越过 BoxSet.LETF_END;MON 后退不越过 BoxSet.RIGHT_END。
|
||||
*/
|
||||
private clampByFacBoundary(x: number, fac: number): number {
|
||||
if (fac === FacSet.HERO) {
|
||||
return Math.max(BoxSet.LETF_END, x);
|
||||
}
|
||||
return Math.min(BoxSet.RIGHT_END, x);
|
||||
}
|
||||
|
||||
private findNearestEnemy(entity: ecs.Entity): HeroViewComp | null {
|
||||
@@ -527,20 +331,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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -53,18 +53,16 @@ const { ccclass } = _decorator;
|
||||
export class MissionHeroComp extends CCComp {
|
||||
// ======================== 常量 ========================
|
||||
|
||||
/** 硬编码的英雄占位点(数量对齐 FightSet.HERO_MAX_NUM,落点 X 统一,英雄落地后由 MoveComp 自行移位到阵型目标) */
|
||||
/**
|
||||
* 英雄登场占位点(固定 3 列):
|
||||
* - index 0/1/2 分别对应 x=-320/-260/-200。
|
||||
* - 分配规则:近战优先靠前(x 更大),远程/中程优先靠后(x 更小);
|
||||
* 同类型按召唤先后从该类型的一侧开始依次填充。
|
||||
*/
|
||||
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
|
||||
v3(-320, BoxSet.GAME_LINE, 0),
|
||||
v3(-260, BoxSet.GAME_LINE, 0),
|
||||
v3(-200, BoxSet.GAME_LINE, 0),
|
||||
];
|
||||
|
||||
/** 英雄出生时的掉落高度(从空中落到地面的像素差) */
|
||||
@@ -203,7 +201,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 +213,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
|
||||
? [2, 1, 0]
|
||||
: [0, 1, 2];
|
||||
for (const idx of slotPriority) {
|
||||
if (!occupied.has(idx) && MissionHeroComp.HERO_POSITIONS[idx]) {
|
||||
return idx;
|
||||
@@ -255,7 +255,7 @@ export class MissionHeroComp extends CCComp {
|
||||
private spawnHeroAt(uuid: number, hero_lv: number, card_lv: number, posIndex: number) {
|
||||
let hero = ecs.getEntity<Hero>(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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user