Files
pixelheros/assets/script/game/map/CardComp.ts
walkpan b32cea1c00 feat: 为卡牌组件添加拖拽使用动画并增强日志
- 为 CardComp 添加拖拽使用交互:上拉超过阈值触发使用,否则回弹
- 增加卡牌刷新、回弹、使用消失的 Tween 动画
- 在 MissionCardComp 和 CardComp 的关键节点添加调试日志
- 修复升级按钮在达到最大等级后隐藏升级提示的问题
- 优化卡牌使用和清槽时的动画与状态重置逻辑
2026-03-14 09:42:08 +08:00

349 lines
11 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.
import { mLogger } from "../common/Logger";
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";
const { ccclass, property } = _decorator;
/** 视图层对象 */
@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!
@property(Node)
ap_node=null!
@property(Node)
hp_node=null!
@property(Node)
name_node=null!
@property(Node)
icon_node=null!
@property(Node)
cost_node=null!
card_cost:number=0
card_type:CardType=CardType.Hero
card_uuid:number=0
/** 是否处于锁定状态(锁定且有卡时,抽卡分发会被跳过) */
private isLocked: boolean = false;
/** 图标图集缓存(后续接图标资源时直接复用) */
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();
}
onDestroy() {
this.unbindEvents();
}
init(){
this.onMissionStart();
}
/** 游戏开始初始化 */
onMissionStart() {
}
/** 游戏结束清理 */
onMissionEnd() {
}
start() {
/** 单卡节点常驻,由数据控制显示内容 */
this.node.active = true;
}
/** 兼容旧接口:外部通过该入口更新卡牌 */
updateCardInfo(card:Node, data: CardConfig){
this.applyDrawCard(data);
}
private updateIcon(node: Node, iconId: string) {
}
/** 兼容旧接口:按索引更新卡牌(当前由 MissionCardComp 顺序分发) */
updateCardData(index: number, data: CardConfig) {
this.applyDrawCard(data);
}
/** 兼容按钮回调入口:触发单卡使用 */
selectCard(e: any, index: string) {
this.useCard();
}
/**
* 关闭界面
*/
close() {
}
/** 抽卡分发入口:返回 true 表示本次已成功接收新卡 */
applyDrawCard(data: CardConfig | null): boolean {
if (!data) return false;
/** 锁定且已有旧卡时,跳过本次刷新,保持老卡 */
if (this.isLocked && this.cardData) {
mLogger.log(this.debugMode, "CardComp", "slot locked, skip update", this.card_uuid);
return false;
}
this.cardData = data;
this.card_uuid = data.uuid;
this.card_type = data.type;
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 || this.isUsing) return null;
this.isUsing = true;
const used = this.cardData;
mLogger.log(this.debugMode, "CardComp", "use card", {
uuid: used.uuid,
type: used.type
});
this.playUseDisappearAnim(() => {
this.clearAfterUse();
this.isUsing = false;
});
return used;
}
/** 查询槽位是否有卡 */
hasCard(): boolean {
return !!this.cardData;
}
/** 外部设置锁定态 */
setLocked(value: boolean) {
this.isLocked = value;
this.updateLockUI();
}
/** 外部读取当前锁定态 */
isSlotLocked(): boolean {
return this.isLocked;
}
/** 系统清槽:用于任务开始/结束等强制重置场景 */
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);
}
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();
}
/** 点击锁控件:切换锁态;空槽不允许锁定 */
private onToggleLock(event?: Event) {
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();
}
/** 根据锁态刷新 Lock / unLock 显示 */
private updateLockUI() {
if (this.Lock) this.Lock.active = this.isLocked;
if (this.unLock) this.unLock.active = !this.isLocked;
}
/** 根据当前 cardData 渲染卡面文字与图标 */
private applyCardUI() {
if (!this.cardData) {
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;
if (this.hp_node) this.hp_node.active = false;
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, "");
this.setLabel(this.cost_node, "");
if (this.ap_node) this.ap_node.active = false;
if (this.hp_node) this.hp_node.active = false;
const sprite = this.icon_node?.getComponent(Sprite);
if (sprite) sprite.spriteFrame = null;
}
/** 安全设置文本,兼容节点上或子节点上的 Label */
private setLabel(node: Node | null, value: string) {
if (!node) return;
const label = node.getComponent(Label) || node.getComponentInChildren(Label);
if (label) label.string = value;
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() {
this.node.destroy();
}
}