Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .agents/skills/migrate-vite-cloudflare-to-void

This file was deleted.

1 change: 0 additions & 1 deletion .agents/skills/void

This file was deleted.

20 changes: 11 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
# ── ElevenLabs voices + sound effects (optional) ────────────────────────────
# Add a key to give the narrator and characters real voices and mood SFX.
# Leave blank to keep the game on procedural ambience only (still works).
# The shipped game already plays pre-rendered files in public/audio/.
# A key is only needed to regenerate those files (`npm run audio`) or to
# synthesize missing lines through the dev server.
#
# Requests are routed through the Vite dev/preview proxy (/eleven), which injects
# this key server-side — so there is NO CORS issue and the key is NOT shipped in
# the client bundle. Either of these names works:
# Put the key in .env.local (gitignored) under this name:
ELEVENLABS_API_KEY=
# VITE_ELEVENLABS_API_KEY=
#
# Keep the key off any VITE_ name. Vite inlines VITE_ variables into client
# code wherever they are read. The dev server reads ELEVENLABS_API_KEY and
# calls ElevenLabs from the `/api/eleven` route.

# Optional: TTS model (default: eleven_turbo_v2_5 — low latency, English).
# Use eleven_multilingual_v2 for higher quality at the cost of latency.
VITE_ELEVENLABS_MODEL=eleven_turbo_v2_5
# Optional: TTS model used by the dev proxy and by `npm run audio`.
# The committed clips were rendered with eleven_v3.
VITE_ELEVENLABS_MODEL=eleven_v3

# Optional: audio output format (default: mp3_44100_128).
VITE_ELEVENLABS_OUTPUT=mp3_44100_128
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run build
env:
STATIC_BUILD: "1"
28 changes: 20 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,32 @@ node_modules/
dist/
test-results/
playwright-report/
coverage/
*.log
logs/
recordings/
*.mp4
*.mov
*.webm
.DS_Store
.idea/
*.swp
*.tsbuildinfo

# Local env files (may contain API keys); keep the template tracked
# Local secrets. The tracked template is .env.example.
.env
.env.local
.env.*.local
.env.*
!.env.example

# Machine-local agent notes
HERMES.md
CLAUDE.md
.agents/

# Large working-copy duplicate of public/assets/map.jpg
# Large working-copy duplicate of public/assets/map.webp
/Map.jpg

# Generated audio is committed so it doesn't need regenerating.
# (Run `npm run audio` only to (re)create or update it.)
# Generated by `void prepare` and local tooling
.void
.wrangler
*.tsbuildinfo
.vercel
.env*
18 changes: 0 additions & 18 deletions AGENTS.md

This file was deleted.

30 changes: 30 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
MIT License

Copyright (c) 2026 Adam

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

---

The MIT license covers the source code in this repository.

It does not cover the story prose (including `story-source/`), the
illustrations in `public/assets/`, or the audio in `public/audio/`. Those
assets keep the separate terms described in the README and in
`public/audio/CREDITS.md`.
117 changes: 42 additions & 75 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,101 +1,68 @@
# The Underwater City of Athel

An interactive, cinematic "choose your own story" set in the underwater city of
Athel. The hand-drawn cross-section map is the stage: the camera pans and zooms
between locations, characters speak on the map, narration is typed out, scenes
fade through black, and your choices funnel Mira toward one of three endings.
A cinematic choose-your-own-adventure through a sealed city under the sea. The cross-section map is the stage: the camera travels between districts, the cast speaks on the glass, and Mira's choices lead to one of three endings.

