diff --git a/PLAN.md b/PLAN.md index d14e924..872345e 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,6 +1,14 @@ # Bloom World Editor — Audit & Completion Plan -**Date:** 2026-07-11 · **Toolchain:** Perry 0.5.1239 · **Status:** audit verified against source; plan not yet started. +**Date:** 2026-07-11 · **Toolchain:** Perry 0.5.1239 · **Status (updated 2026-07-15):** MOSTLY DONE — the ✅ markers through the body are +accurate; this header was not. A/B/C/D/E/F1 and play-in-editor have shipped (and are +now pushed — they sat committed on one machine's local `main` until 2026-07-15). +Still outstanding: **F2** (rename/tint/tags/modelRef editing), **F3** (outliner +depth), **G** (asset thumbnails — `renderAllThumbnails`/`getThumbnail` still have +zero call sites), **H** (recent-projects UI — the write path runs every launch and +nothing reads it), **I** (environment panel — still mutates directly, no +`SetEnvironmentCommand`), **K1** (a REAL round-trip test — see the correction at +§K1) and **K2** (see the correction there — its premise is void). This document is self-contained: it is written for an implementer with no prior context. It replaces the original design plan (which was deleted). Part 1 is a verified audit of the current state, Part 2 defines "done", Part 3 is the work plan. This is a **single unified scope** — do not cut it down to an MVP; if a task has a prerequisite, the prerequisite is in scope too. @@ -16,7 +24,7 @@ This document is self-contained: it is written for an implementer with no prior |---|---| | `../engine/` | Bloom engine: TypeScript API over a Rust/wgpu native library (412 FFI functions listed in `../engine/package.json` under `perry.nativeLibrary.functions`). Shared world module at `../engine/src/world/`. | | `../shooter/` | Arena FPS. Worlds in `assets/worlds/`, editor project file `editor.project.json` at its root. Its `docs/perry-quirks.md` documents historical Perry bugs (now fixed — see §1.6). | -| `../garden/` | 3D collectathon; the only game using the runtime `loadWorld`/`instantiateWorld` path. | +| `../garden/` | 3D collectathon. **Correction 2026-07-15:** the claim that it is *"the only game using the runtime `loadWorld`/`instantiateWorld` path"* is FALSE — garden references neither, nor `bloom/world`, nor any `.world.json` (it predates the world module). **`instantiateWorld` has ZERO game consumers**: only the editor and the engine's own modules. This matters — §C1's acceptance ("garden instantiating a world with water/rivers shows them") was never executable, so the "a river cannot look different in-game than in the editor" guarantee is UNTESTED. | | Perry (installed as `perry` on PATH; source checkout is a sibling of the `bloom/` tree) | The TypeScript→native AOT compiler that builds everything here. | **Build & run:** @@ -29,7 +37,11 @@ PERRY_ALLOW_PERRY_FEATURES=1 perry compile src/main.ts # until task A1 lands Perry 0.5 notes: the CLI is `perry compile ` (there is no `perry build`); it emits `./main` and ignores `perry.toml`'s `out_dir`. Duplicate-symbol `ld` warnings (perry_stdlib vs libbloom_macos both embedding Rust std) are benign — the editor compiles and links clean as of 0.5.1239. The stale `bloom-editor` binary from April 2026 predates all of this. -This directory is **not a git repository** and has no history. Recommended first action: `git init` and commit the current state before changing anything. +~~This directory is **not a git repository** and has no history.~~ **Corrected +2026-07-15:** it is a git repository with history, and as of today it is pushed to +`origin/main`. (For a while the reverse hazard applied: three commits of real work — +prefab authoring, play-in-editor, terrain painting — existed only in this local +clone. If a PLAN item reads "done", check it is *pushed*.) --- @@ -116,7 +128,7 @@ Minor: `label` is imported but unused in `brush-panel.ts` and `environment-panel - `assets/worlds/*.world.json` conform to the schema above and were authored with this editor (shooter's `editor.project.json`: `{ name, gameId, modelsDir: "assets/models", prefabsDir, worldsDir: "assets/worlds", defaultWorld: "arena_01.world.json" }`). `arena_02` ("Outdoor plaza", 32 KB) is the real level: 66 static meshes, 6 water volumes, plus the gameplay entities listed in §1.2(2). - **Gameplay semantics live in `userData`**, discriminated by `userData.kind`: `player_spawn`, `collider_box` (`halfExtents`), `static_mesh` (optional `collider`), `point_light` (`range`,`color`,`intensity`), `enemy_spawner` (`enemyType`,`waveBits`,`maxAlive`,`cooldown`), `weapon_pickup` (`weapon`), `wave_config` (`waves` = escaped-JSON string), `box`. All values are strings. -- **Runtime path:** shooter never calls `loadWorld`. `tools/build-world.ts` (a bun script) bakes a world JSON into flat `export const` arrays at `src/generated/world.ts`, which `src/main.ts` imports. This was a workaround for two Perry 0.4.x runtime bugs (see §1.6 — both now fixed). Editing a world therefore requires a rebake: `bun tools/build-world.ts assets/worlds/arena_02.world.json src/generated/world.ts`. +- **Runtime path (CORRECTED 2026-07-15):** ~~shooter never calls `loadWorld`~~ — it does now. `tools/build-world.ts` and `src/generated/` have both been **DELETED**; `src/world-runtime.ts` reads `assets/worlds/*.world.json` at startup via `loadWorld`. The Perry 0.4.x bugs that forced the bake are fixed, exactly as §1.6 predicted, and the workaround went with them. **There is no rebake step for world data.** What IS still derived: the terrain's *visual mesh* (`bun tools/build-terrain.ts`) — sculpt, save, re-run that one command. Terrain PAINT needs no rebake (the splat uploads at load). - Drawable geometry references GLBs converted offline from Unvanquished assets (`tools/convert-*.ts`, `docs/asset-pipeline.md`). The `_gizmo_box.glb` sentinel is not a file — the baker maps it to "draw a colored box". ### 1.6 Toolchain status (Perry 0.5.1239) @@ -285,7 +297,7 @@ The dividing line, for future schema questions: **lights are engine-universal ### F. Inspector & outliner depth -- **F1. `userData` editor (highest-value item for shooter).** Inspector section listing key/value rows with `textInput` editing, add-row, delete-row. Game-agnostic (free-form strings). Undoable (`SetUserDataCommand`). Acceptance: change an `enemy_spawner`'s `cooldown` in `arena_02`, save, run shooter's `bun tools/build-world.ts`, confirm the value lands in `src/generated/world.ts`. +- **F1. `userData` editor (highest-value item for shooter).** Inspector section listing key/value rows with `textInput` editing, add-row, delete-row. Game-agnostic (free-form strings). Undoable (`SetUserDataCommand`). Acceptance **(corrected 2026-07-15 — the baker is gone):** change an `enemy_spawner`'s `cooldown` in `arena_02`, save, and launch the shooter — the value is live, with no rebake. - **F2.** Entity rename (textInput), tint editing (vec4 field or color widget), tags add/remove, modelRef reassignment via an asset-picker popup. All undoable. - **F3.** Outliner: double-click rename, per-row delete/duplicate, a filter/search box, and sections for entities / water / rivers (per C3). True hierarchy is out of scope (the format has no parenting). @@ -307,8 +319,15 @@ Keep fly-cam as the core mode (matches original scope). Stretch, only if trivial ### K. Shooter round-trip & integration proof -- **K1. Automated lossless round-trip test:** copy both arena JSONs into `src/tests/fixtures/` (or read them from `../shooter` if self-tests can take a path); self-test loads each, saves to a temp path, deep-compares parsed JSON. Must be exact for untouched worlds — any normalization the saver applies is a bug to fix, not a tolerance to add. -- **K2. Bake hook:** ⚠ check whether Perry/Bloom exposes process spawning. If yes: support an optional `postSaveCommand` in `editor.project.json` (shooter would set `bun tools/build-world.ts {world} src/generated/world.ts`) and run it after save, surfacing failures in the status bar. If no: add a status-bar reminder ("world saved — rebake required for shooter") driven by presence of the key, and document the manual command in README. +- **K1. Automated lossless round-trip test:** ⚠️ **STILL OUTSTANDING, and the + test that exists is actively MISLEADING (flagged 2026-07-15).** + `testWorldRoundTrip` in `src/tests/self-tests.ts` round-trips a *synthetic* + 2-entity world with `JSON.stringify` — it never calls `saveWorld`, and + `JSON.stringify` is the very idiom quirk #6 says corrupts a parsed graph (the + bug that made `saveWorld` write 0 bytes and report success). It passes by + construction and covers nothing. A green tick sitting over the exact hole is + worse than no test. The real work, unchanged: copy both arena JSONs into `src/tests/fixtures/` (or read them from `../shooter` if self-tests can take a path); self-test loads each, saves to a temp path, deep-compares parsed JSON. Must be exact for untouched worlds — any normalization the saver applies is a bug to fix, not a tolerance to add. +- **K2. Bake hook:** ⚠ **RESCOPED 2026-07-15 — the original premise is void.** It targeted `bun tools/build-world.ts`, a baker that **no longer exists**: the shooter loads world JSON at runtime, so editing a world needs **no rebake at all** and there is nothing to hook for it. Process spawning was also settled meanwhile (Perry's spawn is a no-op; the engine's `launchProcess` works, and play-in-editor already uses it). What survives is much smaller: the terrain **visual mesh** is still derived (`bun tools/build-terrain.ts`), so a `postSaveCommand` would be worth it *only* for a world whose heightmap changed. Everything else — entities, lights, water, rivers, and terrain paint — is live on the next launch with no command. - **K3. Companion note (shooter repo, optional, not this codebase):** with the Perry bugs fixed (§1.6), shooter can migrate to runtime `loadWorld` + `instantiateWorld` like garden and delete the baker — or keep baking for startup performance. Decision belongs to shooter; the editor only owes lossless files + the hook above. - **Final acceptance walkthrough:** open shooter project → arena_02 fully visible/editable → move a pickup, edit a spawner's userData, add a water volume, save → round-trip test green → rebake → shooter runs with the changes. diff --git a/src/tests/self-tests.ts b/src/tests/self-tests.ts index c6fd9a1..4cfc669 100644 --- a/src/tests/self-tests.ts +++ b/src/tests/self-tests.ts @@ -66,6 +66,21 @@ export function runSelfTests(): number { return failed; } +/// ⚠️ THIS TEST DOES NOT TEST THE SAVE PATH. Read before trusting its green tick. +/// +/// It builds a SYNTHETIC 2-entity world and round-trips it with `JSON.stringify` +/// — which is (a) not what saving does (`saveWorld` → `serialize.ts` hand-writes +/// the text) and (b) the exact idiom `docs/perry-quirks.md` #6 says CORRUPTS a +/// parsed graph, i.e. the bug that made `saveWorld` write 0 bytes and report OK. +/// +/// So this passes by construction on a small fresh object, while covering none of +/// the thing that actually broke. That is worse than no test: it is a green check +/// over the exact hole. +/// +/// PLAN §K1 is the real work and is still OUTSTANDING: load +/// `../shooter/assets/worlds/arena_01|02.world.json`, save them through +/// `saveWorld`, and deep-compare. Per §K1's own wording, any normalisation the +/// saver applies is a bug to fix, not a tolerance to add. No fixture exists yet. function testWorldRoundTrip(): void { const world = createEmptyWorld('test', 'Test World'); world.entities.push(createEntity('ent_1', 'models/tree.glb', [5, 0, 3]));