feat: 新增英雄召唤事件并优化UI布局与组件注释

- 在 MissionHeroComp 中召唤英雄后派发 MasterCalled 事件,以更新英雄信息面板
- 调整 hnode.prefab 中多个节点的位置和尺寸,优化界面布局
- 为多个 TypeScript 组件文件添加详细注释,说明职责、关键设计和依赖关系
- 在 MissionCardComp 中完善英雄信息面板的创建、排序和布局逻辑
This commit is contained in:
walkpan
2026-04-07 19:52:40 +08:00
parent e880613f8f
commit 81a07bc16c
12 changed files with 2952 additions and 2639 deletions

View File

@@ -61,60 +61,102 @@ const { ccclass, property } = _decorator;
@ccclass('MissionCardComp')
@ecs.register('MissionCard', false)
export class MissionCardComp extends CCComp {
/** 是否启用调试日志 */
private debugMode: boolean = true;
/** 卡牌槽位宽度(像素),用于水平等距布局 */
private readonly cardWidth: number = 175;
/** 按钮正常缩放 */
private readonly buttonNormalScale: number = 1;
/** 按钮按下缩放 */
private readonly buttonPressScale: number = 0.94;
/** 按钮弹起缩放(峰值) */
private readonly buttonClickScale: number = 1.06;
/** 抽卡(刷新)费用 */
refreshCost: number = 1;
/** 卡牌面板展开/收起动画时长(秒) */
cardsPanelMoveDuration: number = 0.2;
/** 四个插卡槽位固定顺序分发1~4 */
// ======================== 编辑器绑定节点 ========================
/** 卡牌面板根节点(战斗阶段收起,准备阶段展开) */
@property(Node)
cards_node:Node = null!
/** 卡牌槽位 1 节点 */
@property(Node)
card1:Node = null!
/** 卡牌槽位 2 节点 */
@property(Node)
card2:Node = null!
/** 卡牌槽位 3 节点 */
@property(Node)
card3:Node = null!
/** 卡牌槽位 4 节点 */
@property(Node)
card4:Node = null!
/** 抽卡(刷新)按钮节点 */
@property(Node)
cards_chou:Node = null!
/** 卡池升级按钮节点 */
@property(Node)
cards_up:Node = null!
/** 金币显示节点(含 icon + num 子节点) */
@property(Node)
coins_node:Node = null!
/** 卡池等级显示节点 */
@property(Node)
pool_lv_node:Node = null!
/** 场上英雄信息面板容器节点HInfoComp 实例的父节点) */
@property(Node)
hero_info_node:Node = null! //场上英雄信息面板所在节点
hero_info_node:Node = null!
/** 英雄信息面板预制体(每个英雄上场时实例化一份) */
@property(Prefab)
hero_info_prefab:Prefab=null! //场上英雄信息面板Prefab
hero_info_prefab:Prefab=null!
/** 英雄数量显示节点(含 icon + num 子节点) */
@property(Node)
hero_num_node:Node=null!
// ======================== 运行时状态 ========================
/** 预留图集缓存(后续接入按钮/卡面图标时复用) */
private uiconsAtlas: SpriteAtlas | null = null;
/** 四个槽位对应的单卡控制器缓存 */
/** 四个槽位对应的 CardComp 控制器缓存(有序数组) */
private cardComps: CardComp[] = [];
/** 当前卡池等级(仅影响抽卡来源,不直接改卡槽现有内容) */
private poolLv: number = CARD_POOL_INIT_LEVEL;
private readonly heroInfoItemGap: number = 110;
/** 英雄信息面板项间距(像素) */
private readonly heroInfoItemGap: number = 130;
/** 英雄信息面板项间额外间距(像素) */
private readonly heroInfoItemSpacing: number = 5;
/** 英雄信息面板同步计时器(降频刷新用) */
private heroInfoSyncTimer: number = 0;
/** 是否已缓存卡牌面板基准缩放 */
private hasCachedCardsBaseScale: boolean = false;
/** 卡牌面板基准缩放(从场景读取) */
private cardsBaseScale: Vec3 = new Vec3(1, 1, 1);
/** 卡牌面板展开态缩放 */
private cardsShowScale: Vec3 = new Vec3(1, 1, 1);
/** 卡牌面板收起态缩放scale=0 隐藏) */
private cardsHideScale: Vec3 = new Vec3(0, 0, 1);
/**
* 英雄信息面板映射EID → { node, model, comp }
* 用于追踪每个出战英雄的面板实例和数据引用
*/
private heroInfoItems: Map<number, {
node: Node,
model: HeroAttrsComp,
comp: HInfoComp
}> = new Map();
// ======================== 生命周期 ========================
/**
* 组件加载:
* 1. 绑定生命周期事件和按钮交互事件。
* 2. 缓存 4 个 CardComp 子控制器引用。
* 3. 计算并设置槽位水平布局。
* 4. 初始化卡牌面板缩放参数。
* 5. 触发首次任务开始流程。
*/
onLoad() {
/** 绑定事件 -> 缓存子控制器 -> 初始化UI状态 */
this.bindEvents();
this.cacheCardComps();
this.layoutCardSlots();
@@ -126,15 +168,26 @@ export class MissionCardComp extends CCComp {
});
}
/** 组件销毁时解绑所有事件并清理英雄信息面板 */
onDestroy() {
this.unbindEvents();
this.clearHeroInfoPanels();
}
/** 外部初始化入口(由 CardController 调用) */
init(){
this.onMissionStart();
}
/** 任务开始时重置卡池等级、清空4槽、显示面板 刷新一次卡池*/
/**
* 任务开始:
* 1. 进入准备阶段(展开卡牌面板)。
* 2. 重置卡池等级为初始值。
* 3. 初始化局内数据(金币、英雄数量上限)。
* 4. 清空旧英雄信息面板和卡牌槽位。
* 5. 重置按钮状态和 UI 显示。
* 6. 执行首次抽卡并分发到 4 个槽位。
*/
onMissionStart() {
this.enterPreparePhase();
this.poolLv = CARD_POOL_INIT_LEVEL;
@@ -163,15 +216,20 @@ export class MissionCardComp extends CCComp {
});
}
/** 任务结束:清空4槽并隐藏面板 */
/** 任务结束:清空 4 槽 + 英雄面板并隐藏整个节点 */
onMissionEnd() {
this.clearAllCards();
this.clearHeroInfoPanels();
this.node.active = false;
}
start() {
}
/**
* 帧更新:每 0.15 秒刷新一次场上英雄信息面板(降频)。
* 检测已死亡 / 已失效的面板并移除,刷新存活面板属性。
*/
update(dt: number) {
this.heroInfoSyncTimer += dt;
if (this.heroInfoSyncTimer < 0.15) return;
@@ -186,20 +244,28 @@ export class MissionCardComp extends CCComp {
this.node.active = false;
}
/** 只处理UI层事件不做卡牌效果分发 */
// ======================== 事件绑定 ========================
/**
* 绑定所有事件监听:
* - 节点级事件MissionStart / MissionEnd / NewWave / FightStart
* - 全局消息CoinAdd / MasterCalled / HeroDead / UseHeroCard / UseSpecialCard
* - 按钮触控抽卡cards_chou、升级cards_up
*/
private bindEvents() {
/** 生命周期事件 */
/** 生命周期事件(节点级) */
this.on(GameEvent.MissionStart, this.onMissionStart, this);
this.on(GameEvent.MissionEnd, this.onMissionEnd, this);
this.on(GameEvent.NewWave, this.onNewWave, this);
this.on(GameEvent.FightStart, this.onFightStart, this);
/** 全局消息事件 */
oops.message.on(GameEvent.CoinAdd, this.onCoinAdd, this);
oops.message.on(GameEvent.MasterCalled, this.onMasterCalled, this);
oops.message.on(GameEvent.HeroDead, this.onHeroDead, this);
oops.message.on(GameEvent.UseHeroCard, this.onUseHeroCard, this);
oops.message.on(GameEvent.UseSpecialCard, this.onUseSpecialCard, this);
/** 按钮事件:抽卡与卡池升级 */
/** 按钮触控事件:抽卡与卡池升级 */
this.cards_chou?.on(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
this.cards_chou?.on(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
this.cards_chou?.on(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this);
@@ -207,6 +273,13 @@ export class MissionCardComp extends CCComp {
this.cards_up?.on(NodeEventType.TOUCH_END, this.onUpgradeTouchEnd, this);
this.cards_up?.on(NodeEventType.TOUCH_CANCEL, this.onUpgradeTouchCancel, this);
}
// ======================== 事件回调 ========================
/**
* 金币变化事件回调:
* - syncOnly=true仅同步 UI 显示(金币已被外部修改过)。
* - 否则:累加 delta 到 mission_data.coin 后刷新 UI。
*/
private onCoinAdd(event: string, args: any){
const payload = args ?? event;
if (payload?.syncOnly) {
@@ -221,10 +294,12 @@ export class MissionCardComp extends CCComp {
this.playCoinChangeAnim(v > 0);
}
/** 战斗开始:收起卡牌面板 */
private onFightStart() {
this.enterBattlePhase();
}
/** 新一波:展开面板 → 刷新费用 UI → 重新抽卡分发 */
private onNewWave() {
this.enterPreparePhase();
this.updateCoinAndCostUI();
@@ -248,10 +323,17 @@ export class MissionCardComp extends CCComp {
this.cards_up?.off(NodeEventType.TOUCH_CANCEL, this.onUpgradeTouchCancel, this);
}
/**
* 英雄上场事件回调MasterCalled
* 为新上场英雄创建或更新信息面板,并刷新英雄数量 UI。
*/
private onMasterCalled(event: string, args: any) {
const payload = args ?? event;
const eid = Number(payload?.eid ?? 0);
const model = payload?.model as HeroAttrsComp | undefined;
mLogger.log(this.debugMode, "MissionCardComp", "onMasterCalled received payload:", { eid, hasModel: !!model });
if (!eid || !model) return;
const before = this.getAliveHeroCount();
this.ensureHeroInfoPanel(eid, model);
@@ -259,11 +341,18 @@ export class MissionCardComp extends CCComp {
this.updateHeroNumUI(true, after > before);
}
/** 英雄死亡事件回调:刷新面板列表并更新英雄数量 UI */
private onHeroDead() {
this.refreshHeroInfoPanels();
this.updateHeroNumUI(true, false);
}
/**
* 使用英雄卡的 guard 校验(由 CardComp 通过 UseHeroCard 事件调用):
* - 当前英雄数 < 上限 → 允许使用。
* - 已满但新卡可触发合成(腾位) → 允许使用。
* - 已满且不可合成 → 阻止使用cancel=true弹 toast。
*/
private onUseHeroCard(event: string, args: any) {
const payload = args ?? event;
if (!payload) return;
@@ -274,6 +363,7 @@ export class MissionCardComp extends CCComp {
const heroUuid = Number(payload?.uuid ?? 0);
const heroLv = Math.max(1, Math.floor(Number(payload?.hero_lv ?? 1)));
const cardLv = Math.max(1, Math.floor(Number(payload?.pool_lv ?? 1)));
// 检查是否可以通过合成腾出位置
if (this.canUseHeroCardByMerge(heroUuid, heroLv)) {
payload.cancel = false;
payload.reason = "";
@@ -286,6 +376,10 @@ export class MissionCardComp extends CCComp {
}
}
/**
* 判断新召唤的英雄是否能通过合成腾位:
* 场上同 UUID 同等级数量 + 1新卡自身>= 合成所需数量 → 可以合成。
*/
private canUseHeroCardByMerge(heroUuid: number, heroLv: number): boolean {
if (!heroUuid) return false;
const mergeRule = this.getMergeRule();
@@ -294,6 +388,7 @@ export class MissionCardComp extends CCComp {
return sameCount + 1 >= mergeRule.needCount;
}
/** 统计场上同 UUID 同等级的存活英雄数量 */
private countAliveHeroesByUuidAndLv(heroUuid: number, heroLv: number): number {
let count = 0;
const actors = this.queryAliveHeroActors();
@@ -307,6 +402,11 @@ export class MissionCardComp extends CCComp {
return count;
}
/**
* 从 MissionHeroCompComp 实时读取合成规则。
* 通过 ECS 查询获取,避免硬编码与 MissionHeroComp 不一致。
* @returns { needCount: 合成所需数量, maxLv: 最大合成等级 }
*/
private getMergeRule(): { needCount: number, maxLv: number } {
let needCount = 3;
let maxLv = 2; // 兜底值改为2与 MissionHeroComp 保持一致
@@ -319,6 +419,11 @@ export class MissionCardComp extends CCComp {
return { needCount, maxLv };
}
/**
* 使用特殊卡事件回调:
* - SpecialUpgrade随机选一个指定等级的英雄升级到目标等级。
* - SpecialRefresh按英雄类型 / 指定等级重新抽取英雄卡。
*/
private onUseSpecialCard(event: string, args: any) {
const payload = args ?? event;
const uuid = Number(payload?.uuid ?? 0);
@@ -343,26 +448,29 @@ export class MissionCardComp extends CCComp {
});
}
// ======================== 按钮触控回调 ========================
/** 抽卡按钮按下反馈 */
private onDrawTouchStart() {
this.playButtonPressAnim(this.cards_chou);
}
/** 抽卡按钮释放 → 执行抽卡逻辑 */
private onDrawTouchEnd() {
this.playButtonClickAnim(this.cards_chou, () => this.onClickDraw());
}
/** 抽卡按钮取消 → 恢复缩放 */
private onDrawTouchCancel() {
this.playButtonResetAnim(this.cards_chou);
}
/** 升级按钮按下反馈 */
private onUpgradeTouchStart() {
this.playButtonPressAnim(this.cards_up);
}
/** 升级按钮释放 → 执行升级逻辑 */
private onUpgradeTouchEnd() {
this.playButtonClickAnim(this.cards_up, () => this.onClickUpgrade());
}
/** 升级按钮取消 → 恢复缩放 */
private onUpgradeTouchCancel() {
this.playButtonResetAnim(this.cards_up);
}
@@ -375,7 +483,14 @@ export class MissionCardComp extends CCComp {
.filter((comp): comp is CardComp => !!comp);
}
/** 抽卡按钮每次固定抽4张然后顺序分发给4个单卡脚本 */
// ======================== 核心业务:抽卡 & 升级 ========================
/**
* 抽卡按钮核心逻辑:
* 1. 检查金币是否足够 → 不够则 toast 提示。
* 2. 扣除费用、播放金币动画。
* 3. 重新布局槽位 → 从卡池构建 4 张卡 → 分发到槽位。
*/
private onClickDraw() {
const cost = this.getRefreshCost();
const currentCoin = this.getMissionCoin();
@@ -430,6 +545,9 @@ export class MissionCardComp extends CCComp {
});
}
// ======================== 阶段切换 ========================
/** 缓存卡牌面板的基准缩放值(从场景初始状态读取,仅缓存一次) */
private initCardsPanelPos() {
if (!this.cards_node || !this.cards_node.isValid) return;
if (!this.hasCachedCardsBaseScale) {
@@ -441,6 +559,7 @@ export class MissionCardComp extends CCComp {
this.cardsHideScale = new Vec3(0, 0, this.cardsBaseScale.z);
}
/** 进入准备阶段:展开卡牌面板(立即恢复缩放,无动画) */
private enterPreparePhase() {
if (!this.cards_node || !this.cards_node.isValid) return;
this.initCardsPanelPos();
@@ -637,7 +756,10 @@ export class MissionCardComp extends CCComp {
}
private ensureHeroInfoPanel(eid: number, model: HeroAttrsComp) {
if (!this.hero_info_node || !this.hero_info_prefab) return;
if (!this.hero_info_node || !this.hero_info_prefab) {
mLogger.error(this.debugMode, "MissionCardComp", "ensureHeroInfoPanel: missing hero_info_node or hero_info_prefab");
return;
}
this.hero_info_node.active = true;
const current = this.heroInfoItems.get(eid);
if (current) {
@@ -646,14 +768,24 @@ export class MissionCardComp extends CCComp {
this.updateHeroInfoPanel(current);
return;
}
mLogger.log(this.debugMode, "MissionCardComp", "ensureHeroInfoPanel: creating new panel for eid", eid);
const node = instantiate(this.hero_info_prefab);
node.parent = this.hero_info_node;
node.active = true;
const comp = node.getComponent(HInfoComp);
// 尝试两种方式获取组件,并输出日志
let comp = node.getComponent(HInfoComp) as any;
if (!comp) {
comp = node.getComponent("HInfoComp") as any;
}
if (!comp) {
mLogger.error(this.debugMode, "MissionCardComp", "ensureHeroInfoPanel: Failed to get HInfoComp from prefab!");
node.destroy();
return;
}
const item = {
node,
model,
@@ -663,6 +795,8 @@ export class MissionCardComp extends CCComp {
this.heroInfoItems.set(eid, item);
this.relayoutHeroInfoPanels();
this.updateHeroInfoPanel(item);
mLogger.log(this.debugMode, "MissionCardComp", `ensureHeroInfoPanel: new panel created for eid ${eid}, final position:`, item.node.position);
}
private refreshHeroInfoPanels() {
@@ -707,21 +841,31 @@ export class MissionCardComp extends CCComp {
const bView = bEnt?.get(HeroViewComp);
const aMove = aEnt?.get(MoveComp);
const bMove = bEnt?.get(MoveComp);
// 排序逻辑:
// 1. 如果有 x 坐标,按照 x 坐标从大到小(在前面)排序
// 2. 如果没有 x 坐标,默认退化到按照生成顺序或 eid 排序
const aFrontScore = aView?.node?.position?.x ?? -999999;
const bFrontScore = bView?.node?.position?.x ?? -999999;
if (aFrontScore !== bFrontScore) return aFrontScore - bFrontScore;
if (aFrontScore !== bFrontScore) return bFrontScore - aFrontScore;
const aSpawnOrder = aMove?.spawnOrder ?? 0;
const bSpawnOrder = bMove?.spawnOrder ?? 0;
if (aSpawnOrder !== bSpawnOrder) return aSpawnOrder - bSpawnOrder;
const aEid = aEnt?.eid ?? 0;
const bEid = bEnt?.eid ?? 0;
return aEid - bEid;
});
for (let index = 0; index < sortedItems.length; index++) {
const item = sortedItems[index];
if (!item.node || !item.node.isValid) continue;
// 使用 Widget 布局的话需要禁用它或者单纯调整位置
// 这里我们使用绝对坐标进行排列,假设是从左到右
const targetX = index * (this.heroInfoItemGap + this.heroInfoItemSpacing);
const pos = item.node.position;
item.node.setPosition(index * (this.heroInfoItemGap + this.heroInfoItemSpacing), pos.y, pos.z);
item.node.setPosition(targetX, pos.y, pos.z);
item.node.setSiblingIndex(index);
}
}