MarbleMagnificence

Dev log

The level editor saves a diff, not a level

Tooling commit 94f563b

Hand-placing blocks with a mouse usually means abandoning the generator that made the level. This one keeps both, by never writing a level file at all.

The Mountain is generated. Five octagonal terraces, sixteen units of descent, nine ramps, all falling out of a height field in MountainLevel.swift — which means “make the terraces wider” is still a constant you edit, not a level you rebuild.

That is a good property to have, and it is exactly the property a level editor usually eats. The moment a tool writes out a level file, the generator becomes a historical artifact: a thing that produced the starting point once, before a human took over.

So MarbleEditor doesn’t write a level.

What it writes

MarbleApp/Resources/mountain.edits.json records only what you added and what you removed. At launch, GameBoot applies that diff over MountainLevel.make(). The generator stays in charge of the mountain’s shape; your hand edits survive it changing.

Removals are keyed by cell and block type rather than by index, which is the detail that makes the whole thing hold together. Reorder the generator, add a terrace, change how the routes are laid out — the overlay still knows which block you meant to delete.

The applier itself is LevelOverlay, pure data, living in MarbleCore where the tests can reach it without a simulator.

The tool part

A palette of every block type and every bundled scenery prop. Pick one and it follows the cursor as a ghost preview. Q and E rotate, R resets, F cycles a prop’s footprint, ⌘-click or X deletes, ⌘S saves.

Clicking places against whichever face the pointer is on — on top of an up-facing face, beside a wall, underneath an overhang — so placement follows the surface normal instead of asking you to think in grid coordinates. The camera is orbit/pan/zoom and orthographic, starting at the game’s own isometric pitch, because a tool that shows you a different projection than the game is a tool that lies to you about what you just built.

The refactor hiding in the commit

SurfaceGeometry.swift pulls the face-orientation split and the MeshData → SCNGeometry conversion out of SceneKitRenderer — about 90 lines leaving the renderer to become 112 shared ones.

The editor and the game now bake pixels through the same code. Which is the whole reason the ghost preview is worth trusting: what you see under the cursor is not an approximation of the block, it is the block, shaded by the renderer’s own path.

MarbleEditor is a third target sitting over MarbleCore, alongside the iOS and tvOS apps. The game never links it.

The honest trade

Edits reach the game at the next build of MarbleApp, not live. Save in the editor, rebuild, roll.

That is slower than a hot-reload would be, and it was the right trade at this stage: the alternative was a file watcher and a live level-swap path in the running game, which is real complexity added to the part of the codebase least able to absorb it, in exchange for a few seconds per iteration.