Skip to content

11 — Authoring workflow ​

The order in which to build a game plugin, and what each step depends on. Follow it top to bottom; skipping ahead produces work you will redo.

Step 0 — read the contract ​

Read the SDK documentation pages in full, in the order given in README.md. The questionnaire assumes you know what the engine already provides, so that you can tell the user "the engine does that" instead of designing it again.

Step 1 — interview ​

Run questionnaire.md. Rules:

  • Conduct it in the user's language, whatever the file's language.
  • Ask block by block, not all 14 blocks at once.
  • Offer a "same as tanks" default for every question and let the user accept it with one word.
  • When an answer conflicts with the engine (a custom room setting, a kick vote, runtime localisation), say so immediately and offer the nearest workable alternative.
  • Stop asking about anything the user has already settled implicitly.

Step 2 — design document ​

Write a short design doc and get explicit confirmation before generating code. It must pin down:

DecisionFeeds
Package name, id, titlepackage.json, manifest, both plugins
Repository URLpackage.json repository/homepage — the lobby links to it next to the game's version; without it there is no link and rule A7 warns
Teams and spectator teamgameConfig.teams, stat schema, maps
Entity types and their fieldssnapshot schema, Rust rows, parts
Actions and keysplayerKeys, keySetList, core input handling
Resources shown in the HUDpanel.fields (host) + panel schema (client)
Weapons/abilitiesparts.weapons, core weapon logic, event snapshot keys
What is predicted locallythe 8 × f32 player state, predictor, parity test
Canvases and cameracanvasManager.canvases
Maps and team capacitydata/maps/*, respawns
Sounds and cuessound config, soundCues
ProgressionplayerState.defaultState, onCoreEvent

Step 3 — scaffold ​

Do not hand-write the tree — scaffold it:

bash
npm create vimp-game <directory> -- --id <game-id> --title "<Title>" \
  --repository <user/repo>

Pass --repository (a full URL or the user/repo shorthand) at this step rather than editing package.json later: the platform reads the project link from the published package (homepage, else repository), and adding it after publication costs a republish.

The result is the layout of packaging.md already filled in: the whole build infrastructure (package.json, vite.config.js, vitest.config.js, eslint.config.js, Cargo.toml, the five build scripts), the pins of vimp-engine and vimp-engine-core at their current versions, a dev harness, and a minimal but playable game — two teams, one actor class, one hitscan weapon, one procedural map, bots. It passes check:contract, core:test, test, build and sim from the first commit, so every later step has a green baseline to diff against. The generated CLAUDE.md states the boundaries and the check commands.

From here the work is replacement, not creation: rewrite the configs, the core and the parts of that minimal game into the design document from step 2. assets/ holds two authored inputs: audio-raw/ and img/ — the tile sheets and dynamic-body sprites your maps name. The engine ships none of those.

Writing the tree by hand is still possible (packaging.md specifies every file), but nothing then guards the silent invariants the template already satisfies.

Step 4 — configuration ​

Write the config layer first; it is the contract everything else implements.

  1. src/config/snapshot.js — the wire schema.
  2. src/data/models.js, src/data/weapons.js — the catalogs.
  3. src/config/game.js — HostPlugin.gameConfig (imports the above).
  4. src/config/client.js — the client half.
  5. src/config/auth.js — the auth screen.
  6. src/data/maps/*.js — at least one map with full respawns; every spriteSheet.img and physicsDynamic[].img it names must exist in assets/img/.
  7. src/config/sounds.js — the sound registry.

Cross-check as you go: panel keys match on both halves; playerKeys names match keySetList names; every snapshot key has a gameSets entry; every gameSets class is in entitiesOnCanvas.

Step 5 — Rust core ​

  1. core/src/config.rs — deserialise the game half of the init JSON.
  2. core/src/<actor>.rs — actor spawn/damage/vitals.
  3. core/src/motion.rs — movement math, shared with the predictor.
  4. core/src/<game>.rs — impl GameSim, including build_snapshot_blocks producing rows in the exact schema order.
  5. core/src/client/predictor.rs — local prediction using motion.rs.
  6. core/src/client/mod.rs — impl GameClientDef.
  7. core/src/lib.rs — the two structs plus export_game_core_abi! / export_client_core_abi! and hand-written new + game-specific client methods.

Build: npm run core:build.

Step 6 — client rendering ​

  1. src/client/parts/* — one class per entity projection.
  2. src/client/bakers/* — procedural textures.
  3. src/client/<game>.css — styles for your panel cells and teams.
  4. src/client/index.js — the ClientPlugin with the three hooks.
  5. src/host/index.js, createModules.js, systemMessages.js, chat commands, and the scripted (bot) manager.

Step 7 — tests ​

Two suites:

JavaScript (Vitest) — mirror the tanks arrangement, two projects:

js
projects: [
  {
    test: {
      name: 'game',
      environment: 'happy-dom',
      include: ['tests/host/**', 'tests/client/**', 'tests/config/**'],
    },
  },
  { test: { name: 'integration', environment: 'node', include: ['tests/core/**'] } }, // drives core/pkg-node
];

Cover at minimum:

  • the host plugin exposes every required field and the config passes the engine's required-paths check;
  • the client config and host config agree on panel keys, key names, snapshot keys, gameSets ↔ entitiesOnCanvas;
  • chat commands and the bot manager behave;
  • the manifest script produces a well-formed manifest.

Rust (cargo test --workspace) — unit tests per module plus a motion parity suite: step the authoritative sim and the predictor with the same inputs and assert identical state. Run it after every movement change.

Step 8 — build and run ​

bash
npm run core:build     # wasm-pack web + nodejs
npm run build          # client → host → assets → manifest
npm run dev            # standalone: a match against bots in one tab

vimp-engine/standalone runs the whole match inside one tab of the game repository — no sign-in, no lobby screen; the scaffolded dev/main.js already wires it. startStandaloneGame({ hostPlugin, clientPlugin, wasmUrl, … }) takes the live plugin objects, so it is the fastest loop while the plugin is still taking shape:

js
// dev/main.js in the game repository, loaded by its own index.html
import { startStandaloneGame } from 'vimp-engine/standalone';
import hostPlugin from '../src/host/index.js';
import clientPlugin from '../src/client/index.js';
import wasmUrl from '../core/pkg/my_game_bg.wasm?url';

await startStandaloneGame({
  hostPlugin,
  clientPlugin,
  wasmUrl,
  container: document.getElementById('game'), // full-screen, position: relative
  // dev root of your own assets: the client reads `${assetsBase}img/` and
  // `${assetsBase}sounds/`, both staged into build/ by the copy scripts
  assetsBase: '/build/',
  playerName: 'dev',
  startupVotes: [['teamChange', 'team1']], // leave the spectators first…
  startupCommands: ['/bot 4'], // …only then your own chat commands
});

The engine has no notion of a bot: scripted participants are spawned by your chat command, and the command is rejected while the player is still a spectator — hence the strict order of the last two options. Reference: standalone.md.

Step 9 — headless simulation (do this before the browser) ​

Two browser tabs are the slowest and least informative way to find a broken contract. Run the headless runner first — it closes the whole loop (host → binary frame → client core → scene) in one Node process and names every violated contract in text. Full reference: debugging.md.

bash
npm run sim -- --scenario <scenario.json>
  • [ ] entries.wasmNode points at the copy of the Node core insidedist/ (./core-node/<crate>.js) — otherwise pass --core <path>.
  • [ ] Your own scenarios, not just the built-in one: without --scenario the runner drives a one-key smoke built from your gameConfig and skips invariants 2 and 9, which cannot be meaningful for a game it does not know.
  • [ ] One scenario per major mechanic: movement, firing, death/respawn, round end, map change, a vote.
  • [ ] Every invariant green — in particular snapshotKeysUsed, fieldWidths, renderCoverage, panelContract, keyBindings: these are the silent failures from pitfalls.md, mechanised.
  • [ ] --determinism green once the match is stable.
  • [ ] Prediction drift within threshold (implement GameClientDef::predicted_state for a component-wise report).

Fix everything the runner reports before opening a browser. Anything the browser later catches can be recorded with window.__vimpDebug and replayed here via npm run sim:replay.

Step 10 — manual check ​

Open npm run dev in a browser tab (the standalone match):

  • [ ] The map renders on every canvas; the radar (if any) matches.
  • [ ] Movement is responsive and the bots move smoothly.
  • [ ] Firing produces exactly one effect locally (no duplicate from the authoritative event).
  • [ ] The panel updates: resource bar, ammo, active weapon, round timer.
  • [ ] The stat table (Tab) lists the participants with correct teams and updates score/deaths.
  • [ ] Chat works; a game system message renders with its text.
  • [ ] A vote can be started, voted on and resolved.
  • [ ] Sounds play, spatially for the bots' shots.
  • [ ] Killing ends the round; scores and the winner message are correct.
  • [ ] A map change vote rotates the map and rebuilds everything.
  • [ ] npm run check:contract is green and dist/manifest.json is complete: the platform skips a package it cannot read, so the game would be invisible in the lobby.

Rebuild matrix ​

ChangeRequired commands
src/config/*, src/data/* (JS)dev: nothing (HMR); prod: npm run build
src/client/**, src/host/** (JS)dev: nothing (HMR); prod: npm run build
Maps (src/data/maps/*)npm run build:assets && npm run build:manifest (the platform reads dist/maps/)
Sounds (raw assets)npm run audio:process && npm run build:assets && npm run build:manifest
Images (assets/img/*)npm run build:assets && npm run build:manifest; in dev predev already staged build/img/
Rust corenpm run core:build (+ reload; prod also npm run build)
roomForm / roomDefaultsnpm run build:manifest
Anything, before the first runfull npm run core:build && npm run build

Two rules behind the table: the platform reads dist/manifest.json and dist/maps/ from the built package, and npm run build never rebuilds the WASM core.

Definition of done ​

  • [ ] npx eslint . clean.
  • [ ] npm test green (both Vitest projects).
  • [ ] npm run core:test green, including motion parity.
  • [ ] npm run core:build && npm run build succeeds from a clean dist/.
  • [ ] npm run sim -- --game <package> exits 0 on every scenario you wrote (see debugging.md).
  • [ ] The manual checklist above passes in the standalone tab.
  • [ ] The game is published and submitted (submitting.md).
  • [ ] Every box in pitfalls.md is verified.

vimp-engine 0.38.1 · vimp-engine-core 0.23.2 · create-vimp-game 0.5.4