Обе сцены собраны через Code Mode, не руками. - loader: поднимает модули, прогревает арт карт, уходит в lobby - lobby: обе UI-ветки в одной сцене (по ARCHITECTURE.md), матч — это смена видимого корня, а не загрузка сцены - GameScreen рисует текущее состояние и доводит узлы твинами, а не проигрывает поток событий: узел карты живёт вместе со своей картой, поэтому "шестёрка ушла из руки на стол" не требует отдельного случая - один тап = ход целиком; логика вынесена в core/rules/intent.ts и покрыта тестами, в том числе проверкой, что она никогда не предложит ход, который правила отвергнут - арт карт переехал в assets/resources: .pac — это инструкция упаковки на сборке, а не рантайм-ассет, и без ссылок на кадры в сборку не попадало ничего. Теперь все 37 спрайтов в одной текстуре 2048x2048 - клики кнопок подписываются в коде, а не через clickEvents сцены - строки интерфейса собраны в одном файле Сборка web-mobile проходит чисто, все ссылки в сцене проверены. Вживую не запускалось: браузер в этом окружении не поднимает главный цикл движка. Подробности — в docs/TECHNICAL.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
110 lines
3.4 KiB
TypeScript
110 lines
3.4 KiB
TypeScript
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;
|
|
}
|
|
}
|