refactor(hero): 统一英雄编队与出生点的位置配置

1. 将硬编码的英雄排位置常量迁移至FightSet枚举,实现单一数据源
2. 重构英雄编队逻辑,按职业优先级分为三排站位
3. 调整召唤队列,按职业优先级排序后串行登场
This commit is contained in:
pan
2026-08-20 11:11:44 +08:00
parent 8039b052d2
commit 1523aab402
3 changed files with 90 additions and 47 deletions

View File

@@ -23,6 +23,10 @@ export enum FacSet {
MON = 1, MON = 1,
} }
export enum FightSet { export enum FightSet {
/** 英雄三排出生/编队基准 xy 均为 BoxSet.GAME_LINE前排迎敌、中排、后排靠左 */
HERO_ROW_FRONT_X = -200, // 前排(最靠右迎敌):坦克 / 战士
HERO_ROW_MID_X = -260, // 中排:刺客 / 射手
HERO_ROW_BACK_X = -320, // 后排(最靠左):法师 / 辅助
WAVE_COIN_BASE = 7, // 回合金币基础奖励 WAVE_COIN_BASE = 7, // 回合金币基础奖励
WAVE_COIN_GROW = 2, // 回合金币递增值 WAVE_COIN_GROW = 2, // 回合金币递增值
WAVE_COIN_MAX = 17, // 回合金币最大基础奖励 WAVE_COIN_MAX = 17, // 回合金币最大基础奖励

View File

@@ -14,7 +14,7 @@ import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ec
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, FightSet } from "../common/config/GameSet";
import { HRole } from "../common/config/heroSet"; import { HRole } from "../common/config/heroSet";
import { Node } from "cc"; import { Node } from "cc";
@@ -54,9 +54,16 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
// ======================== 英雄编队排队 ======================== // ======================== 英雄编队排队 ========================
/** 编队锚点:最右迎敌位(与 MissionHeroComp.HERO_ROW_FRONT 的 x 对齐,避免循环依赖在此独立定义) */ /**
private static readonly QUEUE_ANCHOR_X = -200; * 三排编队锚点 x单一数据源取自 FightSet与 MissionHeroComp 的 HERO_ROW_FRONT/MID/BACK 一致):
/** 编队槽位默认间隔(像素);人数超出边界宽度时自动压缩间距 */ * 前排 / 中排 / 后排。准备阶段按"职业优先级名次"映射到三排(见 rebuildHeroQueueSlots
*/
private static readonly ROW_FRONT_X = FightSet.HERO_ROW_FRONT_X;
private static readonly ROW_MID_X = FightSet.HERO_ROW_MID_X;
private static readonly ROW_BACK_X = FightSet.HERO_ROW_BACK_X;
/** 编队排队允许向左扩展到的最小 x后排自锚点向左排开时的下限 */
private static readonly QUEUE_LEFT_LIMIT = FightSet.HERO_ROW_BACK_X - 120;
/** 编队槽位默认间隔(像素);后排人数过多时自动压缩间距 */
private static readonly QUEUE_SPACING = 30; private static readonly QUEUE_SPACING = 30;
/** 编队槽位重算节流间隔(秒):召唤/死亡后最多延迟一次重算 */ /** 编队槽位重算节流间隔(秒):召唤/死亡后最多延迟一次重算 */
private readonly queueRebuildInterval = 0.1; private readonly queueRebuildInterval = 0.1;
@@ -155,8 +162,9 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
/** /**
* 英雄编队排队: * 英雄编队排队:
* 按 坦克→战士→刺客→射手→法师→辅助 的职业优先级(即 HRole 枚举值升序), * 按 坦克→战士→刺客→射手→法师→辅助 的职业优先级(即 HRole 枚举值升序)做全局排序
* 从最右迎敌位QUEUE_ANCHOR_X向左依次排开防止同类型英雄落点重叠 * 再按名次映射三排锚点:第 1 名→前排、第 2 名→中排、第 3 名起→后排向左排开
* 槽位互不重叠,保证一个位置仅一个英雄。
* *
* @param entity 当前英雄实体 * @param entity 当前英雄实体
* @param model 英雄属性(取 speed / role * @param model 英雄属性(取 speed / role
@@ -181,8 +189,10 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
const dir = targetX > selfX ? 1 : -1; const dir = targetX > selfX ? 1 : -1;
move.direction = dir; move.direction = dir;
view.scale = dir; view.scale = dir;
/** 编队排队允许突破战斗左边界LETF_END=后排锚点),扩展至 QUEUE_LEFT_LIMIT */
const queueClamp = (x: number): number => Math.max(MoveSystem.QUEUE_LEFT_LIMIT, x);
const delta = (model.speed / 3) * this.dt * dir; const delta = (model.speed / 3) * this.dt * dir;
const newX = this.clampByFacBoundary(selfX + delta, model.fac); const newX = queueClamp(selfX + delta);
if (Math.abs(newX - selfX) >= 0.01) { if (Math.abs(newX - selfX) >= 0.01) {
view.node.setPosition(newX, view.node.position.y, 0); view.node.setPosition(newX, view.node.position.y, 0);
view.status_change("move"); view.status_change("move");
@@ -204,8 +214,14 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
/** /**
* 重算编队槽位: * 重算编队槽位:
* 收集全部存活英雄,按 职业优先级 → 出生序 → eid 稳定排序 * 收集全部存活英雄,按 职业优先级HRole 升序)→ 出生序 → eid 稳定排序得到全局名次 rank
* 从 QUEUE_ANCHOR_X 向左按间隔分配;人数过多时压缩间距,保证不越过 LETF_END。 * 再按名次映射三排锚点:
* - rank 0最优先→ 前排 ROW_FRONT_X
* - rank 1 → 中排 ROW_MID_X
* - rank ≥ 2 → 后排,自 ROW_BACK_X 起按 (rank-1) 个间隔向左依次排开
* 效果:高优先级职业永远占据更靠前的排;当更高优先级英雄插入时,低优先级英雄自动向后让位
* (如 近战+射手+近战 → 两近战分占前/中排,射手被挤到后排)。
* 后排人数过多时压缩间距,下限不越过 QUEUE_LEFT_LIMIT。
*/ */
private rebuildHeroQueueSlots(): void { private rebuildHeroQueueSlots(): void {
const heroes: { eid: number; role: number; spawnOrder: number }[] = []; const heroes: { eid: number; role: number; spawnOrder: number }[] = [];
@@ -223,15 +239,25 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
(a.eid - b.eid) (a.eid - b.eid)
); );
const count = heroes.length; /** 后排人数 = 总名次从 rank2 起的数量,用于预压缩间距 */
const maxWidth = MoveSystem.QUEUE_ANCHOR_X - BoxSet.LETF_END; const backCount = Math.max(0, heroes.length - 2);
const spacing = count > 1 const backMaxWidth = MoveSystem.ROW_BACK_X - MoveSystem.QUEUE_LEFT_LIMIT;
? Math.min(MoveSystem.QUEUE_SPACING, maxWidth / (count - 1)) const backSpacing = backCount > 1
: 0; ? Math.min(MoveSystem.QUEUE_SPACING, backMaxWidth / (backCount - 1))
: MoveSystem.QUEUE_SPACING;
this.queueSlots.length = 0; this.queueSlots.length = 0;
for (let i = 0; i < count; i++) { for (let rank = 0; rank < heroes.length; rank++) {
this.queueSlots.push({ eid: heroes[i].eid, targetX: MoveSystem.QUEUE_ANCHOR_X - i * spacing }); let targetX: number;
if (rank === 0) {
targetX = MoveSystem.ROW_FRONT_X;
} else if (rank === 1) {
targetX = MoveSystem.ROW_MID_X;
} else {
/** 后排rank2 在锚点rank3 起向左排开 */
targetX = MoveSystem.ROW_BACK_X - (rank - 2) * backSpacing;
}
this.queueSlots.push({ eid: heroes[rank].eid, targetX });
} }
} }

View File

@@ -7,7 +7,8 @@
* 2. 管理英雄的出生点和掉落动画。 * 2. 管理英雄的出生点和掉落动画。
* *
* 关键设计: * 关键设计:
* - summon_queue + processSummonQueue() 确保召唤请求 **串行处理** * - summon_queue + processSummonQueue() 确保召唤请求 **串行处理**
* 且队列按职业优先级(坦克→战士→刺客→射手→法师→辅助)排序后依次登场。
* - spawnHeroAt 负责生成英雄;允许同 uuid 重复召唤,多个相同英雄可同时站场。 * - spawnHeroAt 负责生成英雄;允许同 uuid 重复召唤,多个相同英雄可同时站场。
* - 英雄可通过升级卡SpecialUpgrade升级。 * - 英雄可通过升级卡SpecialUpgrade升级。
* *
@@ -17,8 +18,8 @@
* revive 系技能为"濒死回血"(血量归零时消耗次数回血续命),不触发真死。 * revive 系技能为"濒死回血"(血量归零时消耗次数回血续命),不触发真死。
* *
* 出生点规则: * 出生点规则:
* 按攻击类型HType落排近战 → 前排x=-200最靠右迎敌 * 按职业定位HRole落排坦克/战士 → 前排x=-200最靠右迎敌
* 中距 → 中排x=-260远程 → 后排x=-320最靠左 * 刺客/射手 → 中排x=-260法师/辅助 → 后排x=-320最靠左
* 同类型落点不再做 ± 偏移展开,落点重叠由 MoveSystem 的编队排队解决 * 同类型落点不再做 ± 偏移展开,落点重叠由 MoveSystem 的编队排队解决
* (准备阶段按职业优先级 坦克→战士→刺客→射手→法师→辅助 从最右向左排开)。 * (准备阶段按职业优先级 坦克→战士→刺客→射手→法师→辅助 从最右向左排开)。
* posIndex 取 heroGrid 全局空闲下标0~9用于网格登记/索敌,与落点解耦。 * posIndex 取 heroGrid 全局空闲下标0~9用于网格登记/索敌,与落点解耦。
@@ -36,10 +37,10 @@ import { Hero } from "../hero/Hero";
import { smc } from "../common/SingletonModuleComp"; import { smc } from "../common/SingletonModuleComp";
import { Timer } from "db://oops-framework/core/common/timer/Timer"; import { Timer } from "db://oops-framework/core/common/timer/Timer";
import { GameEvent } from "../common/config/GameEvent"; import { GameEvent } from "../common/config/GameEvent";
import { HeroInfo, HType } from "../common/config/heroSet"; import { HeroInfo, HRole } from "../common/config/heroSet";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { FacSet, BoxSet } from "../common/config/GameSet"; import { FacSet, BoxSet, FightSet } from "../common/config/GameSet";
import { MoveComp } from "../hero/MoveComp"; import { MoveComp } from "../hero/MoveComp";
const { ccclass } = _decorator; const { ccclass } = _decorator;
@@ -55,27 +56,32 @@ export class MissionHeroComp extends CCComp {
// ======================== 常量 ======================== // ======================== 常量 ========================
/** /**
* 三排出生基准点y 均为 BoxSet.GAME_LINE * 三排出生基准点y 均为 BoxSet.GAME_LINEx 取自 FightSet
* - 前排 x=-200(最靠右迎敌):战士 / 坦克 * - 前排 x=FightSet.HERO_ROW_FRONT_X(最靠右迎敌):坦克 / 战士
* - 中排 x=-260射手 / 刺客 * - 中排 x=FightSet.HERO_ROW_MID_X刺客 / 射手
* - 后排 x=-320(最靠左):辅助 / 法师 * - 后排 x=FightSet.HERO_ROW_BACK_X(最靠左):法师 / 辅助
*/ */
public static readonly HERO_ROW_FRONT = v3(-200, BoxSet.GAME_LINE, 0) public static readonly HERO_ROW_FRONT = v3(FightSet.HERO_ROW_FRONT_X, BoxSet.GAME_LINE, 0)
public static readonly HERO_ROW_MID = v3(-260, BoxSet.GAME_LINE, 0) public static readonly HERO_ROW_MID = v3(FightSet.HERO_ROW_MID_X, BoxSet.GAME_LINE, 0)
public static readonly HERO_ROW_BACK = v3(-320, BoxSet.GAME_LINE, 0) public static readonly HERO_ROW_BACK = v3(FightSet.HERO_ROW_BACK_X, BoxSet.GAME_LINE, 0)
/** /**
* 攻击类型 → 出生排映射: * 职业定位 → 出生排映射:
* 近战 → 前排,中距 → 中排(优先中间位),远程 → 后排。 * 坦克/战士 → 前排,刺客/射手 → 中排,法师/辅助 → 后排。
*/ */
private static readonly TYPE_ROW: Record<HType, Vec3> = { private static readonly ROLE_ROW: Record<HRole, Vec3> = {
[HType.Melee]: MissionHeroComp.HERO_ROW_FRONT, [HRole.Tank]: MissionHeroComp.HERO_ROW_FRONT,
[HType.Mid]: MissionHeroComp.HERO_ROW_MID, [HRole.Warrior]: MissionHeroComp.HERO_ROW_FRONT,
[HType.Long]: MissionHeroComp.HERO_ROW_BACK, [HRole.Assassin]: MissionHeroComp.HERO_ROW_MID,
[HRole.Archer]: MissionHeroComp.HERO_ROW_MID,
[HRole.Mage]: MissionHeroComp.HERO_ROW_BACK,
[HRole.Support]: MissionHeroComp.HERO_ROW_BACK,
}; };
/** 英雄出生时的掉落高度(从空中落到地面的像素差) */ /** 英雄出生时的掉落高度(从空中落到地面的像素差) */
private static readonly HERO_DROP_HEIGHT = 260 private static readonly HERO_DROP_HEIGHT = 260
/** 召唤登场间隔(秒):保证"依次上场"节奏,避免同帧堆叠掉落 */
private static readonly SUMMON_INTERVAL = 0.35
// ======================== 运行时属性 ======================== // ======================== 运行时属性 ========================
@@ -89,8 +95,8 @@ export class MissionHeroComp extends CCComp {
current_hero_num: number = -1 current_hero_num: number = -1
/** 是否正在消费召唤队列(防止并发) */ /** 是否正在消费召唤队列(防止并发) */
is_processing_queue: boolean = false is_processing_queue: boolean = false
/** 召唤请求队列:保证召唤按顺序串行执行 */ /** 召唤请求队列:按职业优先级排序后串行登场 */
summon_queue: { uuid: number; hero_lv: number; card_lv: number }[] = [] summon_queue: { uuid: number; hero_lv: number; card_lv: number; role: HRole }[] = []
/** 英雄出生序计数器(编队排队/渲染排序的稳定决胜) */ /** 英雄出生序计数器(编队排队/渲染排序的稳定决胜) */
private heroSpawnOrder = 0 private heroSpawnOrder = 0
/** 预留英雄列表 */ /** 预留英雄列表 */
@@ -155,8 +161,8 @@ export class MissionHeroComp extends CCComp {
/** /**
* 召唤请求入口: * 召唤请求入口:
* 从事件参数中提取 uuid / hero_lv / card_lv放入串行队列。 * 从事件参数中提取 uuid / hero_lv / card_lv放入串行队列。
* 二合一:同 uuid 已召唤且等级未达上限时允许入队,由 handleSingleSummon 执行合成; * 队列按职业优先级坦克→战士→刺客→射手→法师→辅助HRole 升序)排序,
* 同 uuid 且等级已达上限时拒绝(只能通过升级卡继续提升之外无法再合成) * 保证高优先级职业先登场
* *
* @param event 事件名 * @param event 事件名
* @param args { uuid, hero_lv, card_lv } * @param args { uuid, hero_lv, card_lv }
@@ -169,8 +175,11 @@ export class MissionHeroComp extends CCComp {
const outer_lv = smc.getHeroOuterLv(uuid); const outer_lv = smc.getHeroOuterLv(uuid);
const hero_lv = Math.max(outer_lv, Math.max(1, Number(payload?.hero_lv ?? 1))); const hero_lv = Math.max(outer_lv, Math.max(1, Number(payload?.hero_lv ?? 1)));
const card_lv = Math.max(1, Number(payload?.card_lv ?? 1)); const card_lv = Math.max(1, Number(payload?.card_lv ?? 1));
const role = HeroInfo[uuid]?.role ?? HRole.Support;
this.summon_queue.push({ uuid, hero_lv, card_lv }); this.summon_queue.push({ uuid, hero_lv, card_lv, role });
// 按职业优先级稳定排序HRole 升序,同职业保持入队先后
this.summon_queue.sort((a, b) => a.role - b.role);
this.processSummonQueue(); this.processSummonQueue();
} }
@@ -178,17 +187,17 @@ export class MissionHeroComp extends CCComp {
/** /**
* 为英雄分配出生槽位: * 为英雄分配出生槽位:
* 按攻击类型取三排基准点(近战前排、中距优先中排、远程后排),同类型不再做横向偏移展开; * 按职业定位取三排基准点(坦克/战士前排、刺客/射手中排、法师/辅助后排),同类型不再做横向偏移展开;
* 落点重叠由 MoveSystem 的编队排队(按职业优先级从最右向左排开)负责分开。 * 落点重叠由 MoveSystem 的编队排队(按职业优先级从最右向左排开)负责分开。
* *
* 槽位编码posIndex 直接取 heroGrid 的全局空闲下标0~9与落点无映射。 * 槽位编码posIndex 直接取 heroGrid 的全局空闲下标0~9与落点无映射。
* *
* @param type 英雄攻击类型HType决定所属排 * @param role 英雄职业定位HRole决定所属排
* @param heroes 当前存活英雄列表spawnHeroAt 在实体创建前快照,避免复用池残留实体干扰网格判定) * @param heroes 当前存活英雄列表spawnHeroAt 在实体创建前快照,避免复用池残留实体干扰网格判定)
* @returns { posIndex heroGrid 空闲下标(-1 表示满员未登记), landingPos 落地点 } * @returns { posIndex heroGrid 空闲下标(-1 表示满员未登记), landingPos 落地点 }
*/ */
private pickSpawnSlotForHero(type: HType, heroes: Hero[]): { posIndex: number; landingPos: Vec3 } { private pickSpawnSlotForHero(role: HRole, heroes: Hero[]): { posIndex: number; landingPos: Vec3 } {
const basePos = MissionHeroComp.TYPE_ROW[type] ?? MissionHeroComp.HERO_ROW_MID; const basePos = MissionHeroComp.ROLE_ROW[role] ?? MissionHeroComp.HERO_ROW_MID;
// 已登记的网格下标集合(仅用于 posIndex 分配) // 已登记的网格下标集合(仅用于 posIndex 分配)
const occupiedGrid = new Set<number>(); const occupiedGrid = new Set<number>();
@@ -209,7 +218,7 @@ export class MissionHeroComp extends CCComp {
/** /**
* 生成一个英雄: * 生成一个英雄:
* - 按攻击类型落排基准点(近战前排 / 中距中排 / 远程后排),不做横向偏移。 * - 按职业定位落排基准点(坦克/战士前排、刺客/射手中排、法师/辅助后排),不做横向偏移。
* - 计算出生点(空中)和落点(地面),播放掉落动画。 * - 计算出生点(空中)和落点(地面),播放掉落动画。
* - 落地后由 MoveSystem 按职业优先级从最右向左排队分开。 * - 落地后由 MoveSystem 按职业优先级从最右向左排队分开。
* *
@@ -221,13 +230,13 @@ export class MissionHeroComp extends CCComp {
private spawnHeroAt(uuid: number, hero_lv: number, card_lv: number) { private spawnHeroAt(uuid: number, hero_lv: number, card_lv: number) {
let hero = ecs.getEntity<Hero>(Hero); let hero = ecs.getEntity<Hero>(Hero);
let scale = 1 let scale = 1
const type = HeroInfo[uuid]?.type ?? HType.Mid; const role = HeroInfo[uuid]?.role ?? HRole.Support;
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; return !!m && !m.is_dead;
}); });
const slot = this.pickSpawnSlotForHero(type, heroes); const slot = this.pickSpawnSlotForHero(role, heroes);
const finalPosIndex = slot.posIndex; const finalPosIndex = slot.posIndex;
const landingPos = slot.landingPos; const landingPos = slot.landingPos;
@@ -271,7 +280,7 @@ export class MissionHeroComp extends CCComp {
/** /**
* 串行消费召唤队列: * 串行消费召唤队列:
* 使用 is_processing_queue 标志防止同帧多次调用。 * 使用 is_processing_queue 标志防止同帧多次调用。
* 逐个取出队列中的请求并处理 * 队列已按职业优先级排序,逐个取出并间隔登场,保证"依次上场"的节奏
*/ */
private async processSummonQueue() { private async processSummonQueue() {
if (this.is_processing_queue) return; if (this.is_processing_queue) return;
@@ -281,6 +290,10 @@ export class MissionHeroComp extends CCComp {
const payload = this.summon_queue.shift(); const payload = this.summon_queue.shift();
if (!payload) continue; if (!payload) continue;
this.spawnHeroAt(payload.uuid, payload.hero_lv, payload.card_lv); this.spawnHeroAt(payload.uuid, payload.hero_lv, payload.card_lv);
// 登场间隔:让掉落动画逐个播放,避免同帧堆叠
if (this.summon_queue.length > 0) {
await new Promise<void>(resolve => setTimeout(resolve, MissionHeroComp.SUMMON_INTERVAL * 1000));
}
} }
} finally { } finally {
this.is_processing_queue = false; this.is_processing_queue = false;