Files
pixelheros/assets/script/game/map/CardComp.ts
walkpan b630a97f8b feat(ui): 添加卡槽自动布局并禁用角色控制器节点
- 在 CardComp 中新增 setSlotPosition 方法,支持动态设置卡槽位置
- 在 MissionCardComp 中实现 layoutCardSlots 方法,根据卡槽数量自动水平居中布局
- 在任务开始、抽卡等关键时机调用布局更新,确保卡槽位置正确
- 禁用角色控制器预制件中的节点,防止其干扰UI交互
2026-03-14 09:42:20 +08:00

357 lines
12 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;
}
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();
}
}