/** * @file SCardComp.ts * @description 技能卡牌槽位组件(UI 视图层) * * 职责: * 1. 管理技能卡牌槽位的显示和交互(点击使用)。 * 2. 渲染技能卡面。 * 3. 触发使用时扣除费用并分发 UseSkillCard 事件。 */ import { mLogger } from "../common/Logger"; import { _decorator, Animation, AnimationClip, EventTouch, Label, Node, NodeEventType, Sprite, SpriteAtlas, Tween, tween, UIOpacity, Vec3, UITransform, Widget } 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, CKind, CardPoolList } from "../common/config/CardSet"; import { CardBgComp } from "./CardBgComp"; import { SkillSet } from "../common/config/SkillSet"; import { GameEvent } from "../common/config/GameEvent"; import { oops } from "db://oops-framework/core/Oops"; import { smc } from "../common/SingletonModuleComp"; import { MissionEconomy } from "./MissionEconomy"; const { ccclass, property } = _decorator; @ccclass('SCardComp') @ecs.register('SCardComp', false) export class SCardComp extends CCComp { private debugMode: boolean = true; @property(Node) name_node: Node = null! @property(Node) icon_node: Node = null! @property(Node) cost_node: Node = null! @property(Node) Ckind_node: Node = null! @property(Node) BG_node: Node = null! @property(Node) info_node: Node = null! card_cost: number = 0 card_type: CardType = CardType.Skill card_uuid: number = 0 private cardData: CardConfig | null = null; private touchStartY: number = 0; private touchStartX: number = 0; private isDragging: boolean = false; private isUsing: boolean = false; private restPosition: Vec3 = new Vec3(); private hasFixedBasePosition: boolean = false; private fixedBaseY: number = 0; private fixedBaseZ: number = 0; private opacityComp: UIOpacity | null = null; private iconVisualToken: number = 0; onLoad() { this.bindEvents(); this.restPosition = this.node.position.clone(); this.opacityComp = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity); this.opacityComp.opacity = 255; this.applyEmptyUI(); } onDestroy() { super.onDestroy(); this.unbindEvents(); } init() { } start() { this.node.active = true; } applyDrawCard(data: CardConfig | null): boolean { if (!data) return false; this.cardData = data; this.card_uuid = data.uuid; this.card_type = data.type; this.card_cost = Math.floor(data.cost ?? 0); this.node.active = true; this.applyCardUI(); this.playRefreshAnim(); mLogger.log(this.debugMode, "SCardComp", "skill card updated", { uuid: this.card_uuid, cost: this.card_cost }); return true; } useCard(): CardConfig | null { if (!this.cardData || this.isUsing) return null; const cardCost = this.card_cost; const success = MissionEconomy.spendCoin(cardCost); if (!success) { oops.gui.toast(`金币不足,需要${cardCost}`); this.playReboundAnim(); return null; } smc.vmdata.scores.refresh_hit_count++; this.isUsing = true; const used = this.cardData; this.playUseDisappearAnim(() => { this.clearAfterUse(); this.isUsing = false; oops.message.dispatchEvent(GameEvent.UseSkillCard, used); }); return used; } hasCard(): boolean { return !!this.cardData; } setSlotPosition(x: number) { const current = this.node.position; if (!this.hasFixedBasePosition) { this.fixedBaseY = current.y; this.fixedBaseZ = current.z; 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) { Tween.stopAllByTarget(this.opacityComp); this.opacityComp.opacity = 255; } this.cardData = null; this.card_uuid = 0; this.card_cost = 0; this.isDragging = false; this.isUsing = false; this.node.setPosition(this.restPosition); this.node.setScale(new Vec3(1, 1, 1)); 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.isDragging = false; this.node.setPosition(this.restPosition); this.node.setScale(new Vec3(1, 1, 1)); 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); } private unbindEvents() { if (this.node && this.node.isValid) { 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); } } private onCardTouchStart(event: EventTouch) { if (!this.cardData || this.isUsing) return; this.touchStartY = event.getUILocation().y; this.touchStartX = event.getUILocation().x; this.isDragging = true; } private onCardTouchMove(event: EventTouch) { if (!this.isDragging || !this.cardData || this.isUsing) return; // 技能卡不支持上拉移动 } private onCardTouchEnd(event: EventTouch) { if (!this.isDragging || !this.cardData || this.isUsing) return; const endY = event.getUILocation().y; const endX = event.getUILocation().x; const deltaY = endY - this.touchStartY; const deltaX = endX - this.touchStartX; this.isDragging = false; // 点击触发 if (Math.abs(deltaY) < 20 && Math.abs(deltaX) < 20) { const used = this.useCard(); if (!used) { this.playReboundAnim(); } return; } this.playReboundAnim(); } private onCardTouchCancel() { if (!this.isDragging || this.isUsing) return; this.isDragging = false; this.playReboundAnim(); } private applyCardUI() { if (!this.cardData) { this.applyEmptyUI(); return; } this.iconVisualToken += 1; if (this.opacityComp) this.opacityComp.opacity = 255; this.node.setPosition(this.restPosition.x, this.restPosition.y, this.restPosition.z); const kindName = CKind[this.cardData.kind]; if (this.BG_node) { const bgLv = this.cardData.base_pool_lv ?? this.cardData.pool_lv; this.BG_node.children.forEach(child => { child.active = (child.name === kindName); const bg = child.getComponent(CardBgComp); if (bg) child.active ? bg.apply(bgLv) : bg.clear(); }); } if (this.Ckind_node) { this.Ckind_node.children.forEach(child => { child.active = (child.name === kindName); }); } this.node.children.forEach(child => { const widget = child.getComponent(Widget); if (widget) widget.updateAlignment(); child.children.forEach(subChild => { const subWidget = subChild.getComponent(Widget); if (subWidget) subWidget.updateAlignment(); }); }); 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)); const spSuffix = card_lv >= 2 ? "★".repeat(card_lv - 1) : ""; this.setLabel(this.name_node, `${spSuffix}${skillCard?.name || skill?.name || ""}${spSuffix}`); if (this.info_node) this.info_node.active = true; if (this.cost_node) { this.cost_node.active = true; const numNode = this.cost_node.getChildByName("num"); if (numNode) { this.setLabel(numNode, `${this.card_cost}`); } } const iconNode = this.icon_node as Node; if (iconNode) { iconNode.setScale(new Vec3(1, 1, 1)); this.clearIconAnimation(iconNode); const iconId = skill?.icon || `${this.card_uuid}`; this.updateIcon(iconNode, iconId); } } 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: new Vec3(this.restPosition.x, this.restPosition.y, this.restPosition.z), 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() { if (this.BG_node) { this.BG_node.children.forEach(child => { child.active = false; const bg = child.getComponent(CardBgComp); if (bg) bg.clear(); }); } this.node.children.forEach(child => { const widget = child.getComponent(Widget); if (widget) widget.updateAlignment(); child.children.forEach(subChild => { const subWidget = subChild.getComponent(Widget); if (subWidget) subWidget.updateAlignment(); }); }); this.iconVisualToken += 1; this.setLabel(this.name_node, ""); if (this.cost_node) { const numNode = this.cost_node.getChildByName("num"); if (numNode) { this.setLabel(numNode, ""); } this.cost_node.active = false; } if (this.Ckind_node) { this.Ckind_node.children.forEach(child => { child.active = false; }); } if (this.info_node) this.info_node.active = false; if (this.icon_node) { (this.icon_node as Node).setScale(new Vec3(1, 1, 1)); this.clearIconAnimation(this.icon_node as Node); const sprite = this.icon_node.getComponent(Sprite) || this.icon_node.getComponentInChildren(Sprite); if (sprite) sprite.spriteFrame = null; } } private setLabel(node: Node | null, value: string) { if (!node) return; const label = node.getComponent(Label) || node.getComponentInChildren(Label); if (label) label.string = value; } private updateIcon(node: Node, iconId: string) { if (!node || !iconId) return; const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite); if (!sprite) return; if (smc.uiconsAtlas) { const frame = smc.uiconsAtlas.getSpriteFrame(iconId); sprite.spriteFrame = frame || null; } } private clearIconAnimation(node: Node) { const anim = node?.getComponent(Animation); if (!anim) return; anim.stop(); const clips = (anim as any).clips as AnimationClip[] | undefined; if (!clips || clips.length === 0) return; [...clips].forEach(clip => anim.removeClip(clip, true)); } /** ECS 组件移除时的释放钩子:销毁节点 */ reset() { this.node.destroy(); } }