refactor(heroList): 替换卡牌选中高亮方式为显隐标记

1.  重构英雄列表卡牌选中逻辑,移除缩放高亮,改用active子节点显隐标记选中状态
2.  重构CardLiteComp,新增setSelected方法统一处理选中态
3.  调整卡牌预制体尺寸与布局,新增选中标记节点
4.  优化英雄图标加载逻辑,改为静态首帧展示而非播放动画
5.  调整图集边框配置与精灵帧引用
This commit is contained in:
panFD
2026-08-23 23:40:54 +08:00
parent b3781f550d
commit bef521683c
9 changed files with 3414 additions and 4487 deletions

View File

@@ -9,7 +9,7 @@
*
* 关键设计:
* - 轻量版 CardComp无拖拽使用、无锁定、无费用扣除等交互。
* - 英雄卡图标使用 AnimationClip 动态加载 idle 动画;非英雄卡使用静态图标。
* - 英雄卡图标加载 idle 动画剪辑并提取首帧静态展示;非英雄卡使用静态图标。
* - 通过 iconVisualToken 防止异步加载竞态。
*
* 依赖:
@@ -19,12 +19,11 @@
* - 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 { _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 { CardConfig, CardType, SpecialRefreshCardList, SpecialUpgradeCardList, CKind, CardPoolList } from "../common/config/CardSet";
import { SkillCardList } from "../common/config/SCardSet";
import { CardBgComp } from "./CardBgComp";
import { HeroInfo } from "../common/config/heroSet";
import { SkillSet } from "../common/config/SkillSet";
import { getLvColor } from "../common/config/GameSet";
@@ -32,6 +31,9 @@ import { smc } from "../common/SingletonModuleComp";
const { ccclass, property } = _decorator;
/** card_lv → BG_node 子节点颜色名映射1=green 2=blue 3=purple 4=yellow 5=red */
const BG_COLORS = ["green", "blue", "purple", "yellow", "red"];
/**
* CardLiteComp —— 单张卡牌简单视图组件
*
@@ -152,6 +154,15 @@ export class CardLiteComp extends CCComp {
this.node.active = false;
}
/**
* 设置选中态:切换根节点下 `active` 子节点的显隐(不再通过缩放表达选中)。
* @param selected 是否选中
*/
setSelected(selected: boolean) {
const mark = this.node.getChildByName("active");
if (mark) mark.active = selected;
}
// ======================== 事件绑定 ========================
private bindEvents() {
@@ -211,15 +222,11 @@ export class CardLiteComp extends CCComp {
parentName: this.node.parent?.name || "none",
});
const kindName = CKind[this.cardData.kind];
// 背景:按 card_lv 切换颜色底框BG_node 下为 green/blue/purple/yellow/red 子节点)
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();
});
const bgLv = Math.min(Math.max(this.cardData.base_card_lv ?? this.cardData.card_lv ?? 1, 1), 5);
const color = BG_COLORS[bgLv - 1];
this.BG_node.children.forEach(child => child.active = (child.name === color));
}
if (this.card_type === CardType.Hero) {
@@ -271,7 +278,6 @@ export class CardLiteComp extends CCComp {
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);
@@ -297,15 +303,10 @@ export class CardLiteComp extends CCComp {
}
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();
});
this.BG_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);
this.icon_node.setScale(new Vec3(1, 1, 1));
const sprite = this.icon_node?.getComponent(Sprite) || this.icon_node?.getComponentInChildren(Sprite);
if (sprite) sprite.spriteFrame = null;
}
@@ -363,46 +364,42 @@ export class CardLiteComp extends CCComp {
}
/**
* 为英雄图标加载并播放 idle 动画。
* 为英雄图标加载 idle 动画剪辑,提取首帧 SpriteFrame 静止展示(参考 CHeroComp
* 使用 token 做竞态保护,确保异步回调时不会覆盖已更新的图标。
*/
private updateHeroAnimation(node: Node, uuid: number, token: number) {
const sprite = node?.getComponent(Sprite) || node?.getComponentInChildren(Sprite);
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 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);
mLogger.log(this.debugMode, "CardLiteComp", `load hero idle failed ${uuid}`, err);
return;
}
if (token !== this.iconVisualToken || !this.cardData || this.card_type !== CardType.Hero || this.card_uuid !== uuid) {
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;
}
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 组件移除时的释放钩子:销毁节点 */

View File

@@ -226,13 +226,13 @@ export class HerosListComp extends CCComp {
}
private highlightCard(cardNode: Node) {
// 取消旧选中:关闭其 active 标识节点
if (this.selectNode) {
const oldWidget = this.selectNode.getComponent(Widget)
if (oldWidget) oldWidget.enabled = false
this.selectNode.setScale(1, 1, 1)
this.selectNode.getComponent(CardLiteComp)?.setSelected(false)
}
this.selectNode = cardNode
cardNode.setScale(1.1, 1.1, 1)
// 新选中:激活根节点下的 active 标识节点(替代原缩放高亮)
cardNode.getComponent(CardLiteComp)?.setSelected(true)
}
private onCardSelect(uuid: number) {
@@ -336,7 +336,7 @@ export class HerosListComp extends CCComp {
// 碎片图标:使用 ChipComp 渲染英雄图标(自动读取 HeroInfo 配置)
if (this.chip_node) {
const chipComp = this.chip_node.getComponent(ChipComp) || this.chip_node.addComponent(ChipComp)
chipComp.setHeroIconByUuid(uuid)
chipComp.setHeroByUuid(uuid)
}
// 升级按钮状态:未满级且碎片足够时可用