GameBuilder v2 — implementation status and design

Design and implementation notes retained with the GameBuilder system. Updated 2026-10-05.

Purpose

Reshape GameBuilder into a composition workspace where the student configures a game on the left and plays/runs it through the existing GAME_RUNNER on the right. The builder should produce ordinary GameEngine level code that can be inspected, edited, saved, and reused outside the builder.

The v1 builder remains available at /gamebuilder/. The v2 workbench is available at /gamebuilder/v2/, with system source kept under _projects/systems/gamebuilder/, following the _projects/systems/calendar/ project pattern.

Current implementation snapshot

The current v2 page is a working, runner-backed builder for a game name, background, player, zero or more NPCs, and multiple open spline barriers. It generates a standard GameEngine level module and sends it to the existing GAME_RUNNER editor. GAME_RUNNER remains the only code editor and execution surface; GameBuilder does not create a second canvas, game loop, or executor. Replacing runner code is explicit and prompts for confirmation when existing code differs from the last generated version.

The system also owns the lesson notebooks under notebooks/. The characters lesson at /game/essentials/characters connects generated data to object literals, constructors, inheritance, spritesheet indexes, and animation. Its two editable GAME_RUNNER examples progress from a Player to a Player with two NPC instances. The backgrounds and characters lessons use system-owned assets published under /images/projects/gamebuilder/, rather than another game’s distribution. GameBuilder is registered for make dev; its notebook watcher copies and converts source lesson changes for the local preview.

Current page composition

GameBuilder v2 page
├── Shared OCS navigation and page links
├── Workbench heading with builder collapse/reopen control
├── Live status message
└── Responsive workbench
    ├── Left: Level setup (.ocs__card)
    │   ├── Clear / Pull / Push
    │   ├── Game name
    │   ├── Environment fieldset: background selection
    │   ├── Player fieldset: name, sprite, normalized X/Y position
    │   ├── NPCs fieldset: Add NPC and repeatable NPC configuration cards
    │   │   └── Each NPC: name, sprite, normalized X/Y position, greeting
    │   └── Spline barriers: point placement, coordinate list, undo, edit,
    │       visibility, finish/cancel, and removal controls
    └── Right: shared GAME_RUNNER include
        ├── Existing runner controls and source editor
        └── Game output/canvas

On wide screens, the builder occupies the left quarter of a full-width workspace and the runner uses the remaining width. The builder can collapse to give the runner the full workspace. At mobile widths, the panels stack. Controls use semantic fieldsets, labels, inputs, selects, and buttons, enhanced with GameBuilder-scoped OCS classes and preference-aware theme tokens.

Generated code defines backgroundData, playerData, and one named npcDataN or barrierDataN object per configured object separately, then references those data objects in this.classes. Barrier points are stored in normalized 0–1 coordinates and rendered/collided by the reusable assets/js/GameEnginev1.1/essentials/SplineBarrier.js class. The asset manifests and focused .mjs modules are authored under _projects/systems/gamebuilder/ and distributed by the registered project build.

Workspace persistence (implemented)

The workspace now automatically keeps a browser-local recovery draft, including exact editor source, panel settings, unfinished barrier edits/cancel snapshots, object counters, engine selection, and builder visibility. Draft writes are debounced by 200 ms and flushed on page hide/navigation. Reload restores the draft without regenerating code. Save Workspace keeps a separate explicit return point; Load Saved Workspace restores it after confirmation.

Workspace file actions now extend GAME_RUNNER’s existing editor toolbar with compact, accessible icon controls. The existing Save icon saves the workspace; folder/download/upload/code icons provide load and exports/import. Clear restores the runner’s construction-default code (empty for this workspace) without resetting the builder panels or deleting the explicit save. There is no separate page-level save toolbar or New Workspace button.

Fresh startup no longer automatically generates the default background/Player into the runner. The panel keeps its starter selections, while the code stays empty until Push is explicitly clicked. Recovery continues to restore exact source, including an intentionally empty editor.

