308 lines
13 KiB
TypeScript
308 lines
13 KiB
TypeScript
/**
|
||
* @file MissionHeroComp.ts
|
||
* @description 英雄召唤管理组件(逻辑层 + 视图层)
|
||
*
|
||
* 职责:
|
||
* 1. 处理 **英雄召唤**:接收 CallHero 事件 → 通过串行队列执行召唤。
|
||
* 2. 管理英雄的出生点和掉落动画。
|
||
*
|
||
* 关键设计:
|
||
* - summon_queue + processSummonQueue() 确保召唤请求 **串行处理**,
|
||
* 且队列按职业优先级(坦克→战士→刺客→射手→法师→辅助)排序后依次登场。
|
||
* - spawnHeroAt 负责生成英雄;允许同 uuid 重复召唤,多个相同英雄可同时站场。
|
||
* - 英雄可通过升级卡(SpecialUpgrade)升级。
|
||
*
|
||
* 死亡机制:
|
||
* 英雄为真实死亡(HeroAtkSystem.doDead → 快照至 smc.mission.dead_heroes → 实体销毁),
|
||
* 本组件不做回合自动复活;复活由后续复活机制读取 dead_heroes 重新召唤。
|
||
* revive 系技能为"濒死回血"(血量归零时消耗次数回血续命),不触发真死。
|
||
*
|
||
* 出生点规则:
|
||
* 按职业定位(HRole)落排:坦克/战士 → 前排(x=-200,最靠右迎敌),
|
||
* 刺客/射手 → 中排(x=-260),法师/辅助 → 后排(x=-320,最靠左)。
|
||
* 同类型落点不再做 ± 偏移展开,落点重叠由 MoveSystem 的编队排队解决
|
||
* (准备阶段按职业优先级 坦克→战士→刺客→射手→法师→辅助 从最右向左排开)。
|
||
* posIndex 取 heroGrid 全局空闲下标(0~9)用于网格登记/索敌,与落点解耦。
|
||
*
|
||
* 依赖:
|
||
* - Hero(hero/Hero.ts)—— 英雄 ECS 实体类
|
||
* - HeroAttrsComp —— 英雄属性组件
|
||
* - HeroInfo / HType(heroSet)—— 英雄静态配置
|
||
* - FightSet —— 战斗常量
|
||
*/
|
||
import { _decorator, v3, Vec3 } from "cc";
|
||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||
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, HRole } from "../common/config/heroSet";
|
||
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
|
||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||
import { FacSet, BoxSet, FightSet } from "../common/config/GameSet";
|
||
import { MoveComp } from "../hero/MoveComp";
|
||
const { ccclass } = _decorator;
|
||
|
||
/**
|
||
* MissionHeroComp —— 英雄召唤管理器
|
||
*
|
||
* 管理英雄的召唤请求队列和出生动画。
|
||
* 英雄升级由 MissionCardComp 的升级卡系统统一处理。
|
||
*/
|
||
@ccclass('MissionHeroComp')
|
||
@ecs.register('MissionHeroComp', false)
|
||
export class MissionHeroComp extends CCComp {
|
||
// ======================== 常量 ========================
|
||
|
||
/**
|
||
* 三排出生基准点(y 均为 BoxSet.GAME_LINE,x 取自 FightSet):
|
||
* - 前排 x=FightSet.HERO_ROW_FRONT_X(最靠右迎敌):坦克 / 战士
|
||
* - 中排 x=FightSet.HERO_ROW_MID_X:刺客 / 射手
|
||
* - 后排 x=FightSet.HERO_ROW_BACK_X(最靠左):法师 / 辅助
|
||
*/
|
||
public static readonly HERO_ROW_FRONT = v3(FightSet.HERO_ROW_FRONT_X, 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(FightSet.HERO_ROW_BACK_X, BoxSet.GAME_LINE, 0)
|
||
|
||
/**
|
||
* 职业定位 → 出生排映射:
|
||
* 坦克/战士 → 前排,刺客/射手 → 中排,法师/辅助 → 后排。
|
||
*/
|
||
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 HERO_DROP_HEIGHT = 260
|
||
/** 召唤登场间隔(秒):保证"依次上场"节奏,避免同帧堆叠掉落 */
|
||
private static readonly SUMMON_INTERVAL = 0.35
|
||
|
||
// ======================== 运行时属性 ========================
|
||
|
||
/** 预留计时器 */
|
||
timer: Timer = new Timer(2)
|
||
/** 预留状态:友方是否全部死亡 */
|
||
Friend_is_dead: boolean = false
|
||
/** 当前处理的英雄 uuid */
|
||
current_hero_uuid: number = 0
|
||
/** 当前英雄数量缓存 */
|
||
current_hero_num: number = -1
|
||
/** 是否正在消费召唤队列(防止并发) */
|
||
is_processing_queue: boolean = false
|
||
/** 召唤请求队列:按职业优先级排序后串行登场 */
|
||
summon_queue: { uuid: number; hero_lv: number; card_lv: number; role: HRole }[] = []
|
||
/** 英雄出生序计数器(编队排队/渲染排序的稳定决胜) */
|
||
private heroSpawnOrder = 0
|
||
/** 预留英雄列表 */
|
||
heros: any = []
|
||
|
||
// ======================== 生命周期 ========================
|
||
|
||
onLoad() {
|
||
// 注册节点级事件
|
||
this.on(GameEvent.FightReady, this.fight_ready, this)
|
||
this.on(GameEvent.Zhaohuan, this.zhao_huan, this)
|
||
this.on(GameEvent.MissionEnd, this.clear_heros, this)
|
||
// 注册全局消息
|
||
oops.message.on(GameEvent.CallHero, this.call_hero, this)
|
||
oops.message.on("PhasePrepareStart", this.fight_ready, this)
|
||
}
|
||
|
||
onDestroy() {
|
||
super.onDestroy();
|
||
// 清理全部监听
|
||
oops.message.off(GameEvent.CallHero, this.call_hero, this)
|
||
oops.message.off("PhasePrepareStart", this.fight_ready, this)
|
||
}
|
||
|
||
start() {
|
||
}
|
||
|
||
// ======================== 事件处理 ========================
|
||
|
||
/** 关卡结束时清理全部存活英雄 ECS 实体,并清空本局死亡英雄记录 */
|
||
clear_heros() {
|
||
const heroes = this.getAllHeroes();
|
||
for (let i = 0; i < heroes.length; i++) {
|
||
heroes[i].destroy();
|
||
}
|
||
// 整局结束,死亡记录失效(复活机制仅限局内)
|
||
smc.mission.dead_heroes = [];
|
||
}
|
||
|
||
/**
|
||
* 战斗准备阶段:刷新存活英雄计数与血条。
|
||
*
|
||
* 说明:英雄为真实死亡(实体已销毁,快照存于 smc.mission.dead_heroes),
|
||
* 本阶段不再做回合自动复活;复活由后续复活机制(重新召唤)显式触发。
|
||
*/
|
||
fight_ready() {
|
||
const heroes = this.getAllHeroes();
|
||
smc.vmdata.mission_data.hero_num = heroes.length;
|
||
for (let i = 0; i < heroes.length; i++) {
|
||
const model = heroes[i].get(HeroAttrsComp);
|
||
if (model) {
|
||
model.dirty_hp = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 预留:召唤事件扩展入口 */
|
||
private zhao_huan(event: string, args: any) {
|
||
|
||
}
|
||
|
||
/**
|
||
* 召唤请求入口:
|
||
* 从事件参数中提取 uuid / hero_lv / card_lv,放入串行队列。
|
||
* 队列按职业优先级(坦克→战士→刺客→射手→法师→辅助,HRole 升序)排序,
|
||
* 保证高优先级职业先登场。
|
||
*
|
||
* @param event 事件名
|
||
* @param args { uuid, hero_lv, card_lv }
|
||
*/
|
||
private async call_hero(event: string, args: any) {
|
||
const payload = args ?? event;
|
||
const uuid = Number(payload?.uuid ?? 1001);
|
||
// 局内登场等级统一以局外成长等级为准(升级后登场即为升级后的等级)
|
||
// payload.hero_lv 保留作为下限兜底,防止外部显式指定更高等级
|
||
const outer_lv = smc.getHeroOuterLv(uuid);
|
||
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 role = HeroInfo[uuid]?.role ?? HRole.Support;
|
||
|
||
this.summon_queue.push({ uuid, hero_lv, card_lv, role });
|
||
// 按职业优先级稳定排序:HRole 升序,同职业保持入队先后
|
||
this.summon_queue.sort((a, b) => a.role - b.role);
|
||
this.processSummonQueue();
|
||
}
|
||
|
||
// ======================== 英雄生成 ========================
|
||
|
||
/**
|
||
* 为英雄分配出生槽位:
|
||
* 按职业定位取三排基准点(坦克/战士前排、刺客/射手中排、法师/辅助后排),同类型不再做横向偏移展开;
|
||
* 落点重叠由 MoveSystem 的编队排队(按职业优先级从最右向左排开)负责分开。
|
||
*
|
||
* 槽位编码:posIndex 直接取 heroGrid 的全局空闲下标(0~9),与落点无映射。
|
||
*
|
||
* @param role 英雄职业定位(HRole,决定所属排)
|
||
* @param heroes 当前存活英雄列表(spawnHeroAt 在实体创建前快照,避免复用池残留实体干扰网格判定)
|
||
* @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;
|
||
|
||
// 已登记的网格下标集合(仅用于 posIndex 分配)
|
||
const occupiedGrid = new Set<number>();
|
||
for (const h of heroes) {
|
||
const m = h.get(HeroAttrsComp)!;
|
||
if (m.posIndex >= 0) occupiedGrid.add(m.posIndex);
|
||
}
|
||
|
||
const landingPos = v3(basePos.x, 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 };
|
||
}
|
||
|
||
/**
|
||
* 生成一个英雄:
|
||
* - 按职业定位落排基准点(坦克/战士前排、刺客/射手中排、法师/辅助后排),不做横向偏移。
|
||
* - 计算出生点(空中)和落点(地面),播放掉落动画。
|
||
* - 落地后由 MoveSystem 按职业优先级从最右向左排队分开。
|
||
*
|
||
* @param uuid 英雄 UUID
|
||
* @param hero_lv 英雄等级
|
||
* @param card_lv 卡牌等级(驱动 bg_node 颜色)
|
||
* @returns 创建的 Hero 实体
|
||
*/
|
||
private spawnHeroAt(uuid: number, hero_lv: number, card_lv: number) {
|
||
let hero = ecs.getEntity<Hero>(Hero);
|
||
let scale = 1
|
||
const role = HeroInfo[uuid]?.role ?? HRole.Support;
|
||
|
||
const heroes = this.getAllHeroes().filter(h => {
|
||
const m = h.get(HeroAttrsComp);
|
||
return !!m && !m.is_dead;
|
||
});
|
||
const slot = this.pickSpawnSlotForHero(role, heroes);
|
||
const finalPosIndex = slot.posIndex;
|
||
const 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);
|
||
|
||
// 登记出生序,供编队排队/渲染排序做稳定决胜
|
||
const move = hero.get(MoveComp);
|
||
if (move) {
|
||
move.spawnOrder = ++this.heroSpawnOrder;
|
||
}
|
||
|
||
// 召唤完成后,派发事件以更新英雄面板
|
||
const model = hero.get(HeroAttrsComp);
|
||
if (model) {
|
||
oops.message.dispatchEvent(GameEvent.MasterCalled, {
|
||
eid: hero.eid,
|
||
model: model
|
||
});
|
||
}
|
||
|
||
return hero;
|
||
}
|
||
|
||
// ======================== 英雄查询 ========================
|
||
|
||
/** 获取当前全部友方英雄 ECS 实体列表(包括存活和墓地) */
|
||
private getAllHeroes(): Hero[] {
|
||
const heroes: Hero[] = [];
|
||
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
|
||
const model = entity.get(HeroAttrsComp);
|
||
if (!model) return;
|
||
if (model.fac !== FacSet.HERO) return;
|
||
heroes.push(entity as Hero);
|
||
});
|
||
return heroes;
|
||
}
|
||
|
||
// ======================== 召唤队列 ========================
|
||
|
||
/**
|
||
* 串行消费召唤队列:
|
||
* 使用 is_processing_queue 标志防止同帧多次调用。
|
||
* 队列已按职业优先级排序,逐个取出并间隔登场,保证"依次上场"的节奏。
|
||
*/
|
||
private async processSummonQueue() {
|
||
if (this.is_processing_queue) return;
|
||
this.is_processing_queue = true;
|
||
try {
|
||
while (this.summon_queue.length > 0) {
|
||
const payload = this.summon_queue.shift();
|
||
if (!payload) continue;
|
||
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 {
|
||
this.is_processing_queue = false;
|
||
}
|
||
}
|
||
|
||
/** ECS 组件移除时触发(当前不销毁节点,保留引用) */
|
||
reset() {
|
||
// this.node.destroy();
|
||
}
|
||
}
|