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.
49 lines
1.2 KiB
TypeScript
49 lines
1.2 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { EventBus } from '../../game/events/EventBus';
|
|
|
|
interface CombatHud {
|
|
playerHp: number;
|
|
playerMaxHp: number;
|
|
playerBlock: number;
|
|
energy: number;
|
|
maxEnergy: number;
|
|
handCount: number;
|
|
drawCount: number;
|
|
discardCount: number;
|
|
}
|
|
|
|
const EMPTY_HUD: CombatHud = {
|
|
playerHp: 0,
|
|
playerMaxHp: 0,
|
|
playerBlock: 0,
|
|
energy: 0,
|
|
maxEnergy: 0,
|
|
handCount: 0,
|
|
drawCount: 0,
|
|
discardCount: 0,
|
|
};
|
|
|
|
/** Live combat HUD state, driven by the CombatScene via the EventBus. */
|
|
export function useCombatHud() {
|
|
const [hud, setHud] = useState<CombatHud>(EMPTY_HUD);
|
|
|
|
useEffect(() => {
|
|
return EventBus.on('combat:state-changed', setHud);
|
|
}, []);
|
|
|
|
return hud;
|
|
}
|
|
|
|
/** Current floor/encounter, driven by run + combat events. */
|
|
export function useRunProgress() {
|
|
const [floor, setFloor] = useState(1);
|
|
|
|
useEffect(() => {
|
|
const unsubNode = EventBus.on('run:node-changed', d => setFloor(d.floor));
|
|
const unsubCombat = EventBus.on('combat:started', d => setFloor(d.floor));
|
|
return () => { unsubNode(); unsubCombat(); };
|
|
}, []);
|
|
|
|
return floor;
|
|
}
|