chore(01-01): scaffold Phaser 4 + React 19 + Vite + TS template + Phase-1 deps + firewall directories

- Built equivalent React + Vite + TypeScript scaffold by hand because the official
  npm create @phaserjs/game@latest scaffolder is interactive-only and the documented
  --template/--yes flags are ignored (verified 2026-05-08 with create-game v1.3.2).
  Plan Step 1 explicitly authorizes this fallback. Resulting tree mirrors the
  official template shape: index.html, src/main.tsx, src/App.tsx, src/PhaserGame.tsx,
  src/game/main.ts, src/game/scenes/Boot.ts.
- Installed Phase-1 production deps at versions verified in RESEARCH.md:
  phaser@4.1.0, react@19.2.6, react-dom@19.2.6, idb@8.0.3, lz-string@1.5.0,
  zod@4.4.3, crc-32@1.2.2, gray-matter@4.0.3, yaml@2.8.4, inkjs@2.4.0.
- Installed Phase-1 dev deps: vite@8.0.11, @vitejs/plugin-react@6.0.1,
  typescript@6.0.3, @types/react@19, @types/react-dom@19, @types/node@22,
  vitest@4.1.5, @vitest/ui, happy-dom, fake-indexeddb@6 (for Plan 03 IDB tests),
  @playwright/test@1.59.1, eslint@9, eslint-plugin-boundaries@6.0.2, inklecate@1.8.1.
- Created the seven architectural-firewall directories under src/ with .gitkeep
  markers (sim, render, ui, save, content, audio, store) — siblings to the
  template-provided src/game/ — so Plan 02's ESLint boundaries rule has clean
  targets per CLAUDE.md 'Architectural Firewall'.
- Created repo-root /content/ (with /dialogue/ and /seasons/ subdirs) and /assets/
  trees per CONTEXT D-11, D-12.
- Pre-declared all downstream-required scripts in package.json so Plans 02–06 only
  edit code, not script keys: dev, build, preview, lint (--max-warnings 0 per
  RESEARCH CI Pitfall C), test (--passWithNoTests=false per CI Pitfall B),
  test:watch, validate:assets, compile:ink (no-op stub for Phase 1; Phase 2
  replaces with real inklecate invocation), ci.
- TypeScript strict mode enforced via tsconfig.json + tsconfig.app.json + tsconfig.node.json.
- npm run build succeeds (tsc -b && vite build) producing dist/index.html and
  dist/assets/index-*.js (~1.5MB Phaser bundle; code-splitting deferred to Phase 2+
  when actual scenes exist).
This commit is contained in:
2026-05-08 23:17:17 -04:00
parent 39563f6934
commit df7d687da4
25 changed files with 3641 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
import { useRef } from 'react';
import { PhaserGame, type IRefPhaserGame } from './PhaserGame.tsx';
function App() {
// PhaserGame ref — Phase 2+ will use this to access the active scene from React.
const phaserRef = useRef<IRefPhaserGame | null>(null);
return (
<div id="app">
<PhaserGame ref={phaserRef} />
</div>
);
}
export default App;
+47
View File
@@ -0,0 +1,47 @@
import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef } from 'react';
import StartGame from './game/main.ts';
import type * as Phaser from 'phaser';
export interface IRefPhaserGame {
game: Phaser.Game | null;
scene: Phaser.Scene | null;
}
interface IProps {
currentActiveScene?: (sceneInstance: Phaser.Scene) => void;
}
export const PhaserGame = forwardRef<IRefPhaserGame, IProps>(function PhaserGame(_props, ref) {
const game = useRef<Phaser.Game | null>(null);
useLayoutEffect(() => {
if (game.current === null) {
game.current = StartGame('game-container');
if (typeof ref === 'function') {
ref({ game: game.current, scene: null });
} else if (ref) {
ref.current = { game: game.current, scene: null };
}
}
return () => {
if (game.current) {
game.current.destroy(true);
game.current = null;
}
};
}, [ref]);
useEffect(() => {
// Phase 2+: subscribe to scene-ready events here and surface the active scene
// through `currentActiveScene` so React can talk to Phaser.
}, []);
useImperativeHandle(ref, () => ({
game: game.current,
scene: null,
}));
return <div id="game-container" />;
});
View File
View File
+26
View File
@@ -0,0 +1,26 @@
import * as Phaser from 'phaser';
import { Boot } from './scenes/Boot.ts';
// Phase 1: minimal Phaser config that boots cleanly. Real scenes (garden, weather,
// watercolor post-process) land in Phase 2+. The architectural-firewall directories
// (src/sim, src/render, src/ui) are siblings to this one — see `.planning/phases/
// 01-foundations-and-doctrine/01-RESEARCH.md` § "Architectural Responsibility Map".
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: 1024,
height: 768,
parent: 'game-container',
backgroundColor: '#1a1a1a',
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
scene: [Boot],
};
const StartGame = (parent: string): Phaser.Game => {
return new Phaser.Game({ ...config, parent });
};
export default StartGame;
+19
View File
@@ -0,0 +1,19 @@
import * as Phaser from 'phaser';
// Phase 1 placeholder: empty Boot scene that just proves Phaser starts.
// Phase 2 replaces this with the real boot → preloader → garden flow,
// gated behind the "Tend the garden / Begin" gesture screen that calls
// AudioContext.resume() per CLAUDE.md banner concern #7.
export class Boot extends Phaser.Scene {
constructor() {
super('Boot');
}
preload(): void {
// No assets in Phase 1.
}
create(): void {
// Phase 2 will start the preloader from here.
}
}
+14
View File
@@ -0,0 +1,14 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
const rootEl = document.getElementById('root');
if (!rootEl) {
throw new Error('Root element #root not found in index.html');
}
createRoot(rootEl).render(
<StrictMode>
<App />
</StrictMode>
);
View File
View File
View File
View File
View File
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />