@jgengine/github 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,18 +4,140 @@
4
4
 
5
5
  Reach for these before hand-rolling. Each row is *the thing you need* → *the primitive that already does it*.
6
6
 
7
- ## define-game — single public game-authoring path — compose systems, world, and loop in one definition
7
+ ## authored-spawn — read spawn points and markers from the editor document
8
8
 
9
- - `defineGame` (function) · `import { defineGame } from "@jgengine/core/authoring"`
9
+ - `authoredSpawnPosition` (function) · `import { authoredSpawnPosition } from "@jgengine/shell/gameKit"`
10
+
11
+ ## authored-triggers — schema'd on/action vocabulary on volumes and markers with runtime dispatch
12
+
13
+ - `collectAuthoredTriggers` (function) · `import { collectAuthoredTriggers } from "@jgengine/shell/gameKit"`
14
+
15
+ ## collider-autofit — hitboxes and physical bodies fit each kind's rendered model bounds automatically — no hand-tuned sizes
16
+
17
+ - `GameContextModels` (interface) · `import { GameContextModels } from "@jgengine/core/runtime/gameContext"`
18
+
19
+ ## default-walk-codes — stock WASD + jump key codes for the shell walk controller
20
+
21
+ - `DEFAULT_WALK_CODES` (const) · `import { DEFAULT_WALK_CODES } from "@jgengine/shell/gameKit"`
22
+
23
+ ## define-game — single public game-authoring path — compose systems, world, presentation, and authored scene in one definition
24
+
25
+ - `defineGame` (function) · `import { defineGame } from "@jgengine/shell/gameKit"`
26
+
27
+ ## define-system — composable game behavior — pure data + hooks the engine schedules; pass via `defineGame({ systems })`
28
+
29
+ - `defineSystem` (function) · `import { defineSystem } from "@jgengine/shell/gameKit"`
30
+
31
+ ## editor-catalogs — Export typed gameplay catalogs for in-editor tuning.
32
+
33
+ - `EditorCatalogDefinition` (interface) · `import { EditorCatalogDefinition } from "@jgengine/shell/gameKit"`
34
+
35
+ ## game-system — declare a composable capability with its own schedule and lifecycle
36
+
37
+ - `SystemDefinition` (interface) · `import { SystemDefinition } from "@jgengine/shell/gameKit"`
38
+
39
+ ## grounded — ground an XZ point onto the world surface as a pose triple
40
+
41
+ - `grounded` (function) · `import { grounded } from "@jgengine/shell/gameKit"`
10
42
 
11
43
  ## headless-runner — play a real game loop with no renderer — tick, feed input, read the world snapshot
12
44
 
13
45
  - `HeadlessRunner` (interface) · `import { HeadlessRunner } from "@jgengine/core/runtime/headlessRunner"`
14
46
 
47
+ ## host-join-code-gate — membership-or-code gate for private hosted servers
48
+
49
+ - `canJoinPrivateServer` (function) · `import { canJoinPrivateServer } from "@jgengine/core/runtime/hostPolicy"`
50
+
51
+ ## host-listing-filter — exclude private sessions from public browse results
52
+
53
+ - `isListablePublicly` (function) · `import { isListablePublicly } from "@jgengine/core/runtime/hostPolicy"`
54
+
55
+ ## hud-canvas — the HUD root surface — design-resolution scaling plus live panel-placement editing (F2+C)
56
+
57
+ - `HudCanvas` (function) · `import { HudCanvas } from "@jgengine/shell/gameKit"`
58
+
59
+ ## hud-clock — in-game time-of-day readout bound to the sim clock
60
+
61
+ - `Clock` (function) · `import { Clock } from "@jgengine/shell/gameKit"`
62
+
63
+ ## hud-coins — currency counter widget bound to a currency id
64
+
65
+ - `Coins` (function) · `import { Coins } from "@jgengine/shell/gameKit"`
66
+
67
+ ## hud-hotbar — optional inventory hotbar building block — games own slot chrome
68
+
69
+ - `Hotbar` (function) · `import { Hotbar } from "@jgengine/shell/gameKit"`
70
+
71
+ ## hud-layout — persistent, live-editable HUD panel layout state for `HudCanvas`
72
+
73
+ - `useHudLayout` (function) · `import { useHudLayout } from "@jgengine/shell/gameKit"`
74
+
75
+ ## hud-panel — anchored HUD slot — drop any widget in a named, player-movable panel
76
+
77
+ - `HudPanel` (function) · `import { HudPanel } from "@jgengine/shell/gameKit"`
78
+
79
+ ## hud-stat-bar — optional stat-bound bar building block (health, mana, stamina) — prefer game-owned chrome
80
+
81
+ - `StatBar` (function) · `import { StatBar } from "@jgengine/shell/gameKit"`
82
+
15
83
  ## loot-table — validate a loot table definition for use with the registry
16
84
 
17
85
  - `lootTable` (function) · `import { lootTable } from "@jgengine/core/authoring"`
18
86
 
87
+ ## mount-game — mount a defined game in the browser — `<GameHost playable={game} editor={() => import("@jgengine/editor")} />`
88
+
89
+ - `GameHost` (function) · `import { GameHost } from "@jgengine/shell/gameKit"`
90
+
91
+ ## multiplayer-offline — explicit solo adapter — omit `multiplayer` instead; offline is the shell default
92
+
93
+ - `offline` (function) · `import { offline } from "@jgengine/shell/gameKit"`
94
+
95
+ ## multiplayer-p2p — serverless co-op over WebRTC — `multiplayer: p2p({ room })`
96
+
97
+ - `p2p` (function) · `import { p2p } from "@jgengine/core/runtime/adapter"`
98
+
99
+ ## multiplayer-ws — server-hosted shared world over WebSocket — `multiplayer: ws({ authority: "server" })`
100
+
101
+ - `ws` (function) · `import { ws } from "@jgengine/core/runtime/adapter"`
102
+
103
+ ## render-fallback-diagnostics — Enable/disable the dev-only placeholder-fallback probe.
104
+
105
+ - `armFallbackSeams` (function) · `import { armFallbackSeams } from "@jgengine/core/devtools/fallbackSeams"`
106
+ - `armTextureErrors` (function) · `import { armTextureErrors } from "@jgengine/core/devtools/textureErrors"`
107
+ - `fallbackSeamsSnapshot` (function) · `import { fallbackSeamsSnapshot } from "@jgengine/core/devtools/fallbackSeams"`
108
+ - `reportFallbackSeam` (function) · `import { reportFallbackSeam } from "@jgengine/core/devtools/fallbackSeams"`
109
+ - `reportTextureLoadError` (function) · `import { reportTextureLoadError } from "@jgengine/core/devtools/textureErrors"`
110
+ - `resetTextureErrors` (function) · `import { resetTextureErrors } from "@jgengine/core/devtools/textureErrors"`
111
+ - `textureErrorsSnapshot` (function) · `import { textureErrorsSnapshot } from "@jgengine/core/devtools/textureErrors"`
112
+
19
113
  ## runtime-save — save/load the whole game world through a pluggable backend, autosave or save points
20
114
 
21
115
  - `createRuntimeSave` (function) · `import { createRuntimeSave } from "@jgengine/core/runtime/runtimeSave"`
