A pure-frontend roguelite detective game. Every case is uniquely generated. 18 case templates, 23 achievements, 5 skill trees. Play offline as a PWA.
- Read the briefing — Victim, location, cause of death, initial suspects
- Collect clues — Unlock clues and drag them onto the evidence board
- Connect the dots — Double-click clues to draw relation lines (Supports / Contradicts / Related)
- Interrogate suspects — Multi-turn dialogue with pressure mechanics, present evidence to break lies
- Rebuild timeline — Drag 7 case events into chronological order
- Accuse the killer — Stakes: wrong pick raises difficulty; correct pick rewards skill points
Sidebar panels (swipe on mobile):
- 👥 Suspects — Bio, motive, personality, testimony
- 💬 Interrogate — Question trees + evidence presentation + pressure system
- 🕸 Relations — SVG network graph with click-to-highlight
- ⏱ Timeline — Drag-to-sort event reconstruction
- 🌟 Skills — 5 trees × 3 levels (Observation/Logic/Intuition/Interrogation/Memory)
- 💾 Save — Manual save + auto-save, localStorage persistence
- 🏆 Achievements — 23 badges across 4 categories
- 🔊 Volume — Independent ambient/SFX sliders with crossfade
- ♿ Accessibility — Font size (14-24px) / High Contrast / Reduce Motion
Controls:
- Desktop: drag & drop, double-click to connect clues, click suspect to highlight
- Mobile: pinch-to-zoom board, long-press for clue detail, swipe to switch sidebar tabs
src/
├── engine/ # Core game logic (pure TS, zero React dependency)
│ ├── types.ts # 60+ type definitions
│ ├── templates.ts # 18 case template configurations
│ ├── characterPool.ts # 30 preset character templates + 6 motive categories
│ ├── generator.ts # Procedural case generation engine
│ ├── achievements.ts # 23 achievement definitions + unlock logic
│ └── sound.ts # Web Audio API sound engine (ambient + SFX)
├── store/ # Zustand state management
│ ├── gameStore.ts # Global game state + actions
│ └── saveLoad.ts # localStorage serialization + versioning
├── components/ # React UI (17 components)
│ ├── GameLoop.tsx # Main loop orchestrator
│ ├── EvidenceBoard.tsx # Drag-and-drop evidence board + pinch zoom
│ ├── InterrogationPanel.tsx # v2 multi-turn interrogation
│ ├── RelationGraph.tsx # SVG network visualization
│ ├── TimelinePanel.tsx # Drag-to-sort timeline
│ ├── AchievementPanel.tsx # Badge display + category filter
│ ├── TitleScreen.tsx # Launch menu + stats
│ ├── [other components...]
├── hooks/ # Custom hooks
│ ├── useMobileGestures.ts # Pinch zoom, long press, swipe
│ └── A11yContext.tsx # Accessibility settings provider
├── i18n/ # Internationalization
│ ├── I18nContext.tsx # Language context + hook
│ └── locales/{zh,en}.json # Full UI string dictionaries
└── test/ # Test setup
Tech Stack:
- React 19 + TypeScript 6 (strict mode)
- Zustand 5 (state management)
- Vite 8 (build tool)
- Web Audio API (procedural sound synthesis)
- SVG native (board connections + relation graph)
- localStorage (save games, achievements, settings)
- PWA (Service Worker, offline play, installable)
npm install
npm run dev # Start dev server on :5173
npm run build # Type-check + production build
npm test # Run 9 engine unit tests
npm run preview # Preview production build- Add template ID to
src/engine/types.ts→CaseTemplateId - Add config to
src/engine/templates.ts→CASE_TEMPLATES - Add template-specific clues to
src/engine/generator.ts - Add motive/background entries to
src/engine/characterPool.ts - Update
gameStore.getAvailableTemplates()progression
npm test # Single run (9 tests, <1s)
npm run test:watch # Watch modeAutomatic (GitHub Actions):
Push to main → CI builds + deploys to gh-pages branch.
Manual:
bash deploy.shLive at: https://WindRiders.github.io/darkroom-game/
MIT — Copyright (c) 2024
"Every case is unique. Every truth is buried. Can you find it?"





