Files
pixelheros/assets/script/game/map/CardLiteComp.ts
pan aca32fd5c8 refactor: 移除技能卡牌系统相关代码和资源
1. 删除MSkillBoxComp组件文件及对应的meta配置
2. 从UI配置中移除SkillBox相关注册项
3. 删除MSkillBox.prefab及其meta配置
4. 新增winbox和winboxbg弹窗预制体资源
5. 优化CardLiteComp的注释描述
2026-08-26 09:39:46 +08:00

278 lines
9.6 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)切换选中英雄详情。
*
* 关键设计:
* - 轻量展示组件,仅用于英雄展示,无拖拽使用、无锁定、无费用扣除等交互。
* - 英雄卡图标加载 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;
/** 选中态放大倍率 */
private static readonly SELECT_SCALE: number = 1.08;
// ======================== 编辑器绑定节点 ========================
@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;
}
/**
* 设置选中态:选中卡微微放大,取消选中恢复原尺寸。
* @param selected 是否选中
*/
setSelected(selected: boolean) {
Tween.stopAllByTarget(this.node);
const target = selected ? CardLiteComp.SELECT_SCALE : 1;
tween(this.node)
.to(0.12, { scale: new Vec3(target, target, 1) }, { easing: 'quadOut' })
.start();
}
// ======================== 事件绑定 ========================
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();
}
}