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,93 @@
import type { CardData } from './card.types';
/**
* Central card library. All card definitions live here, keyed by id.
* Content is data — add cards by adding entries, not code paths.
*/
export const CARD_LIBRARY: Record<string, CardData> = {
strike: {
id: 'strike',
name: 'Strike',
type: 'attack',
rarity: 'starter',
cost: 1,
target: 'enemy',
description: 'Deal 6 damage.',
effect: { damage: 6 },
},
defend: {
id: 'defend',
name: 'Defend',
type: 'skill',
rarity: 'starter',
cost: 1,
target: 'self',
description: 'Gain 5 block.',
effect: { block: 5 },
},
bash: {
id: 'bash',
name: 'Bash',
type: 'attack',
rarity: 'starter',
cost: 2,
target: 'enemy',
description: 'Deal 10 damage.',
effect: { damage: 10 },
},
quick_slash: {
id: 'quick_slash',
name: 'Quick Slash',
type: 'attack',
rarity: 'common',
cost: 1,
target: 'enemy',
description: 'Deal 4 damage. Draw 1 card.',
effect: { damage: 4, draw: 1 },
},
cleave: {
id: 'cleave',
name: 'Cleave',
type: 'attack',
rarity: 'common',
cost: 1,
target: 'all-enemies',
description: 'Deal 8 damage to ALL enemies.',
effect: { damage: 8 },
},
flurry: {
id: 'flurry',
name: 'Flurry',
type: 'attack',
rarity: 'uncommon',
cost: 1,
target: 'enemy',
description: 'Deal 3 damage 3 times.',
effect: { damage: 3, hits: 3 },
},
adrenaline: {
id: 'adrenaline',
name: 'Adrenaline',
type: 'skill',
rarity: 'uncommon',
cost: 0,
target: 'none',
description: 'Gain 1 energy. Draw 2 cards. Exhaust.',
effect: { energy: 1, draw: 2 },
exhaust: true,
},
};
/** Look up a card definition by id; throws early on typos rather than failing silently. */
export function getCard(id: string): CardData {
const card = CARD_LIBRARY[id];
if (!card) throw new Error(`Unknown card id: ${id}`);
return card;
}
/** The deck every run begins with (Slay the Spire-style: 5 Strike, 4 Defend, 1 Bash). */
export const STARTER_DECK: readonly string[] = [
'strike', 'strike', 'strike', 'strike', 'strike',
'defend', 'defend', 'defend', 'defend',
'bash',
];