Files
ftcandClaude Opus 5 4f3a26dc45 M5-M7: сцены, лобби, экран игры и цикл матча
Обе сцены собраны через 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>
2026-09-13 11:30:28 +03:00

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;
}
}