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
82 changes: 67 additions & 15 deletions .claude/skills/verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ How to build, run and drive this repo for runtime verification.

```bash
npm install # workspace root
npm run build -w @takeon/engine # engine must build before web
npm run build -w takeon-web
npm run build # engine → pixi → ui → editor → web (order matters)
cd web && npm run start & # serves on http://127.0.0.1:3400
```

Individual packages: `npm run build -w @takeon/engine`, `-w @takeon/ui`,
`-w @takeon/editor`, `-w takeon-web`. The engine must build before the rest.

Backend (optional — the web app falls back to localStorage without it):

```bash
Expand All @@ -28,25 +30,75 @@ before `next build` (env is inlined at build time).
it (exit 144). Use `pkill -f 'next[-]server'`.
- Rebuilding `.next` while an old `next start` is running serves stale chunk
hashes (404/400 on `_next/static`). Always kill + restart after rebuild.
- Playwright: launch Chromium with `executablePath: '/opt/pw-browsers/chromium'`.
- Playwright: launch Chromium with
`executablePath: '/opt/pw-browsers/chromium-1194/chrome-linux/chrome'`
(check the versioned directory name — the unversioned `chromium/` path has no
binary in it).
- HUD markup comes from `@takeon/ui` and is `tk-` prefixed; editor markup comes
from `@takeon/editor` and is `tke-` prefixed. Site chrome (garage, customiser,
launch) still uses the plain classes in `web/app/globals.css`.

## Flows worth driving (Playwright)

1. `/customize` → fill `input[aria-label="Rover name"]`, click part options
(`.part-option:has-text("Field Lab")` etc.), click `button.primary:has-text("Buy rover")`.
Full science build (Field Lab + Scoop + NavCam + Sounder) costs 1540 of
1600 starting credits.
(`.part-option:has-text("Field Lab")` etc. — module options *toggle*, so
clicking "Sample Scoop"/"NavCam" removes the defaults), then `button.primary`
(label is "Buy rover & pick target"); it routes to `/launch?rover=<id>`.
2. `/launch` → `.card:has-text("The Moon") button.primary` → lands on `/mission`.
3. Mission: wait `.hud-top .meter`; canvas is `.mission-canvas`. Click canvas
to focus, then Arrow keys drive. Action buttons: `.action-btn:has-text("Mine")`
etc. — Photo/Scan only render when the rover has those modules.
4. Mining yield appears as `.toast.good` with `+N <resource>`; cargo meter is
the 3rd `.hud-top .meter`.
Launch needs the `?rover=` param; visiting it bare shows "Pick a rover first".
3. Mission: wait `.tk-hudbar .tk-meter` (power/hull/hold gauges); canvas is
`.tk-canvas`. Click the canvas for the tile menu (`.tk-tilemenu`, with
"Drive here" / "Mine …"), or use `.tk-dpad button[aria-label^="Drive"]`.
The hotbar (`.tk-actions`) only shows the frequent, capability-gated verbs
directly — `.tk-action:has-text("Mine")` / `"Photo"` / `"Scan"` — plus one
`.tk-action-more` hex. Everything occasional (Build/Craft/Place/Repair/
Deposit/Hold) is behind it: click `.tk-action-more` to open `.tk-tray`, then
`.tk-tray button:has-text("Build")` etc. The tray closes itself after any
click, on outside click, or on Escape. Modals close via `.tk-modal-close` or
the footer `button:has-text("Close")`.
4. Mining yield appears as `.tk-toast-good` with `+N <resource>`; cargo meter is
the 3rd `.tk-hudbar .tk-meter`; open the tray and click
`.tk-tray button:has-text("Hold")` for the cargo table (`.tk-table tbody tr`).
5. Resume: go `/` — active mission card has `button:has-text("Resume mission")`;
cargo must survive the round-trip.
6. End: `.hud-top button:has-text("End")` → modal → `button.primary` → back on
`/` with credits chip updated.
7. Canvas paint check: sample `getImageData` for >10 distinct colors.
6. End: `.tk-hudbar button:has-text("End")` → modal → `.tk-btn-primary` → back
on `/` with the credits chip updated.
7. Canvas paint check: sample `getImageData` for >10 distinct colors. The iso
view paints ~1000 distinct colours at 1600×1000; the flat map ~100+.
8. Views: `.tk-hudbar button[title*="2D map"]` switches to the top-down map
(`game.view === 'flat'`), the same button (`title*="3D diorama"`) switches
back. Tile picking, tap-to-drive and the tile menu work in both.

### Editor (`/editor`)

Layout is one grid of uniform `.tke-card`s (Scene 2×2, Inspector, Maps,
Analysis, Rover, then Destinations and Events spanning 2) — no sidebars, no
tab-switching between Maps/Analysis/Rover. Chrome is light by default.

1. Wait for `.tke-toolbar`. `.tke-list-item` lists 6 built-in destinations;
`.tke-field` are inspector rows; `.tke-status` shows `96² · N ms`.
2. Drag a terrain slider (`.tke-group:has-text("Terrain") input[type="range"]`)
→ the world regenerates (status ms changes) and the entry gets an
`.tke-badge` reading "edited". Undo is `button[title^="Undo"]`.
3. Maps: `.tke-tab:has-text("Slope"|"Surface"|"Ore density"|"Drivable"|"Noise")`
then sample `.tke-map` with `getImageData` (each view should paint several
distinct colours). `.tke-slice` is the cross-section.
4. Analysis card: `.tke-stat` tiles (Columns, Height, Relief, Drivable, Cliffs,
Landing) — visible without switching views.
5. Play mode: `.tke-primary:has-text("Play")` mounts the real HUD inside the
scene card (`.tk-hudbar .tk-meter` = 3). Drive with the d-pad; engine events
land in `.tke-console-line`. `.tke-primary:has-text("Stop")` returns.
6. Scene view is `.tke-scene` (drag to pan, wheel to zoom, R rotates).
`.tke-viewport-bar button:has-text("3D")` switches it to the 2D map (the
button then reads "2D"); the card hint says which is live.
9. Noise: the inspector has a *Noise field* group (7 fields). Setting type +
fractal regenerates the world, and the Maps `Noise` tab previews the exact
field with a caption from `describeNoise`; `Scatter` previews blue-noise
placement.
7. Theme toggle: `.tke-toolbar button[title*="dark chrome"]` / `[title*="light
chrome"]`, persisted in `localStorage` under `takeon.editor.theme`.
8. Layout check worth keeping: every `.tke-card-head` should measure the same
height, and standard cards the same width/height, at any viewport.

Bennu (irregular asteroid) needs a fuel tank module; good probe for void-edge
terrain. Mobile: 390x760 viewport, d-pad `.dpad button[aria-label^="Drive"]`.
terrain. Mobile: 390x760 viewport, d-pad `.tk-dpad button[aria-label^="Drive"]`.
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ jobs:
- run: npm run build -w @takeon/engine
- run: npm test -w @takeon/engine
- run: npm run build -w @takeon/pixi
- run: npm run build -w @takeon/ui
- run: npm test -w @takeon/ui
- run: npm run build -w @takeon/editor
- run: npm run build -w takeon-web

backend:
Expand All @@ -32,7 +35,7 @@ jobs:
- run: go build ./... && go vet ./... && go test ./...
working-directory: pocketbase

# Publishes @takeon/engine and @takeon/pixi to npmjs.org when a v* tag is
# Publishes the @takeon packages to npmjs.org when a v* tag is
# pushed. Requires the NPM_TOKEN repo secret (an npm automation token with
# publish rights on the @takeon scope). Idempotent: versions already on the
# registry are skipped, so re-running a tag build never fails on republish.
Expand All @@ -50,11 +53,13 @@ jobs:
- run: npm install
- run: npm run build -w @takeon/engine && npm test -w @takeon/engine
- run: npm run build -w @takeon/pixi
- run: npm run build -w @takeon/ui && npm test -w @takeon/ui
- run: npm run build -w @takeon/editor
- name: Publish packages not yet on the registry
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
for pkg in @takeon/engine @takeon/pixi; do
for pkg in @takeon/engine @takeon/pixi @takeon/ui @takeon/editor; do
version=$(npm pkg get version -w "$pkg" | node -e "process.stdin.once('data',d=>console.log(Object.values(JSON.parse(d))[0]))")
if npm view "$pkg@$version" version >/dev/null 2>&1; then
echo "$pkg@$version already published — skipping"
Expand Down
104 changes: 101 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,110 @@
# Changelog

`@takeon/engine` and `@takeon/pixi` are versioned **in lockstep** and released
by tagging: pushing a `v*` tag (e.g. `v0.2.0`) runs the CI publish job, which
`@takeon/engine`, `@takeon/pixi`, `@takeon/ui` and `@takeon/editor` are
versioned **in lockstep** and released by tagging: pushing a `v*` tag (e.g. `v0.2.0`) runs the CI publish job, which
builds, tests, and publishes any package whose version isn't already on
npmjs.org. Bump both `package.json` versions + add an entry here in the same
npmjs.org. Bump every `package.json` version + add an entry here in the same
change. Semver: breaking engine/adapter API → major (minor while pre-1.0),
additive → minor, fixes → patch.

## Unreleased

### @takeon/engine

- **Runtime body registry** (`world/registry.ts`): `registerBody`,
`registerBodies`, `unregisterBody`, `listBodies`, `isRegisteredBody`.
`getBody` now resolves registered bodies first, so an editor draft or a
backend row can shadow a built-in without patching the catalog.
- **Terrain analysis** (`world/analysis.ts`): `heightField`, `slopeField`,
`reachableMask`, `crossSection` and `analyzeTerrain` — relief, slope
distribution, drivable fraction for a given climb limit, surface/material
mix and total recoverable resources. Pure and DOM-free.
- **Authoring** (`world/authoring.ts`): `createBodyDraft`, `cloneBody`,
`forkBody`, `validateBody` (errors + playability warnings), the
`BODY_FIELDS` inspector schema with `getBodyField`/`setBodyField`, and
`bodyToJson` / `bodyToTypeScript` / `parseBodyJson`.
- `GAME_EVENT_KEYS` lists every event key at runtime (with a compile-time
exhaustiveness guard) so hosts can subscribe to everything.
- `skinBiome` is exported for tools that need the surface-patch field.

### @takeon/ui (new)

- React mission shell: `MissionProvider` (engine lifecycle, HUD snapshot,
toasts, autosave, persistence via `SyncAdapter`), `MissionScreen` and the
full HUD component set, plus `TakeOnMission` for one-line embedding.
- Component registry: every component is registered through `createSlot` and
resolved via `TakeOnUIProvider`, so a parent app can replace, wrap, restyle
(`classNames`, `theme`), relabel (`labels`) or extend (`slots`) any part of
the interface. See `docs/UI.md`.

### @takeon/engine — engine structure

- **Scene layer** (`scene/`): `buildScene(sim)` flattens a running mission into
renderer-agnostic entities (position, height, facing, variant, flags), so a
host can draw TakeOn with its own technology. Enforced in the layering test.
- **View abstraction** (`render/view.ts`): `SceneView` — draw, pick, resize,
rotate, photo, minimap — implemented by the isometric renderer and by a new
**`FlatRenderer`**: a top-down 2D map with hillshading, contour shading, grid,
scanner reach, home pin and entity glyphs, rasterised once per world change.
`RoverGame.setView('iso' | 'flat')` / `toggleView()` switch at runtime on a
shared camera; `startView` picks the initial one.
- **Entity parts**: the rover moved to `render/rover.ts` and is now assembled
from registered parts (`registerRoverPart`, `listRoverParts`) with
build/state conditions, plus new detail — cargo crates that stack with the
hold, a charge LED strip, hull scorch and sparks, a status strobe, drilling
chips, a sweeping dish, a pulsing scanner tip, solar glint and RTG heat glow.
The 2D view has a painter registry per entity kind (`registerFlatPainter`).
- **Noise library** (`util/noise/`): Perlin, simplex, Worley (F1 / F2−F1 /
cells), white, and blue noise (void-and-cluster mask, Poisson-disk sampling,
`scatterPoints`); `fbm`/`ridged`/`billow` fractals, domain warp, and a
`NoiseConfig` + `makeNoise` factory. Bodies opt in via `terrain.noise`;
without it the shipped worlds regenerate byte-for-byte.
- `Simulation.landingSite`, `BodyField.defaultValue` (so inspectors show the
effective default instead of 0), and a `viewChanged` game event.

### @takeon/ui — visual direction

- New default skin: a pulp sci-fi homage (Out There: Ω). Hex cells for the
action bank and drive pad, notched panel frames with hairline neon edges,
segmented resource gauges, uppercase condensed type, comic-caption toasts and
a scene vignette. Pure CSS — no web fonts, no images, nothing fetched.
- `Meter` gained `tone`, `Modal` gained a title bar with a close control, and
`Chip` gained `tone`; all still overridable through the registry, and the
whole palette is `--tk-*` variables.
- View toggle in the HUD bar; `hud.view`, `actions.setView`/`toggleView` and a
`view` prop on `MissionProvider`.
- `ActionBar` decluttered: only capability-gated, frequent verbs (mine, photo,
scan) sit on the hotbar. Occasional actions (build, craft, place, repair,
deposit, hold) collapse behind a single "More" hex that opens `.tk-tray`, a
pop-up list that closes itself on selection, outside click or Escape. New
`ActionItem.menu` flag controls the split for custom action sets.

### @takeon/editor (new)

- `TakeOnEditor`: mountable world editor — project tree of built-ins and
drafts, schema-generated inspector with live validation, isometric scene
view with camera controls and a day/night scrub, elevation / slope /
surface / ore / drivability maps, raw noise preview, vertical cross-section,
terrain analysis, rover picker, undo/redo, JSON/TS import-export and an
in-editor play mode wired to the real HUD. See `docs/EDITOR.md`.
- Layout is one grid of uniform cards (scene 2×2, everything else the same
footprint, each scrolling internally) instead of sidebars, so panels stay in
line at every width. Chrome is light by default with a dark toggle
(remembered); the scene stays dark either way.
- Scene card switches between the 3D diorama and the 2D map; the inspector
gained a *Noise field* group; Maps gained a live preview of the body's actual
noise field and a blue-noise **Scatter** preview.

### web

- `/editor` route; the mission screen is now a thin host over `@takeon/ui`,
and destination lists resolve through `listBodies()` so edited worlds are
immediately launchable.

### desktop (new, not published)

- Optional Electron shell so the editor runs as a macOS/Windows/Linux app.

## 0.2.0 — first published release

### @takeon/engine
Expand Down
45 changes: 40 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ Isometric voxel rover game. npm workspaces monorepo + Go PocketBase spoke.
## Commands

```bash
npm install && npm run build # builds engine → pixi adapter → web (order matters)
npm test # engine unit tests (vitest)
npm run dev # Next.js dev server on :3400
npm install && npm run build # engine → pixi → ui → editor → web (order matters)
npm test # engine + @takeon/ui unit tests (vitest)
npm run dev # Next.js dev server on :3400 (editor at /editor)
cd desktop && npm install && npm start # optional Electron shell for the editor
cd pocketbase && TAKEON_ALLOW_ANON=true go run . serve --http 127.0.0.1:8094
node scripts/export-catalog.mjs # regen pocketbase/seed/*.json after catalog edits
node scripts/fetch-dem.mjs # embed real MOLA/LOLA DEM patches (needs trek.nasa.gov)
Expand All @@ -18,10 +19,35 @@ Runtime verification recipe: `.claude/skills/verify/SKILL.md`.
## Architecture rules

- **All gameplay rules live in `packages/engine/src/sim/` and `parts/`** —
renderer and web UI only observe. The sim is deterministic, fixed 10 Hz.
renderers and web UI only observe. The sim is deterministic, fixed 10 Hz.
- **Layers are enforced**: `util → world → sim → scene → render → input`, with
`core` on top (`test/layering.test.ts` fails on an upward import). `scene/`
flattens a frame into renderer-agnostic entities; `render/` has two views
(`IsoRenderer`, `FlatRenderer`) behind the `SceneView` interface. Architecture
overview: `docs/ENGINE.md`.
- **Entity art is componentised**: the rover is a list of registered parts
(`render/rover.ts`, `registerRoverPart`), the 2D view has a painter per entity
kind (`registerFlatPainter`). Add hardware as a part, not as more branches.
- **UI lives in `packages/ui` (`@takeon/ui`), never in `web/`.** Every
component is registered by key via `createSlot` and resolved through
`TakeOnUIProvider`, so a parent app (Landnam, the editor, the Next.js app)
can replace/wrap/restyle any of them. Adding a component means adding a slot
key + a `SLOT_KEYS` entry; `web/` stays a thin host. See `docs/UI.md`.
- **Authoring APIs belong in the engine, not the editor** — `world/authoring.ts`
(draft/validate/field schema/export), `world/analysis.ts` (heightfield,
slopes, reachability, stats) and `world/registry.ts` (runtime body registry)
are pure and DOM-free so they stay testable and reusable. `@takeon/editor`
is only a UI over them.
- Worlds regenerate deterministically from `(BodyDef, seed)`; persistence
stores only voxel `edits` + entity state (`MissionState`). Never persist raw
voxels.
voxels. **Anything that changes generation output must be opt-in per body** —
that is why `terrain.noise` exists; `test/noise.test.ts` asserts the shipped
bodies stay byte-identical. Same rule for `terrain.biomes` (chunked biome
skins, `world/biomes.ts`) and `world/kinds.ts` (planet/moon/asteroid/gaseous
preset inheritance + `instantiateBody()` for host-supplied properties like
real temperature) — both additive, both opt-in. `world/layers.ts` is the
seam for a *module* to add a second world grid (caves, sky); core registers
none, so mining stays top-down deposits + ice. See `docs/ENGINE.md`.
- The engine has **zero runtime dependencies** and must keep working without
DOM access for tests (canvas bits are guarded; `OffscreenCanvas` fallback).
- Web ↔ storage goes through `SyncAdapter` (`net/sync.ts`) only. The web app
Expand All @@ -32,6 +58,8 @@ Runtime verification recipe: `.claude/skills/verify/SKILL.md`.
- Part/body catalogs: source of truth is the engine
(`parts/catalog.ts`, `world/bodies.ts`); `scripts/export-catalog.mjs` exports
them to `pocketbase/seed/` which the Go backend embeds + upserts on serve.
Resolve destinations with `getBody`/`listBodies` (registry-aware), never by
scanning `BODIES` — editor drafts and backend rows register at runtime.

## PocketBase spoke conventions (Star Sailors ecosystem)

Expand All @@ -51,6 +79,13 @@ Runtime verification recipe: `.claude/skills/verify/SKILL.md`.
`pkill -f 'next[-]server'`; always restart `next start` after rebuilding.
- Engine tsconfig uses `moduleResolution: bundler` with `.js` import suffixes;
keep both or Next/tsc will disagree.
- New `GameEvents` keys must be added to `GAME_EVENT_KEYS` (`types.ts`); a
compile-time guard fails the build otherwise, and the UI firehose iterates it.
- `RoverGameOptions.startView` (not `view`) picks the initial renderer —
`@takeon/pixi`'s mount options already use `view` for the host canvas.
- React packages build with `tsc`; stylesheets are copied by
`scripts/copy-assets.mjs` (tsc ignores CSS). Their `exports` maps need a
`default` condition or Next's webpack resolver rejects the package path.
- The iso painter's order is diagonal (`x+y` asc, then z asc); dynamic entities
are drawn after chunk blits and occluders re-stamped
(`renderer.redrawOccluders`) — don't reorder without checking cliffs.
Loading
Loading