Scaffold RogueHunter: roguelike deck-builder on Phaser 4 + React

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.
This commit is contained in:
Nicolas Civade
2026-08-12 22:10:32 +02:00
parent 10e5e982dd
commit c55e0751e6
41 changed files with 5523 additions and 2 deletions

View File

@@ -0,0 +1,48 @@
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;
}