Files
pixelheros/assets/script/game/map/CardLiteComp.ts
panFD cf4beadeb4 refactor(CardLiteComp): 重构英雄图鉴卡组件逻辑
1. 移除对全类型卡牌数据的依赖,仅保留英雄卡展示逻辑
2. 简化数据流转,直接通过英雄UUID渲染卡面
3. 更新注释文档与变量命名,统一业务语义
4. 调整预制体节点命名与配置项
2026-08-24 00:00:34 +08:00

272 lines
9.4 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 CardLiteComp.ts
* @description 英雄图鉴卡预制体组件UI 视图层)
*
* 职责:
* 1. 管理英雄图鉴中单张英雄卡牌的 **显示**(名称、图标动画、卡池等级、英雄等级)。
* 2. 接收英雄 UUID 并渲染对应卡面。
* 3. 点击卡牌时通过事件通知父组件HerosListComp切换选中英雄详情。
*
* 关键设计:
* - 轻量版 CardComp仅用于英雄展示无拖拽使用、无锁定、无费用扣除等交互。
* - 英雄卡图标加载 idle 动画剪辑并提取首帧静态展示。
* - 通过 iconVisualToken 防止异步加载竞态。
*
* 依赖:
* - HeroInfoheroSet—— 用于渲染英雄卡面信息
* - 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();
}
}