diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..681dfb4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +# Pushes to main deploy straight to the live game, so the gate has to sit on the +# pull request — by the time a bad commit reaches main, it is already shipping. +on: + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +# A newer push to the same PR makes the previous run irrelevant. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify: + name: Typecheck, test, build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Test + run: npm test + + # Same build the deploy workflow runs, including the base path, so a + # PR cannot pass here and then fail on the way to production. + - name: Build + run: npm run build + env: + VITE_BASE_PATH: /lighthouse-ttrpg/ diff --git a/CLAUDE.md b/CLAUDE.md index 258c7bb..e1b13fa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,3 +35,7 @@ One-way layered deps: `types → data → engine → net → store → component ## Deploy Pushes to `main` auto-deploy to GitHub Pages (`.github/workflows/deploy.yml`). Open a PR for review; don't push straight to `main` unless asked. + +Every PR into `main` runs `.github/workflows/ci.yml` — typecheck, the full test +suite, and the same production build the deploy uses. Because main ships on +merge, the PR is the only place a break can still be caught. diff --git a/docs/MAPS.md b/docs/MAPS.md new file mode 100644 index 0000000..e77cc92 --- /dev/null +++ b/docs/MAPS.md @@ -0,0 +1,339 @@ +# The Atlas — Maps, Worlds & the Places You Fight In + +> **The vision.** Creators build and sell **maps** the way they already build and +> sell systems. A map ranges from a single island to a whole globe, and you can +> zoom from the full world down to one room in one building — with basements, +> upper floors, and caves. When a fight starts, the battlefield **is** the place +> the party is standing in. Parties can split up and explore separately. + +This doc is the canonical map design, the way [RULES.md](RULES.md) is the +canonical rules. It records **what was decided and why**, so the reasoning +survives longer than any one implementation. + +Status tags, matching RULES.md: **✅ shipped** · **🔧 designed, not built** · +**💭 deliberately deferred**. + +--- + +## 1. The one-sentence architecture + +> **The world is a seed. The battlefield is compiled data.** + +Two layers, one hard one-way boundary, because the two halves of this feature +have *opposite* constraints. + +| | **World layer** | **Battle layer** | +|---|---|---| +| What it is | An `Atlas`: a seed + tuning + the edits a human made | A `Battlefield`: walls, doors, solid hexes, deploy zones | +| Size | 600 bytes – 30 KB | ~30 bytes open ground, ~1.7 KB a seven-room tavern | +| Where it lives | Persisted, sold, shared | Inside the `CombatState` snapshot, like everything else | +| How it's produced | Generated lazily from `f(seed, params, path)` | `compileBattlefield(spec, place)` — a pure function | +| Status | 🔧 | ✅ shipped | + +The atlas does **not** point at a battlefield. It **compiles** one. That single +choice is what lets the map ship with **zero new combat concepts**: `HexCoord` +never grows a Z axis, `CombatState` gains no geometry field it doesn't already +have, and a map-derived fight travels inside the `combat_start` message that +already exists. + +### Why a seed, and not a map file + +Every hard constraint in this app is a constraint on **bytes**: + +- Distribution is a **clipboard paste** into a `