refactor(heroSpawn): 重构英雄出生点位分配逻辑
1. 新增按职业分三排出生规则:战士/坦克前排、射手/刺客中排、辅助/法师后排 2. 同职业重复召唤时按±20像素交替展开,避免落点重叠 3. 新增posIndex编码规则:role*10+n,同步登记heroGrid网格索引 4. 废弃旧的近战/远程分位逻辑,替换为职业映射规则 5. 修复旧版点位分配的槽位冲突问题,优化溢出兜底逻辑
This commit is contained in:
@@ -20,10 +20,17 @@
|
||||
* 本组件不做回合自动复活;复活由后续复活机制读取 dead_heroes 重新召唤。
|
||||
* revive 系技能为"濒死回血"(血量归零时消耗次数回血续命),不触发真死。
|
||||
*
|
||||
* 出生点规则:
|
||||
* 按职业(HRole)分三排:战士/坦克 → 前排(x=-200,最靠右迎敌),
|
||||
* 射手/刺客 → 中排(x=-260),辅助/法师 → 后排(x=-320,最靠左)。
|
||||
* 同一职业重复召唤时,从该职业基准点向两侧 ±20 依次交替展开,避免落点重叠。
|
||||
* posIndex 语义:role * 10 + n(n 为同职业展开序,0=居中,1=+20,2=-20,3=+40…),
|
||||
* 同步登记 heroGrid 供 SCastSystem 网格索敌使用。
|
||||
*
|
||||
* 依赖:
|
||||
* - Hero(hero/Hero.ts)—— 英雄 ECS 实体类
|
||||
* - HeroAttrsComp —— 英雄属性组件
|
||||
* - HeroInfo / HeroPos / HType(heroSet)—— 英雄静态配置
|
||||
* - HeroInfo / HeroPos / HType / HRole(heroSet)—— 英雄静态配置
|
||||
* - FightSet —— 战斗常量
|
||||
*/
|
||||
import { _decorator, instantiate, Prefab, v3, Vec3, BoxCollider2D } from "cc";
|
||||
@@ -33,7 +40,7 @@ import { Hero } from "../hero/Hero";
|
||||
import { smc } from "../common/SingletonModuleComp";
|
||||
import { Timer } from "db://oops-framework/core/common/timer/Timer";
|
||||
import { GameEvent } from "../common/config/GameEvent";
|
||||
import { HeroInfo, HeroPos, HType } from "../common/config/heroSet";
|
||||
import { HeroInfo, HRole } from "../common/config/heroSet";
|
||||
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
|
||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||
import { FacSet, FightSet, BoxSet } from "../common/config/GameSet";
|
||||
@@ -54,17 +61,43 @@ export class MissionHeroComp extends CCComp {
|
||||
// ======================== 常量 ========================
|
||||
|
||||
/**
|
||||
* 英雄登场占位点(固定 3 列):
|
||||
* - index 0/1/2 分别对应 x=-320/-260/-200。
|
||||
* - 分配规则:近战优先靠前(x 更大),远程/中程优先靠后(x 更小);
|
||||
* 同类型按召唤先后从该类型的一侧开始依次填充。
|
||||
* 三排出生基准点(y 均为 BoxSet.GAME_LINE):
|
||||
* - 前排 x=-200(最靠右迎敌):战士 / 坦克
|
||||
* - 中排 x=-260:射手 / 刺客
|
||||
* - 后排 x=-320(最靠左):辅助 / 法师
|
||||
*/
|
||||
public static readonly HERO_ROW_FRONT = v3(-200, BoxSet.GAME_LINE, 0)
|
||||
public static readonly HERO_ROW_MID = v3(-260, BoxSet.GAME_LINE, 0)
|
||||
public static readonly HERO_ROW_BACK = v3(-320, BoxSet.GAME_LINE, 0)
|
||||
|
||||
/**
|
||||
* 英雄登场占位点(兼容旧引用,等价于后/中/前三排基准点):
|
||||
* - index 0=后排、1=中排、2=前排。
|
||||
*/
|
||||
public static readonly HERO_POSITIONS: Vec3[] = [
|
||||
v3(-320, BoxSet.GAME_LINE, 0),
|
||||
v3(-260, BoxSet.GAME_LINE, 0),
|
||||
v3(-200, BoxSet.GAME_LINE, 0),
|
||||
MissionHeroComp.HERO_ROW_BACK,
|
||||
MissionHeroComp.HERO_ROW_MID,
|
||||
MissionHeroComp.HERO_ROW_FRONT,
|
||||
];
|
||||
|
||||
/**
|
||||
* 职业 → 出生排映射:
|
||||
* 战士/坦克 → 前排,射手/刺客 → 中排,辅助/法师 → 后排。
|
||||
*/
|
||||
private static readonly ROLE_ROW: Record<HRole, Vec3> = {
|
||||
[HRole.Tank]: MissionHeroComp.HERO_ROW_FRONT,
|
||||
[HRole.Warrior]: MissionHeroComp.HERO_ROW_FRONT,
|
||||
[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 SLOT_OFFSET = 20
|
||||
/** 同职业槽位步长:posIndex = role * 10 + n */
|
||||
private static readonly SLOT_STRIDE = 10
|
||||
|
||||
/** 英雄出生时的掉落高度(从空中落到地面的像素差) */
|
||||
private static readonly HERO_DROP_HEIGHT = 260
|
||||
|
||||
@@ -200,34 +233,42 @@ export class MissionHeroComp extends CCComp {
|
||||
|
||||
// ======================== 英雄生成 ========================
|
||||
|
||||
/**
|
||||
* 动态分配英雄上场的位置
|
||||
* @param excludeEids 排除计算的实体ID数组(避免复活时把自己算成占据的位置)
|
||||
/**
|
||||
* 为英雄分配出生槽位:
|
||||
* 按职业取三排基准点(战士/坦克前排、射手/刺客中排、辅助/法师后排),
|
||||
* 同职业已存在时在基准点 ±20 的倍数次交替展开(0, +20, -20, +40, -40 …)。
|
||||
*
|
||||
* @param role 英雄职业(决定所属排)
|
||||
* @param heroes 当前存活英雄列表(spawnHeroAt 在实体创建前快照,避免复用池残留实体干扰槽位判定)
|
||||
* @returns { posIndex 槽位索引(role*10+n,-1 表示分配失败), landingPos 落地点 }
|
||||
*/
|
||||
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);
|
||||
});
|
||||
private pickSpawnSlotForHero(role: HRole, heroes: Hero[]): { posIndex: number; landingPos: Vec3 } {
|
||||
const basePos = MissionHeroComp.ROLE_ROW[role] ?? MissionHeroComp.HERO_ROW_MID;
|
||||
|
||||
const occupied = new Set<number>();
|
||||
// 只统计同职业英雄的展开序 n(posIndex % 10),避免不同职业间槽位互相挤占
|
||||
const usedSlots = new Set<number>();
|
||||
for (const h of heroes) {
|
||||
const m = h.get(HeroAttrsComp);
|
||||
if (m && m.posIndex >= 0) occupied.add(m.posIndex);
|
||||
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(",")}]`);
|
||||
|
||||
// 近战优先靠前(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;
|
||||
// 展开序 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 };
|
||||
}
|
||||
}
|
||||
|
||||
// 溢出兜底:复用 index 0(坐标有效,避免越界崩溃)
|
||||
return 0;
|
||||
// 同职业槽位占满时溢出兜底:落在该职业基准点,posIndex=-1 表示未登记网格,
|
||||
// 避免与友方技能遍历 heroGrid 时产生重复目标
|
||||
return { posIndex: -1, landingPos: v3(basePos.x, basePos.y, 0) };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,7 +287,7 @@ export class MissionHeroComp extends CCComp {
|
||||
|
||||
/**
|
||||
* 在指定站位生成英雄:
|
||||
* - posIndex < 0 时自动分配空位。
|
||||
* - posIndex < 0 时按职业自动分配空位(三排基准点 ±20 展开)。
|
||||
* - 计算出生点(空中)和落点(地面),播放掉落动画。
|
||||
*
|
||||
* @param uuid 英雄 UUID
|
||||
@@ -258,11 +299,31 @@ 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([], HeroInfo[uuid]?.type ?? HType.Melee);
|
||||
// 兜底:索引越界或点位缺失时复用 index 0,保证 landingPos 一定有效
|
||||
const landingPos = MissionHeroComp.HERO_POSITIONS[finalPosIndex] ?? MissionHeroComp.HERO_POSITIONS[0];
|
||||
const role = HeroInfo[uuid]?.role ?? HRole.Warrior;
|
||||
|
||||
let finalPosIndex = posIndex;
|
||||
let landingPos: Vec3;
|
||||
if (posIndex >= 0) {
|
||||
// 指定槽位:按 role*10+n 反解排与偏移,落点与自动分配保持一致
|
||||
landingPos = this.slotIndexToLandingPos(posIndex);
|
||||
} else {
|
||||
const heroes = this.getAllHeroes().filter(h => {
|
||||
const m = h.get(HeroAttrsComp);
|
||||
return !!m && !m.is_dead;
|
||||
});
|
||||
const slot = this.pickSpawnSlotForHero(role, heroes);
|
||||
finalPosIndex = slot.posIndex;
|
||||
landingPos = slot.landingPos;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -354,7 +415,7 @@ export class MissionHeroComp extends CCComp {
|
||||
const oldView = oldHero.get(HeroViewComp);
|
||||
const birthPos = oldView?.node?.isValid
|
||||
? oldView.node.getPosition()
|
||||
: (posIndex >= 0 ? MissionHeroComp.HERO_POSITIONS[posIndex] : MissionHeroComp.HERO_POSITIONS[1]);
|
||||
: (posIndex >= 0 ? this.slotIndexToLandingPos(posIndex) : MissionHeroComp.HERO_ROW_MID);
|
||||
|
||||
oldHero.mergeToBirthAndDestroy(birthPos, () => {
|
||||
// 旧实体销毁后 posIndex 已释放,直接按原站位生成新英雄
|
||||
@@ -364,6 +425,20 @@ export class MissionHeroComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 槽位索引 → 落点坐标(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