Files
pixelheros/assets/script/game/map/MissionHeroComp.ts
pan 2498eb598c refactor(mission): 移除英雄二合一合成逻辑,改为允许同英雄同场
1.  删除Hero类中的mergeToBirthAndDestroy方法
2.  移除MissionHeroComp中的合成相关校验与处理逻辑
3.  简化英雄召唤流程,不再限制同uuid英雄重复召唤
4.  更新注释文档,调整相关导入依赖
2026-08-18 09:51:19 +08:00

314 lines
13 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. 管理英雄的出生点和掉落动画。
*
* 关键设计:
* - summon_queue + processSummonQueue() 确保召唤请求 **串行处理**。
* - spawnHeroAt 负责生成英雄;允许同 uuid 重复召唤,多个相同英雄可同时站场。
* - 英雄可通过升级卡SpecialUpgrade升级。
*
* 死亡机制:
* 英雄为真实死亡HeroAtkSystem.doDead → 快照至 smc.mission.dead_heroes → 实体销毁),
* 本组件不做回合自动复活;复活由后续复活机制读取 dead_heroes 重新召唤。
* revive 系技能为"濒死回血"(血量归零时消耗次数回血续命),不触发真死。
*
* 出生点规则:
* 按攻击类型HType分三排近战 → 前排x=-200最靠右迎敌
* 中距 → 中排x=-260远程 → 后排x=-320最靠左
* 同一类型重复召唤时,从该类型基准点向两侧 ±SLOT_OFFSET 依次交替展开,避免落点重叠。
* posIndex 取 heroGrid 全局空闲下标0~9用于网格登记/索敌,落点偏移与 posIndex、职业均解耦。
*
* 依赖:
* - Herohero/Hero.ts—— 英雄 ECS 实体类
* - HeroAttrsComp —— 英雄属性组件
* - HeroInfo / HTypeheroSet—— 英雄静态配置
* - 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, HType } from "../common/config/heroSet";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { FacSet, BoxSet } from "../common/config/GameSet";
import { HeroViewComp } from "../hero/HeroViewComp";
const { ccclass } = _decorator;
/**
* MissionHeroComp —— 英雄召唤管理器
*
* 管理英雄的召唤请求队列和出生动画。
* 英雄升级由 MissionCardComp 的升级卡系统统一处理。
*/
@ccclass('MissionHeroComp')
@ecs.register('MissionHeroComp', false)
export class MissionHeroComp extends CCComp {
// ======================== 常量 ========================
/**
* 三排出生基准点y 均为 BoxSet.GAME_LINE
* - 前排 x=-200最靠右迎敌战士 / 坦克
* - 中排 x=-260射手 / 刺客
* - 后排 x=-320最靠左辅助 / 法师
*/
public static readonly HERO_ROW_FRONT = v3(-200, BoxSet.GAME_LINE, 0)
public static readonly HERO_ROW_MID = v3(-260, BoxSet.GAME_LINE, 0)
public static readonly HERO_ROW_BACK = v3(-320, BoxSet.GAME_LINE, 0)
/**
* 英雄登场占位点(兼容旧引用,等价于后/中/前三排基准点):
* - index 0=后排、1=中排、2=前排。
*/
public static readonly HERO_POSITIONS: Vec3[] = [
MissionHeroComp.HERO_ROW_BACK,
MissionHeroComp.HERO_ROW_MID,
MissionHeroComp.HERO_ROW_FRONT,
];
/**
* 攻击类型 → 出生排映射:
* 近战 → 前排,中距 → 中排,远程 → 后排。
*/
private static readonly TYPE_ROW: Record<HType, Vec3> = {
[HType.Melee]: MissionHeroComp.HERO_ROW_FRONT,
[HType.Mid]: MissionHeroComp.HERO_ROW_MID,
[HType.Long]: MissionHeroComp.HERO_ROW_BACK,
};
/** 同类型重复召唤时的落点横向间隔(像素) */
private static readonly SLOT_OFFSET = 30
/** 英雄出生时的掉落高度(从空中落到地面的像素差) */
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);
// 局内登场等级统一以局外成长等级为准(升级后登场即为升级后的等级)
// 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));
this.summon_queue.push({ uuid, hero_lv, card_lv });
this.processSummonQueue();
}
// ======================== 英雄生成 ========================
/**
* 为英雄分配出生槽位:
* 按攻击类型取三排基准点(近战前排、中距中排、远程后排),
* 同类型第 1 个落基准点,之后按 ±SLOT_OFFSET 交替展开0, +30, -30, +60, -60 …)。
*
* 槽位编码posIndex 直接取 heroGrid 的全局空闲下标0~9与落点无映射。
* 落点偏移由「同攻击类型已占位英雄的 x 偏移」推展开序计算,与 posIndex、职业均解耦。
*
* @param type 英雄攻击类型HType决定所属排
* @param heroes 当前存活英雄列表spawnHeroAt 在实体创建前快照,避免复用池残留实体干扰槽位判定)
* @returns { posIndex heroGrid 空闲下标(-1 表示满员未登记), landingPos 落地点 }
*/
private pickSpawnSlotForHero(type: HType, heroes: Hero[]): { posIndex: number; landingPos: Vec3 } {
const basePos = MissionHeroComp.TYPE_ROW[type] ?? MissionHeroComp.HERO_ROW_MID;
// 同攻击类型已占位的 x 偏移集合(依据落点 x 反推,与 posIndex 解耦)
const usedOffsets = new Set<number>();
const occupiedGrid = new Set<number>();
for (const h of heroes) {
const m = h.get(HeroAttrsComp)!;
if (m.posIndex >= 0) occupiedGrid.add(m.posIndex);
if (m.type !== type) continue;
const view = h.get(HeroViewComp);
if (view?.node?.isValid) {
usedOffsets.add(Math.round(view.node.position.x - basePos.x));
}
}
// 展开序 n0=居中1=+302=-303=+60 …,找到第一个未被同类型占用的偏移
// 上限取 heroGrid 容量,同类型超过该数量时复用基准点(不再向外展开)
let offsetX = 0;
for (let n = 0; n < smc.mission.heroGrid.length; n++) {
const step = Math.ceil(n / 2);
const dir = n % 2 === 1 ? 1 : -1;
const candidate = n === 0 ? 0 : dir * step * MissionHeroComp.SLOT_OFFSET;
if (!usedOffsets.has(candidate)) { offsetX = candidate; break; }
}
const landingPos = v3(basePos.x + offsetX, 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 };
}
/**
* 生成一个英雄:
* - 按攻击类型自动分配三排槽位并展开落点。
* - 计算出生点(空中)和落点(地面),播放掉落动画。
*
* @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 type = HeroInfo[uuid]?.type ?? HType.Mid;
const heroes = this.getAllHeroes().filter(h => {
const m = h.get(HeroAttrsComp);
return !!m && !m.is_dead;
});
const slot = this.pickSpawnSlotForHero(type, 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 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);
}
} finally {
this.is_processing_queue = false;
}
}
/** ECS 组件移除时触发(当前不销毁节点,保留引用) */
reset() {
// this.node.destroy();
}
}