Export/import workspace JSON and exact JavaScript export are implemented. Runner Save Code also saves this workspace through an awaited opt-in hook. Storage failures/conflicts are visible and stop automatic writes; invalid imports do not replace open work. These are single-level, one-return-point saves local to the browser/origin/page, not named game libraries or account backups. Export remains important: clearing browser data removes local saves, and a crash before a pending draft write can lose the latest edit.

Panel actions and safe Pull (implemented)

Clear Builder, Pull left and Push right are compact outlined icons. Clear Builder resets panels only; runner Clear resets source only. Both confirm replacement and preserve the explicit workspace save.

Pull uses the shared, checked-in Acorn browser asset at runtime (no npm build step), never executing source, to read supported literal data from one GameBuilder-style level: catalog background/sprites, Player, NPCs and splines. Source stays untouched. Generated IDs reconstruct character names; the original display capitalization is not recoverable. Unsupported shapes fail without replacing settings. Custom behavior can remain code-owned: Push is blocked unless the whole module is representable by the generator.

Routine operation feedback appears briefly near the controls. Errors remain visible; automatic recovery writes do not repeatedly show notices. Existing runner controls use the same outlined SVG treatment on this page only.

Still not implemented

General legacy code import, multi-module game loading/saving, named workspace libraries, Gamify relocation, and direct writing into VS Code remain future work. NPC, spline barriers, and single-level workspace persistence are implemented.

Current state and reuse opportunities

Target product boundaries and future workspace

Next priority: saved games and the Gamify reference game

The next workspace priority is loading, editing, and saving complete games, followed by bringing Gamify and its levels into this registered system. See GameBuilder and Gamify workspace roadmap for the source findings, proposed ownership, ordered milestones, and verification criteria. This is a plan, not an implemented migration.

The first save milestone will use browser-local persistence plus portable JSON/source export. Existing Gamify behavior must be retained; unsupported objects, callbacks, and custom minigames remain code-owned until panel support can preserve them. Multi-module saves must retain actual level sources, not just a runner entry module that imports the published originals. Game-in-Game and the existing Player gravity flag follow the load/edit/save foundation.

At the planning baseline, GAME_RUNNER restored editor text while GameBuilder reset its panel configuration on reload. The implemented workspace recovery now restores both. Startup does not regenerate over saved manual source.

GameBuilder page
└── .ocs__container.ocs__gamebuilder
    ├── workspace header
    │   ├── project name and save/load controls
    │   ├── builder panel visibility control
    │   └── builder/code mode controls
    └── .ocs__gamebuilder-workspace
        ├── Builder panel (left)
        │   ├── Environment
        │   ├── Player
        │   ├── NPCs / objects
        │   └── Walls / barriers
        └── GAME_RUNNER panel (right)
            ├── standard GAME_RUNNER controls and code editor
            └── game output

The left side is the authoring surface: forms, asset selection, object properties, and point placement. The right side is the canonical execution surface: use GAME_RUNNER to edit and run the generated level code. Do not maintain a second canvas lifecycle, run loop, or game editor in GameBuilder.

The builder panel should be collapsible from a clearly labeled, keyboard accessible control in the workspace header. Collapsing it gives the runner more room without switching pages or discarding builder state. Expose the control’s state with aria-expanded and aria-controls; retain a visible way to reopen the panel. On narrow screens, the same control can hide/show the builder above the runner. Persisting the collapsed state is optional UI preference, not part of the game document.

On wide screens, give the builder a narrower, independently scrollable column and the runner the remaining width. When collapsed, the runner should take the available workspace width. On tablet/mobile widths, stack the panels with the builder first and the runner second. Keep controls keyboard accessible and avoid fixed viewport heights that hide builder fields or runner controls. The runner’s canvas should size from its actual output container, as it already does.

