/** * @file MissionHeroComp.ts * @description 英雄召唤与合成管理组件(逻辑层 + 视图层) * * 职责: * 1. 处理 **英雄召唤**:接收 CallHero 事件 → 通过串行队列执行召唤。 * 2. 处理 **英雄合成**:检测同 UUID 同等级英雄是否达到合成条件 → * 执行合成动画 → 销毁素材 → 生成高一级英雄。 * 3. 支持 **链式合成**:合成完成后自动检测更高等级是否也满足合成条件。 * 4. 管理英雄的出生点和掉落动画。 * * 关键设计: * - summon_queue + processSummonQueue() 确保召唤请求 **串行处理**, * 避免同帧并发导致合成判断错误。 * - handleSingleSummon() 在每次召唤后检测是否触发合成。 * - mergeGroupHeroes() 执行完整合成流程: * 聚合属性 → 向出生点汇聚动画 → 爆点特效 → 生成高级英雄。 * - merge_need_count 控制合成所需数量(2 合 1 或 3 合 1)。 * - merge_max_lv 控制合成上限等级。 * * 依赖: * - Hero(hero/Hero.ts)—— 英雄 ECS 实体类 * - HeroAttrsComp —— 英雄属性组件 * - HeroInfo / HeroPos / HType(heroSet)—— 英雄静态配置 * - FightSet —— 战斗常量(MERGE_NEED / MERGE_MAX) * - oneCom —— 一次性特效组件(控制爆点特效生命周期) */ import { _decorator, instantiate, Prefab, 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, HeroPos, HType } from "../common/config/heroSet"; import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { FacSet, FightSet } from "../common/config/GameSet"; import { oneCom } from "../skill/oncend"; const { ccclass } = _decorator; /** * MissionHeroCompComp —— 英雄召唤与合成管理器 * * 管理英雄的召唤请求队列、出生动画和合成系统。 * 合成支持 2 合 1 或 3 合 1,且可链式合成至上限等级。 */ @ccclass('MissionHeroCompComp') @ecs.register('MissionHeroComp', false) export class MissionHeroCompComp extends CCComp { // ======================== 常量 ======================== /** 英雄出生时的掉落高度(从空中落到地面的像素差) */ private static readonly HERO_DROP_HEIGHT = 260 /** 近战英雄起始出生 X 坐标 */ private static readonly HERO_SPAWN_START_MELEE_X = -280 /** 远程(含中程)英雄起始出生 X 坐标 */ private static readonly HERO_SPAWN_START_RANGED_X = -280 // ======================== 运行时属性 ======================== /** 预留计时器 */ timer:Timer=new Timer(2) /** 预留状态:友方是否全部死亡 */ Friend_is_dead:boolean=false /** 当前处理的英雄 uuid */ current_hero_uuid:number=0 /** 当前英雄数量缓存 */ current_hero_num:number=-1 /** 合成规则:需要几个同级英雄才能合成(2 或 3) */ merge_need_count:number=FightSet.MERGE_NEED /** 允许合成的最高等级(合成产物不超过此等级) */ merge_max_lv:number=FightSet.MERGE_MAX /** 是否正在执行一次合成流程(防止并发) */ is_merging:boolean=false /** 是否正在消费召唤队列(防止并发) */ 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) } onDestroy(){ // 清理全部监听 oops.message.off(GameEvent.CallHero,this.call_hero,this) oops.message.off(GameEvent.FightReady,this.fight_ready,this) oops.message.off(GameEvent.Zhaohuan,this.zhao_huan,this) oops.message.off(GameEvent.MissionEnd,this.clear_heros,this) } start() { } // ======================== 事件处理 ======================== /** 关卡结束时清理全部存活英雄 ECS 实体 */ clear_heros(){ const heroes = this.getAliveHeroes(); for (let i = 0; i < heroes.length; i++) { heroes[i].destroy(); } } /** 战斗准备阶段:重置出战英雄计数 */ fight_ready(){ smc.vmdata.mission_data.hero_num=0 } /** 预留:召唤事件扩展入口 */ private zhao_huan(event: string, args: any){ } /** * 召唤请求入口: * 从事件参数中提取 uuid / hero_lv / pool_lv,放入串行队列。 * * @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)); this.summon_queue.push({ uuid, hero_lv, pool_lv }); this.processSummonQueue(); } // ======================== 英雄生成 ======================== /** * 生成一个英雄 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) { console.log("addHero uuid:",uuid) let hero = ecs.getEntity(Hero); let scale = 1 const landingPos = this.resolveHeroLandingPos(uuid); let spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroCompComp.HERO_DROP_HEIGHT, 0); hero.load(spawnPos,scale,uuid,landingPos.y,hero_lv,pool_lv); // 召唤完成后,派发事件以更新英雄面板 const model = hero.get(HeroAttrsComp); if (model) { oops.message.dispatchEvent(GameEvent.MasterCalled, { eid: hero.eid, model: model }); } return hero; } /** * 计算英雄落点位置。 * Y 坐标来自 HeroPos 配置,X 坐标根据英雄类型(近战/远程)决定。 * * @param uuid 英雄 UUID * @returns 落点 Vec3 */ private resolveHeroLandingPos(uuid: number): Vec3 { const hero_pos = 0; const baseY = HeroPos[hero_pos].pos.y; const startX = this.resolveSpawnStartX(uuid); return v3(startX, baseY, 0); } /** * 根据英雄类型决定出生 X 坐标。 * @param uuid 英雄 UUID * @returns 近战 or 远程的起始 X */ private resolveSpawnStartX(uuid: number): number { const heroType = HeroInfo[uuid]?.type; return heroType === HType.Melee ? MissionHeroCompComp.HERO_SPAWN_START_MELEE_X : MissionHeroCompComp.HERO_SPAWN_START_RANGED_X; } /** * 生成合成后的高级英雄,并覆盖为聚合后的属性。 * * @param uuid 英雄 UUID * @param hero_lv 合成后等级 * @param pool_lv 卡池等级 * @param ap 聚合后攻击力 * @param hp_max 聚合后最大生命值 * @returns 实际生成的英雄等级 */ private addMergedHero(uuid:number, hero_lv:number, pool_lv:number, ap:number, hp_max:number): number { const hero = this.addHero(uuid, hero_lv, pool_lv); const model = hero.get(HeroAttrsComp); if (!model) return hero_lv; model.ap = Math.max(0, ap); model.hp_max = Math.max(1, hp_max); model.hp = model.hp_max; model.dirty_hp = true; return model.lv; } // ======================== 英雄查询 ======================== /** 获取当前全部存活友方英雄 ECS 实体列表 */ private getAliveHeroes(): 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; if (model.is_dead) return; heroes.push(entity as Hero); }); return heroes; } /** * 从存活英雄中挑选可参与本次合成的英雄组。 * * @param aliveHeroes 存活英雄列表 * @param uuid 目标英雄 UUID * @param hero_lv 目标等级 * @param needCount 合成需要数量 * @returns 匹配的英雄数组(长度 = needCount 或不足) */ private pickMergeHeroes(aliveHeroes: Hero[], uuid: number, hero_lv: number, needCount: number = 3): Hero[] { const mergeHeroes: Hero[] = []; for (let i = 0; i < aliveHeroes.length; i++) { const model = aliveHeroes[i].get(HeroAttrsComp); if (!model) continue; if (model.hero_uuid !== uuid) continue; if (model.lv !== hero_lv) continue; mergeHeroes.push(aliveHeroes[i]); if (mergeHeroes.length === needCount) break; } return mergeHeroes; } /** 统计满足同 UUID 同等级的可合成英雄数量 */ private countMergeHeroes(aliveHeroes: Hero[], uuid: number, hero_lv: number): number { let count = 0; for (let i = 0; i < aliveHeroes.length; i++) { const model = aliveHeroes[i].get(HeroAttrsComp); if (!model) continue; if (model.hero_uuid !== uuid) continue; if (model.lv !== hero_lv) continue; count += 1; } return count; } // ======================== 合成规则 ======================== /** * 读取合成所需数量(仅支持 2 或 3)。 * 由 FightSet.MERGE_NEED 配置。 */ private getMergeNeedCount(): number { return this.merge_need_count === 2 ? 2 : 3; } /** * 判断该等级是否还能继续向上合成。 * @param hero_lv 当前等级 * @returns true = 可以合成(未达上限) */ private canMergeLevel(hero_lv: number): boolean { return hero_lv < Math.max(1, this.merge_max_lv); } // ======================== 召唤队列 ======================== /** * 串行消费召唤队列: * 使用 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; } } /** * 处理单次召唤: * 1. 生成英雄。 * 2. 检测是否满足合成条件。 * 3. 满足则执行合成 + 链式合成。 * * @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); if (!this.canMergeLevel(hero_lv)) return; const needCount = this.getMergeNeedCount(); const aliveHeroes = this.getAliveHeroes(); const mergeHeroes = this.pickMergeHeroes(aliveHeroes, uuid, hero_lv, needCount); if (mergeHeroes.length !== needCount) return; this.is_merging = true; try { const mergedLv = await this.mergeGroupHeroes(mergeHeroes, uuid, hero_lv, pool_lv); await this.tryChainMerge(uuid, mergedLv, pool_lv); } finally { this.is_merging = false; } } // ======================== 合成动画 ======================== /** * 将一组合成素材英雄向出生点汇聚并销毁。 * 所有素材动画完成后 Promise resolve。 * * @param mergeHeroes 合成素材英雄数组 * @param spawnPos 汇聚目标位置 */ private mergeDestroyAtBirth(mergeHeroes: Hero[], spawnPos: Vec3): Promise { return new Promise((resolve) => { let doneCount = 0; const total = mergeHeroes.length; if (total <= 0) { resolve(); return; } const onDone = () => { doneCount += 1; if (doneCount >= total) { resolve(); } }; for (let i = 0; i < mergeHeroes.length; i++) { mergeHeroes[i].mergeToBirthAndDestroy(spawnPos, onDone); } }); } /** * 播放合成爆点特效(使用 oneCom 控制生命周期)。 * 延迟 0.4 秒后 resolve。 * * @param worldPos 特效播放位置 */ private playMergeBoomFx(worldPos: Vec3): Promise { return new Promise((resolve) => { const scene = smc.map?.MapView?.scene; const layer = scene?.entityLayer?.node; if (!layer || !layer.isValid) { resolve(); return; } const prefab: Prefab = oops.res.get("game/skill/end/dead", Prefab)!; if (!prefab) { resolve(); return; } const fx = instantiate(prefab); if (!fx || !fx.isValid) { resolve(); return; } fx.parent = layer; fx.setPosition(worldPos); fx.getComponent(oneCom) || fx.addComponent(oneCom); this.scheduleOnce(() => resolve(), 0.4); }); } /** * 执行一次完整合成流程: * 1. 聚合素材的 AP 和 HP。 * 2. 将素材向出生点汇聚并销毁。 * 3. 播放爆点特效。 * 4. 生成高一级英雄(属性为聚合值)。 * * @param mergeHeroes 合成素材 * @param uuid 英雄 UUID * @param hero_lv 素材等级 * @param pool_lv 卡池等级 * @returns 合成产物的实际等级 */ private async mergeGroupHeroes(mergeHeroes: Hero[], uuid: number, hero_lv: number, pool_lv: number): Promise { // 聚合属性 let sumAp = 0; let sumHpMax = 0; for (let i = 0; i < mergeHeroes.length; i++) { const model = mergeHeroes[i].get(HeroAttrsComp); if (!model) continue; sumAp += model.ap; sumHpMax += model.hp_max; } // 计算出生点 const landingPos = this.resolveHeroLandingPos(uuid); const spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroCompComp.HERO_DROP_HEIGHT, 0); // 汇聚 → 特效 → 生成 await this.mergeDestroyAtBirth(mergeHeroes, spawnPos); await this.playMergeBoomFx(spawnPos); return this.addMergedHero(uuid, Math.min(this.merge_max_lv, hero_lv + 1), pool_lv, sumAp, sumHpMax); } /** * 链式合成:合成完成后继续检测更高等级是否也满足条件。 * 最多循环 20 次作为安全上限。 * * @param uuid 英雄 UUID * @param startLv 起始检测等级 * @param pool_lv 卡池等级 */ private async tryChainMerge(uuid: number, startLv: number, pool_lv: number) { let checkLv = Math.max(1, startLv); const needCount = this.getMergeNeedCount(); let guard = 0; while (guard < 20) { guard += 1; if (!this.canMergeLevel(checkLv)) { break; } const aliveHeroes = this.getAliveHeroes(); const sameCount = this.countMergeHeroes(aliveHeroes, uuid, checkLv); if (sameCount < needCount) { break; } const mergeHeroes = this.pickMergeHeroes(aliveHeroes, uuid, checkLv, needCount); if (mergeHeroes.length < needCount) { break; } checkLv = await this.mergeGroupHeroes(mergeHeroes, uuid, checkLv, pool_lv); } } /** ECS 组件移除时触发(当前不销毁节点,保留引用) */ reset() { // this.node.destroy(); } }