Files
pixelheros/assets/script/game/map/ChipComp.ts
panFD 6eb51c8535 feat(map): add hero card chip view component
新增了图鉴卡牌预制体组件ChipComp,负责渲染卡牌图标与背景底框,支持英雄卡动画播放和普通卡牌静态图标展示,同时提供了数据设置、清空等对外接口。
2026-08-23 16:10:06 +08:00

259 lines
9.0 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 ChipComp.ts
* @description 英雄图鉴卡预制体组件UI 视图层·精简版)
*
* 职责:
* 1. 根据卡牌配置渲染 **图标**(英雄卡为 idle 动画,其他卡为静态图集帧)。
* 2. 根据 card_lv / kind 渲染 **背景底框**。
*
* 说明:
* - 仅保留图标与背景渲染能力,名称、费用、等级等展示由外部组件负责。
* - 通过 iconVisualToken 防止异步动画加载竞态。
*
* 依赖:
* - CardConfig / CardType / CKind / CardPoolListCardSet
* - HeroInfoheroSet、SkillSetSkillSet
* - smc —— 全局单例,访问图标图集缓存
*/
import { mLogger } from "../common/Logger";
import { _decorator, Animation, AnimationClip, Node, Sprite, 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 { CardConfig, CardType, CKind, CardPoolList } from "../common/config/CardSet";
import { CardBgComp } from "./CardBgComp";
import { HeroInfo } from "../common/config/heroSet";
import { SkillSet } from "../common/config/SkillSet";
import { smc } from "../common/SingletonModuleComp";
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
card_type: CardType = CardType.Hero
private cardData: CardConfig | null = null;
private iconVisualToken: number = 0;
// ======================== 生命周期 ========================
onLoad() {
if (!this.cardData) {
this.applyEmptyUI();
}
}
// ======================== 对外接口 ========================
/**
* 设置卡牌数据并渲染图标与背景。
* @param data 卡牌配置数据
*/
setData(data: CardConfig) {
if (!data) return;
this.cardData = data;
this.card_uuid = data.uuid;
this.card_type = data.type;
this.node.active = true;
this.applyCardUI();
mLogger.log(this.debugMode, "ChipComp", "setData", {
uuid: this.card_uuid,
type: this.card_type
});
}
/**
* 便捷方法:通过英雄 UUID 和卡池等级直接设置英雄卡。
* 自动从 CardPoolList 查找匹配的卡牌配置。
* @param uuid 英雄 UUID
* @param cardLv 卡牌等级(默认 1
*/
setHeroByUuid(uuid: number, cardLv: number = 1) {
const card = CardPoolList.find(c => c.uuid === uuid && c.card_lv === cardLv);
if (card) {
this.setData(card);
return;
}
const hero = HeroInfo[uuid];
if (hero) {
this.setData({
uuid: hero.uuid,
type: CardType.Hero,
cost: 0,
weight: 0,
card_lv: cardLv,
kind: CKind.Hero,
hero_lv: smc.getHeroOuterLv(hero.uuid)
});
}
}
/** 查询当前是否有卡牌数据 */
hasCard(): boolean {
return !!this.cardData;
}
/** 获取当前卡牌数据 */
getCardData(): CardConfig | null {
return this.cardData;
}
/** 清空卡牌数据并重置 UI */
clear() {
this.cardData = null;
this.card_uuid = 0;
this.card_type = CardType.Hero;
this.applyEmptyUI();
this.node.active = false;
}
// ======================== UI 渲染 ========================
/** 渲染图标与背景 */
private applyCardUI() {
if (!this.cardData) {
this.applyEmptyUI();
return;
}
this.iconVisualToken += 1;
const kindName = CKind[this.cardData.kind];
// 背景:按 kind 显示对应底框,按 card_lv 应用等级样式
if (this.BG_node) {
const bgLv = this.cardData.base_card_lv ?? this.cardData.card_lv;
this.BG_node.children.forEach(child => {
child.active = (child.name === kindName);
const bg = child.getComponent(CardBgComp);
if (bg) child.active ? bg.apply(bgLv) : bg.clear();
});
}
// 图标:英雄卡播放 idle 动画,其他卡使用静态图集帧
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;
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.setScale(new Vec3(1, 1, 1));
this.clearIconAnimation(this.icon_node);
const sprite = this.icon_node.getComponent(Sprite) || this.icon_node.getComponentInChildren(Sprite);
if (sprite) sprite.spriteFrame = null;
}
}
// ======================== 工具方法 ========================
/** 更新图标:从全局缓存图集中获取对应帧 */
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, "ChipComp", `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.clips as AnimationClip[] | undefined;
if (!clips || clips.length === 0) return;
[...clips].forEach(clip => anim.removeClip(clip, true));
}
// ======================== 生命周期钩子 ========================
/** ECS 组件移除时的释放钩子:销毁节点 */
reset() {
this.node.destroy();
}
}