Use semantic OCS classes for the workspace and panels, and existing .ocs__btn controls, inputs, tables, and callouts where applicable. The workspace should use theme variables and existing OCS tokens; avoid hard-coded colors and inline styles. Keep the scoped workspace styles and section-level purpose/reuse/usage documentation in sass/main.scss, rather than extending generic OCS layout rules for this page.

Data flow

Keep three representations distinct:

  1. Builder document — versioned structured configuration; the source of truth for the left-side controls.
  2. Generated level code — deterministic JavaScript generated from the builder document and inserted into the GAME_RUNNER editor.
  3. Runner execution state — the code currently in GAME_RUNNER and the live game instance controlled by GameExecutor.

The flow is:

asset manifests + builder document
                ↓
          validate/generate
                ↓
GAME_RUNNER BaseRunner.setValue(generated code)
                ↓
       GAME_RUNNER Run
                ↓
        GameExecutor

The intended local-development workflow is to run the site and GameBuilder on localhost while editing source files in VS Code. VS Code/Git remain the source of truth for project artifacts; the browser is the interactive authoring, preview, and code-transfer surface. Build/watch distribution should make changes from _projects/systems/gamebuilder/ visible on the local site. Browsers cannot write arbitrary files into a VS Code workspace directly, so the first version should transfer code/configuration through explicit download/upload or copy/paste. Do not imply that localhost grants workspace file access. A local bridge/server that writes files would be a separate, explicitly secured feature.

Builder document shape

Start with a small, explicitly versioned JSON document. Exact field names are implementation details, but its contents should cover:

{
  "schemaVersion": 1,
  "name": "My Game",
  "backgroundKey": "alien_planet",
  "player": {
    "name": "Player",
    "spriteKey": "chillguy",
    "position": { "x": 0.5, "y": 0.8 }
  },
  "npcs": [],
  "barriers": [
    {
      "id": "barrier-1",
      "name": "Barrier 1",
      "points": [{ "x": 0.1, "y": 0.3 }, { "x": 0.5, "y": 0.25 }, { "x": 0.9, "y": 0.3 }]
    }
  ]
}

This is the current v2 document shape. The background is selected by its manifest-derived key; player and NPC positions are normalized from 0 to 1. Each barrier is an open spline with at least two normalized control points. During authoring, click the GAME_RUNNER preview directly to add points. There are no coordinate-editing inputs, Add point button, or separate placement panel. Undo point, Finish barrier, and Cancel live inside the active barrier card. Undo works as a stack: each press removes the most recently added point. Finish requires at least two points. Edit reopens the same controls for a completed barrier; Cancel restores its points from before editing. Finish the active barrier before generating code. Each completed barrier can be hidden, shown, or removed from its card. Hidden barriers are not drawn in the editor or runtime, but remain collision obstacles. Editing a hidden barrier temporarily displays its curve and markers without changing its saved visibility. Curves are smoothed with Catmull–Rom interpolation by the shared GameEngine class. Control-point markers are shown while a barrier is being edited, and each card lists every point’s X/Y coordinates. The runtime renderer uses the OCS accent color. During game updates, the runtime class resolves player overlap against the spline. Barriers and NPCs resize with the logical canvas dimensions, not the browser’s incidental display pixels.

Store manifest keys (or another stable asset identifier), not display labels or duplicated asset metadata. At generation time, resolve keys through the manifest-derived asset catalog and report missing assets as visible validation errors. Player/NPC positions and spline control points use normalized coordinates from 0 through 1, mapped against the runner’s logical game dimensions at runtime rather than persisting incidental screen pixels.

Keep the schema extensible for future object types, but do not build a generic plugin system until there is a real second use case. Treat each object as a typed record with a stable ID and explicit type/properties.

Code generation and runner integration

Extract the generator from the inline v1 page code into a focused module. It should accept validated builder data and an asset catalog, and return either complete level source code or structured validation errors. Keep output compatible with the GAME_RUNNER contract (gameLevelClasses export and GameEngine imports); reuse/adapt v1’s existing level-template logic where practical.

