Files
pixelheros/assets/script/game/map/MissionHeroComp.ts
walkpan 81a07bc16c feat: 新增英雄召唤事件并优化UI布局与组件注释
- 在 MissionHeroComp 中召唤英雄后派发 MasterCalled 事件,以更新英雄信息面板
- 调整 hnode.prefab 中多个节点的位置和尺寸,优化界面布局
- 为多个 TypeScript 组件文件添加详细注释,说明职责、关键设计和依赖关系
- 在 MissionCardComp 中完善英雄信息面板的创建、排序和布局逻辑
2026-04-07 19:52:40 +08:00

466 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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 控制合成上限等级。
*
* 依赖:
* - Herohero/Hero.ts—— 英雄 ECS 实体类
* - HeroAttrsComp —— 英雄属性组件
* - HeroInfo / HeroPos / HTypeheroSet—— 英雄静态配置
* - 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>(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<void> {
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<void> {
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<number> {
// 聚合属性
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();
}
}