import { _decorator, Component, Sprite, Tween, tween, UIOpacity, UITransform, v3, Vec3 } from 'cc'; import { Card } from '../../core/cards/Card'; import { Layout, Timing } from '../common/Theme'; import { CardLibrary } from './CardLibrary'; const { ccclass, property } = _decorator; /** * One card on screen. * * A card node belongs to a `Card` for as long as that card is visible, and * follows it from hand to table to discard. Movement is therefore just a tween * to a new position, and the natural-looking animation falls out of the layout * rather than having to be choreographed per event. */ @ccclass('CardView') export class CardView extends Component { @property(Sprite) sprite: Sprite | null = null; private _library: CardLibrary | null = null; private _card: Card | null = null; private _faceUp = false; get card(): Card | null { return this._card; } get faceUp(): boolean { return this._faceUp; } /** Called once, when the view is taken from the pool. */ bind(library: CardLibrary, card: Card | null, faceUp: boolean): void { this._library = library; this._card = card; this._faceUp = faceUp; this._applyFrame(); const transform = this.getComponent(UITransform); transform?.setContentSize(Layout.cardWidth, Layout.cardHeight); } /** Flips the card, turning it edge-on halfway through so the swap is hidden. */ flipTo(faceUp: boolean, animated = true): void { if (this._faceUp === faceUp) { return; } this._faceUp = faceUp; if (!animated) { this._applyFrame(); return; } const scale = this.node.scale.clone(); tween(this.node) .to(Timing.flip / 2, { scale: v3(0.02, scale.y, scale.z) }) .call(() => this._applyFrame()) .to(Timing.flip / 2, { scale }) .start(); } /** Moves, rotates and scales the card into place. */ moveTo(position: Vec3, scale: number, angle: number, duration: number, delay = 0): void { const target = { position, scale: v3(scale, scale, 1), angle }; if (duration <= 0) { this.node.setPosition(position); this.node.setScale(target.scale); this.node.angle = angle; return; } tween(this.node) .delay(delay) .to(duration, target, { easing: 'quadOut' }) .start(); } /** Fades out, then hands the node back through `done`. */ fadeOut(duration: number, done: () => void): void { const opacity = this.node.getComponent(UIOpacity) ?? this.node.addComponent(UIOpacity); tween(opacity) .to(duration, { opacity: 0 }) .call(() => { opacity.opacity = 255; done(); }) .start(); } /** Stops any running tween. Used when a match is torn down mid-animation. */ cancelAnimations(): void { Tween.stopAllByTarget(this.node); const opacity = this.node.getComponent(UIOpacity); if (opacity) { Tween.stopAllByTarget(opacity); opacity.opacity = 255; } } private _applyFrame(): void { if (!this.sprite || !this._library) { return; } this.sprite.spriteFrame = this._faceUp && this._card ? this._library.frameFor(this._card) : this._library.backFrame; } }