feat: 为卡牌组件添加拖拽使用动画并增强日志

- 为 CardComp 添加拖拽使用交互:上拉超过阈值触发使用,否则回弹
- 增加卡牌刷新、回弹、使用消失的 Tween 动画
- 在 MissionCardComp 和 CardComp 的关键节点添加调试日志
- 修复升级按钮在达到最大等级后隐藏升级提示的问题
- 优化卡牌使用和清槽时的动画与状态重置逻辑
This commit is contained in:
walkpan
2026-03-14 09:42:08 +08:00
parent d0e824e93b
commit b32cea1c00
2 changed files with 153 additions and 8 deletions

View File

@@ -1,5 +1,5 @@
import { mLogger } from "../common/Logger";
import { _decorator, Label, Node, NodeEventType, Sprite, SpriteAtlas, resources } from "cc";
import { _decorator, EventTouch, Label, Node, NodeEventType, Sprite, SpriteAtlas, Tween, tween, UIOpacity, Vec3 } 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 } from "../common/config/CardSet";
@@ -38,10 +38,19 @@ export class CardComp extends CCComp {
private uiconsAtlas: SpriteAtlas | null = null;
/** 当前槽位承载的卡牌数据null 表示空槽 */
private cardData: CardConfig | null = null;
private readonly dragUseThreshold: number = 70;
private touchStartY: number = 0;
private isDragging: boolean = false;
private isUsing: boolean = false;
private restPosition: Vec3 = new Vec3();
private opacityComp: UIOpacity | null = null;
onLoad() {
/** 初始阶段只做UI状态准备不触发业务逻辑 */
this.bindEvents();
this.restPosition = this.node.position.clone();
this.opacityComp = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity);
this.opacityComp.opacity = 255;
this.updateLockUI();
this.applyEmptyUI();
@@ -109,14 +118,28 @@ export class CardComp extends CCComp {
this.card_cost = data.cost;
this.node.active = true;
this.applyCardUI();
this.playRefreshAnim();
mLogger.log(this.debugMode, "CardComp", "card updated", {
uuid: this.card_uuid,
type: this.card_type,
cost: this.card_cost
});
return true;
}
/** 使用当前卡牌仅做UI层清空不触发效果事件下一步再接 */
useCard(): CardConfig | null {
if (!this.cardData) return null;
if (!this.cardData || this.isUsing) return null;
this.isUsing = true;
const used = this.cardData;
this.clearAfterUse();
mLogger.log(this.debugMode, "CardComp", "use card", {
uuid: used.uuid,
type: used.type
});
this.playUseDisappearAnim(() => {
this.clearAfterUse();
this.isUsing = false;
});
return used;
}
@@ -138,43 +161,95 @@ export class CardComp extends CCComp {
/** 系统清槽:用于任务开始/结束等强制重置场景 */
clearBySystem() {
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
this.opacityComp.opacity = 255;
}
this.cardData = null;
this.card_uuid = 0;
this.card_cost = 0;
this.card_type = CardType.Hero;
this.isLocked = false;
this.isDragging = false;
this.isUsing = false;
this.node.setPosition(this.restPosition);
this.node.setScale(new Vec3(1, 1, 1));
this.updateLockUI();
this.applyEmptyUI();
this.node.active = false;
}
/** 卡牌被玩家使用后的清槽行为 */
private clearAfterUse() {
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
this.opacityComp.opacity = 255;
}
this.cardData = null;
this.card_uuid = 0;
this.card_cost = 0;
this.card_type = CardType.Hero;
this.isLocked = false;
this.isDragging = false;
this.node.setPosition(this.restPosition);
this.node.setScale(new Vec3(1, 1, 1));
this.updateLockUI();
this.applyEmptyUI();
this.node.active = false;
}
/** 绑定触控:卡面点击使用,锁按钮点击切换锁定 */
private bindEvents() {
this.node.on(NodeEventType.TOUCH_START, this.onCardTouchStart, this);
this.node.on(NodeEventType.TOUCH_MOVE, this.onCardTouchMove, this);
this.node.on(NodeEventType.TOUCH_END, this.onCardTouchEnd, this);
this.node.on(NodeEventType.TOUCH_CANCEL, this.onCardTouchCancel, this);
this.Lock?.on(NodeEventType.TOUCH_END, this.onToggleLock, this);
this.unLock?.on(NodeEventType.TOUCH_END, this.onToggleLock, this);
}
/** 解绑触控,防止节点销毁后残留回调 */
private unbindEvents() {
this.node.off(NodeEventType.TOUCH_START, this.onCardTouchStart, this);
this.node.off(NodeEventType.TOUCH_MOVE, this.onCardTouchMove, this);
this.node.off(NodeEventType.TOUCH_END, this.onCardTouchEnd, this);
this.node.off(NodeEventType.TOUCH_CANCEL, this.onCardTouchCancel, this);
this.Lock?.off(NodeEventType.TOUCH_END, this.onToggleLock, this);
this.unLock?.off(NodeEventType.TOUCH_END, this.onToggleLock, this);
}
/** 点击卡面执行单卡使用仅UI变化 */
private onCardTouchEnd() {
this.useCard();
private onCardTouchStart(event: EventTouch) {
if (!this.cardData || this.isUsing) return;
this.touchStartY = event.getUILocation().y;
this.isDragging = true;
}
private onCardTouchMove(event: EventTouch) {
if (!this.isDragging || !this.cardData || this.isUsing) return;
const currentY = event.getUILocation().y;
const deltaY = Math.max(0, currentY - this.touchStartY);
this.node.setPosition(this.restPosition.x, this.restPosition.y + deltaY, this.restPosition.z);
}
/** 上拉超过阈值才视为使用,否则回弹原位 */
private onCardTouchEnd(event: EventTouch) {
if (!this.isDragging || !this.cardData || this.isUsing) return;
const endY = event.getUILocation().y;
const deltaY = endY - this.touchStartY;
this.isDragging = false;
if (deltaY >= this.dragUseThreshold) {
this.useCard();
return;
}
this.playReboundAnim();
}
private onCardTouchCancel() {
if (!this.isDragging || this.isUsing) return;
this.isDragging = false;
this.playReboundAnim();
}
/** 点击锁控件:切换锁态;空槽不允许锁定 */
@@ -182,6 +257,10 @@ export class CardComp extends CCComp {
if (!this.cardData) return;
this.isLocked = !this.isLocked;
this.updateLockUI();
mLogger.log(this.debugMode, "CardComp", "toggle lock", {
uuid: this.card_uuid,
locked: this.isLocked
});
event?.stopPropagation();
}
@@ -197,6 +276,8 @@ export class CardComp extends CCComp {
this.applyEmptyUI();
return;
}
if (this.opacityComp) this.opacityComp.opacity = 255;
this.node.setPosition(this.restPosition);
this.setLabel(this.name_node, `${CardType[this.card_type]}-${this.card_uuid}`);
this.setLabel(this.cost_node, `${this.card_cost}`);
if (this.ap_node) this.ap_node.active = false;
@@ -204,6 +285,45 @@ export class CardComp extends CCComp {
this.updateIcon(this.icon_node, `${this.card_uuid}`);
}
private playRefreshAnim() {
Tween.stopAllByTarget(this.node);
this.node.setPosition(this.restPosition);
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();
}
private playReboundAnim() {
Tween.stopAllByTarget(this.node);
tween(this.node)
.to(0.12, {
position: this.restPosition,
scale: new Vec3(1, 1, 1)
})
.start();
}
private playUseDisappearAnim(onComplete: () => void) {
const targetPos = new Vec3(this.restPosition.x, this.restPosition.y + 120, this.restPosition.z);
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
this.opacityComp.opacity = 255;
tween(this.opacityComp)
.to(0.18, { opacity: 0 })
.start();
}
tween(this.node)
.to(0.18, {
position: targetPos,
scale: new Vec3(0.8, 0.8, 1)
})
.call(onComplete)
.start();
}
/** 渲染空槽状态 */
private applyEmptyUI() {
this.setLabel(this.name_node, "");

View File

@@ -40,6 +40,10 @@ export class MissionCardComp extends CCComp {
this.bindEvents();
this.cacheCardComps();
this.onMissionStart();
mLogger.log(this.debugMode, "MissionCardComp", "onLoad init", {
slots: this.cardComps.length,
poolLv: this.poolLv
});
}
onDestroy() {
@@ -53,8 +57,14 @@ export class MissionCardComp extends CCComp {
onMissionStart() {
this.poolLv = CARD_POOL_INIT_LEVEL;
this.clearAllCards();
if (this.cards_up) {
this.cards_up.active = true;
}
this.updatePoolLvUI();
this.node.active = true;
mLogger.log(this.debugMode, "MissionCardComp", "mission start", {
poolLv: this.poolLv
});
}
/** 任务结束时清空4槽并隐藏面板 */
@@ -100,14 +110,23 @@ export class MissionCardComp extends CCComp {
/** 抽卡按钮每次固定抽4张然后顺序分发给4个单卡脚本 */
private onClickDraw() {
mLogger.log(this.debugMode, "MissionCardComp", "click draw", {
poolLv: this.poolLv
});
const cards = this.buildDrawCards();
this.dispatchCardsToSlots(cards);
}
/** 升级按钮:仅提升卡池等级,卡槽是否更新由下一次抽卡触发 */
private onClickUpgrade() {
if (this.poolLv >= CARD_POOL_MAX_LEVEL) return;
if (this.poolLv >= CARD_POOL_MAX_LEVEL) {
mLogger.log(this.debugMode, "MissionCardComp", "pool already max", this.poolLv);
return;
}
this.poolLv += 1;
if (this.poolLv >= CARD_POOL_MAX_LEVEL && this.cards_up) {
this.cards_up.active = false;
}
this.updatePoolLvUI();
mLogger.log(this.debugMode, "MissionCardComp", "pool level up", this.poolLv);
}
@@ -130,7 +149,12 @@ export class MissionCardComp extends CCComp {
/** 全量分发给4槽每个槽位是否接收由 CardComp 自己判断(锁定可跳过) */
private dispatchCardsToSlots(cards: CardConfig[]) {
for (let i = 0; i < this.cardComps.length; i++) {
this.cardComps[i].applyDrawCard(cards[i] ?? null);
const accepted = this.cardComps[i].applyDrawCard(cards[i] ?? null);
mLogger.log(this.debugMode, "MissionCardComp", "dispatch card", {
index: i,
card: cards[i]?.uuid ?? 0,
accepted
});
}
}
@@ -145,6 +169,7 @@ export class MissionCardComp extends CCComp {
const label = this.cards_up.getComponentInChildren(Label);
if (!label) return;
label.string = `卡池Lv.${this.poolLv}`;
mLogger.log(this.debugMode, "MissionCardComp", "pool lv ui update", this.poolLv);
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */