docs: 为游戏地图模块添加详细的代码注释

为游戏地图模块的脚本文件添加全面的注释,说明每个组件的职责、关键设计、依赖关系和使用方式。注释覆盖了英雄信息面板、技能卡槽位管理器、排行榜弹窗、卡牌控制器、背景滚动组件等核心功能模块,提高了代码的可读性和维护性。

同时修复了英雄预制体的激活状态和技能效果预制体的尺寸参数。
This commit is contained in:
walkpan
2026-04-07 19:00:30 +08:00
parent 9a1d517aa9
commit e880613f8f
21 changed files with 1840 additions and 242 deletions

View File

@@ -1,3 +1,24 @@
/**
* @file CardComp.ts
* @description 单张卡牌槽位组件UI 视图层)
*
* 职责:
* 1. 管理单个卡牌槽位的 **显示** 和 **交互**(触摸拖拽 / 点击使用 / 锁定切换)。
* 2. 接收来自 MissionCardComp 分发的 CardConfig 数据,渲染对应卡面(英雄 / 技能 / 特殊卡)。
* 3. 在玩家触发"使用卡牌"操作时,执行 **费用扣除 → 动画表现 → 效果分发** 的完整流程。
*
* 关键设计:
* - 槽位可 **锁定**:锁定后刷新卡池不会覆盖旧卡,由 `isLocked` 控制。
* - 卡牌使用支持 **上划手势** 与 **点击** 两种触发方式。
* - 英雄卡图标使用 AnimationClip 动态加载 idle 动画;技能 / 特殊卡使用 SpriteAtlas 静态图标。
* - 通过 `iconVisualToken` 防止异步加载资源回调与当前显示不一致(竞态保护)。
*
* 依赖:
* - CardConfig / CardType / CKind 等卡牌数据结构CardSet
* - HeroInfoheroSet、SkillSetSkillSet—— 用于渲染卡面信息
* - GameEvent 事件总线 —— 分发 CallHero / UseSkillCard / UseSpecialCard 等效果
* - smc.vmdata.mission_data —— 读写局内金币
*/
import { mLogger } from "../common/Logger";
import { _decorator, Animation, AnimationClip, EventTouch, Label, Node, NodeEventType, Sprite, SpriteAtlas, Tween, tween, UIOpacity, Vec3, resources, Light } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
@@ -14,55 +35,99 @@ import { UIID } from "../common/config/GameUIConfig";
const { ccclass, property } = _decorator;
/** 视图层对象 */
/**
* CardComp —— 单张卡牌槽位视图组件
*
* 挂载在每个卡牌槽位节点上,由 MissionCardComp 统一管理 4 个实例。
* 负责单卡的渲染、交互、动画以及使用逻辑。
*/
@ccclass('CardComp')
@ecs.register('CardComp', false)
export class CardComp extends CCComp {
/** 是否开启调试日志 */
private debugMode: boolean = true;
// ======================== 编辑器绑定节点 ========================
/** 锁定态图标节点(显示时表示本槽位锁定) */
@property(Node)
Lock: Node = null!
/** 解锁态图标节点(显示时表示本槽位未锁定,可点击上锁) */
@property(Node)
unLock: Node = null!
/** 英雄卡信息面板(显示 AP / HP */
@property(Node)
info_node=null!
/** 非英雄卡信息面板(显示技能 / 特殊卡描述文本) */
@property(Node)
oinfo_node=null!
/** 卡牌名称标签节点 */
@property(Node)
name_node=null!
/** 卡牌图标节点(英雄动画 / 技能图标) */
@property(Node)
icon_node=null!
/** 费用显示节点 */
@property(Node)
cost_node=null!
/** 卡牌种类标识节点(如近战 / 远程 / 辅助等分类子节点的容器) */
@property(Node)
Ckind_node=null!
/** 卡牌背景底框节点(按卡池等级切换子节点显示) */
@property(Node)
BG_node=null!
/** 普通品质边框节点hero_lv / card_lv <= 1 时使用) */
@property(Node)
NF_node=null!
/** 高品质边框节点hero_lv / card_lv > 1 时使用) */
@property(Node)
HF_node=null!
// ======================== 运行时状态 ========================
/** 当前卡牌的金币费用 */
card_cost:number=0
/** 当前卡牌类型(英雄 / 技能 / 特殊升级 / 特殊刷新) */
card_type:CardType=CardType.Hero
/** 当前卡牌的唯一标识 UUID */
card_uuid:number=0
/** 是否处于锁定状态(锁定且有卡时,抽卡分发会被跳过) */
private isLocked: boolean = false;
/** 图标图集缓存(后续接图标资源时直接复用 */
/** 图标图集缓存(首次加载后复用,避免重复 IO */
private uiconsAtlas: SpriteAtlas | null = null;
/** 当前槽位承载的卡牌数据null 表示空槽 */
private cardData: CardConfig | null = null;
/** 上划使用阈值(像素):拖拽距离 >= 此值视为"使用卡牌" */
private readonly dragUseThreshold: number = 70;
/** 触摸起始 Y 坐标,用于计算拖拽距离 */
private touchStartY: number = 0;
/** 当前是否正在拖拽 */
private isDragging: boolean = false;
/** 当前是否正在执行"使用"流程(防止重复触发) */
private isUsing: boolean = false;
/** 卡牌的静止位置(未拖拽时应在此位置) */
private restPosition: Vec3 = new Vec3();
/** 是否已缓存基准 Y/Z 坐标(首次 setSlotPosition 时确定) */
private hasFixedBasePosition: boolean = false;
/** 基准 Y 坐标(由场景布局决定) */
private fixedBaseY: number = 0;
/** 基准 Z 坐标 */
private fixedBaseZ: number = 0;
/** UIOpacity 组件引用,用于使用消失动画中的渐隐 */
private opacityComp: UIOpacity | null = null;
/**
* 图标视觉令牌:每次更新图标时 +1
* 异步加载回调通过比对 token 判断是否仍为当前可见内容,
* 防止快速切卡时旧回调错误覆盖新图标。
*/
private iconVisualToken: number = 0;
// ======================== 生命周期 ========================
/**
* 组件加载时:绑定交互事件,初始化基础 UI 状态。
* 此阶段不触发业务逻辑。
*/
onLoad() {
/** 初始阶段只做UI状态准备不触发业务逻辑 */
this.bindEvents();
@@ -74,36 +139,53 @@ export class CardComp extends CCComp {
}
/** 组件销毁时解绑所有事件,防止残留回调 */
onDestroy() {
this.unbindEvents();
}
/** 外部初始化入口(由 MissionCardComp 调用) */
init(){
this.onMissionStart();
}
/** 游戏开始初始化 */
/** 游戏开始初始化(预留扩展) */
onMissionStart() {
}
/** 游戏结束清理 */
/** 游戏结束清理(预留扩展) */
onMissionEnd() {
}
/** 节点启动时确保可见 */
start() {
/** 单卡节点常驻,由数据控制显示内容 */
this.node.active = true;
}
/** 兼容旧接口:外部通过该入口更新卡牌 */
// ======================== 对外接口 ========================
/**
* 兼容旧接口:外部通过该入口更新卡牌
* @param card 卡牌节点引用(历史遗留参数,当前未使用)
* @param data 卡牌配置数据
*/
updateCardInfo(card:Node, data: CardConfig){
this.applyDrawCard(data);
}
/**
* 更新卡牌图标:先尝试从缓存图集获取,未缓存则异步加载图集后获取。
* @param node 图标所在节点
* @param iconId 图标在 SpriteAtlas 中的帧名称
*/
private updateIcon(node: Node, iconId: string) {
if (!node || !iconId) return;
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
if (!sprite) return;
// 已缓存图集 → 直接获取帧
if (this.uiconsAtlas) {
const frame = this.uiconsAtlas.getSpriteFrame(iconId);
if (frame) {
@@ -113,6 +195,7 @@ export class CardComp extends CCComp {
}
return;
}
// 首次加载图集
resources.load("gui/uicons", SpriteAtlas, (err, atlas) => {
if (err || !atlas) {
mLogger.log(this.debugMode, "CardComp", "load uicons atlas failed", err);
@@ -128,25 +211,42 @@ export class CardComp extends CCComp {
});
}
/** 兼容旧接口:按索引更新卡牌(当前由 MissionCardComp 顺序分发) */
/**
* 兼容旧接口:按索引更新卡牌(当前由 MissionCardComp 顺序分发)
* @param index 槽位索引0~3
* @param data 卡牌配置数据
*/
updateCardData(index: number, data: CardConfig) {
this.applyDrawCard(data);
}
/** 兼容按钮回调入口:触发单卡使用 */
/**
* 兼容按钮回调入口:触发单卡使用
* @param e 事件对象
* @param index 索引字符串(历史遗留参数)
*/
selectCard(e: any, index: string) {
this.useCard();
}
/**
* 关闭界面
*/
/** 关闭界面(预留) */
close() {
}
/** 抽卡分发入口:返回 true 表示本次已成功接收新卡 */
// ======================== 核心业务方法 ========================
/**
* 抽卡分发入口:由 MissionCardComp 调用以向本槽位放入新卡。
*
* 流程:
* 1. 若本槽位已锁定且已有卡 → 跳过,返回 false。
* 2. 更新 cardData 及派生字段 → 刷新 UI → 播放入场动画。
*
* @param data 要放入的卡牌数据null 表示无卡可放
* @returns true = 成功接收false = 跳过(锁定 / 无数据)
*/
applyDrawCard(data: CardConfig | null): boolean {
if (!data) return false;
/** 锁定且已有旧卡时,跳过本次刷新,保持老卡 */
@@ -169,10 +269,24 @@ export class CardComp extends CCComp {
return true;
}
/**
* 使用当前卡牌的核心逻辑。
*
* 完整流程:
* 1. 前置校验:是否有卡、是否正在使用中。
* 2. 金币检查:不够则 toast 提示 + 回弹动画。
* 3. 英雄卡额外校验:通过 GameEvent.UseHeroCard 事件向 MissionCardComp 询问
* 英雄上限是否允许guard 模式:外部可设 cancel=true 阻止使用)。
* 4. 扣除金币,同步 CoinAdd 事件。
* 5. 播放消失动画 → 动画结束后清空槽位并分发卡牌效果。
*
* @returns 被使用的卡牌数据,若未成功使用则返回 null
*/
useCard(): CardConfig | null {
if (!this.cardData || this.isUsing) return null;
const cardCost = Math.max(0, Math.floor(this.cardData.cost ?? 0));
const currentCoin = this.getMissionCoin();
// 金币不足 → 提示并回弹
if (currentCoin < cardCost) {
oops.gui.toast(`金币不足,召唤需要${cardCost}`);
this.playReboundAnim();
@@ -184,6 +298,7 @@ export class CardComp extends CCComp {
});
return null;
}
// 英雄卡特殊校验:通过 guard 对象实现"可取消"模式
if (this.cardData.type === CardType.Hero) {
const guard = {
cancel: false,
@@ -198,11 +313,13 @@ export class CardComp extends CCComp {
return null;
}
}
// 扣除金币
this.setMissionCoin(currentCoin - cardCost);
oops.message.dispatchEvent(GameEvent.CoinAdd, {
syncOnly: true,
delta: -cardCost
});
// 标记使用中,阻止并发操作
this.isUsing = true;
const used = this.cardData;
mLogger.log(this.debugMode, "CardComp", "use card", {
@@ -211,6 +328,7 @@ export class CardComp extends CCComp {
cost: cardCost,
leftCoin: this.getMissionCoin()
});
// 播放消失动画 → 动画结束后清槽并分发效果
this.playUseDisappearAnim(() => {
this.clearAfterUse();
this.isUsing = false;
@@ -219,6 +337,14 @@ export class CardComp extends CCComp {
return used;
}
/**
* 根据卡牌类型分发对应的游戏效果事件。
* - 英雄卡 → CallHero
* - 技能卡 → UseSkillCard
* - 特殊升级 / 特殊刷新 → UseSpecialCard
*
* @param payload 被使用的卡牌数据
*/
private executeCardEffectEntry(payload: CardConfig) {
switch (payload.type) {
case CardType.Hero:
@@ -239,7 +365,10 @@ export class CardComp extends CCComp {
return !!this.cardData;
}
/** 外部设置锁定态 */
/**
* 外部设置锁定态
* @param value true=锁定刷新时保留旧卡false=解锁
*/
setLocked(value: boolean) {
this.isLocked = value;
this.updateLockUI();
@@ -250,6 +379,11 @@ export class CardComp extends CCComp {
return this.isLocked;
}
/**
* 设置槽位的水平位置(由 MissionCardComp 根据槽位数量计算布局后调用)。
* 首次调用时会记录基准 Y/Z后续只更新 X。
* @param x 目标水平坐标
*/
setSlotPosition(x: number) {
const current = this.node.position;
if (!this.hasFixedBasePosition) {
@@ -258,12 +392,16 @@ export class CardComp extends CCComp {
this.hasFixedBasePosition = true;
}
this.restPosition = new Vec3(x, this.fixedBaseY, this.fixedBaseZ);
// 拖拽/使用中不立即移动,等状态结束后归位
if (!this.isDragging && !this.isUsing) {
this.node.setPosition(this.restPosition);
}
}
/** 系统清槽:用于任务开始/结束等强制重置场景 */
/**
* 系统清槽:用于任务开始/结束等强制重置场景。
* 停止所有动画 → 重置全部状态 → 清空显示 → 隐藏节点。
*/
clearBySystem() {
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
@@ -284,7 +422,12 @@ export class CardComp extends CCComp {
this.node.active = false;
}
/** 卡牌被玩家使用后的清槽行为 */
// ======================== 内部清理 ========================
/**
* 卡牌被玩家使用后的清槽行为。
* 与 clearBySystem 类似,但不重置锁定态。
*/
private clearAfterUse() {
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
@@ -304,7 +447,9 @@ export class CardComp extends CCComp {
this.node.active = false;
}
/** 绑定触控:卡面点击使用,锁按钮点击切换锁定 */
// ======================== 事件绑定 ========================
/** 绑定触控事件:卡面点击/ 拖拽使用,锁按钮点击切换锁定 */
private bindEvents() {
this.node.on(NodeEventType.TOUCH_START, this.onCardTouchStart, this);
this.node.on(NodeEventType.TOUCH_MOVE, this.onCardTouchMove, this);
@@ -324,12 +469,18 @@ export class CardComp extends CCComp {
this.unLock?.off(NodeEventType.TOUCH_END, this.onToggleLock, this);
}
// ======================== 触摸交互 ========================
/** 触摸开始:记录起点 Y进入拖拽状态 */
private onCardTouchStart(event: EventTouch) {
if (!this.cardData || this.isUsing) return;
this.touchStartY = event.getUILocation().y;
this.isDragging = true;
}
/**
* 触摸移动跟随手指向上偏移卡牌仅允许向上拖拽deltaY < 0 被 clamp 为 0
*/
private onCardTouchMove(event: EventTouch) {
if (!this.isDragging || !this.cardData || this.isUsing) return;
const currentY = event.getUILocation().y;
@@ -337,7 +488,11 @@ export class CardComp extends CCComp {
this.node.setPosition(this.restPosition.x, this.restPosition.y + deltaY, this.restPosition.z);
}
/** 上拉超过阈值才视为使用,否则回弹原位 */
/**
* 触摸结束:
* - 上拉距离 >= dragUseThreshold → 视为"使用卡牌"
* - 否则视为"点击",打开英雄信息弹窗(仅英雄卡)并回弹
*/
private onCardTouchEnd(event: EventTouch) {
if (!this.isDragging || !this.cardData || this.isUsing) return;
const endY = event.getUILocation().y;
@@ -351,13 +506,17 @@ export class CardComp extends CCComp {
this.playReboundAnim();
}
/** 触摸取消:回弹至原位 */
private onCardTouchCancel() {
if (!this.isDragging || this.isUsing) return;
this.isDragging = false;
this.playReboundAnim();
}
/** 点击锁控件:切换锁态;空槽不允许锁定 */
/**
* 点击锁控件:切换锁态(空槽不允许锁定)。
* 阻止事件冒泡,避免触发卡面的点击使用。
*/
private onToggleLock(event?: EventTouch) {
if (!this.cardData) return;
this.isLocked = !this.isLocked;
@@ -372,22 +531,38 @@ export class CardComp extends CCComp {
}
}
/** 根据锁态刷新 Lock / unLock 显示Lock=可点击上锁unLock=可点击解锁) */
// ======================== UI 渲染 ========================
/**
* 根据锁态刷新 Lock / unLock 节点显示。
* 当前功能已注释(锁定 UI 暂未启用),保留接口以备后续启用。
*/
private updateLockUI() {
// if (this.Lock) this.Lock.active = !this.isLocked;
// if (this.unLock) this.unLock.active = this.isLocked;
}
/** 根据当前 cardData 渲染卡面文字与图标 */
/**
* 根据当前 cardData 渲染卡面文字与图标。
*
* 渲染逻辑根据卡牌类型分三路:
* 1. 英雄卡显示英雄名带星级后缀、AP / HP 数值、idle 动画。
* 2. 技能卡:显示技能名(带品质后缀)、描述文本、静态图标。
* 3. 特殊卡:显示卡名(带品质后缀)、描述文本、静态图标。
*
* 同时根据 pool_lv 切换背景底框,根据 hero_lv / card_lv 切换普通/高级边框。
*/
private applyCardUI() {
if (!this.cardData) {
this.applyEmptyUI();
return;
}
// 递增视觉令牌,用于异步加载竞态保护
this.iconVisualToken += 1;
if (this.opacityComp) this.opacityComp.opacity = 255;
this.node.setPosition(this.restPosition);
// ---- 卡牌种类标识(近战 / 远程 / 辅助等) ----
if (this.Ckind_node) {
const kindName = CKind[this.cardData.kind];
this.Ckind_node.children.forEach(child => {
@@ -395,6 +570,7 @@ export class CardComp extends CCComp {
});
}
// ---- 背景底框(按卡池等级显示对应子节点) ----
const cardLvStr = `lv${this.cardData.pool_lv}`;
if (this.BG_node) {
this.BG_node.children.forEach(child => {
@@ -402,6 +578,7 @@ export class CardComp extends CCComp {
});
}
// ---- 品质边框(高级 vs 普通) ----
const card_lv_val = this.cardData.card_lv ?? 1;
const isHighLevel = (this.cardData.hero_lv ?? 0) > 1 || card_lv_val > 1;
if (this.HF_node) this.HF_node.active = isHighLevel;
@@ -415,7 +592,9 @@ export class CardComp extends CCComp {
if(isHighLevel){activeFrameNode.getChildByName("light").active=true}
}
// ---- 按卡牌类型渲染具体内容 ----
if(this.card_type===CardType.Hero){
// 英雄卡:显示英雄名 + 星级 + AP/HP
const hero = HeroInfo[this.card_uuid];
const heroLv = Math.max(1, Math.floor(this.cardData.hero_lv ?? hero?.lv ?? 1));
const suffix = heroLv >= 2 ? "★".repeat(heroLv - 1) : "";
@@ -425,6 +604,7 @@ export class CardComp extends CCComp {
this.info_node.getChildByName("ap").getChildByName("val").getComponent(Label).string = `${(hero?.ap ?? 0) * heroLv}`;
this.info_node.getChildByName("hp").getChildByName("val").getComponent(Label).string = `${(hero?.hp ?? 0) * heroLv}`;
}else if(this.card_type===CardType.Skill){
// 技能卡:显示技能名 + 品质后缀 + 描述
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));
@@ -434,6 +614,7 @@ export class CardComp extends CCComp {
this.oinfo_node.active = true;
this.oinfo_node.getChildByName("info").getComponent(Label).string = `${skillCard?.info || skill?.info || ""}`;
}else{
// 特殊卡(升级 / 刷新):显示卡名 + 品质后缀 + 描述
const specialCard = this.card_type === CardType.SpecialUpgrade
? SpecialUpgradeCardList[this.card_uuid]
: SpecialRefreshCardList[this.card_uuid];
@@ -445,13 +626,17 @@ export class CardComp extends CCComp {
this.oinfo_node.getChildByName("info").getComponent(Label).string = `${specialCard?.info || ""}`;
}
// ---- 费用标签 ----
this.setLabel(this.cost_node, `${this.card_cost}`);
// ---- 图标 ----
const iconNode = this.icon_node as Node;
if (this.card_type === CardType.Hero) {
// 英雄卡使用 AnimationClip加载 idle 动画
this.updateHeroAnimation(iconNode, this.card_uuid, this.iconVisualToken);
return;
}
// 非英雄卡使用静态图标
this.clearIconAnimation(iconNode);
const iconId = this.resolveCardIconId(this.card_type, this.card_uuid);
if (iconId) {
@@ -462,6 +647,9 @@ export class CardComp extends CCComp {
}
}
// ======================== 动画表现 ========================
/** 卡牌入场动画:先缩小再弹大再回归正常比例 */
private playRefreshAnim() {
Tween.stopAllByTarget(this.node);
this.node.setPosition(this.restPosition);
@@ -472,6 +660,7 @@ export class CardComp extends CCComp {
.start();
}
/** 回弹动画:从当前位置平滑回到静止位并恢复缩放 */
private playReboundAnim() {
Tween.stopAllByTarget(this.node);
tween(this.node)
@@ -482,6 +671,14 @@ export class CardComp extends CCComp {
.start();
}
/**
* 使用消失动画:
* - 节点向上移动 120px 并缩小至 0.8
* - 同时 UIOpacity 渐隐至 0
* - 动画完成后调用 onComplete 回调
*
* @param onComplete 动画完成后的回调
*/
private playUseDisappearAnim(onComplete: () => void) {
const targetPos = new Vec3(this.restPosition.x, this.restPosition.y + 120, this.restPosition.z);
Tween.stopAllByTarget(this.node);
@@ -501,7 +698,10 @@ export class CardComp extends CCComp {
.start();
}
/** 渲染空槽状态 */
/**
* 渲染空槽状态:
* 清空名称、费用、信息面板、种类标识、背景底框、边框、图标。
*/
private applyEmptyUI() {
this.iconVisualToken += 1;
this.setLabel(this.name_node, "");
@@ -523,7 +723,13 @@ export class CardComp extends CCComp {
if (sprite) sprite.spriteFrame = null;
}
/** 安全设置文本,兼容节点上或子节点上的 Label */
// ======================== 工具方法 ========================
/**
* 安全设置文本,兼容节点上或子节点上的 Label
* @param node 标签所在节点
* @param value 要设置的文本
*/
private setLabel(node: Node | null, value: string) {
if (!node) return;
const label = node.getComponent(Label) || node.getComponentInChildren(Label);
@@ -531,6 +737,12 @@ export class CardComp extends CCComp {
}
/**
* 根据卡牌类型和 UUID 解析出图标 ID在 SpriteAtlas 中的帧名)。
* @param type 卡牌类型
* @param uuid 卡牌 UUID
* @returns 图标帧名称
*/
private resolveCardIconId(type: CardType, uuid: number): string {
if (type === CardType.Skill) {
return SkillSet[uuid]?.icon || `${uuid}`;
@@ -541,6 +753,10 @@ export class CardComp extends CCComp {
return `${uuid}`;
}
/**
* 打开英雄信息弹窗IBox
* 仅当当前卡为英雄卡且 HeroInfo 有效时生效。
*/
private openHeroInfoIBox() {
if (!this.cardData) return;
if (this.cardData.type !== CardType.Hero) return;
@@ -554,6 +770,14 @@ export class CardComp extends CCComp {
});
}
/**
* 为英雄卡图标加载并播放 idle 动画。
* 使用 token 做竞态保护,确保异步回调时不会覆盖已更新的图标。
*
* @param node 图标节点
* @param uuid 英雄 UUID
* @param token 当前视觉令牌
*/
private updateHeroAnimation(node: Node, uuid: number, token: number) {
const sprite = node?.getComponent(Sprite) || node?.getComponentInChildren(Sprite);
if (sprite) sprite.spriteFrame = null;
@@ -567,6 +791,7 @@ export class CardComp extends CCComp {
mLogger.log(this.debugMode, "CardComp", `load hero animation failed ${uuid}`, err);
return;
}
// 竞态保护:若加载期间卡面已变更则丢弃
if (token !== this.iconVisualToken || !this.cardData || this.card_type !== CardType.Hero || this.card_uuid !== uuid) {
return;
}
@@ -576,6 +801,7 @@ export class CardComp extends CCComp {
});
}
/** 清除图标节点上的动画(停止播放并移除所有 clip */
private clearIconAnimation(node: Node) {
const anim = node?.getComponent(Animation);
if (!anim) return;
@@ -583,24 +809,32 @@ export class CardComp extends CCComp {
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));
}
// ======================== 数据访问 ========================
/** 从全局单例获取当前局内金币数量 */
private getMissionCoin(): number {
const missionData = smc?.vmdata?.mission_data;
return Math.max(0, Math.floor(missionData?.coin ?? 0));
}
/** 设置当前局内金币数量(自动向下取整并 clamp 至 >= 0 */
private setMissionCoin(value: number) {
const missionData = smc?.vmdata?.mission_data;
if (!missionData) return;
missionData.coin = Math.max(0, Math.floor(value));
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
/** ECS 组件移除时的释放钩子:销毁节点 */
reset() {
this.node.destroy();
}