294 lines
11 KiB
TypeScript
294 lines
11 KiB
TypeScript
/**
|
||
* @file MissionHeroComp.ts
|
||
* @description 英雄召唤管理组件(逻辑层 + 视图层)
|
||
*
|
||
* 职责:
|
||
* 1. 处理 **英雄召唤**:接收 CallHero 事件 → 通过串行队列执行召唤。
|
||
* 2. 管理英雄的出生点和掉落动画。
|
||
*
|
||
* 关键设计:
|
||
* - summon_queue + processSummonQueue() 确保召唤请求 **串行处理**。
|
||
* - handleSingleSummon() 仅负责生成英雄;英雄升级由升级卡系统(MissionCardComp)独立处理。
|
||
*
|
||
* 历史:
|
||
* 旧版本曾包含"三合一合成 + 链式合成"机制,已移除。
|
||
* 英雄等级提升现在仅通过升级卡(SpecialUpgrade)实现。
|
||
*
|
||
* 依赖:
|
||
* - Hero(hero/Hero.ts)—— 英雄 ECS 实体类
|
||
* - HeroAttrsComp —— 英雄属性组件
|
||
* - HeroInfo / HeroPos / HType(heroSet)—— 英雄静态配置
|
||
* - FightSet —— 战斗常量
|
||
*/
|
||
import { _decorator, instantiate, Prefab, v3, Vec3, BoxCollider2D } 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, HeroPos, HType } 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";
|
||
import { HeroViewComp } from "../hero/HeroViewComp";
|
||
import { FieldSkillSet, FieldSkillType } from "../common/config/SkillSet";
|
||
import { MoveComp } from "../hero/MoveComp";
|
||
const { ccclass } = _decorator;
|
||
|
||
/**
|
||
* MissionHeroComp —— 英雄召唤管理器
|
||
*
|
||
* 管理英雄的召唤请求队列和出生动画。
|
||
* 英雄升级由 MissionCardComp 的升级卡系统统一处理。
|
||
*/
|
||
@ccclass('MissionHeroComp')
|
||
@ecs.register('MissionHeroComp', false)
|
||
export class MissionHeroComp extends CCComp {
|
||
// ======================== 常量 ========================
|
||
|
||
/** 硬编码的6个英雄占位点 */
|
||
public static readonly HERO_POSITIONS: Vec3[] = [
|
||
v3(-180, BoxSet.GAME_LINE, 0), // index 0 (node_index 1): Top Front 第二
|
||
v3(-80, BoxSet.GAME_LINE, 0), // index 1 (node_index 2): Mid Front 第一
|
||
v3(-280, BoxSet.GAME_LINE, 0), // index 2 (node_index 3): Bot Front 第三
|
||
// v3(-280, BoxSet.GAME_LINE + 100, 0), // index 3 (node_index 4): Top Back
|
||
// v3(-280, BoxSet.GAME_LINE, 0), // index 4 (node_index 5): Mid Back
|
||
// v3(-280, BoxSet.GAME_LINE - 100, 0), // index 5 (node_index 6): Bot Back
|
||
];
|
||
|
||
/** 英雄出生时的掉落高度(从空中落到地面的像素差) */
|
||
private static readonly HERO_DROP_HEIGHT = 260
|
||
|
||
// ======================== 运行时属性 ========================
|
||
|
||
/** 预留计时器 */
|
||
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; pool_lv: number }[]=[]
|
||
/** 预留英雄列表 */
|
||
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();
|
||
}
|
||
}
|
||
|
||
/** 战斗准备阶段:重置出战英雄计数,恢复满血重新登场 */
|
||
fight_ready(){
|
||
const heroes = this.getAllHeroes();
|
||
smc.vmdata.mission_data.hero_num = heroes.length;
|
||
for (let i = 0; i < heroes.length; i++) {
|
||
const hero = heroes[i];
|
||
const model = hero.get(HeroAttrsComp);
|
||
const view = hero.get(HeroViewComp);
|
||
if (model && view) {
|
||
if (model.is_dead) {
|
||
view.alive();
|
||
const posIndex = this.pickPositionIndexForHero([hero.eid]);
|
||
const landingPos = MissionHeroComp.HERO_POSITIONS[posIndex];
|
||
// 计算出生点(空中)
|
||
const spawnPos: Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0);
|
||
view.node.setPosition(spawnPos);
|
||
model.posIndex = posIndex;
|
||
if (posIndex >= 0) smc.mission.heroGrid[posIndex] = hero.eid;
|
||
hero.playDropAnim(spawnPos, landingPos.y);
|
||
}
|
||
model.dirty_hp = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 预留:召唤事件扩展入口 */
|
||
private zhao_huan(event: string, args: any){
|
||
|
||
}
|
||
|
||
/**
|
||
* 召唤请求入口:
|
||
* 从事件参数中提取 uuid / hero_lv / pool_lv,放入串行队列。
|
||
* 防御:已召唤的英雄 uuid 拒绝重复召唤(只能通过升级卡升级)。
|
||
*
|
||
* @param event 事件名
|
||
* @param args { uuid, hero_lv, pool_lv }
|
||
*/
|
||
private async call_hero(event: string, args: any){
|
||
const payload = args ?? event;
|
||
const uuid = Number(payload?.uuid ?? 1001);
|
||
const hero_lv = Math.max(1, Number(payload?.hero_lv ?? 1));
|
||
const pool_lv = Math.max(1, Number(payload?.pool_lv ?? 1));
|
||
|
||
// 防御:场上已有同 uuid 的存活英雄时,拒绝重复召唤
|
||
if (this.isHeroAlreadySummoned(uuid)) {
|
||
oops.gui.toast(`该英雄已召唤,只能通过升级卡升级`);
|
||
return;
|
||
}
|
||
|
||
this.summon_queue.push({ uuid, hero_lv, pool_lv });
|
||
this.processSummonQueue();
|
||
}
|
||
|
||
/**
|
||
* 检查场上是否已存在指定 uuid 的存活英雄。
|
||
* @param uuid 英雄模板 uuid
|
||
* @returns true 表示场上已有该英雄,不可重复召唤
|
||
*/
|
||
private isHeroAlreadySummoned(uuid: number): boolean {
|
||
let exists = false;
|
||
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
|
||
if (exists) return;
|
||
const model = entity.get(HeroAttrsComp);
|
||
if (!model || model.fac !== FacSet.HERO || model.is_dead) return;
|
||
if (model.hero_uuid === uuid) exists = true;
|
||
});
|
||
return exists;
|
||
}
|
||
|
||
// ======================== 英雄生成 ========================
|
||
|
||
/**
|
||
* 动态分配英雄上场的位置
|
||
* @param excludeEids 排除计算的实体ID数组(避免复活时把自己算成占据的位置)
|
||
*/
|
||
private pickPositionIndexForHero(excludeEids: number[] = []): number {
|
||
const heroes = this.getAllHeroes().filter(h => {
|
||
const m = h.get(HeroAttrsComp);
|
||
return m && !m.is_dead && !excludeEids.includes(h.eid);
|
||
});
|
||
|
||
const occupied = new Set<number>();
|
||
for (const h of heroes) {
|
||
const m = h.get(HeroAttrsComp);
|
||
if (m && m.posIndex >= 0) occupied.add(m.posIndex);
|
||
}
|
||
|
||
// 优先中前(1) -> 上前(0) -> 下前(2) -> 中后(4) -> 上后(3) -> 下后(5)
|
||
const slotPriority = [1, 0, 2, 4, 3, 5];
|
||
for (const idx of slotPriority) {
|
||
if (!occupied.has(idx)) {
|
||
return idx;
|
||
}
|
||
}
|
||
|
||
// 溢出:默认中前
|
||
return 1;
|
||
}
|
||
|
||
/**
|
||
* 生成一个英雄 ECS 实体:
|
||
* - 计算出生点(空中)和落点(地面)。
|
||
* - 调用 hero.load() 初始化并播放掉落动画。
|
||
*
|
||
* @param uuid 英雄 UUID
|
||
* @param hero_lv 英雄等级
|
||
* @param pool_lv 卡池等级(历史遗留,新机制下不再使用)
|
||
* @returns 创建的 Hero 实体
|
||
*/
|
||
private addHero(uuid:number=1001,hero_lv:number=1, pool_lv:number=1) {
|
||
let hero = ecs.getEntity<Hero>(Hero);
|
||
let scale = 1
|
||
const posIndex = this.pickPositionIndexForHero();
|
||
const landingPos = MissionHeroComp.HERO_POSITIONS[posIndex];
|
||
let spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0);
|
||
hero.load(spawnPos,scale,uuid,landingPos.y,hero_lv,pool_lv,posIndex);
|
||
|
||
// 召唤完成后,派发事件以更新英雄面板
|
||
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;
|
||
await this.handleSingleSummon(payload.uuid, payload.hero_lv, payload.pool_lv);
|
||
}
|
||
} finally {
|
||
this.is_processing_queue = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理单次召唤:仅生成英雄,不再触发合成。
|
||
*
|
||
* @param uuid 英雄 UUID
|
||
* @param hero_lv 英雄等级
|
||
* @param pool_lv 卡池等级(历史遗留)
|
||
*/
|
||
private async handleSingleSummon(uuid: number, hero_lv: number, pool_lv: number = 1) {
|
||
this.addHero(uuid, hero_lv, pool_lv);
|
||
}
|
||
|
||
|
||
/** ECS 组件移除时触发(当前不销毁节点,保留引用) */
|
||
reset() {
|
||
// this.node.destroy();
|
||
}
|
||
}
|