Reuse _includes/runners/game.html with a unique runner_id for the GameBuilder instance. The include currently owns its BaseRunner and GameExecutor in a module-local closure, so a small explicit integration hook is needed to let the builder set code and invoke the runner without reaching into editor internals. Expose only the minimum page-scoped controller operations needed, such as setCode, run, and stop, or an equivalent runner-ready callback. setCode must delegate to BaseRunner.setValue() so editor content and the code read by GameExecutor stay synchronized.

Do not silently replace manually edited code when a builder field changes:

This explicit boundary avoids a confusing two-way sync between arbitrary JavaScript and structured form fields.

Object-literal-aware code import

GameBuilder should be able to import supported level code from the GAME_RUNNER editor and populate the left-side panels. In particular, it should recognize the object-literal configuration patterns used by GameEngine levels: background data, player/NPC data, typed objects, and the this.classes entries that pair a GameEngine class with its data.

This is a structured source-code import, not reverse execution and not a promise to round-trip arbitrary JavaScript. Implement it with a JavaScript parser/AST (select the dependency after checking repository tooling), never regular-expression scraping or eval. Identify bindings and references so a literal such as data: sprite_data_tux can be associated with its declaration. Resolve supported asset path expressions such as path + "/images/..." into project-relative asset references where the manifest/catalog can identify them.

Recommended interaction:

  1. The user selects Import from Code while the runner editor contains the source to inspect.
  2. The importer parses the module and reports syntax errors without changing the current builder document.
  3. It shows a preview of recognized levels and objects, with a mapping from each source binding to the builder section it will populate.
  4. The user accepts the import. The importer updates builder state as one operation and marks generated code as synchronized to that imported document.
  5. The user can then change supported fields in the panels and explicitly regenerate code.

Support a deliberately bounded subset first: static object literals; nested plain object/array values; supported identifiers that refer to other object-literal declarations; known GameEngine class references; and documented asset path expressions. Preserve source spans and unrecognized properties or expressions as opaque source when practical. If a value depends on arbitrary runtime logic (for example a function, computed property, conditional expression, or unsupported constructor), keep it in the code and mark it as code-owned/read-only rather than inventing a panel value or dropping it. Callback fields such as interact and reaction are executable code, not ordinary form data; preserve them unchanged unless a future dedicated code editor for callbacks is added.

Import must be non-destructive. Show unsupported constructs and fields in the preview, distinguish fully editable fields from preserved code-owned fields, and leave the runner source untouched until the user explicitly accepts a conversion or requests regenerated code. If a panel edit would overwrite opaque source that cannot safely be preserved, block that generation and explain which field requires code-mode editing. Add round-trip fixtures based on a small canonical game supplied for v2, plus representative object literals from _projects/games/gamify/levels/; verify import → no-op generate does not silently lose supported configuration or preserved custom code.

Asset handling

System project structure and distribution

The v2 source package lives at _projects/systems/gamebuilder/. Its index.md is the workbench page and authored implementation files, documentation, and images stay inside the registered project:

_projects/systems/gamebuilder/
├── index.md                 # Entry point; mounts the builder and GAME_RUNNER
├── js/
│   ├── app.mjs              # Page wiring and UI lifecycle
│   ├── builder-state.mjs    # Versioned document and validation
│   ├── asset-catalog.mjs    # Manifest loading and asset resolution
│   ├── code-generator.mjs   # Builder document → GameEngine module
│   └── runner-bridge.mjs    # Minimal integration with GAME_RUNNER
├── sass/
│   └── main.scss            # OCS-token styling with SassDoc-style section docs
├── images/                  # Starter backgrounds and spritesheet manifests
├── docs/
│   └── GameBuilderDocV2.md  # Current status and forward-looking design
└── tests/
    └── gamebuilder-contract.test.mjs

