Files
pixelheros/assets/script/game/map/MissionHeroComp.ts
panFD 6c82c632b6 feat: 重构英雄编队逻辑,优化出生与排队规则
1. 移除英雄出生落点偏移,改为按职业优先级从右向左排队
2. 重构英雄卡点击逻辑,支持点击整张卡牌选取
3. 新增英雄血量背景预制体与UI按钮样式
4. 优化AOE技能目标选取逻辑,使用中排基准点
2026-08-19 22:52:17 +08:00

295 lines
12 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最靠左
* 同类型落点不再做 ± 偏移展开,落点重叠由 MoveSystem 的编队排队解决
* (准备阶段按职业优先级 坦克→战士→刺客→射手→法师→辅助 从最右向左排开)。
* posIndex 取 heroGrid 全局空闲下标0~9用于网格登记/索敌,与落点解耦。
*
* 依赖:
* - 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 { MoveComp } from "../hero/MoveComp";
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)
/**
* 攻击类型 → 出生排映射:
* 近战 → 前排,中距 → 中排(优先中间位),远程 → 后排。
*/
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 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 }[] = []
/** 英雄出生序计数器(编队排队/渲染排序的稳定决胜) */
private heroSpawnOrder = 0
/** 预留英雄列表 */
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();
}
// ======================== 英雄生成 ========================
/**
* 为英雄分配出生槽位:
* 按攻击类型取三排基准点(近战前排、中距优先中排、远程后排),同类型不再做横向偏移展开;
* 落点重叠由 MoveSystem 的编队排队(按职业优先级从最右向左排开)负责分开。
*
* 槽位编码posIndex 直接取 heroGrid 的全局空闲下标0~9与落点无映射。
*
* @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;
// 已登记的网格下标集合(仅用于 posIndex 分配)
const occupiedGrid = new Set<number>();
for (const h of heroes) {
const m = h.get(HeroAttrsComp)!;
if (m.posIndex >= 0) occupiedGrid.add(m.posIndex);
}
const landingPos = v3(basePos.x, 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 };
}
/**
* 生成一个英雄:
* - 按攻击类型落排基准点(近战前排 / 中距中排 / 远程后排),不做横向偏移。
* - 计算出生点(空中)和落点(地面),播放掉落动画。
* - 落地后由 MoveSystem 按职业优先级从最右向左排队分开。
*
* @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 move = hero.get(MoveComp);
if (move) {
move.spawnOrder = ++this.heroSpawnOrder;
}
// 召唤完成后,派发事件以更新英雄面板
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();
}
}