Skip to content

Load the world at runtime: terrain, forest and lights become data#10

Merged
proggeramlug merged 2 commits into
mainfrom
world/schema-v2-lights
Jul 12, 2026
Merged

Load the world at runtime: terrain, forest and lights become data#10
proggeramlug merged 2 commits into
mainfrom
world/schema-v2-lights

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

The game no longer bakes its world into TypeScript, and the level is now editable in the Bloom editor rather than being half code.

No more baker

src/world-runtime.ts reads assets/worlds/arena_02.world.json at startup via the engine's loadWorld and presents it as the same flat number arrays the game loop already consumed — so the 2,200-line main.ts is untouched. src/generated/ and tools/build-world.ts are deleted (390 lines of generated code, plus a whole build step).

The baker existed because Perry 0.4.x returned arrays from JSON.parse whose .length read as undefined. That is fixed (verified on 0.5.1208), so the workaround goes with it. The two rules that still bind are honoured in the new module: arrays are built with new Array(n) + index assignment, never .push(), and strings are parsed at load, never on a per-frame path.

npm run dev is now just compile-and-run.

Three things that were code are now data

Terrain. The heightmap lives in world.terrain (128×128 over the arena) and is fed straight to Jolt — so sculpting in the editor changes collision, enemy ground-following, and the scatters on the next launch, with nothing to regenerate. Only the visual mesh is still baked (bun tools/build-terrain.ts), and that generator now reads those heights and adds the horizon skirt outside the arena, rather than inventing the ground itself.

The forest. 88 trees were scattered from a fixed LCG seed at startup, so no tree had an identity and "move that one off the path" was not expressible. They are entities now (kind: prop_tree) — movable, retintable, deletable, addable.

The scatters' keep-out shapes. The forest and grass rejected hardcoded rectangles copied out of the level by hand: the river at z = 12, the building at x -30..-12. Move the river in the editor and the grass kept avoiding the old channel while growing out of the new one. They now derive from the world itself — the water volumes, and the bounding box of the building's colliders (W.keepOut). The computed building footprint (−30.2…−11.8, −19.2…−6.8) matches the rect that was previously hardcoded, which is how I know the derivation is right.

The river channel carved into the terrain likewise comes from the water volume's own position and width, so the ground and the water can no longer disagree.

Schema v2: lights out of userData

arena_01 and arena_02 are schemaVersion: 2, with arena_02's five point_light entities lifted into the top-level lights array (see Bloom-Engine/engine#90). Verified semantically identical: every field outside entities unchanged, all 85 ids accounted for, zero non-light entities touched, all five lights' values carried over exactly — and the generated runtime data was byte-identical apart from one comment before the baker was removed.

The arena JSON diff is large but is mostly reformatting: the migration writes standard JSON.stringify(…, null, 2), which is what the engine's saver produces, so this just front-loads what the first editor Save would have done anyway.

Seeding tools (run once, not part of the build)

bake-terrain-to-world.ts and bake-forest-to-world.ts re-derive that data from its original recipe (tools/terrain-shape.ts). Re-running either overwrites editor work on that data — they say so, loudly.

Verification

Builds, runs, and renders the arena — forest, grass, carved river, building, lighting — with no generated code in the tree. The editor, opened on the same file, shows the terrain, the river, and all 88 trees as individual editable objects.

Depends on Bloom-Engine/engine#90.

Ralph Kuepper added 2 commits July 11, 2026 16:56
arena_01 and arena_02 are now schemaVersion 2, with arena_02's five point_light
entities lifted into the top-level `lights` array. tools/build-world reads
world.lights, falling back to the old point_light entity form so an un-migrated
world still bakes.

The generated runtime module is byte-identical apart from one comment: the
migration is semantically a no-op for the game, and a deliberate one for the
editor, which can now show and edit lights as lights.

Also point editor.project.json at arena_02 (the real level; arena_01 is the old
box-in-a-box), and gitignore the editor's scratch state.
The game no longer bakes its world into TypeScript. src/world-runtime.ts reads
assets/worlds/arena_02.world.json at startup via the engine's loadWorld and
presents it as the same flat number arrays the game loop already consumed, so
the 2,200-line main loop is unchanged. src/generated/ and tools/build-world.ts
are gone.

The baker existed because Perry 0.4.x returned arrays from JSON.parse whose
.length read as undefined. That is fixed (verified on 0.5.1208), so the
workaround can go with it. The two rules that still bind are honoured in the new
module: arrays are built with new Array(n) + index assignment, never .push(),
and strings are parsed at load, never on a per-frame path.

Three things that were code are now data:

- Terrain. The heightmap lives in world.terrain, and the game feeds it straight
  to Jolt — so sculpting in the editor changes collision, enemy
  ground-following, and the scatters on the next launch, with nothing to
  regenerate. Only the visual mesh is still baked (bun tools/build-terrain.ts),
  which now READS those heights and adds the horizon skirt outside the arena
  rather than inventing the ground itself.

- The forest. 88 trees were scattered from a fixed LCG seed at startup, so no
  tree had an identity and "move that one off the path" was not expressible.
  They are entities now (kind: prop_tree) — movable, retintable, deletable,
  addable — seeded from that same scatter by bake-forest-to-world.ts.

- The scatters' keep-out shapes. The forest and grass rejected hardcoded
  rectangles copied out of the level by hand: the river at "z = 12", the
  building at "x -30..-12". Move the river in the editor and the grass kept
  avoiding the old channel while growing out of the new one. They now derive
  from the world itself — the water volumes, and the bounding box of the
  building's colliders (W.keepOut).

The river channel carved into the terrain likewise comes from the water volume's
own position and width, so the ground and the water can no longer disagree.

Verified: the game builds, runs, and renders the arena — forest, grass, carved
river, building, lighting — with no generated code anywhere in the tree.
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@proggeramlug, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 59 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 98820d80-638c-454e-9abe-128499e5cd64

📥 Commits

Reviewing files that changed from the base of the PR and between 9748368 and 9da3922.

⛔ Files ignored due to path filters (2)
  • src/generated/terrain.ts is excluded by !**/generated/**
  • src/generated/world.ts is excluded by !**/generated/**
📒 Files selected for processing (16)
  • .gitignore
  • CLAUDE.md
  • assets/models/terrain_hills.glb
  • assets/worlds/arena_01.world.json
  • assets/worlds/arena_02.world.json
  • editor.project.json
  • package.json
  • src/main.ts
  • src/world-runtime.ts
  • tools/bake-forest-to-world.ts
  • tools/bake-terrain-to-world.ts
  • tools/build-terrain.ts
  • tools/build-world.ts
  • tools/deploy-ios.sh
  • tools/gen-building.ts
  • tools/terrain-shape.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch world/schema-v2-lights

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug proggeramlug merged commit f2e4368 into main Jul 12, 2026
1 check passed
@proggeramlug proggeramlug deleted the world/schema-v2-lights branch July 12, 2026 07:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant