diff --git a/assets/script/game/map/MissionHeroComp.ts b/assets/script/game/map/MissionHeroComp.ts index 5c65ef1d..618304cd 100644 --- a/assets/script/game/map/MissionHeroComp.ts +++ b/assets/script/game/map/MissionHeroComp.ts @@ -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(); + // 同职业已占位的展开序集合(依据落点 x 反推,与 posIndex 解耦) + const usedOffsets = new Set(); + const occupiedGrid = new Set(); 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); 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((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() {