Vite + TypeScript + React 19 shell hosting a Phaser 4 game canvas,
structured as a Slay the Spire-style single-player deck-builder.
Architecture - strict core/render split:
- core/ holds all game rules as pure TypeScript with zero Phaser
imports, so the domain is deterministic and engine-agnostic:
- cards/ data-driven card model, library, and starter deck
- combat/ turn-based CombatEngine (energy, block, draw/discard/
exhaust piles, enemy intents) + enemy definitions
- run/ RunManager (deck/hp/gold) and seeded MapGenerator
- shared/rng seedable mulberry32 for reproducible runs
- game/ is the Phaser layer: it renders core state and forwards input
- scenes/ Menu -> Map -> Combat -> Reward -> GameOver
- components/ CardSprite, CombatantSprite, Button
- events/ typed EventBus bridging combat/run state to a React HUD
- config/ centralized scene keys, colors, fonts, layout constants
Playable loop: traverse a deterministic 10-node map, fight turn-based
card combat against enemies with telegraphed intents, pick a card
reward after each win, culminating in a boss.
Verified: npm run lint and npm run build pass.
74 lines
2.0 KiB
TypeScript
74 lines
2.0 KiB
TypeScript
import * as Phaser from 'phaser';
|
|
import { SceneKeys, Colors, Fonts } from '../../config';
|
|
import { Button } from '../../components/button/Button';
|
|
|
|
interface GameOverData {
|
|
floor?: number;
|
|
victory?: boolean;
|
|
}
|
|
|
|
export class GameOverScene extends Phaser.Scene {
|
|
private floor = 1;
|
|
private victory = false;
|
|
|
|
constructor() {
|
|
super({ key: SceneKeys.GAME_OVER });
|
|
}
|
|
|
|
init(data: GameOverData): void {
|
|
this.floor = data.floor ?? 1;
|
|
this.victory = data.victory ?? false;
|
|
}
|
|
|
|
create(): void {
|
|
this.layout();
|
|
|
|
const onResize = () => this.layout();
|
|
this.scale.on('resize', onResize);
|
|
this.events.once('shutdown', () => this.scale.off('resize', onResize));
|
|
}
|
|
|
|
private layout(): void {
|
|
this.children.removeAll(true);
|
|
|
|
const cx = this.scale.width / 2;
|
|
const cy = this.scale.height / 2;
|
|
|
|
this.add.text(cx, cy - 140, this.victory ? 'RUN COMPLETE' : 'DEFEATED', {
|
|
...Fonts.title,
|
|
color: this.victory ? '#66cc88' : '#ff4444',
|
|
stroke: Colors.stroke,
|
|
strokeThickness: 6,
|
|
}).setOrigin(0.5);
|
|
|
|
this.add.text(cx, cy - 60,
|
|
this.victory
|
|
? `You conquered all ${this.floor} floors.`
|
|
: `You fell on floor ${this.floor}.`,
|
|
{ ...Fonts.button, color: Colors.text, stroke: Colors.stroke, strokeThickness: 3 },
|
|
).setOrigin(0.5);
|
|
|
|
new Button({
|
|
scene: this,
|
|
x: cx,
|
|
y: cy + 40,
|
|
label: 'NEW RUN',
|
|
onClick: () => {
|
|
this.registry.remove('run');
|
|
this.scene.start(SceneKeys.MAP);
|
|
},
|
|
});
|
|
|
|
new Button({
|
|
scene: this,
|
|
x: cx,
|
|
y: cy + 120,
|
|
label: 'MAIN MENU',
|
|
onClick: () => {
|
|
this.registry.remove('run');
|
|
this.scene.start(SceneKeys.MENU);
|
|
},
|
|
});
|
|
}
|
|
}
|