feat(02-04): Lura dialogue overlay + Ink runtime + gate visual + Garden scene wiring

- src/ui/dialogue/ink-runtime.ts: thin wrapper around inkjs Story —
  nextLine() with text-message cadence (1500ms base + 20ms/char, capped
  at 4000ms), skipDelay() for tap-to-advance, choice surface forwarded
  to ChooseChoiceIndex. Constants exported for Plan 02-05's UX-05
  reduced-motion hook + playtest tuning.
- src/ui/dialogue/ink-runtime.test.ts: 7 cases pinning the cadence
  bounds, skipDelay one-shot semantics, choice forwarding (uses
  vi.useFakeTimers() to validate timing without wall-clock waits).
- src/ui/dialogue/ink-renderer.tsx: drips lines into the DOM as the
  runtime yields them; userSelect: 'text' for MEMR-05 copy-paste;
  click-anywhere skip; choice buttons stop event propagation.
- src/ui/dialogue/LuraDialogue.tsx: D-15 — full-screen DOM overlay
  driven by dialogueOverlayOpen + luraBeatProgress.pending. Loads the
  compiled Ink JSON via loadInkStory, binds variables from store
  snapshot, ChoosePathString into the named knot ('arrival'/'mid'/
  'farewell'), then runs InkRenderer. Close button calls
  resolvePendingLuraBeat to mark visited and clear pending.
- src/ui/dialogue/LuraDialogue.test.tsx: 6 cases — closed-state null,
  dialog renders on open+pending, Close fires resolvePendingLuraBeat
  for all three beats, loadInkStory called with correct beat name +
  knot. Mocks the loadInkStory + ink-runtime layer to keep happy-dom
  out of inkjs internals (Plan 02-05 e2e exercises the live path).
- src/render/garden/gate-renderer.ts: drawGate() + updateGateIndicator()
  — Phaser primitive gate (body / glow / hit) at canvas (880, 384).
  Glow alpha-pulses via Sine-yoyo tween when isPending=true; idempotent.
- src/game/scenes/Garden.ts: gate added in create(); pointerdown
  dispatches setDialogueOverlayOpen(true) only when a beat is pending.
  storeUnsubscribe also drives updateGateIndicator on luraBeatProgress
  changes. update() loop now calls simAdapter.applyLuraProgress when
  the sim's luraBeatProgress differs from the store's so harvests
  trigger the gate indicator. destroy() cleans up the gate's tween.
- src/App.tsx: <LuraDialogue /> mounted as DOM sibling of PhaserGame.
- src/ui/index.ts + src/render/garden/index.ts: re-exports.

13 new tests across dialogue layer; 264/264 total green; npm run ci
exits 0; Vite emits 4 lazy ink-*.js chunks (compiled JSON code-split
per file); ESLint sim-purity rule still green (sim/narrative imports
no inkjs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 10:33:22 -04:00
parent 7b79d11584
commit 661f990e9a
11 changed files with 792 additions and 2 deletions
+2 -1
View File
@@ -3,6 +3,7 @@ import { PhaserGame, type IRefPhaserGame } from './PhaserGame.tsx';
import { BeginScreen } from './ui/begin';
import { SeedPicker } from './ui/garden';
import { FragmentRevealModal, JournalIcon } from './ui/journal';
import { LuraDialogue } from './ui/dialogue';
function App() {
// PhaserGame ref — Phase 2+ will use this to access the active scene from React.
@@ -15,7 +16,7 @@ function App() {
<SeedPicker />
<FragmentRevealModal />
<JournalIcon />
{/* Plan 02-04 mounts: <LuraDialogue /> */}
<LuraDialogue />
{/* Plan 02-05 mounts: <Letter />, <Settings />, <PersistenceToast /> */}
</div>
);