## Run
**Voices and sound: [elevenlabs.io](https://elevenlabs.io)**

```bash
npm install
npm run dev # start the dev server
npm run build # typecheck + production build
npm test # unit tests (vitest)
npm run e2e # end-to-end tests (Playwright)
npm run lint
```
Play it: **[underwater-city-story.vercel.app](https://underwater-city-story.vercel.app)**

![Title screen over the city map](docs/screenshots/title.jpg)

## Deployment targets
![Torin speaks at the market stall, with the city map behind the line](docs/screenshots/scene.jpg)

Vercel is the static production target. Its repository configuration runs the
same typechecked `npm run build` command used locally and serves `dist/` as a
single-page app. Vite-hashed assets and content-addressed voice files are cached
immutably; HTML and `public/audio/manifest.json` are revalidated.
![The world map of Athel, opened from inside the story](docs/screenshots/map.jpg)

Cloudflare remains supported through Void for anyone who needs the server routes
or D1-backed demo API. The `Deploy to Cloudflare with Void` workflow is explicit
about that target and runs only from `main` or by manual dispatch.
## How it was made

## Voices & sound (ElevenLabs, optional)
The story follows Mira, a student in the Tank, after she finds proof that the poisoned-surface story her city teaches is a lie. The prose began as the drafts in [`story-source/`](story-source). The playable version turns that prose into a scene graph: optional detours rejoin the main path, and the climax branches for real.

The narrator and each character can be voiced with ElevenLabs text-to-speech,
and an optional sound-effect "sting" plays when the scene mood changes.
The map and the character sheet are original illustrations made for Athel, including AI-generated art by the author. Dialogue, mood beds, and sound effects were pre-rendered with ElevenLabs and committed under `public/audio/`, so the public site plays locally and does not call the API.

1. Copy `.env.example` to `.env.local`.
2. Set `ELEVENLABS_API_KEY` to your ElevenLabs key.
3. (Optional) Override the model with `VITE_ELEVENLABS_MODEL` (default
`eleven_turbo_v2_5` — low latency, English; use `eleven_multilingual_v2`
for higher quality at the cost of speed) or the per-character voices with
`VITE_ELEVENLABS_VOICE_*`.
4. Restart `npm run dev`. Use the top-right control cluster:
- **Ambience** — procedural underwater soundscape that retunes per mood
(works with no key); also triggers the ElevenLabs mood stingers when a key
is present.
- **Voices** — ElevenLabs narration; the on-screen text paces itself to the
spoken line and the speaking portrait pulses.
## Tech stack

Without a key the game runs normally (procedural ambience only) and the Voices
toggle is inert.
- [React 19](https://react.dev) and TypeScript, bundled with [Vite](https://vite.dev)
- [Framer Motion](https://www.framer.com/motion/) for the camera, travel, and fades
- Vitest and Testing Library for unit tests; Playwright for the ending paths
- A static production build on [Vercel](https://vercel.com)
- [Void](https://void.cloud) for an optional dev-time `/api/eleven` route that keeps an ElevenLabs key on the server

Requests are routed through a same-origin Vite proxy (`/eleven`) that injects
the key server-side — this avoids browser CORS errors and keeps the key out of
the client bundle. The proxy is active under `npm run dev` and `npm run preview`.
## Quick start

```bash
npm install
npm run dev # http://127.0.0.1:5173
npm test # unit tests
npm run lint
npm run build # void prepare, typecheck, production bundle
npm run e2e # Playwright (installs a browser on first run)
```

### Pre-generate audio (recommended)
`npm run build` writes `dist/`. Set `STATIC_BUILD=1` to skip the Void plugin and emit the same static site Vercel serves. Vercel sets that mode itself.

For instant, reliable playback (no runtime API calls, no CORS, no latency),
pre-render every line and mood SFX to `public/audio/` once:
Copy `.env.example` to `.env.local` only if you want to regenerate audio. The key name is `ELEVENLABS_API_KEY`. Leave it unset to play the committed clips. The `/api/eleven` route is for that local workflow. The public site is the static build, which never receives the key.

```bash
npm run audio
npm run audio # re-render missing voice, music, and effect files
```

This reads your key, writes `public/audio/voice/*.mp3`, `public/audio/sfx/*.mp3`,
and `public/audio/manifest.json`, then the app plays those local files (falling
back to the live proxy for anything not pre-generated). Re-running only fetches
what's new. `public/audio/` is git-ignored.
## Playing

> On the **free ElevenLabs plan**, only default voices work via the API (library
> voices return `402`). The defaults shipped here are free-tier voices. Override
> any of them with `VITE_ELEVENLABS_VOICE_*` if you have a paid plan.
Each scene offers choices. Some collect clues; the climax splits into different endings. Four hidden traits — evidence, trust, risk, and collective — decide the finale:

> **Security note:** `VITE_` variables are bundled into the client, so the key
> is visible to anyone who loads the app. That is acceptable for a local/demo
> build. For a public deployment, proxy the ElevenLabs requests through a small
> backend that keeps the key server-side.
- **Hopeful** — proof, allies, and a public that can hear it.
- **Bittersweet** — the truth spreads faster than anyone can hold it.
- **Dark** — an escape with too little proof and no one beside her.

## How the branching works
The map overlay opens the whole cross-section. The atlas keeps collected items, the cast, and achievements. An in-progress run is saved in the browser and can be continued from the title screen.

- Each scene offers choices. Some are optional detours (collect items, build
trust) that rejoin the main path; the **climax** opens genuinely different
branches (run to Torin / record more / get caught).
- Every choice can adjust four hidden traits — **evidence, trust, risk,
collective** — and can add items to the **Atlas**.
- The ending is resolved from your accumulated traits:
- **Hopeful** — strong proof, allies, and public support with low recklessness.
- **Bittersweet** — the truth spreads, but faster than it can be organised.
- **Dark** — an isolated escape without enough proof or allies.
## Credits

## Overlays
- Story, illustrations, and code by [Adam](https://github.com/Programcode).
- Cinzel, EB Garamond, and Inter are loaded from Google Fonts. Each is licensed under the [SIL Open Font License](https://openfontlicense.org/).
- Voice, music beds, and sound effects: ElevenLabs. Details and the free-plan limits are in [`public/audio/CREDITS.md`](public/audio/CREDITS.md).

- **Map** — the full cross-section world map; click any brass marker to read its
lore, related scenes, and collectibles. "Return to the story" closes it.
- **Atlas** — your foldout journal of collected items and charted places, plus
character bios (Cast) and Achievements (persisted across runs).
## Licence

## Saving & progress
Source code is [MIT](LICENSE), copyright 2026 Adam.

- The current run autosaves; the title screen offers **Continue** to resume it.
- Reaching an ending records it and unlocks achievements (kept across runs), and
the finale shows **the path you walked** and your trait scores.
The MIT license stops at the code. The story prose in `story-source/` and `src/data/`, the illustrations in `public/assets/`, and the audio in `public/audio/` stay under the terms in the credits above. The prose and illustrations are the author's, all rights reserved, and are included so the project can be read and run.
6 changes: 0 additions & 6 deletions db/migrations/20260607071511_brainy_maggott.sql

This file was deleted.

57 changes: 0 additions & 57 deletions db/migrations/meta/20260607071511_snapshot.json

This file was deleted.

13 changes: 0 additions & 13 deletions db/migrations/meta/_journal.json

This file was deleted.

9 changes: 0 additions & 9 deletions db/schema.ts

This file was deleted.

Binary file added docs/screenshots/map.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/scene.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/title.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading