- 在 CardComp 中新增 setSlotPosition 方法,支持动态设置卡槽位置 - 在 MissionCardComp 中实现 layoutCardSlots 方法,根据卡槽数量自动水平居中布局 - 在任务开始、抽卡等关键时机调用布局更新,确保卡槽位置正确 - 禁用角色控制器预制件中的节点,防止其干扰UI交互
357 lines
12 KiB
TypeScript
357 lines
12 KiB
TypeScript
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;
|
||
}
|
||
|
||
setSlotPosition(x: number) {
|
||
const current = this.node.position;
|
||
this.restPosition = new Vec3(x, current.y, current.z);
|
||
if (!this.isDragging && !this.isUsing) {
|
||
this.node.setPosition(this.restPosition);
|
||
}
|
||
}
|
||
|
||
/** 系统清槽:用于任务开始/结束等强制重置场景 */
|
||
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();
|
||
}
|
||
}
|