{
  "schema": "leumas.docs.page/1",
  "id": "pkg:@leumas/game-3d",
  "slug": "packages/game-3d",
  "kind": "capabilities",
  "bucket": "package",
  "title": "@leumas/game-3d",
  "name": "@leumas/game-3d",
  "eyebrow": null,
  "chip": null,
  "summary": "Presentation for the Leumas game engine: the ECS-to-Object3D bridge, pointer-lock controllers and camera rigs, asset loading, and debug draw. Browser only â€” the simulation kernel is...",
  "keywords": [
    "game-3d",
    "pointer-lock",
    "game-3d game-kit",
    "rigs",
    "debug",
    "game 3d api",
    "leumas game 3d",
    "how to use game 3d"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# @leumas/game-3d\n\nPresentation for the Leumas game engine: the ECS-to-`Object3D` bridge, pointer-lock input and camera\nrigs, asset loading with reference counting, debug draw, and the runtime that wires them together.\n\nThe simulation lives in [`@leumas/game-kit`](../game-kit) and **never imports this package**. The\ndependency runs one way — presentation reads the world and never writes to it. If the renderer could\nnudge a position, the picture would be an input to the game and a headless server would play a\ndifferent match than the one on screen.\n\n## The five things worth knowing\n\n**1. Three-ful at the root, and that is legal *here*.** The rule the bundle guards enforce is about\n**entry points**, not packages: this one is never on a boot path, and every reach into it is a dynamic\n`import()` from a route-split surface. That is the same allowance `check-features-eager-three`\ndocuments for the `solar` nav-view. Import it eagerly from a page that renders before login and you\nwill make that guard right and this README wrong.\n\n**2. Interpolation is the bridge's reason to exist.** The simulation runs at a fixed rate the display\ndoes not share — 30 Hz sim on a 144 Hz monitor is normal. Every `Transform` carries the previous\ntick's pose alongside the current one, and `sync(alpha)` blends between them. Skip it and a perfectly\ngood 30 Hz simulation looks like a 30 Hz slideshow no matter how fast the client renders. Rotation is\n**slerped**, not lerped: component-wise interpolation of a quaternion shortens the vector and the\nresult speeds up and slows down through the turn.\n\n**3. Reference counting is what keeps a long session alive.** Geometry and materials hold WebGL\nbuffers that garbage collection does not touch. Dispose too eagerly and the first crate destroyed\nblanks the other nine hundred, with no error. Never dispose and GPU memory climbs until the canvas\ngoes black, minutes into play and nowhere near the cause. So the registry hands out **clones that\nshare geometry**, counts them, and frees the original only when the last one is released.\n\n**4. Actions, not keys.** Every binding is `move.forward -> [KeyW, ArrowUp]`. Codes are\n`KeyboardEvent.code`, which is *physical position*, so WASD stays under the same fingers on AZERTY and\nDvorak. Remapping is not optional — left-handed players and anyone who cannot reach a chorded key need\nit — and this is the difference between it being a feature and being a refactor.\n\n**5. Every browser dependency is injected.** `element`, `doc`, `now`, `createRenderer`. Not for\npurity: it is what lets the whole assembly, including its teardown, be tested in Node with no GPU. A\nruntime that can only be verified by looking at it is a runtime whose `dispose()` is never checked.\n\n## Entry points\n\n| Subpath | What |\n|---|---|\n| `.` | the barrel — everything below |\n| `./runtime` | `createGameRuntime` — scene, loop, input, bridge, wired in the right order |\n| `./bridge` | ECS → `Object3D`, interpolated |\n| `./assets` | source strings → models, cached and reference counted |\n| `./controller` | action bindings, pointer lock, the FPS camera |\n| `./debug` | collider overlays and the performance snapshot |\n| `./demo` | `createBoxArena` — the walkable test level, as pure data |\n| `./react` | `<GameCanvas>` — a sized box and a teardown, nothing more |\n\n## Four traps\n\n**A `Modal` must never live inside the canvas host.** `.lms-chud` sets `container-type`, which makes\nit the containing block for `position: fixed`, and Studio's modal backdrop is fixed and not portalled.\n\n**Never mount two runtimes on one element.** Each takes a WebGL context; browsers cap live contexts at\nroughly sixteen, and after a dozen navigations every canvas in the tab renders black. `dispose()`\ncalls `forceContextLoss()` for exactly this reason — `renderer.dispose()` alone is not enough.\n\n**Resize from a `ResizeObserver` on the element, never a window listener.** A panel can be dragged\nwider without the window changing at all, and the canvas would stay stretched until reload.\n\n**Do not drive the HUD from React state.** A game that calls `setState` sixty times a second spends\nits frame budget on reconciliation. `GameCanvas` takes a render prop and the runtime exposes\n`stats()`; read it on an interval, not per frame.\n\n## Example\n\n```js\nimport { createGameRuntime, createBoxArena } from '@leumas/game-3d';\nimport { createPrefabPack } from '@leumas/game-kit';\n\nconst arena = createBoxArena();\nconst pack = createPrefabPack(arena.prefabs, 'arena-v1');\n\nconst runtime = createGameRuntime({\n  element,\n  arena,\n  pack,\n  hz: 60,\n  onFrame: ({ character }) => hud.setHeight(character.y),\n});\n\nruntime.start();\nelement.addEventListener('click', () => runtime.controller.requestLock());\n\n// Later, without fail:\nruntime.dispose();\n```\n\n## Verify\n\n```sh\npnpm --filter @leumas/game-3d test   # 91 tests\npnpm check:cinematic                 # the bundle-weight and JSX guards\n```\n\nThe test worth knowing about is `arena.test.js`: it walks a character across the demo level headless —\nup the staircase without jumping, onto the 1-unit platform only *with* a jump, into cover, and around\nthe perimeter for three thousand steps without escaping or producing a `NaN`. It cannot prove a\npicture appeared, but it proves the kernel, the collision world and the controller agree about a\nlevel, which is the part that would otherwise only surface by walking around in a browser.\n",
  "source": {
    "path": "shared/packages/game-3d/README.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 5602,
    "hash": "e540fd0ee3e4e433a6801665270550e54946272b"
  },
  "urls": {
    "html": "/p/packages/game-3d",
    "json": "/docs/packages/game-3d.json",
    "md": "/docs/packages/game-3d.md"
  },
  "links": {
    "composes": [
      "pkg:@leumas/body-3d",
      "pkg:@leumas/game-kit",
      "pkg:@leumas/sound"
    ],
    "usedBy": [
      "pkg:@leumas/features",
      "pkg:@leumas/studio",
      "pkg:@leumas/web"
    ],
    "product": [
      "pkg:@leumas/admin",
      "pkg:@leumas/studio",
      "pkg:@leumas/web"
    ],
    "howTo": [],
    "skills": []
  },
  "exports": {
    "total": 68,
    "component": 20,
    "hook": 0,
    "helper": 48,
    "names": [
      {
        "n": "BANDS",
        "k": "component"
      },
      {
        "n": "CLIENT_SCHEMA",
        "k": "component"
      },
      {
        "n": "CROSSFADE_MS",
        "k": "component"
      },
      {
        "n": "DEFAULT_BINDINGS",
        "k": "component"
      },
      {
        "n": "DEFAULT_BINDINGS",
        "k": "component"
      },
      {
        "n": "DWELL_MS",
        "k": "component"
      },
      {
        "n": "ENVIRONMENTS",
        "k": "component"
      },
      {
        "n": "ENVIRONMENTS",
        "k": "component"
      },
      {
        "n": "GAITS",
        "k": "component"
      },
      {
        "n": "GameCanvas",
        "k": "component"
      },
      {
        "n": "HUD_PANELS",
        "k": "component"
      },
      {
        "n": "LAND_MS",
        "k": "component"
      },
      {
        "n": "MAX_LAMPS",
        "k": "component"
      },
      {
        "n": "ORBIT",
        "k": "component"
      },
      {
        "n": "ORBIT",
        "k": "component"
      },
      {
        "n": "SPECTATE",
        "k": "component"
      },
      {
        "n": "SPECTATE",
        "k": "component"
      },
      {
        "n": "STEAL_OLDEST",
        "k": "component"
      },
      {
        "n": "STEAL_QUIETEST",
        "k": "component"
      },
      {
        "n": "STINGER_DEBOUNCE_MS",
        "k": "component"
      },
      {
        "n": "applyEnvironment",
        "k": "helper"
      },
      {
        "n": "applyFpsCamera",
        "k": "helper"
      },
      {
        "n": "applyFpsCamera",
        "k": "helper"
      },
      {
        "n": "applySideCamera",
        "k": "helper"
      },
      {
        "n": "applyThirdPersonCamera",
        "k": "helper"
      },
      {
        "n": "applyThirdPersonCamera",
        "k": "helper"
      },
      {
        "n": "collectStats",
        "k": "helper"
      },
      {
        "n": "collectStats",
        "k": "helper"
      },
      {
        "n": "createAnimator",
        "k": "helper"
      },
      {
        "n": "createAssetRegistry",
        "k": "helper"
      },
      {
        "n": "createAssetRegistry",
        "k": "helper"
      },
      {
        "n": "createBoxArena",
        "k": "helper"
      },
      {
        "n": "createBoxArena",
        "k": "helper"
      },
      {
        "n": "createBoxWorldDebugView",
        "k": "helper"
      },
      {
        "n": "createBoxWorldDebugView",
        "k": "helper"
      },
      {
        "n": "createChunkMesher",
        "k": "helper"
      },
      {
        "n": "createFpsController",
        "k": "helper"
      },
      {
        "n": "createFpsController",
        "k": "helper"
      },
      {
        "n": "createGameRuntime",
        "k": "helper"
      },
      {
        "n": "createGameRuntime",
        "k": "helper"
      },
      {
        "n": "createInputState",
        "k": "helper"
      },
      {
        "n": "createInputState",
        "k": "helper"
      },
      {
        "n": "createMusic",
        "k": "helper"
      },
      {
        "n": "createNetGameRuntime",
        "k": "helper"
      },
      {
        "n": "createPhysicsDebugView",
        "k": "helper"
      },
      {
        "n": "createPhysicsDebugView",
        "k": "helper"
      },
      {
        "n": "createPointerLook",
        "k": "helper"
      },
      {
        "n": "createPointerLook",
        "k": "helper"
      },
      {
        "n": "createPrimitiveResolver",
        "k": "helper"
      },
      {
        "n": "createPrimitiveResolver",
        "k": "helper"
      },
      {
        "n": "createRenderBridge",
        "k": "helper"
      },
      {
        "n": "createRenderBridge",
        "k": "helper"
      },
      {
        "n": "createSilentAudio",
        "k": "helper"
      },
      {
        "n": "createSpatialAudio",
        "k": "helper"
      },
      {
        "n": "createSpectatorController",
        "k": "helper"
      },
      {
        "n": "createSpectatorController",
        "k": "helper"
      },
      {
        "n": "createTerrainStreamer",
        "k": "helper"
      },
      {
        "n": "createThreeAudio",
        "k": "helper"
      },
      {
        "n": "crossfadeGains",
        "k": "helper"
      },
      {
        "n": "defaultRenderer",
        "k": "helper"
      },
      {
        "n": "gaitPose",
        "k": "helper"
      },
      {
        "n": "groupByChunk",
        "k": "helper"
      },
      {
        "n": "lampSpots",
        "k": "helper"
      },
      {
        "n": "pickGait",
        "k": "helper"
      },
      {
        "n": "readHud",
        "k": "helper"
      },
      {
        "n": "releaseObject",
        "k": "helper"
      },
      {
        "n": "releaseObject",
        "k": "helper"
      },
      {
        "n": "resolveArena",
        "k": "helper"
      }
    ]
  }
}
