refactor(mission): 重构英雄出生槽位分配逻辑
1. 解耦落点偏移与posIndex,改用全局网格下标作为posIndex 2. 调整同职业横向间隔为30像素,修正槽位编码溢出问题 3. 移除废弃的slotIndexToLandingPos方法与调试日志 4. 支持合成继承原英雄位置,避免阵型跳动
This commit is contained in:
@@ -23,9 +23,8 @@
|
||||
* 出生点规则:
|
||||
* 按职业(HRole)分三排:战士/坦克 → 前排(x=-200,最靠右迎敌),
|
||||
* 射手/刺客 → 中排(x=-260),辅助/法师 → 后排(x=-320,最靠左)。
|
||||
* 同一职业重复召唤时,从该职业基准点向两侧 ±20 依次交替展开,避免落点重叠。
|
||||
* posIndex 语义:role * 10 + n(n 为同职业展开序,0=居中,1=+20,2=-20,3=+40…),
|
||||
* 同步登记 heroGrid 供 SCastSystem 网格索敌使用。
|
||||
* 同一职业重复召唤时,从该职业基准点向两侧 ±SLOT_OFFSET 依次交替展开,避免落点重叠。
|
||||
* posIndex 取 heroGrid 全局空闲下标(0~9)用于网格登记/索敌,落点偏移与 posIndex 解耦。
|
||||
*
|
||||
* 依赖:
|
||||
* - Hero(hero/Hero.ts)—— 英雄 ECS 实体类
|
||||
@@ -94,9 +93,7 @@ export class MissionHeroComp extends CCComp {
|
||||
};
|
||||
|
||||
/** 同职业重复召唤时的落点横向间隔(像素) */
|
||||
private static readonly SLOT_OFFSET = 20
|
||||
/** 同职业槽位步长:posIndex = role * 10 + n */
|
||||
private static readonly SLOT_STRIDE = 10
|
||||
private static readonly SLOT_OFFSET = 30
|
||||
|
||||
/** 英雄出生时的掉落高度(从空中落到地面的像素差) */
|
||||
private static readonly HERO_DROP_HEIGHT = 260
|
||||
@@ -236,39 +233,49 @@ export class MissionHeroComp extends CCComp {
|
||||
/**
|
||||
* 为英雄分配出生槽位:
|
||||
* 按职业取三排基准点(战士/坦克前排、射手/刺客中排、辅助/法师后排),
|
||||
* 同职业已存在时在基准点 ±20 的倍数次交替展开(0, +20, -20, +40, -40 …)。
|
||||
* 同职业第 1 个落基准点,之后按 ±SLOT_OFFSET 交替展开(0, +20, -20, +40, -40 …)。
|
||||
*
|
||||
* 槽位编码:posIndex 直接取 heroGrid 的全局空闲下标(0~9),不做 role 分段,
|
||||
* 避免 role*10 编码超出 heroGrid 容量(长度 10)导致非坦克职业全部越界兜底。
|
||||
* 落点偏移由「同职业已占位英雄数」推展开序 n 计算,与 posIndex 解耦。
|
||||
*
|
||||
* @param role 英雄职业(决定所属排)
|
||||
* @param heroes 当前存活英雄列表(spawnHeroAt 在实体创建前快照,避免复用池残留实体干扰槽位判定)
|
||||
* @returns { posIndex 槽位索引(role*10+n,-1 表示分配失败), landingPos 落地点 }
|
||||
* @returns { posIndex heroGrid 空闲下标(-1 表示满员未登记), landingPos 落地点 }
|
||||
*/
|
||||
private pickSpawnSlotForHero(role: HRole, heroes: Hero[]): { posIndex: number; landingPos: Vec3 } {
|
||||
const basePos = MissionHeroComp.ROLE_ROW[role] ?? MissionHeroComp.HERO_ROW_MID;
|
||||
|
||||
// 只统计同职业英雄的展开序 n(posIndex % 10),避免不同职业间槽位互相挤占
|
||||
const usedSlots = new Set<number>();
|
||||
// 同职业已占位的展开序集合(依据落点 x 反推,与 posIndex 解耦)
|
||||
const usedOffsets = new Set<number>();
|
||||
const occupiedGrid = new Set<number>();
|
||||
for (const h of heroes) {
|
||||
const m = h.get(HeroAttrsComp)!;
|
||||
if (m.role !== role || m.posIndex < 0) continue;
|
||||
usedSlots.add(m.posIndex % MissionHeroComp.SLOT_STRIDE);
|
||||
}
|
||||
// DEBUG: 输出槽位判定过程,验证同职业展开是否生效(排查后删除)
|
||||
console.log(`[SpawnSlot] role=${role} heroes=${heroes.length} used=[${[...usedSlots].join(",")}]`);
|
||||
|
||||
// 展开序 n:0=居中,1=+20,2=-20,3=+40 …,越界按无槽位落基准点(不写网格)
|
||||
for (let n = 0; n < MissionHeroComp.SLOT_STRIDE; n++) {
|
||||
const posIndex = role * MissionHeroComp.SLOT_STRIDE + n;
|
||||
if (posIndex >= smc.mission.heroGrid.length) break;
|
||||
if (!usedSlots.has(n)) {
|
||||
const lp = this.slotIndexToLandingPos(posIndex);
|
||||
console.log(`[SpawnSlot] → posIndex=${posIndex} landing=(${lp.x},${lp.y})`);
|
||||
return { posIndex, landingPos: lp };
|
||||
if (m.posIndex >= 0) occupiedGrid.add(m.posIndex);
|
||||
if (m.role !== role) continue;
|
||||
const view = h.get(HeroViewComp);
|
||||
if (view?.node?.isValid) {
|
||||
usedOffsets.add(Math.round(view.node.position.x - basePos.x));
|
||||
}
|
||||
}
|
||||
|
||||
// 同职业槽位占满时溢出兜底:落在该职业基准点,posIndex=-1 表示未登记网格,
|
||||
// 避免与友方技能遍历 heroGrid 时产生重复目标
|
||||
return { posIndex: -1, landingPos: v3(basePos.x, basePos.y, 0) };
|
||||
// 展开序 n:0=居中,1=+20,2=-20,3=+40 …,找到第一个未被同职业占用的偏移
|
||||
// 上限取 heroGrid 容量,同职业超过该数量时复用基准点(不再向外展开)
|
||||
let offsetX = 0;
|
||||
for (let n = 0; n < smc.mission.heroGrid.length; n++) {
|
||||
const step = Math.ceil(n / 2);
|
||||
const dir = n % 2 === 1 ? 1 : -1;
|
||||
const candidate = n === 0 ? 0 : dir * step * MissionHeroComp.SLOT_OFFSET;
|
||||
if (!usedOffsets.has(candidate)) { offsetX = candidate; break; }
|
||||
}
|
||||
const landingPos = v3(basePos.x + offsetX, basePos.y, 0);
|
||||
|
||||
// posIndex 取 heroGrid 全局空闲下标,仅用于网格登记/索敌,与落点无映射
|
||||
let posIndex = -1;
|
||||
for (let i = 0; i < smc.mission.heroGrid.length; i++) {
|
||||
if (smc.mission.heroGrid[i] < 0 && !occupiedGrid.has(i)) { posIndex = i; break; }
|
||||
}
|
||||
return { posIndex, landingPos };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -296,16 +303,16 @@ export class MissionHeroComp extends CCComp {
|
||||
* @param posIndex 指定站位索引,-1 表示自动分配
|
||||
* @returns 创建的 Hero 实体
|
||||
*/
|
||||
private spawnHeroAt(uuid: number, hero_lv: number, card_lv: number, posIndex: number) {
|
||||
private spawnHeroAt(uuid: number, hero_lv: number, card_lv: number, posIndex: number, inheritPos: Vec3 | null = null) {
|
||||
let hero = ecs.getEntity<Hero>(Hero);
|
||||
let scale = 1
|
||||
const role = HeroInfo[uuid]?.role ?? HRole.Warrior;
|
||||
|
||||
let finalPosIndex = posIndex;
|
||||
let landingPos: Vec3;
|
||||
if (posIndex >= 0) {
|
||||
// 指定槽位:按 role*10+n 反解排与偏移,落点与自动分配保持一致
|
||||
landingPos = this.slotIndexToLandingPos(posIndex);
|
||||
if (inheritPos) {
|
||||
// 合成继承:沿用旧英雄节点位置,posIndex 透传登记网格
|
||||
landingPos = v3(inheritPos.x, MissionHeroComp.ROLE_ROW[role]?.y ?? BoxSet.GAME_LINE, 0);
|
||||
} else {
|
||||
const heroes = this.getAllHeroes().filter(h => {
|
||||
const m = h.get(HeroAttrsComp);
|
||||
@@ -318,12 +325,6 @@ export class MissionHeroComp extends CCComp {
|
||||
|
||||
let spawnPos: Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0);
|
||||
hero.load(spawnPos, scale, uuid, landingPos.y, hero_lv, card_lv, finalPosIndex);
|
||||
// DEBUG: 验证实体创建后写入的实际槽位与节点位置(排查后删除)
|
||||
{
|
||||
const m = hero.get(HeroAttrsComp);
|
||||
const v = hero.get(HeroViewComp);
|
||||
console.log(`[SpawnSlot] spawned eid=${hero.eid} uuid=${uuid} posIndex=${m?.posIndex} node.x=${v?.node?.position.x}`);
|
||||
}
|
||||
|
||||
// 召唤完成后,派发事件以更新英雄面板
|
||||
const model = hero.get(HeroAttrsComp);
|
||||
@@ -413,32 +414,17 @@ export class MissionHeroComp extends CCComp {
|
||||
return new Promise<void>((resolve) => {
|
||||
// 以旧英雄节点位置作为合并目标点,保证动画收拢到原站位
|
||||
const oldView = oldHero.get(HeroViewComp);
|
||||
const birthPos = oldView?.node?.isValid
|
||||
? oldView.node.getPosition()
|
||||
: (posIndex >= 0 ? this.slotIndexToLandingPos(posIndex) : MissionHeroComp.HERO_ROW_MID);
|
||||
const oldPos = oldView?.node?.isValid ? oldView.node.getPosition() : null;
|
||||
const birthPos = oldPos ?? MissionHeroComp.HERO_ROW_MID;
|
||||
|
||||
oldHero.mergeToBirthAndDestroy(birthPos, () => {
|
||||
// 旧实体销毁后 posIndex 已释放,直接按原站位生成新英雄
|
||||
this.spawnHeroAt(uuid, mergedLv, card_lv, posIndex);
|
||||
// 旧实体销毁后沿用其站位与 posIndex 生成新英雄,避免阵型跳动
|
||||
this.spawnHeroAt(uuid, mergedLv, card_lv, posIndex, oldPos);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 槽位索引 → 落点坐标(role*10+n 反解,供合成动画目标点等兜底场景使用)。
|
||||
* @param posIndex 槽位索引,越界时回退到中排基准点
|
||||
*/
|
||||
private slotIndexToLandingPos(posIndex: number): Vec3 {
|
||||
const slotRole = Math.floor(posIndex / MissionHeroComp.SLOT_STRIDE) as HRole;
|
||||
const n = posIndex % MissionHeroComp.SLOT_STRIDE;
|
||||
const basePos = MissionHeroComp.ROLE_ROW[slotRole] ?? MissionHeroComp.HERO_ROW_MID;
|
||||
const step = Math.ceil(n / 2);
|
||||
const dir = n % 2 === 1 ? 1 : -1;
|
||||
const offsetX = n === 0 ? 0 : dir * step * MissionHeroComp.SLOT_OFFSET;
|
||||
return v3(basePos.x + offsetX, basePos.y, 0);
|
||||
}
|
||||
|
||||
|
||||
/** ECS 组件移除时触发(当前不销毁节点,保留引用) */
|
||||
reset() {
|
||||
|
||||
Reference in New Issue
Block a user