1. 移除对全类型卡牌数据的依赖,仅保留英雄卡展示逻辑 2. 简化数据流转,直接通过英雄UUID渲染卡面 3. 更新注释文档与变量命名,统一业务语义 4. 调整预制体节点命名与配置项
272 lines
9.4 KiB
TypeScript
272 lines
9.4 KiB
TypeScript
/**
|
||
* @file CardLiteComp.ts
|
||
* @description 英雄图鉴卡预制体组件(UI 视图层)
|
||
*
|
||
* 职责:
|
||
* 1. 管理英雄图鉴中单张英雄卡牌的 **显示**(名称、图标动画、卡池等级、英雄等级)。
|
||
* 2. 接收英雄 UUID 并渲染对应卡面。
|
||
* 3. 点击卡牌时通过事件通知父组件(HerosListComp)切换选中英雄详情。
|
||
*
|
||
* 关键设计:
|
||
* - 轻量版 CardComp,仅用于英雄展示,无拖拽使用、无锁定、无费用扣除等交互。
|
||
* - 英雄卡图标加载 idle 动画剪辑并提取首帧静态展示。
|
||
* - 通过 iconVisualToken 防止异步加载竞态。
|
||
*
|
||
* 依赖:
|
||
* - HeroInfo(heroSet)—— 用于渲染英雄卡面信息
|
||
* - smc —— 全局单例,访问图标图集缓存与英雄局外等级
|
||
*/
|
||
import { mLogger } from "../common/Logger";
|
||
import { _decorator, AnimationClip, EventTouch, Label, Node, NodeEventType, Sprite, SpriteFrame, 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";
|
||
import { HeroInfo } from "../common/config/heroSet";
|
||
import { getLvColor, CARD_LV_BG_COLORS } from "../common/config/GameSet";
|
||
import { smc } from "../common/SingletonModuleComp";
|
||
|
||
const { ccclass, property } = _decorator;
|
||
|
||
/**
|
||
* CardLiteComp —— 单张英雄卡简单视图组件
|
||
*
|
||
* 挂载在英雄图鉴卡预制体上,由 HerosListComp 实例化并管理。
|
||
* 负责单张英雄卡的渲染与点击选择。
|
||
*/
|
||
@ccclass('CardLiteComp')
|
||
@ecs.register('CardLiteComp', false)
|
||
export class CardLiteComp extends CCComp {
|
||
private debugMode: boolean = false;
|
||
|
||
// ======================== 编辑器绑定节点 ========================
|
||
|
||
@property(Node)
|
||
name_node: Node = null!
|
||
@property(Node)
|
||
icon_node: Node = null!
|
||
@property(Node)
|
||
cost_node: Node = null!
|
||
@property(Node)
|
||
Ckind_node: Node = null!
|
||
@property(Node)
|
||
BG_node: Node = null!
|
||
@property(Label)
|
||
lvl_node: Label = null!
|
||
|
||
// ======================== 运行时状态 ========================
|
||
|
||
card_uuid: number = 0
|
||
private iconVisualToken: number = 0;
|
||
private opacityComp: UIOpacity | null = null;
|
||
|
||
/** 点击回调(由外部 HerosListComp 设置) */
|
||
onClickCallback: ((comp: CardLiteComp) => void) | null = null;
|
||
|
||
// ======================== 生命周期 ========================
|
||
|
||
onLoad() {
|
||
this.bindEvents();
|
||
this.opacityComp = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity);
|
||
this.opacityComp.opacity = 255;
|
||
}
|
||
|
||
onDestroy() {
|
||
super.onDestroy();
|
||
this.unbindEvents();
|
||
}
|
||
|
||
start() {
|
||
this.node.active = true;
|
||
}
|
||
|
||
// ======================== 对外接口 ========================
|
||
|
||
/**
|
||
* 通过英雄 UUID 渲染英雄卡(纯展示场景)。
|
||
* 从 HeroInfo 读取静态配置,从 smc 读取局外等级。
|
||
* @param uuid 英雄 UUID
|
||
*/
|
||
setHeroByUuid(uuid: number) {
|
||
const hero = HeroInfo[uuid];
|
||
if (!hero) return;
|
||
this.card_uuid = uuid;
|
||
this.node.active = true;
|
||
this.applyHeroUI(hero);
|
||
mLogger.log(this.debugMode, "CardLiteComp", "setHeroByUuid", { uuid });
|
||
}
|
||
|
||
/** 查询当前是否有英雄数据 */
|
||
hasCard(): boolean {
|
||
return this.card_uuid > 0;
|
||
}
|
||
|
||
/**
|
||
* 清空英雄数据并重置 UI。
|
||
*/
|
||
clear() {
|
||
this.card_uuid = 0;
|
||
this.applyEmptyUI();
|
||
this.node.active = false;
|
||
}
|
||
|
||
/**
|
||
* 设置选中态:切换根节点下 `active` 子节点的显隐(不再通过缩放表达选中)。
|
||
* @param selected 是否选中
|
||
*/
|
||
setSelected(selected: boolean) {
|
||
const mark = this.node.getChildByName("active");
|
||
if (mark) mark.active = selected;
|
||
}
|
||
|
||
// ======================== 事件绑定 ========================
|
||
|
||
private bindEvents() {
|
||
this.node.on(NodeEventType.TOUCH_END, this.onCardClick, this);
|
||
}
|
||
|
||
private unbindEvents() {
|
||
if (this.node && this.node.isValid) {
|
||
this.node.off(NodeEventType.TOUCH_END, this.onCardClick, this);
|
||
}
|
||
}
|
||
|
||
/** 点击卡牌:触发回调通知父组件 */
|
||
private onCardClick(event: EventTouch) {
|
||
if (!this.hasCard()) return;
|
||
event.propagationStopped = true;
|
||
if (this.onClickCallback) {
|
||
this.onClickCallback(this);
|
||
}
|
||
mLogger.log(this.debugMode, "CardLiteComp", "card clicked", { uuid: this.card_uuid });
|
||
}
|
||
|
||
// ======================== UI 渲染 ========================
|
||
|
||
/**
|
||
* 渲染英雄卡面。
|
||
*
|
||
* 1. 背景:按 card_lv 切换颜色底框(green/blue/purple/yellow/red)。
|
||
* 2. 名称:英雄名。
|
||
* 3. 等级:局外成长等级(Lv.X),颜色按 getLvColor。
|
||
* 4. 图标:idle 动画首帧静态展示。
|
||
*/
|
||
private applyHeroUI(hero: typeof HeroInfo[number]) {
|
||
this.iconVisualToken += 1;
|
||
if (this.opacityComp) this.opacityComp.opacity = 255;
|
||
|
||
// 背景:按 card_lv 切换颜色底框
|
||
if (this.BG_node) {
|
||
const bgLv = Math.min(Math.max(hero.card_lv ?? 1, 1), 5);
|
||
const color = CARD_LV_BG_COLORS[bgLv - 1];
|
||
this.BG_node.children.forEach(child => child.active = (child.name === color));
|
||
}
|
||
|
||
// 名称
|
||
this.setLabel(this.name_node, `${hero?.name || ""}`);
|
||
|
||
// 等级:局外成长等级
|
||
if (this.lvl_node) {
|
||
const heroLv = Math.max(1, smc.getHeroOuterLv(this.card_uuid) || hero?.lv || 1);
|
||
this.lvl_node.string = `Lv.${heroLv}`;
|
||
this.lvl_node.node.active = true;
|
||
}
|
||
|
||
// 费用:英雄卡不显示
|
||
if (this.cost_node) {
|
||
this.cost_node.active = false;
|
||
}
|
||
|
||
// 图标:idle 动画首帧静态展示(镜像翻转,原图朝右,卡面需朝左)
|
||
if (this.icon_node) {
|
||
this.updateHeroAnimation(this.icon_node, this.card_uuid, this.iconVisualToken);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 渲染空槽状态:清空名称、等级、背景、图标等。
|
||
*/
|
||
private applyEmptyUI() {
|
||
this.iconVisualToken += 1;
|
||
this.setLabel(this.name_node, "");
|
||
if (this.cost_node) {
|
||
this.cost_node.active = false;
|
||
}
|
||
if (this.lvl_node) this.lvl_node.node.active = false;
|
||
if (this.BG_node) {
|
||
this.BG_node.children.forEach(child => child.active = false);
|
||
}
|
||
if (this.icon_node) {
|
||
const sprite = this.icon_node?.getComponent(Sprite) || this.icon_node?.getComponentInChildren(Sprite);
|
||
if (sprite) sprite.spriteFrame = null;
|
||
}
|
||
}
|
||
|
||
// ======================== 动画 ========================
|
||
|
||
/**
|
||
* 入场动画:先缩小再弹大再回归正常比例。
|
||
*/
|
||
playRefreshAnim() {
|
||
Tween.stopAllByTarget(this.node);
|
||
this.node.setScale(new Vec3(0.92, 0.92, 1));
|
||
tween(this.node)
|
||
.to(0.08, { scale: new Vec3(1.06, 1.06, 1) })
|
||
.to(0.1, { scale: new Vec3(1, 1, 1) })
|
||
.start();
|
||
}
|
||
|
||
// ======================== 工具方法 ========================
|
||
|
||
/**
|
||
* 安全设置文本,兼容节点上或子节点上的 Label。
|
||
*/
|
||
private setLabel(node: Node | null, value: string) {
|
||
if (!node) return;
|
||
const label = node.getComponent(Label) || node.getComponentInChildren(Label);
|
||
if (label) label.string = value;
|
||
}
|
||
|
||
/**
|
||
* 为英雄图标加载 idle 动画剪辑,提取首帧 SpriteFrame 静止展示。
|
||
* 使用 token 做竞态保护,确保异步回调时不会覆盖已更新的图标。
|
||
*/
|
||
private updateHeroAnimation(node: Node, uuid: number, token: number) {
|
||
if (!node || !node.isValid) return;
|
||
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
|
||
if (sprite) sprite.spriteFrame = null;
|
||
const hero = HeroInfo[uuid];
|
||
if (!hero) return;
|
||
const path = `game/heros/hero/${hero.path}/idle`;
|
||
resources.load(path, AnimationClip, (err, clip) => {
|
||
if (err || !clip) {
|
||
mLogger.log(this.debugMode, "CardLiteComp", `load hero idle failed ${uuid}`, err);
|
||
return;
|
||
}
|
||
// 竞态保护:仅允许最新一次图标请求落地
|
||
if (token !== this.iconVisualToken || !node.isValid) return;
|
||
const target = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
|
||
if (!target || !target.isValid) return;
|
||
|
||
// 帧动画数据存于 spriteFrame 轨道的 _channel._curve._values(裸 SpriteFrame 数组)
|
||
let firstFrame: SpriteFrame | undefined;
|
||
for (const track of clip.tracks) {
|
||
const curve = (track as unknown as { channel?: { curve?: { _values?: unknown[] } } }).channel?.curve;
|
||
const frame = curve?._values?.[0];
|
||
if (frame instanceof SpriteFrame) {
|
||
firstFrame = frame;
|
||
break;
|
||
}
|
||
}
|
||
if (firstFrame) {
|
||
target.spriteFrame = firstFrame;
|
||
}
|
||
});
|
||
}
|
||
|
||
// ======================== 生命周期钩子 ========================
|
||
|
||
/** ECS 组件移除时的释放钩子:销毁节点 */
|
||
reset() {
|
||
this.node.destroy();
|
||
}
|
||
}
|