/** * @file CardLiteComp.ts * @description 英雄图鉴卡预制体组件(UI 视图层) * * 职责: * 1. 管理英雄图鉴中单张卡牌的 **显示**(名称、图标动画、费用、卡池等级、等级)。 * 2. 接收 CardConfig 数据并渲染对应卡面(英雄 / 技能 / 特殊卡)。 * 3. 点击卡牌时通过事件通知父组件(HerosListComp)切换选中英雄详情。 * * 关键设计: * - 轻量版 CardComp,无拖拽使用、无锁定、无费用扣除等交互。 * - 英雄卡图标使用 AnimationClip 动态加载 idle 动画;非英雄卡使用静态图标。 * - 通过 iconVisualToken 防止异步加载竞态。 * * 依赖: * - CardConfig / CardType / CKind 等卡牌数据结构(CardSet) * - HeroInfo(heroSet)—— 用于渲染英雄卡面信息 * - SkillSet(SkillSet)—— 用于渲染技能卡图标 * - 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 { 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(Node) pool_lv_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, hasPoolLv: !!this.pool_lv_node, hasLvl: !!this.lvl_node, bgChildren: this.BG_node?.children.map(c => c.name) || [], poolChildren: this.pool_lv_node?.children.map(c => c.name) || [], 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 cardLvStr = `lv${this.cardData.pool_lv}`; if (this.pool_lv_node) { this.pool_lv_node.children.forEach(child => { child.active = (child.name === cardLvStr); }); } 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); } if (this.pool_lv_node) { this.pool_lv_node.children.forEach(child => child.active = false); } 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(); } }