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>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user