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
33 changes: 26 additions & 7 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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:**
Expand All @@ -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 <entry>` (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*.)

---

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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).

Expand All @@ -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.

Expand Down
15 changes: 15 additions & 0 deletions src/tests/self-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]));
Expand Down