1. 统一英雄真实死亡流程:死亡时快照数据到全局列表,动画结束后统一销毁实体 2. 移除回合自动复活逻辑,改为由复活系统主动读取快照重新召唤 3. 修复近战追敌可能脱离战场的问题,增加推进距离上限 4. 重构死亡计数与全员存活检测逻辑,基于死亡快照列表而非实体状态
365 lines
14 KiB
TypeScript
365 lines
14 KiB
TypeScript
/**
|
||
* @file MissionHeroComp.ts
|
||
* @description 英雄召唤管理组件(逻辑层 + 视图层)
|
||
*
|
||
* 职责:
|
||
* 1. 处理 **英雄召唤**:接收 CallHero 事件 → 通过串行队列执行召唤。
|
||
* 2. 管理英雄的出生点和掉落动画。
|
||
*
|
||
* 关键设计:
|
||
* - summon_queue + processSummonQueue() 确保召唤请求 **串行处理**。
|
||
* - handleSingleSummon() 负责生成英雄;同 uuid 重复召唤走二合一合成升级。
|
||
*
|
||
* 合成规则:
|
||
* 场上存在同 uuid 英雄时再次召唤触发二合一:
|
||
* 旧英雄移动合并销毁,新英雄以 **两者最高等级 + 1** 落地(封顶 HERO_MAX_LV),
|
||
* 等级已达上限时拒绝召唤。英雄也可通过升级卡(SpecialUpgrade)升级。
|
||
*
|
||
* 死亡机制:
|
||
* 英雄为真实死亡(HeroAtkSystem.doDead → 快照至 smc.mission.dead_heroes → 实体销毁),
|
||
* 本组件不做回合自动复活;复活由后续复活机制读取 dead_heroes 重新召唤。
|
||
* revive 系技能为"濒死回血"(血量归零时消耗次数回血续命),不触发真死。
|
||
*
|
||
* 依赖:
|
||
* - 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(-220, BoxSet.GAME_LINE, 0), // index 0 (node_index 1): Top Front 第二
|
||
v3(-140, BoxSet.GAME_LINE, 0), // index 1 (node_index 2): Mid Front 第一
|
||
v3(-300, 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; card_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();
|
||
}
|
||
// 整局结束,死亡记录失效(复活机制仅限局内)
|
||
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,放入串行队列。
|
||
* 二合一:同 uuid 已召唤且等级未达上限时允许入队,由 handleSingleSummon 执行合成;
|
||
* 同 uuid 且等级已达上限时拒绝(只能通过升级卡继续提升之外无法再合成)。
|
||
*
|
||
* @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);
|
||
const hero_lv = Math.max(1, Number(payload?.hero_lv ?? 1));
|
||
const card_lv = Math.max(1, Number(payload?.card_lv ?? 1));
|
||
|
||
// 二合一前置校验:同 uuid 已存在且合成后等级会超上限时,直接拒绝
|
||
if (this.isHeroAlreadySummoned(uuid)) {
|
||
const existing = this.findHeroByUuid(uuid);
|
||
const existingLv = existing ? (existing.get(HeroAttrsComp)?.lv ?? 1) : 1;
|
||
if (Math.max(existingLv, hero_lv) + 1 > FightSet.HERO_MAX_LV) {
|
||
oops.gui.toast(`该英雄已达等级上限,无法继续合成`);
|
||
return;
|
||
}
|
||
}
|
||
|
||
this.summon_queue.push({ uuid, hero_lv, card_lv });
|
||
this.processSummonQueue();
|
||
}
|
||
|
||
/**
|
||
* 检查场上是否已存在指定 uuid 的存活英雄。
|
||
* @param uuid 英雄模板 uuid
|
||
* @returns true 表示场上已有该英雄,不可重复召唤
|
||
*/
|
||
private isHeroAlreadySummoned(uuid: number): boolean {
|
||
return this.findHeroByUuid(uuid) !== null;
|
||
}
|
||
|
||
/**
|
||
* 查找场上指定 uuid 的存活英雄实体。
|
||
* @param uuid 英雄模板 uuid
|
||
* @returns 命中的 Hero 实体,未命中返回 null
|
||
*/
|
||
private findHeroByUuid(uuid: number): Hero | null {
|
||
let found: Hero | null = null;
|
||
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
|
||
if (found) return;
|
||
const model = entity.get(HeroAttrsComp);
|
||
if (!model || model.fac !== FacSet.HERO || model.is_dead) return;
|
||
if (model.hero_uuid === uuid) found = entity as Hero;
|
||
});
|
||
return found;
|
||
}
|
||
|
||
// ======================== 英雄生成 ========================
|
||
|
||
/**
|
||
* 动态分配英雄上场的位置
|
||
* @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 card_lv 卡牌等级(驱动 bg_node 颜色)
|
||
* @returns 创建的 Hero 实体
|
||
*/
|
||
private addHero(uuid: number = 1001, hero_lv: number = 1, card_lv: number = 1) {
|
||
return this.spawnHeroAt(uuid, hero_lv, card_lv, -1);
|
||
}
|
||
|
||
/**
|
||
* 在指定站位生成英雄:
|
||
* - posIndex < 0 时自动分配空位。
|
||
* - 计算出生点(空中)和落点(地面),播放掉落动画。
|
||
*
|
||
* @param uuid 英雄 UUID
|
||
* @param hero_lv 英雄等级
|
||
* @param card_lv 卡牌等级(驱动 bg_node 颜色)
|
||
* @param posIndex 指定站位索引,-1 表示自动分配
|
||
* @returns 创建的 Hero 实体
|
||
*/
|
||
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();
|
||
const landingPos = MissionHeroComp.HERO_POSITIONS[finalPosIndex];
|
||
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 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.card_lv);
|
||
}
|
||
} finally {
|
||
this.is_processing_queue = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理单次召唤:
|
||
* - 场上无同 uuid 英雄:直接生成新英雄。
|
||
* - 场上已有同 uuid 英雄:触发二合一,旧英雄合并销毁后生成等级 +1 的新英雄。
|
||
*
|
||
* @param uuid 英雄 UUID
|
||
* @param hero_lv 英雄等级
|
||
* @param card_lv 卡牌等级
|
||
*/
|
||
private async handleSingleSummon(uuid: number, hero_lv: number, card_lv: number = 1) {
|
||
const existing = this.findHeroByUuid(uuid);
|
||
if (!existing) {
|
||
this.addHero(uuid, hero_lv, card_lv);
|
||
return;
|
||
}
|
||
|
||
const oldModel = existing.get(HeroAttrsComp);
|
||
const oldLv = oldModel?.lv ?? 1;
|
||
// 合成后等级 = 两者最高等级 + 1,封顶 HERO_MAX_LV
|
||
const mergedLv = Math.min(FightSet.HERO_MAX_LV, Math.max(oldLv, hero_lv) + 1);
|
||
// 继承旧英雄站位,避免新英雄重新抢位导致阵型跳动
|
||
const posIndex = oldModel?.posIndex ?? -1;
|
||
|
||
await this.mergeHeroAndRespawn(existing, uuid, mergedLv, card_lv, posIndex);
|
||
}
|
||
|
||
/**
|
||
* 二合一合成流程:
|
||
* 1. 旧英雄播放移动合并动画并销毁(释放站位与实体)。
|
||
* 2. 在原站位生成 mergedLv 级新英雄并播放入场动画。
|
||
*
|
||
* @param oldHero 场上已有的同 uuid 英雄
|
||
* @param uuid 英雄 UUID
|
||
* @param mergedLv 合成后的英雄等级
|
||
* @param card_lv 卡牌等级
|
||
* @param posIndex 继承的站位索引(-1 表示重新分配)
|
||
*/
|
||
private mergeHeroAndRespawn(oldHero: Hero, uuid: number, mergedLv: number, card_lv: number, posIndex: number): Promise<void> {
|
||
return new Promise<void>((resolve) => {
|
||
// 以旧英雄节点位置作为合并目标点,保证动画收拢到原站位
|
||
const oldView = oldHero.get(HeroViewComp);
|
||
const birthPos = oldView?.node?.isValid
|
||
? oldView.node.getPosition()
|
||
: (posIndex >= 0 ? MissionHeroComp.HERO_POSITIONS[posIndex] : MissionHeroComp.HERO_POSITIONS[1]);
|
||
|
||
oldHero.mergeToBirthAndDestroy(birthPos, () => {
|
||
// 旧实体销毁后 posIndex 已释放,直接按原站位生成新英雄
|
||
this.spawnHeroAt(uuid, mergedLv, card_lv, posIndex);
|
||
resolve();
|
||
});
|
||
});
|
||
}
|
||
|
||
|
||
/** ECS 组件移除时触发(当前不销毁节点,保留引用) */
|
||
reset() {
|
||
// this.node.destroy();
|
||
}
|
||
}
|