Files
pixelheros/assets/script/game/map/CardLiteComp.ts
panw f114fca2ce refactor(map): 抽象卡牌背景颜色逻辑,简化代码
将多个文件中重复的卡池颜色切换逻辑提取为CardBgComp组件,
减少重复代码,提高可维护性
2026-05-28 09:23:01 +08:00

411 lines
14 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. 接收 CardConfig 数据并渲染对应卡面(英雄 / 技能 / 特殊卡)。
* 3. 点击卡牌时通过事件通知父组件HerosListComp切换选中英雄详情。
*
* 关键设计:
* - 轻量版 CardComp无拖拽使用、无锁定、无费用扣除等交互。
* - 英雄卡图标使用 AnimationClip 动态加载 idle 动画;非英雄卡使用静态图标。
* - 通过 iconVisualToken 防止异步加载竞态。
*
* 依赖:
* - CardConfig / CardType / CKind 等卡牌数据结构CardSet
* - HeroInfoheroSet—— 用于渲染英雄卡面信息
* - SkillSetSkillSet—— 用于渲染技能卡图标
* - smc —— 全局单例,访问图标图集缓存
*/
import { mLogger } from "../common/Logger";
import { _decorator, Animation, AnimationClip, EventTouch, Label, Node, NodeEventType, Sprite, SpriteAtlas, Tween, tween, UIOpacity, Vec3, resources, UITransform, Widget } 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 { CardConfig, CardType, SpecialRefreshCardList, SpecialUpgradeCardList, CKind, CardPoolList } from "../common/config/CardSet";
import { CardBgComp } from "./CardBgComp";
import { HeroInfo } from "../common/config/heroSet";
import { SkillSet } from "../common/config/SkillSet";
import { getLvColor } 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_cost: number = 0
card_type: CardType = CardType.Hero
card_uuid: number = 0
private cardData: CardConfig | null = null;
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;
if (!this.cardData) {
this.applyEmptyUI();
}
}
onDestroy() {
super.onDestroy();
this.unbindEvents();
}
start() {
this.node.active = true;
}
// ======================== 对外接口 ========================
/**
* 设置卡牌数据并渲染。
* @param data 卡牌配置数据
*/
setData(data: CardConfig) {
if (!data) return;
this.cardData = data;
this.card_uuid = data.uuid;
this.card_type = data.type;
this.card_cost = data.cost ?? 0;
this.node.active = true;
this.applyCardUI();
mLogger.log(this.debugMode, "CardLiteComp", "setData", {
uuid: this.card_uuid,
type: this.card_type,
cost: this.card_cost
});
}
/**
* 便捷方法:通过英雄 UUID 和卡池等级直接设置英雄卡。
* 自动从 CardPoolList 查找匹配的卡牌配置。
* @param uuid 英雄 UUID
* @param poolLv 卡池等级(默认 1
*/
setHeroByUuid(uuid: number, poolLv: number = 1) {
const card = CardPoolList.find(c => c.uuid === uuid && c.pool_lv === poolLv);
if (card) {
this.setData(card);
return;
}
const hero = HeroInfo[uuid];
if (hero) {
this.setData({
uuid: hero.uuid,
type: CardType.Hero,
cost: 5,
weight: 25,
pool_lv: poolLv as any,
kind: CKind.Hero,
hero_lv: hero.lv || 1
});
}
}
/** 查询当前是否有卡牌数据 */
hasCard(): boolean {
return !!this.cardData;
}
/** 获取当前卡牌数据 */
getCardData(): CardConfig | null {
return this.cardData;
}
/**
* 清空卡牌数据并重置 UI。
*/
clear() {
this.cardData = null;
this.card_uuid = 0;
this.card_cost = 0;
this.card_type = CardType.Hero;
this.applyEmptyUI();
this.node.active = false;
}
// ======================== 事件绑定 ========================
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.cardData) return;
event.propagationStopped = true;
if (this.onClickCallback) {
this.onClickCallback(this);
}
mLogger.log(this.debugMode, "CardLiteComp", "card clicked", {
uuid: this.card_uuid,
type: this.card_type
});
}
// ======================== UI 渲染 ========================
/**
* 根据当前 cardData 渲染卡面。
*
* 渲染逻辑根据卡牌类型分三路:
* 1. 英雄卡显示英雄名、等级、idle 动画。
* 2. 技能卡:显示技能名、静态图标。
* 3. 特殊卡:显示卡名、静态图标。
*
* 同时根据 pool_lv 切换背景底框,根据 kind 切换种类标识。
*/
private applyCardUI() {
if (!this.cardData) {
this.applyEmptyUI();
return;
}
this.iconVisualToken += 1;
if (this.opacityComp) this.opacityComp.opacity = 255;
mLogger.log(this.debugMode, "CardLiteComp", "applyCardUI nodes", {
uuid: this.card_uuid,
hasName: !!this.name_node,
hasIcon: !!this.icon_node,
hasCost: !!this.cost_node,
hasBG: !!this.BG_node,
hasLvl: !!this.lvl_node,
nodeActive: this.node.active,
nodeScale: this.node.scale.toString(),
nodePos: this.node.position.toString(),
parentName: this.node.parent?.name || "none",
});
const kindName = CKind[this.cardData.kind];
if (this.BG_node) {
this.BG_node.children.forEach(child => {
child.active = (child.name === kindName);
const bg = child.getComponent(CardBgComp);
if (bg) child.active ? bg.apply(this.cardData.pool_lv) : bg.clear();
});
}
if (this.card_type === CardType.Hero) {
const hero = HeroInfo[this.card_uuid];
const heroLv = Math.max(1, this.cardData.hero_lv ?? hero?.lv ?? 1);
this.setLabel(this.name_node, `${hero?.name || ""}`);
if (this.lvl_node) {
this.lvl_node.string = `Lv.${heroLv}`;
this.lvl_node.color = getLvColor(heroLv);
this.lvl_node.node.active = true;
}
} else if (this.card_type === CardType.Skill) {
if (this.lvl_node) this.lvl_node.node.active = false;
const skill = SkillSet[this.card_uuid];
const skillCard = CardPoolList.find(c => c.uuid === this.card_uuid);
const card_lv = Math.max(1, Math.floor(this.cardData.card_lv ?? 1));
const spSuffix = card_lv >= 2 ? "★".repeat(card_lv - 1) : "";
this.setLabel(this.name_node, `${spSuffix}${skillCard?.name || skill?.name || ""}${spSuffix}`);
} else {
if (this.lvl_node) this.lvl_node.node.active = false;
const specialCard = this.card_type === CardType.SpecialUpgrade
? SpecialUpgradeCardList[this.card_uuid]
: SpecialRefreshCardList[this.card_uuid];
const card_lv = Math.max(1, Math.floor(this.cardData.card_lv ?? 1));
const spSuffix = card_lv >= 2 ? "★".repeat(card_lv - 1) : "";
this.setLabel(this.name_node, `${spSuffix}${specialCard?.name || ""}${spSuffix}`);
}
if (this.cost_node) {
this.cost_node.active = true;
const numNode = this.cost_node.getChildByName("num");
if (numNode) {
this.setLabel(numNode, `${this.card_cost}`);
}
}
if (this.icon_node) {
const iconNode = this.icon_node;
if (this.card_type === CardType.Hero) {
iconNode.setScale(new Vec3(-1.5, 1.5, 1));
this.updateHeroAnimation(iconNode, this.card_uuid, this.iconVisualToken);
return;
}
iconNode.setScale(new Vec3(1, 1, 1));
this.clearIconAnimation(iconNode);
const iconId = this.resolveCardIconId(this.card_type, this.card_uuid);
if (iconId) {
this.updateIcon(iconNode, iconId);
} else {
const sprite = iconNode?.getComponent(Sprite) || iconNode?.getComponentInChildren(Sprite);
if (sprite) sprite.spriteFrame = null;
}
}
}
/**
* 渲染空槽状态:清空名称、费用、等级、图标等。
*/
private applyEmptyUI() {
this.iconVisualToken += 1;
this.setLabel(this.name_node, "");
if (this.cost_node) {
const numNode = this.cost_node.getChildByName("num");
if (numNode) {
this.setLabel(numNode, "");
}
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;
const bg = child.getComponent(CardBgComp);
if (bg) bg.clear();
});
}
if (this.icon_node) {
(this.icon_node as Node).setScale(new Vec3(1, 1, 1));
this.clearIconAnimation(this.icon_node as 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;
}
/**
* 更新图标:从全局缓存图集中获取对应帧。
*/
private updateIcon(node: Node, iconId: string) {
if (!node || !iconId) return;
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
if (!sprite) return;
if (smc.uiconsAtlas) {
const frame = smc.uiconsAtlas.getSpriteFrame(iconId);
sprite.spriteFrame = frame || null;
}
}
/**
* 根据卡牌类型和 UUID 解析出图标 ID在 SpriteAtlas 中的帧名)。
*/
private resolveCardIconId(type: CardType, uuid: number): string {
if (type === CardType.Skill) {
return SkillSet[uuid]?.icon || `${uuid}`;
}
if (type === CardType.Hero) {
return HeroInfo[uuid]?.icon || `${uuid}`;
}
return `${uuid}`;
}
/**
* 为英雄卡图标加载并播放 idle 动画。
* 使用 token 做竞态保护,确保异步回调时不会覆盖已更新的图标。
*/
private updateHeroAnimation(node: Node, uuid: number, token: number) {
const sprite = node?.getComponent(Sprite) || node?.getComponentInChildren(Sprite);
if (sprite) sprite.spriteFrame = null;
const hero = HeroInfo[uuid];
if (!hero) return;
const anim = node.getComponent(Animation) || node.addComponent(Animation);
this.clearAnimationClips(anim);
const path = `game/heros/hero/${hero.path}/idle`;
resources.load(path, AnimationClip, (err, clip) => {
if (err || !clip) {
mLogger.log(this.debugMode, "CardLiteComp", `load hero animation failed ${uuid}`, err);
return;
}
if (token !== this.iconVisualToken || !this.cardData || this.card_type !== CardType.Hero || this.card_uuid !== uuid) {
return;
}
this.clearAnimationClips(anim);
anim.addClip(clip);
anim.play("idle");
});
}
/** 清除图标节点上的动画(停止播放并移除所有 clip */
private clearIconAnimation(node: Node) {
const anim = node?.getComponent(Animation);
if (!anim) return;
anim.stop();
this.clearAnimationClips(anim);
}
/** 移除 Animation 组件上的全部 AnimationClip */
private clearAnimationClips(anim: Animation) {
const clips = (anim as any).clips as AnimationClip[] | undefined;
if (!clips || clips.length === 0) return;
[...clips].forEach(clip => anim.removeClip(clip, true));
}
// ======================== 生命周期钩子 ========================
/** ECS 组件移除时的释放钩子:销毁节点 */
reset() {
this.node.destroy();
}
}