116
+
117
+ ## scene-ownership — audit a whole ownership manifest for boundary violations
118
+
119
+ - `auditManifest` (function) · `import { auditManifest } from "@jgengine/core/authoring"`
120
+ - `classifyOwnership` (function) · `import { classifyOwnership } from "@jgengine/core/authoring"`
121
+ - `collectOwnershipDiagnostics` (function) · `import { collectOwnershipDiagnostics } from "@jgengine/core/authoring"`
122
+ - `isSceneOwnershipManifest` (function) · `import { isSceneOwnershipManifest } from "@jgengine/core/authoring"`
123
+ - `ownershipKey` (function) · `import { ownershipKey } from "@jgengine/core/authoring"`
124
+
125
+ ## seeded-random — injected deterministic randomness for game logic — never `Math.random` in a simulation
126
+
127
+ - `seededRng` (function) · `import { seededRng } from "@jgengine/shell/gameKit"`
128
+
129
+ ## typed-keyed-store — a cast-free typed handle onto a per-owner keyed family of reactive game-store slots, replayable and host-authoritative
130
+
131
+ - `defineKeyedStore` (function) · `import { defineKeyedStore } from "@jgengine/shell/gameKit"`
132
+
133
+ ## typed-store — a cast-free typed handle onto one reactive game-store slot, replayable and host-authoritative
134
+
135
+ - `defineStore` (function) · `import { defineStore } from "@jgengine/shell/gameKit"`
136
+
137
+ ## world-convergence — derive environment coordinate content from the scene document
138
+
139
+ - `environmentContentFromDocument` (function) · `import { environmentContentFromDocument } from "@jgengine/shell/gameKit"`
140
+
141
+ ## world-environment — compose editor/preset-derived outdoor dressing — terrain, weather, water, structures — into a legacy environment feature
142
+
143
+ - `environment` (function) · `import { environment } from "@jgengine/shell/gameKit"`
@@ -0,0 +1,91 @@
1
+ # Recipe — minimal game (scaffold → authored scene → win)
2
+
3
+ **What this wires:** the default end-to-end path from nothing to a complete, genre-free
4
+ walk/interact/win game: a scaffold, one `defineGame` config off the happy-path kit, an
5
+ authored scene edited in the browser, one system, one store slot, an empty-canvas HUD,
6
+ and a win condition. Every new game starts here; add combat, multiplayer, or deeper
7
+ world features only after this loop plays.
8
+
9
+ ## 1. Scaffold
10
+
11
+ `npx jgengine create <name>` (inside the engine monorepo: `bun run new:game "<id-or-title>"`).
12
+ The file set is thin and already runs:
13
+
14
+ - `src/main.tsx` — mounts `GameHost` with `editor={() => import("@jgengine/editor")}`; the
15
+ host owns the F2+E / `?mode=editor` editor summon and the dev save endpoint.
16
+ - `src/game.config.ts` — the whole game definition (below).
17
+ - `src/editor.scene.json` + `src/editorLayers.ts` — the authored scene document, normalized
18
+ with `normalizeEditorLayers`. All placement lives here, never as coordinates in code.
19
+ - `src/loop.ts` and `src/game/ui/GameUI.tsx` — systems and HUD.
20
+
21
+ ## 2. Define the game — one import surface
22
+
23
+ ```ts
24
+ // game.config.ts — everything below comes from the kit; deeper paths are for outgrowing it.
25
+ import { defineGame } from "@jgengine/shell/gameKit";
26
+
27
+ import { editorLayers } from "./editorLayers";
28
+ import { GameUI } from "./game/ui/GameUI";
29
+ import { onNewPlayer, systems } from "./loop";
30
+
31
+ export const game = defineGame({
32
+ name: "my-game",
33
+ // Binding movement actions makes the shell drive the walk controller — the game walks day one.
34
+ input: { moveForward: ["KeyW"], moveBack: ["KeyS"], moveLeft: ["KeyA"], moveRight: ["KeyD"], interact: ["KeyE"] },
35
+ systems,
36
+ loop: { onNewPlayer },
37
+ GameUI,
38
+ editorLayers, // the authored scene auto-mounts; the player spawns at its player_spawn marker
39
+ });
40
+ // Solo game: omit `multiplayer` entirely — offline is the shell default.
41
+ ```
42
+
43
+ **World is optional and thin.** Omitting `world` is valid — a pure UI/rules game is not a
44
+ spatial place. When the game *is* a place, declare only substrate + laws with `world()` from
45
+ `@jgengine/core/world/place` (e.g. `{ id, ground: { mode: "flat", size: { x: Infinity, z: Infinity } }, physics: { gravity: -24 } }`,
46
+ or `mode: "board"` for 2D). Never seed, sky, or vegetation in `world` — the engine renders its
47
+ default sky until you author one in the editor, and foliage is editor scatter over the ground.
48
+
49
+
50
+ ```ts
51
+ // loop.ts — one store slot for run state, one system for the rules tick.
52
+ import { defineStore, defineSystem, authoredSpawnPosition, type GameContext } from "@jgengine/shell/gameKit";
53
+
54
+ import { editorLayers } from "./editorLayers";
55
+
56
+ export const run = defineStore("run", { collected: 0, won: false });
57
+
58
+ export const systems = [
59
+ defineSystem({
60
+ id: "rules",
61
+ tick: { type: "fixed" },
62
+ update(ctx: GameContext) {
63
+ // System-check win alternative: read/update `run` here (e.g. win at collected >= 3).
64
+ if (run.read(ctx).collected >= 3) run.update(ctx, (s) => ({ ...s, won: true }));
65
+ },
66
+ }),
67
+ ];
68
+
69
+ export function onNewPlayer(ctx: GameContext): void {
70
+ ctx.scene.entity.spawn("player", { id: ctx.player.userId, position: authoredSpawnPosition(editorLayers) ?? [0, 0, 0] });
71
+ }
72
+ ```
73
+
74
+ `GameUI` starts as an empty `HudCanvas` (`useHudLayout` + `HudCanvas` from the kit). That is
75
+ intentional: **every game owns its UI composition** — layout, terminology, placement, and art
76
+ direction. Compose the shipped building blocks the moment you need them (`StatBar`, `Hotbar`,
77
+ `Coins`, `InventoryGrid`, `CharacterSheet`, window shells from `jgengine-ui`), then reskin via
78
+ `HudTheme` tokens. Reaching for those is using the engine correctly, not incomplete work. What
79
+ the engine does not ship is a whole finished game *face* or genre theme preset — do not leave an
80
+ unarranged, unskinned generic dashboard as the product. Hand-roll markup only where no building
81
+ block fits (see `jgengine-ui`).
82
+
83
+ ## 3. Author, play, win
84
+
85
+ 1. `bun dev`, then F2+E opens the editor on `editor.scene.json` — move the spawn, place
86
+ props and trigger markers, Ctrl+S saves back to the document.
87
+ 2. The starter scene ships a goal marker with `{ on: "enter", action: "win" }` — walking
88
+ into it wins with zero game code. Author more rules the same way (triggers in the
89
+ editor), or read volumes yourself via the kit's `collectAuthoredTriggers(editorLayers)`.
90
+ 3. Prove it: `bun test` asserts the document (spawn, goal), and `jgengine-verify` covers
91
+ screenshot and gameplay evidence before any "it works" claim.
@@ -1,72 +0,0 @@
1
- # jgengine-api — Cartridge
2
-
3
- Reference module for the [`jgengine`](./SKILL.md) skill. Load this when building a game as a **cartridge** — one declarative config instead of hand-wired loop/state/UI files. A cartridge game is ~75% smaller than the classic shape and nearly all of it is data; reach for `defineGame` + `defineSystem` ([reference-systems.md](./reference-systems.md)) when the simulation outgrows cartridge archetypes — same single authoring model, no rewrite cliff.
4
-
5
- ## Why
6
-
7
- Across the classic games, 78–91% of each game's LOC was generic wiring restated per game: a run-state store with subscribe/notify, chase-and-contact enemy loops, auto-fire weapon routines, magnet pickups, draft plumbing, phase-gated HUD trees, and tests for all of it. The cartridge layer owns every one of those; the game ships only its **data** (catalogs, tuning, layout) and any genuinely bespoke rules through typed escape hatches.
8
-
9
- ## Shape
10
-
11
- `game.config.ts` is the whole game — `cartridge({...})` from `@jgengine/shell/cartridge` compiles the config into a `PlayableGame` (validating it first — a bad reference throws at import with a list of problems). `loop.ts` and `world.ts` disappear; the spec carries the world feature and the loop is engine-owned. `main.tsx` is 5 lines via `mountGame` (`@/components/ui/mount`, registry). Sprites/SVG art stay in `src/game/assets.ts`; the game keeps one test file under `src/game/`.
12
-
13
- ```ts
14
- import { cartridge, type CartridgeConfig } from "@jgengine/shell/cartridge";
15
- import { standardCartridgePanels } from "@/components/ui/cartridge-panels";
16
-
17
- export const config: CartridgeConfig = {
18
- name, seed, panels: standardCartridgePanels, assets, entitySprites,
19
- flow: { start: "gate", countdownSeconds: 3, restart: true }, // press-to-start → 3-2-1 → playing; "restart" command
20
- player: { kind, health, walkSpeed }, // compiled into content + spawned per player
21
- enemies: { id: { label, health, walkSpeed, xp, contact: { damage, intervalSeconds }, behavior: "chase" | "none" } },
22
- combat: { contactRadius },
23
- spawning: { director: SpawnDirectorConfig, placement: { kind: "ring", radius } },
24
- weapons: { id: { kind: "projectile" | "orbit" | "pulse" | "custom", damage, cooldownMs, maxLevel, ... } },
25
- progression: { xp: Curve, maxLevel, draft: { choices, upgrades } },
26
- fields: { magnetRadius, damageMultiplier }, // named run-scalars upgrades mutate
27
- xpGems: { collectRadius, pullSpeed, rarityThresholds, defaultRarity },
28
- rules: { win: { kind: "survive", seconds }, lose: { kind: "playerDeath" } | { kind: "custom", check }, killLeaderboardStat },
29
- world, physics, camera, worldItem, theme, hud, screens, // screens: start/win/lose
30
- };
31
- export const game = cartridge(config);
32
- ```
33
-
34
- The run is a phase machine every game gets for free: `start → countdown → playing → won | lost`. `flow.start: "gate"` renders `screens.start` (TitleScreen binding) until begin; `flow.countdownSeconds` renders the big-number countdown; `flow.restart: true` registers a `restart` command that resets the run in place, clears cartridge entities, and restores player stats — no hand-rolled session store, phase union, or reset function. `run.playingSeconds` is the run clock (drives the survive rule and the timer panel), so pauses and pre-game phases never skew timing.
35
-
36
- ## Cartridge surface (`@jgengine/shell/cartridge/`)
37
-
38
- The survivor-shaped defaults (XP gems, contact-radius melee, orbit/pulse/projectile weapons) are a genre archetype, not a core primitive — the whole cartridge implementation lives in `@jgengine/shell` (opt-in, alongside its React binding), never in `@jgengine/core`. `core` only supplies the generic pieces it composes: `spawnDirector`, `abilityKit`, `leveling`/`progression`, `runDraft`, `autoTarget`. A puzzle or tactics game imports none of this.
39
-
40
- - `@jgengine/shell/cartridge/spec` — the `CartridgeSpec` types, `Leveled` values (`number | { base, perLevel, min?, max? } | { table }` resolved by `leveled(value, level)`), `WASD_KEYBINDS` (the default when `input` is omitted).
41
- - `@jgengine/shell/cartridge/runtime` — `createCartridge(spec)` → `{ content, loop, run(ctx), weaponKit(ctx), chooseUpgrade(ctx, offerId) }`. Owns the run store (outcome/kills/fields/weapon levels, subscribe/notify), spawn director advance + ring placement, chase + contact-damage enemies (terrain-grounded), the three weapon archetypes with auto-targeting, magnet xp-gem pickups feeding `leveling()`, pause-and-draft level-ups, kill → gem drop + leaderboard, and win/lose rules.
42
- - `@jgengine/shell/cartridge/validate` — `validateCartridge(spec)` → problem list: spawn waves must reference declared enemies, upgrades must reference declared weapons/fields, stacks can't exceed weapon `maxLevel`, tuning must be positive, rarity thresholds descending. The game's test asserts it returns `[]`.
43
-
44
- Weapon archetypes: `projectile` (auto-target nearest in `range`, travel-time bolt fx), `orbit` (leveled blade count/radius sweeping `hitRadius`), `pulse` (radial AoE with linear falloff and an expanding-ring fx). Upgrade effects: `weaponLevel` (+1 level per stack), `statBonus` (+max and +current on pick), `fieldAdd` / `fieldMultiply` (recomputed from stacks). Weapon damage is multiplied by the `damageMultiplier` field when declared.
45
-
46
- ## Presentation (shell + registry)
47
-
48
- `hud.panels` is a schema — items `vital | xp | timer | score | abilityBar | component`, anchored via the movable HUD layout; `screens.win/lose` render the results/death screens; the upgrade-draft modal appears whenever a draft is pending. The shell renders the frame and injects concrete components through the `panels` seam — `standardCartridgePanels` (registry `cartridge-panels`) binds the jgengine UI kit; swap any binding for a custom component without touching the engine. Weapon fx render engine-side from the run store's bolt/pulse feeds (`fxColor`/`fxEmissive` per weapon). `theme` is the `--jg-*` CSS var block — hand-pick it or derive from a few seed colors with `deriveJgTheme` (registry `jg-theme`).
49
-
50
- ## Escape hatches — bespoke logic without leaving the cartridge
51
-
52
- - `weapons.<id> = { kind: "custom", fire(ctx, run, args) }` — cooldown/level/damage handled for you; the fire shape is yours.
53
- - `spawning.placement = { kind: "custom", position(ctx, run) }`.
54
- - `rules.win = { kind: "custom", check(ctx, run) }`.
55
- - `progression.draft.upgrades[].effect = { kind: "custom", apply(ctx, run, stacks) }`.
56
- - `systems: [(ctx, run, dt) => ...]` — extra per-tick simulation after the built-ins, only while playing.
57
- - `hud` items `{ kind: "component", Component }` — arbitrary React panels alongside the schema ones.
58
-
59
- A game that is *mostly* bespoke simulation (custom physics, session machines) should keep `defineGame` and write its systems — the cartridge is for games whose shape the archetypes already cover.
60
-
61
- ## Testing
62
-
63
- Cartridge behaviors are engine-tested once (`packages/shell/src/cartridge/runtime.test.ts`); the game test is one call plus any game-specific assertions:
64
-
65
- ```ts
66
- import { bootCartridge, cartridgeSmokeTest, tickCartridge } from "@jgengine/shell/cartridge/testkit";
67
- cartridgeSmokeTest(config); // validate + world summary + headless run/spawn/kill/gem smoke
68
- ```
69
-
70
- `bootCartridge`/`tickCartridge` build a headless `GameContext` and drive the loop (auto-choosing drafts) for custom assertions — see `@jgengine/shell/cartridge/testkit`. The testkit imports `bun:test`; import it from test files only.
71
-
72
- Barrel surface tokens: createKeyValueStore createUnlocks durabilityFraction isComplete isOverdrawn memorySaveBackend partInSlot pruneToasts policyProjectsViewers projectByVisibleIds projectEntitiesForViewer projectPerUserForViewer visibleEntityIds applyDeltaToSnapshot applySurfaceDeltaToSnapshot boundaryNeighbors catenaryCurve distanceToPolygonEdge footprintObstacles hasValidAdjacency pointInPolygon polygonArea polygonBounds populateNavGridFromEnvironment readScatterPalette readScatterRules revertDeltaFromSnapshot revertSurfaceDeltaFromSnapshot slopeStepCost solveLock solveLockPath
@@ -1,9 +0,0 @@
1
- # Barrel helpers (orphan anchors)
2
-
3
- Surfaced so the orphan gate keeps public barrel re-exports discoverable.
4
-
5
- Gameplay: `createKeyValueStore` `createUnlocks` `durabilityFraction` `isComplete` `isOverdrawn` `memorySaveBackend` `partInSlot` `pruneToasts`
6
-
7
- World projection: `policyProjectsViewers` `projectByVisibleIds` `projectEntitiesForViewer` `projectPerUserForViewer` `visibleEntityIds`
8
-
9
- World: `applyDeltaToSnapshot` `applySurfaceDeltaToSnapshot` `boundaryNeighbors` `catenaryCurve` `distanceToPolygonEdge` `footprintObstacles` `hasValidAdjacency` `pointInPolygon` `polygonArea` `polygonBounds` `populateNavGridFromEnvironment` `readScatterPalette` `readScatterRules` `revertDeltaFromSnapshot` `revertSurfaceDeltaFromSnapshot` `slopeStepCost` `solveLock` `solveLockPath`
@@ -1,60 +0,0 @@
1
- # Composable systems (`defineSystem`)
2
-
3
- Single public authoring path: `defineGame({ systems, loop?, … })` from `@jgengine/shell/defineGame` (or core). Systems are meaningful capabilities — not micro-ticks.
4
-
5
- ## API
6
-
7
- ```ts
8
- import { defineSystem } from "@jgengine/core/game/defineSystem";
9
- import { compileSystemSchedule, DEFAULT_FIXED_STAGES, DEFAULT_FRAME_STAGES } from "@jgengine/core/game/systemSchedule";
10
- import { composeGameLoop, installSystems } from "@jgengine/core/game/systemRuntime";
11
-
12
- export const combat = defineSystem({
13
- id: "combat",
14
- feature: "quest", // optional — enables ctx.game.quest without features: { quest: true }
15
- dependsOn: ["movement"], // optional — must also be in systems[]
16
- tick: {
17
- type: "fixed", // | "frame" | "interval" | "manual"
18
- rate: 60, // fixed only (Hz); default 60
19
- every: 1, // interval only (game-seconds)
20
- stage: "combat",
21
- after: "movement", // optional local order within the stage
22
- before: "cleanup",
23
- },
24
- create(ctx) {},
25
- start(ctx) {},
26
- update(ctx, dt) {},
27
- events: {
28
- "entity.died"(ctx, event) {},
29
- },
30
- save: { key: "combat", snapshot: () => state, hydrate: (data) => { state = data; } },
31
- replicate: (ctx) => ({ key: "combat-pub", snapshot: () => pub, hydrate: (d) => { pub = d; } }),
32
- reset(ctx) {},
33
- dispose(ctx) {},
34
- });
35
- ```
36
-
37
- `defineGame` OR-merges `feature` flags, then `composeGameLoop(systems, loop)` so systems install on `onInit` and tick before residual `loop.onTick`. Classic games migrate by moving fan-out out of `onTick` into systems while keeping boot in `loop`.
38
-
39
- ## Timing channels
40
-
41
- | `tick.type` | When it runs |
42
- |-------------|--------------|
43
- | `fixed` | Accumulator at `rate` Hz (default 60); multi-subscribe OK |
44
- | `frame` | Once per `onTick` with the frame's game `dt` |
45
- | `interval` | Every `every` game-seconds (own accumulator per system) |
46
- | `manual` | Installed only; call `systemsOf(ctx)?.runManual(ctx, id)` |
47
- | *(omit)* | Event-only — handlers in `events` |
48
-
49
- Multiple systems may share a channel. Order is stage tables + optional `before`/`after` — **never import order**. `compileSystemSchedule` validates unique ids, `dependsOn`, and cycles.
50
-
51
- Default stages: fixed `input → movement → combat → ai → activities → cleanup`; frame includes those plus `animation → camera → effects`.
52
-
53
- ## Cartridge path
54
-
55
- Cartridge remains a convenience wrapper. Express the same shape as ordinary systems (`runFlow`, wave director, combat) under `defineGame({ systems })` when you outgrow the cartridge schema — one authoring model, no rewrite cliff.
56
-
57
- ## Ownership
58
-
59
- - **Save / replicate** — system modules register via `ctx.game.registerSave` / `registerReplicate` at install.
60
- - **Reset / dispose** — `loop.onReset` / `loop.onDispose` (composed) run system hooks.