The structure above reflects the current implementation; add focused modules only as future features require them. The normal registered-project build distributes the page to _posts/projects/, JavaScript to assets/js/projects/gamebuilder/, Sass to _sass/projects/gamebuilder/ (and its CSS entry point), and project images to images/projects/gamebuilder/. Register the system through the existing project registry and use the standard make dev/build workflow, including the repository’s SASS import-generation/build requirements.

Keep source and distribution boundaries clear:

Save, load, and export

Keep the workspace document separate from the runner’s legacy source slot. The implemented workspace document contains matching configuration and exact source together; source-only runner saves outside GameBuilder remain unchanged.

When a saved workspace is loaded, restore its exact editor source and matching panel configuration without regeneration. For a configuration-only import, offer generation explicitly. Preserve existing saved/manual code until the user accepts replacement.

Runner save-state notification (implemented)

After successfully persisting editor source, the runner emits a bubbling ocs:runner-saved notification with a versioned payload identifying the runner, its storage key, the exact saved source, and a save revision. GameBuilder uses the controller’s awaited workspace-save hook to associate that snapshot with matching panel configuration. The event remains available for other consumers; workspace persistence does not depend on event timing. Other runner pages keep their current Save Code behavior without a workspace subscriber.

This event means source saved, not complete workspace saved, code valid, or safe to regenerate. GameBuilder must report its own persistence success or failure separately. When complete-workspace saving is wired into the runner’s Save action, use an explicit awaited save hook rather than relying on asynchronous event listeners to delay success feedback.

Track saved/dirty source separately from builder/code synchronization: saved manual edits may still differ from generated code. A save notification can offer later AST-based panel import, but must never automatically parse, execute, convert, or overwrite code. Storage failure must produce visible error feedback and no saved event. getSaveState() makes saved source queryable through the runner controller so a late subscriber can initialize correctly.

See the save-state contract in the workspace roadmap for the proposed payload and acceptance criteria.

Implementation sequence

Stage 1 — runner-backed vertical slice (implemented)

Stage 2 — complete builder behaviors

  1. Spline barriers and single-level workspace save/load/recovery are implemented, retaining panel state and exact runner source together, with visible storage errors and portable JSON/source exports.
  2. Add multi-level, multi-module loading and source editing before relocating Gamify; run saved module edits rather than unchanged published imports.
  3. Move Gamify into the registered system with explicit metadata and make-generated catalogs, preserving existing behavior and URLs. Then add bounded AST-based panel import with a non-destructive preview.
  4. Add regression coverage for manifest resolution, generation from representative configurations, missing assets, schema validation, code import/round-trip preservation, and the runner integration hook.

Stage 3 — advanced authoring

  1. Add richer asset metadata and per-object animation/direction overrides, followed by Game-in-Game authoring using the existing nested-game lifecycle.
  2. Expose existing Player gravity without confusing it with custom platformer physics; expand typed objects and placement as compatibility requires.
  3. Consider server/account persistence only after the desired ownership, sharing, and collaboration behavior is specified.

Decisions to confirm before implementation

  1. Draft persistence (confirmed): browser-local complete-game saves plus portable JSON/source exports are the first milestone.
  2. Editor visibility: should the GAME_RUNNER editor always be shown, or should Builder/Code modes be used to show the editor only when requested?
  3. Runner layout: should the runner editor and game output remain stacked as in the existing include, or should the runner itself become a nested code/game split on wide screens?
  4. Import scope: which object-literal patterns in the simple game should be panel-editable in the first importer? The proposal recommends a safe static-literal subset with unsupported code preserved.
  5. Local artifact workflow (confirmed): download/upload and VS Code-managed source files are sufficient initially; direct workspace writes are deferred.
  6. Migration scope: should v2 initially support the current background, player, NPC, and barriers feature set, or may some v1 controls be deferred?

Risks and safeguards