feat(hero): 新增英雄职业定位系统,优化编队站位逻辑
1. 新增HRole枚举与角色定位配置,解耦成长类型与阵容职能 2. 重构移动系统的编队优先级计算,基于职业定位重新排序站位 3. 重写单行编队站位算法,根据队伍人数自动适配紧凑/松散排布 4. 为所有预设英雄配置对应职业定位
This commit is contained in:
@@ -3,7 +3,7 @@ import { HeroViewComp } from "./HeroViewComp";
|
||||
import { HeroAttrsComp } from "./HeroAttrsComp";
|
||||
import { smc } from "../common/SingletonModuleComp";
|
||||
import { BoxSet, FacSet } from "../common/config/GameSet";
|
||||
import { HeroDisVal, HType } from "../common/config/heroSet";
|
||||
import { HeroDisVal, HType, HRole } from "../common/config/heroSet";
|
||||
import { BoxCollider2D, Node } from "cc";
|
||||
import { MonMoveComp } from "./MonMoveComp";
|
||||
|
||||
@@ -46,8 +46,6 @@ interface MoveFacConfig {
|
||||
retreatBackX: number;
|
||||
}
|
||||
|
||||
import { MissionHeroComp } from "../map/MissionHeroComp";
|
||||
|
||||
@ecs.register('MoveSystem')
|
||||
export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
|
||||
private readonly heroFrontAnchorX = -200;
|
||||
@@ -106,7 +104,7 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
update(e: ecs.Entity) {
|
||||
/** 战斗未开始/暂停时不驱动移动 */
|
||||
if (!smc.mission.play || smc.mission.pause) return;
|
||||
|
||||
|
||||
const model = e.get(HeroAttrsComp);
|
||||
const move = e.get(MoveComp);
|
||||
const view = e.get(HeroViewComp);
|
||||
@@ -125,55 +123,37 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
move.baseY = slot.targetY;
|
||||
move.targetX = slot.targetX;
|
||||
|
||||
// 战斗阶段不移动
|
||||
const canMove = !smc.mission.in_fight;
|
||||
|
||||
// 2. 平滑 Y 轴换路
|
||||
// 2. 平滑 Y 轴换路(不分阶段,立即响应编队变化)
|
||||
let isChangingLane = false;
|
||||
if (canMove) {
|
||||
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);
|
||||
}
|
||||
if (Math.abs(view.node.position.y - move.baseY) > 2) {
|
||||
const currentY = view.node.position.y;
|
||||
const deltaY = move.baseY - currentY;
|
||||
const step = 400 * this.dt; // 换路速度
|
||||
const newY = currentY + Math.sign(deltaY) * Math.min(Math.abs(deltaY), step);
|
||||
view.node.setPosition(view.node.position.x, newY, 0);
|
||||
isChangingLane = true;
|
||||
} else {
|
||||
view.node.setPosition(view.node.position.x, move.baseY, 0);
|
||||
}
|
||||
|
||||
|
||||
// 渲染层级重排
|
||||
this.updateRenderOrder();
|
||||
|
||||
const nearestEnemy = this.findNearestEnemy(e);
|
||||
if (nearestEnemy) {
|
||||
/** 有敌人:进入战斗位移逻辑 */
|
||||
if (canMove) {
|
||||
this.processCombatLogic(e, move, view, model, nearestEnemy);
|
||||
} else {
|
||||
model.is_atking = true; // 战斗中不移动,但保持攻击状态
|
||||
}
|
||||
/** 有敌人:进入战斗位移逻辑(立即向编队目标移动) */
|
||||
this.processCombatLogic(e, move, view, model, nearestEnemy);
|
||||
this.syncCombatTarget(model, view, nearestEnemy);
|
||||
} else {
|
||||
/** 无敌人:清目标并回归编队站位 */
|
||||
this.clearCombatTarget(model);
|
||||
if (canMove) {
|
||||
this.moveToSlot(view, move, model, move.targetX);
|
||||
}
|
||||
this.moveToSlot(view, move, model, move.targetX);
|
||||
model.is_atking = false;
|
||||
}
|
||||
|
||||
if (canMove) {
|
||||
// 如果只在 Y 轴移动,也要播放 move 动画
|
||||
if (isChangingLane && view.status !== "move" && view.status !== "atk") {
|
||||
view.status_change("move");
|
||||
}
|
||||
} else {
|
||||
// 战斗阶段如果不移动,确保不在攻击时恢复 idle 状态
|
||||
if (!model.is_atking && view.status === "move") {
|
||||
view.status_change("idle");
|
||||
}
|
||||
// 如果只在 Y 轴移动,也要播放 move 动画
|
||||
if (isChangingLane && view.status !== "move" && view.status !== "atk") {
|
||||
view.status_change("move");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,14 +232,10 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
allAllies.sort((a, b) => {
|
||||
const attrsA = a.get(HeroAttrsComp);
|
||||
const attrsB = b.get(HeroAttrsComp);
|
||||
const priorityA = attrsA ? this.getCombatPriority(attrsA) : 0;
|
||||
const priorityB = attrsB ? this.getCombatPriority(attrsB) : 0;
|
||||
const priorityA = attrsA ? this.getFormationPriority(attrsA) : 0;
|
||||
const priorityB = attrsB ? this.getFormationPriority(attrsB) : 0;
|
||||
if (priorityA !== priorityB) return priorityB - priorityA;
|
||||
|
||||
const deadA = (attrsA?.dead && attrsA.dead.length > 0) ? 1 : 0;
|
||||
const deadB = (attrsB?.dead && attrsB.dead.length > 0) ? 1 : 0;
|
||||
if (deadA !== deadB) return deadB - deadA;
|
||||
|
||||
const lvA = attrsA?.lv ?? 1;
|
||||
const lvB = attrsB?.lv ?? 1;
|
||||
if (lvA !== lvB) return lvB - lvA;
|
||||
@@ -271,20 +247,48 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
return a.eid - b.eid;
|
||||
});
|
||||
|
||||
const slotIndex = Math.max(0, allAllies.findIndex(entity => entity === self));
|
||||
|
||||
// 优先中前(1) -> 上前(0) -> 下前(2) -> 中后(4) -> 上后(3) -> 下后(5)
|
||||
const slotPriority = [1, 0, 2, 4, 3, 5];
|
||||
const posIndex = slotPriority[slotIndex % 6];
|
||||
const pos = MissionHeroComp.HERO_POSITIONS[posIndex];
|
||||
|
||||
return { targetX: pos.x, targetY: pos.y };
|
||||
// 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;
|
||||
|
||||
/**
|
||||
* 计算某个槽位的目标 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);
|
||||
@@ -307,7 +311,7 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
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) {
|
||||
@@ -345,12 +349,30 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
view.status_change("move");
|
||||
}
|
||||
|
||||
private getCombatPriority(model: HeroAttrsComp): number {
|
||||
/** 数值越大越靠前:近战 > 中程 > 远程 */
|
||||
/**
|
||||
* 编队站位优先级(数值越大越靠前/越靠右)。
|
||||
* 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 3;
|
||||
if (rangeType === HType.Mid) return 2;
|
||||
return 1;
|
||||
if (rangeType === HType.Melee) return 30;
|
||||
if (rangeType === HType.Mid) return 20;
|
||||
return 10;
|
||||
}
|
||||
|
||||
private resolveCombatRange(model: HeroAttrsComp, defaultMin: number, defaultMax: number): [number, number] {
|
||||
@@ -364,13 +386,13 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
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);
|
||||
@@ -405,7 +427,7 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
const actorView = e.get(HeroViewComp);
|
||||
if (!attrs || !actorView?.node || attrs.is_dead) return;
|
||||
if (attrs.fac !== FacSet.HERO && attrs.fac !== FacSet.MON) return;
|
||||
|
||||
|
||||
if (actorView.node.parent !== actorRoot) {
|
||||
actorView.node.parent = actorRoot;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user