/** * @file ChipComp.ts * @description 英雄图鉴卡预制体组件(UI 视图层·精简版) * * 职责: * 1. 根据英雄 UUID 渲染 **图标**(idle 动画首帧静态展示,零运行时动画开销)。 * 2. 根据 HeroInfo.rarity 切换 **背景底框颜色**(BG_node 下的 green/blue/purple/yellow/red 子节点)。 * * 说明: * - 仅处理英雄展示,数据源为 heroSet 的 HeroInfo,不依赖 CardSet。 * - 图标采用 CHeroComp 同款方案:加载 idle AnimationClip 取首帧 SpriteFrame 静止展示, * 通过 iconVisualToken 防止异步加载竞态。 */ import { mLogger } from "../common/Logger"; import { _decorator, AnimationClip, Node, Sprite, resources, SpriteFrame } 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 { RARITY_BG_COLORS } from "../common/config/GameSet"; const { ccclass, property } = _decorator; /** * ChipComp —— 单张英雄卡图标/背景视图组件 * * 挂载在图鉴卡预制体上,仅负责英雄图标(静态首帧)与背景底框的渲染。 */ @ccclass('ChipComp') @ecs.register('ChipComp', false) export class ChipComp extends CCComp { private debugMode: boolean = false; // ======================== 编辑器绑定节点 ======================== @property(Node) icon_node: Node = null! @property(Node) BG_node: Node = null! // ======================== 运行时状态 ======================== card_uuid: number = 0 private iconVisualToken: number = 0; // ======================== 对外接口 ======================== /** * 通过英雄 UUID 渲染英雄卡(静态图标 + 背景底框)。 * @param uuid 英雄 UUID(须存在于 HeroInfo) */ setHeroByUuid(uuid: number) { const hero = HeroInfo[uuid]; if (!hero) return; this.card_uuid = uuid; this.node.active = true; this.iconVisualToken += 1; // 背景:按 rarity 切换颜色底框(green/blue/purple/yellow/red) if (this.BG_node) { const color = RARITY_BG_COLORS[Math.min(Math.max(hero.rarity ?? 1, 1), 5) - 1]; this.BG_node.children.forEach(child => child.active = (child.name === color)); } // 图标:idle 动画首帧静态展示 if (this.icon_node) { this.updateHeroIcon(this.icon_node, uuid, this.iconVisualToken); } mLogger.log(this.debugMode, "ChipComp", "setHeroByUuid", { uuid }); } /** 清空并重置 UI */ clear() { this.card_uuid = 0; this.iconVisualToken += 1; 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; } this.node.active = false; } // ======================== 图标(静态首帧) ======================== /** * 加载英雄 idle 动画剪辑,提取首帧 SpriteFrame 静止展示(零运行时动画开销)。 * 使用 token 做竞态保护,确保异步回调时不会覆盖已更新的图标。 */ private updateHeroIcon(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, "ChipComp", `load hero animation failed ${uuid}`, err); return; } // 竞态保护:仅允许最新一次图标请求落地 if (token !== this.iconVisualToken || this.card_uuid !== uuid) return; if (!node || !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(); } }