feat(ui): 添加战场英雄信息面板并增强主角召唤事件

扩展主角召唤事件,传递更多实体信息供UI系统使用。新增HInfoComp组件作为英雄信息面板基础,并在MissionCardComp中动态生成和管理英雄信息面板,实时显示英雄属性。同时调整相关预制体引用和布局配置。
This commit is contained in:
panw
2026-03-25 17:23:22 +08:00
parent 338394f6ff
commit de90dadaed
6 changed files with 209 additions and 28 deletions

View File

@@ -0,0 +1,20 @@
import { mLogger } from "../common/Logger";
import { _decorator, Animation, AnimationClip, EventTouch, Label, Node, NodeEventType, Sprite, SpriteAtlas, Tween, tween, UIOpacity, Vec3, resources } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
const { ccclass, property } = _decorator;
/** 视图层对象 */
@ccclass('HInfoComp')
@ecs.register('HInfoComp', false)
export class HInfoComp extends CCComp {
private debugMode: boolean = true;
/** 锁定态图标节点(显示时表示本槽位锁定) */
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() {
this.node.destroy();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "a832f87d-c91f-4b24-ad64-47eb59d658a7",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -1,5 +1,5 @@
import { mLogger } from "../common/Logger";
import { _decorator, Label, Node, NodeEventType, Prefab, SpriteAtlas, Tween, tween, Vec3 } from "cc";
import { _decorator, instantiate, Label, Node, NodeEventType, Prefab, SpriteAtlas, Tween, tween, 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 { GameEvent } from "../common/config/GameEvent";
@@ -7,6 +7,7 @@ import { CARD_POOL_INIT_LEVEL, CARD_POOL_MAX_LEVEL, CardConfig, CardInitCoins, C
import { smc } from "../common/SingletonModuleComp";
import { CardComp } from "./CardComp";
import { oops } from "db://oops-framework/core/Oops";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
const { ccclass, property } = _decorator;
@@ -38,9 +39,9 @@ export class MissionCardComp extends CCComp {
@property(Node)
pool_lv_node:Node = null!
@property(Node)
hero_info_node:Node = null!
hero_info_node:Node = null! //场上英雄信息面板所在节点
@property(Prefab)
hero_info_prefab:Prefab=null!
hero_info_prefab:Prefab=null! //场上英雄信息面板Prefab
/** 预留图集缓存(后续接入按钮/卡面图标时复用) */
private uiconsAtlas: SpriteAtlas | null = null;
/** 四个槽位对应的单卡控制器缓存 */
@@ -48,6 +49,14 @@ export class MissionCardComp extends CCComp {
/** 当前卡池等级(仅影响抽卡来源,不直接改卡槽现有内容) */
private poolLv: number = CARD_POOL_INIT_LEVEL;
private coin: number = CardInitCoins;
private readonly heroInfoItemGap: number = 86;
private heroInfoSyncTimer: number = 0;
private heroInfoItems: Map<number, {
node: Node,
model: HeroAttrsComp,
apLabel: Label | null,
hpLabel: Label | null
}> = new Map();
onLoad() {
/** 绑定事件 -> 缓存子控制器 -> 初始化UI状态 */
this.bindEvents();
@@ -62,6 +71,7 @@ export class MissionCardComp extends CCComp {
onDestroy() {
this.unbindEvents();
this.clearHeroInfoPanels();
}
init(){
this.onMissionStart();
@@ -71,6 +81,7 @@ export class MissionCardComp extends CCComp {
onMissionStart() {
this.poolLv = CARD_POOL_INIT_LEVEL;
this.coin = CardInitCoins
this.clearHeroInfoPanels();
this.layoutCardSlots();
this.clearAllCards();
if (this.cards_up) {
@@ -91,12 +102,18 @@ export class MissionCardComp extends CCComp {
/** 任务结束时清空4槽并隐藏面板 */
onMissionEnd() {
this.clearAllCards();
this.clearHeroInfoPanels();
this.node.active = false;
}
start() {
}
update(dt: number) {
this.heroInfoSyncTimer += dt;
if (this.heroInfoSyncTimer < 0.15) return;
this.heroInfoSyncTimer = 0;
this.refreshHeroInfoPanels();
}
@@ -111,6 +128,7 @@ export class MissionCardComp extends CCComp {
this.on(GameEvent.MissionStart, this.onMissionStart, this);
this.on(GameEvent.MissionEnd, this.onMissionEnd, this);
this.on(GameEvent.CoinAdd, this.onCoinAdd, this);
oops.message.on(GameEvent.MasterCalled, this.onMasterCalled, this);
/** 按钮事件:抽卡与卡池升级 */
this.cards_chou?.on(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
@@ -129,6 +147,7 @@ export class MissionCardComp extends CCComp {
/** 解除按钮监听,避免节点销毁后回调泄漏 */
private unbindEvents() {
oops.message.off(GameEvent.MasterCalled, this.onMasterCalled, this);
this.cards_chou?.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
this.cards_chou?.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
this.cards_chou?.off(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this);
@@ -137,6 +156,14 @@ export class MissionCardComp extends CCComp {
this.cards_up?.off(NodeEventType.TOUCH_CANCEL, this.onUpgradeTouchCancel, this);
}
private onMasterCalled(event: string, args: any) {
const payload = args ?? event;
const eid = Number(payload?.eid ?? 0);
const model = payload?.model as HeroAttrsComp | undefined;
if (!eid || !model) return;
this.ensureHeroInfoPanel(eid, model);
}
private onDrawTouchStart() {
this.playButtonPressAnim(this.cards_chou);
}
@@ -339,8 +366,110 @@ export class MissionCardComp extends CCComp {
return CardsUpSet[lv] ?? 0;
}
private ensureHeroInfoPanel(eid: number, model: HeroAttrsComp) {
if (!this.hero_info_node || !this.hero_info_prefab) return;
this.hero_info_node.active = true;
const current = this.heroInfoItems.get(eid);
if (current) {
current.model = model;
this.updateHeroInfoPanel(current);
return;
}
const node = instantiate(this.hero_info_prefab);
node.parent = this.hero_info_node;
node.active = true;
const item = {
node,
model,
apLabel: this.resolvePanelLabel(node, [["ap", "val"]]),
hpLabel: this.resolvePanelLabel(node, [["hp", "val"]])
};
this.heroInfoItems.set(eid, item);
this.relayoutHeroInfoPanels();
this.updateHeroInfoPanel(item);
}
private refreshHeroInfoPanels() {
const removeKeys: number[] = [];
this.heroInfoItems.forEach((item, eid) => {
if (!item.node || !item.node.isValid) {
removeKeys.push(eid);
return;
}
const ent = (item.model as any)?.ent;
if (!ent) {
if (item.node.isValid) item.node.destroy();
removeKeys.push(eid);
return;
}
this.updateHeroInfoPanel(item);
});
for (let i = 0; i < removeKeys.length; i++) {
this.heroInfoItems.delete(removeKeys[i]);
}
if (removeKeys.length > 0) {
this.relayoutHeroInfoPanels();
}
}
private updateHeroInfoPanel(item: {
node: Node,
model: HeroAttrsComp,
apLabel: Label | null,
hpLabel: Label | null
}) {
if (item.apLabel) item.apLabel.string = `${Math.max(0, Math.floor(item.model.ap ?? 0))}`;
if (item.hpLabel) item.hpLabel.string = `${Math.max(0, Math.floor(item.model.hp_max ?? 0))}`;
}
private relayoutHeroInfoPanels() {
let index = 0;
this.heroInfoItems.forEach(item => {
if (!item.node || !item.node.isValid) return;
const pos = item.node.position;
item.node.setPosition(pos.x, -index * this.heroInfoItemGap, pos.z);
index += 1;
});
}
private clearHeroInfoPanels() {
this.heroInfoItems.forEach(item => {
if (item.node && item.node.isValid) {
item.node.destroy();
}
});
this.heroInfoItems.clear();
if (this.hero_info_node && this.hero_info_node.isValid) {
for (let i = this.hero_info_node.children.length - 1; i >= 0; i--) {
const child = this.hero_info_node.children[i];
if (child && child.isValid) child.destroy();
}
}
this.heroInfoSyncTimer = 0;
}
private resolvePanelLabel(root: Node, paths: string[][]): Label | null {
for (let i = 0; i < paths.length; i++) {
const node = this.findNodeByPath(root, paths[i]);
if (!node) continue;
const label = node.getComponent(Label) || node.getComponentInChildren(Label);
if (label) return label;
}
return null;
}
private findNodeByPath(root: Node, path: string[]): Node | null {
let current: Node | null = root;
for (let i = 0; i < path.length; i++) {
current = current?.getChildByName(path[i]) ?? null;
if (!current) return null;
}
return current;
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() {
this.clearHeroInfoPanels();
this.resetButtonScale(this.cards_chou);
this.resetButtonScale(this.cards_up);
this.node.destroy();