npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nosafesky/moku-game

v0.3.0

Published

An ECS game framework for Moku (Spark-style public API & memory layout) with PixiJS rendering, built on @moku-labs/core and composable with @moku-labs/web

Downloads

1,190

Readme

game

An ECS game framework for Moku — Spark-style API and memory layout, PixiJS v8 rendering, with the live runtime exposed to agents over MCP.

game is a typed Entity-Component-System runtime you compose into a Moku app: archetype object-SoA component storage, a fixed-timestep loop that bypasses the kernel for the hot path, typed world resources for first-class systems access, and sixteen plugins that wire ECS, scheduling, rendering, input, assets, scenes, framework context, an MCP server, native WebAudio audio, versioned save persistence, per-portal publishing, particle/juice VFX, a PixiJS-native UI layer, a shared object-tweening layer, and a 2D follow/shake/parallax camera together. It is not a game engine UI or a level editor — there is no scene graph GUI, no asset pipeline, no runtime of its own beyond @moku-labs/core. You define components and systems in TypeScript and drive frames; the framework owns the data layout, the stage order, and the GPU lifecycle.

version types node license: MIT

Why game · Quick start · How it works · Core concepts · Plugins · Events · Scripts · Requirements · Docs · License


Why game

  • Archetype, object-SoA storage. Entities that share a component signature live in one archetype with parallel columns, so queries iterate cache-coherently — not a Map<id, object> scan per frame. A per-component storage seam lets high-churn tags opt into "sparse" storage instead.
  • A runtime, not an engine. No editor, no scene-graph GUI, no built-in asset pipeline — you compose plugins into your own Moku app via createApp and write systems in TypeScript. Define by negation: it owns the data model and the frame, you own the game.
  • The hot path bypasses the kernel. The fixed-timestep loop drives scheduler.tick(dt)world.tick(dt)renderer.render() directly, with no event-bus round-trip per frame. Per-frame work is deliberately not emitted as kernel events.
  • Spark-style public API. defineComponent, callable component tokens, typed variadic queries (arities 1–8), a deferred command buffer, and world.tick(dt) — the API and memory layout are modeled on AlexTiTanium/spark.
  • First-class systems access. A typed world resource registry (defineResource / setResource / getResource / resource / hasResource / removeResource) lets systems reach shared singletons through the world they already receive — no closure capture. The context plugin binds the well-known Assets and GameContext resources, and loop publishes a Time clock — all read the same way as any consumer resource. The system signature stays (world, dt).
  • First-class MCP. A Model Context Protocol server exposes the live runtime to agent clients — query state, step the loop, spawn entities, load scenes, screenshot the frame — without touching game code. The transport is environment-aware: ["stdio"] under Node/Bun, an in-page ["inMemory"] pair in the browser (no socket), plus optional Streamable HTTP. In-page agents reach the server via app.mcp.clientTransport().
  • Runs headless. The renderer has a first-class headless mode (auto-detected when there is no DOM) that skips Pixi/GPU entirely while still defining Transform and running the sync system, so the same framework drives a real game in the browser and boots cleanly under Bun/Node for tests, simulation, or agent-only hosts.
  • Composable with the Moku family. Built on @moku-labs/core, logs and reads env via @moku-labs/common, and mounts its Pixi canvas into a DOM surface from @moku-labs/web.

Quick start

bun add game @moku-labs/core @moku-labs/common pixi.js

[!NOTE] Status: 0.x — early. Pre-1.0; the public surface may still shift. game is unpublished (version 0.0.0) — install from the repository. Consumers use createApp from the framework and never import @moku-labs/core directly.

import { createApp } from "game";
import { Container } from "pixi.js";

// 1. Create the app (synchronous); start() is async and boots the Pixi Application.
const app = createApp({
  pluginConfigs: {
    renderer: { width: 1280, height: 720, background: 0x1099bb, mount: "#game" },
    loop: { fixedDt: 1 / 60 }
  }
});

await app.start();

// 2. Define components on the ECS world (app.ecs IS the World facade).
const Velocity = app.ecs.defineComponent(() => ({ dx: 0, dy: 0 }));

// 3. Define a scene — entities spawned here are owned by the scene.
app.scene.define("level1", {
  setup: (world) => {
    const player = world.spawn(
      app.renderer.Transform({ x: 100, y: 100, rotation: 0, scaleX: 1, scaleY: 1 }),
      Velocity({ dx: 60, dy: 0 })
    );
    app.renderer.attach(player, new Container());
  }
});

// 4. A movement system runs every "update" stage; input is polled, not subscribed.
app.scheduler.addSystem("update", (world, dt) => {
  const input = app.input.snapshot();
  world.query(app.renderer.Transform, Velocity).updateEach(([t, v]) => {
    if (input.isDown("ArrowRight")) t.x += v.dx * dt;
  });
});

await app.scene.load("level1"); // the loop is already driving frames (autoStart: true)

How it works

The loop plugin owns the frame. Each fixed step it drives the scheduler, which runs every system in canonical stage order against the single ECS world; the renderer draws once per frame. mcp reaches into the same runtime so agents can observe and control it.

flowchart LR
  IN["Input / agent<br/>(keys · pointer · MCP)"] --> LOOP["loop<br/>fixed-timestep rAF"]
  LOOP -->|"scheduler.tick(dt)"| SCH["scheduler<br/>input→update→physics→sync→render"]
  SCH -->|"world.tick(dt)"| ECS["ecs<br/>archetype data core"]
  ECS --> SYNC["renderer sync<br/>Transform → Pixi"]
  LOOP -->|"render()"| REND["renderer<br/>PixiJS v8 draw"]
  REND --> OUT["Frame on canvas"]
  MCP["mcp server<br/>stdio · http · inMemory"] -.->|observe + control| ECS
  MCP -.-> LOOP
  classDef u fill:#0b7285,stroke:#08525f,color:#fff;
  classDef m fill:#1864ab,stroke:#0d3d6e,color:#fff;
  class IN,OUT u
  class LOOP,SCH,ECS,SYNC,REND,MCP m

Core concepts

  • The ECS is the data core. app.ecs returns the World facade directly — no wrapper. defineComponent(create, opts?) registers a component (callable token: Position({ x, y }) produces a spawn payload); spawn, despawn, add/remove/get/set/has, and typed query(...) over arities 1–8 are the surface. Entity is a generational handle, so stale references are detectably dead via isAlive.
  • Stages are the contract. Systems register into one of five fixed, ordered stages — input → update → physics → sync → render. The order is canonical; the scheduler validates stage names and forwards to the world.
  • The command buffer is the only mutation path during iteration. Inside updateEach (or any system), structural ops (spawn/despawn/add/remove) are deferred and flushed at each stage boundary inside tick. This is the path every mcp mutating tool uses.
  • World resources are first-class systems access. The world owns a typed singleton registry — defineResource(create?) mints a Resource<T> token; setResource/getResource/resource/hasResource/removeResource read and write it. resource(token) asserts presence (throws an actionable error if unset with no factory); getResource returns T | undefined. Resource ops are immediate — they bypass the command buffer even mid-iteration, and aren't counted by maxStructuralOpsWarn. Systems reach shared services through the world argument, with no closure over app. The framework's well-known resources — Assets + GameContext (from context) and Time (from loop) — are wired at app.start().
  • The loop is fixed-timestep. Real time is accumulated and consumed in fixedDt slices (clamped by maxFrameDelta, capped at maxStepsPerFrame) so simulation is frame-rate independent; step() advances exactly one deterministic tick + render. Each fixed step the loop updates the Time world resource in place ({ dt, elapsed, frame }, seconds), readable as app.loop.time or world.resource(Time).
  • Three-layer Moku model. createCoreConfig (config + events) → createCore (framework + the sixteen plugins) → createApp({ pluginConfigs }) (your app). Consumers use createApp / createPlugin from game and never import @moku-labs/core directly. For advanced / headless assembly, game also re-exports createCore (compose a custom core from a plugin subset) and createCoreConfig (build a bespoke Layer-1 config) — escape hatches for tooling, tests, and agent-only hosts; createApp stays the default.

Plugins

The framework is sixteen plugins, built and resolved in dependency order: ecsschedulerrenderer + inputloop + assetscontextsceneaudio + storageplatformvfxuitweencameramcp. (audio and storage are dependency-free; platform depends on audio + loop + storage; vfx depends on ecs + scheduler + renderer; ui depends on renderer + scheduler + input; tween depends on scheduler only; camera depends on renderer + scheduler + tween; mcp stays last.)

| Plugin | Tier | Responsibility | Key API | |---|---|---|---| | ecs | Complex | Generational entities, archetype object-SoA storage, typed queries, deferred command buffer, world resource registry, read-only introspection facet, world.tick. | app.ecs.defineComponent · spawn · query(...).updateEach · addSystem · defineResource · resource · componentByName · tick | | scheduler | Standard | The ordered stage contract; thin facade forwarding to the ECS world. | app.scheduler.addSystem(stage, fn) · tick(dt) · stages | | renderer | Complex | PixiJS v8 backend — owns the GPU Application, defines Transform, syncs ECS → display objects, attaches plain-data primitives. First-class headless mode (no Pixi/GPU). | app.renderer.Transform · attach · attachPrimitive · render · screenshot · tree · getView · getStage | | input | Standard | Polled keyboard/pointer captured from DOM, frozen into a per-frame snapshot; programmatic key injection with alias normalization. | app.input.snapshot()isDown · justPressed · pointer; keyDown · keyUp · keyPress | | loop | Standard | Fixed-timestep rAF loop driving scheduler.tick then renderer.render each frame; publishes the Time world resource. | app.loop.start · stop · step (→ TimeStepResult) · isRunning · time | | assets | Standard | Thin wrapper over Pixi v8 Assets — load/cache textures + bundles by alias, build sprites. | app.assets.load · loadBundle · sprite · get · isLoaded | | context | Standard | Binds the well-known Assets + GameContext world resources so systems reach them via world.resource(token). | app.contextassets · game | | scene | Standard | Named scene lifecycle with entity-ownership tracking, clean transitions, bundle pre-load. | app.scene.define · load · unload · currentScene · sceneNames · ownedEntities | | mcp | Complex | First-class MCP server exposing the runtime to agents over stdio / Streamable HTTP / in-page inMemory (env-aware default); 15 registered tools (4 read-only, the rest mutation/play gated by enableMutations). | app.mcp.isRunning · httpEndpoint · toolNames · clientTransport | | audio | Standard | Native WebAudio SFX + music — master mute bus (single-call duck for ad breaks), per-channel + master volume, user-gesture unlock() (no autoplay), decoded-buffer cache. Zero deps, headless-safe. | app.audio.unlock · load · play · playMusic · stopMusic · mute/unmute · setVolume · getVolume | | storage | Standard | Namespaced, versioned key/value save persistence with a migration chain behind a pluggable StorageBackend seam; safe localStorage-or-memory default that never throws (in-memory fallback when storage is partitioned/blocked/absent). Zero deps. | app.storage.get · set · has · remove · clear · keys · isPersistent · getVersion · setBackend | | platform | Complex | Portal-SDK adapter layer (CrazyGames / Poki / Newgrounds / no-op) selected per build via ctx.env; promise-based ads that capture-then-restore-pause loop + mute audio (frequency-capped, re-entrancy-guarded); injects a portal-native StorageBackend into storage; persists + rehydrates audio prefs. Zero new deps (SDKs runtime-injected). | app.platform.getPortal · gameplayStart/gameplayStop · loadingStart/loadingFinished · commercialBreak · rewardedAd · isAdPlaying | | vfx | Complex | Game-juice layer — ECS-native particle emitters + one-shot bursts (renderer-owned Graphics views, fade-by-shrink), trauma-based screen shake, Transform scale-pop, floating damage/score text, and pure easing curves. Every effect is a scheduler system over ordinary ECS entities. Emits no events, headless-safe, zero new deps (Pixi via renderer). | app.vfx.createEmitter/configureEmitter/removeEmitter · burst · shake/stopShake · pop · floatText · easing/lerp | | ui | Complex | Game-UI layer — a screen stack (title / pause / game-over / modal cards), a declarative widget set (label / button / panel / bar), a persistent HUD, and pointer/touch hit-testing — all rendered natively into the renderer's Pixi stage (retained-mode plain-data specs, opaque handles). Emits no events, headless-safe, zero new deps (Pixi via renderer). | app.ui.pushScreen/popScreen/replaceScreen/clearScreens · addHud/removeHud · getWidget/setText/setValue/setVisible · getRoot | | tween | Standard | Shared, ECS-agnostic tweening — animate the numeric props of any plain object (to/from) or a scalar (value) over time with easing / delay / repeat / yoyo; opaque TweenHandle (stop/pause/resume/active + a done Promise). One scheduler "update" system advances all tweens, so a paused loop freezes them for free (pause-safe). Re-exposes the canonical easing table + lerp. Emits no events, headless-safe, zero new deps. | app.tween.to/from/value · killAll · count · easing/lerp | | camera | Standard | A 2D game camerafollow (per-frame smoothing), instant + animated pan / zoom / rotate (moveTo/zoomTo/rotateTo delegate to app.tween), decaying screen shake, and parallax across world-space layer containers it owns (the HUD stays screen-fixed); maps screenToWorld/worldToScreen. One scheduler "sync" system applies the transform each frame. Pause-safe (rides scheduler.tick) and headless-safe (numeric state still tracks). Emits no events, zero new deps (Pixi via renderer). | app.camera.follow · setPosition/moveTo · setZoom/zoomTo · setRotation/rotateTo · shake · world/addLayer/layer · screenToWorld/worldToScreen |

Events

The event catalog is intentionally tiny — hot-path frame work (ticks, sync, render, input) is not emitted as kernel events. Only coarse milestones cross the bus:

| Event | Payload | When | |---|---|---| | assets:loaded | { alias: string; kind: "asset" \| "bundle" } | After app.assets.load() or loadBundle() succeeds (once per call, not per texture). | | scene:loaded | { name: string } | After a scene's setup completes during app.scene.load(). | | game:reset | { reason: "mcp" } | After the mcp game:reset tool despawns all MCP-tracked entities and unloads the current scene — listen to re-initialize consumer state. | | audio:muteChanged | { muted: boolean } | After app.audio.mute() / unmute() / setMuted() changes the mute state — a storage/platform plugin persists it. | | audio:volumeChanged | { channel: "master" \| "sfx" \| "music"; value: number } | After app.audio.setVolume() changes a channel volume. | | platform:ready | { portal: Portal } | Once the active portal adapter is initialised and loading is signalled finished. | | platform:adStart | { type: "interstitial" \| "rewarded" } | When an ad begins (after loop is paused + audio muted) — a HUD can show an "ad playing" overlay. | | platform:adEnd | { type: "interstitial" \| "rewarded"; rewarded?: boolean } | When an ad ends (after loop + audio are restored). rewarded is set for rewarded ads. |

assets:loaded and scene:loaded are declared on the framework Events (src/config.ts); game:reset (from mcp), audio:muteChanged / audio:volumeChanged (from audio), and platform:ready / platform:adStart / platform:adEnd (from platform) are plugin-level events declared and emitted by their plugins. Subscribe from a consumer plugin via the hooks map (depends: [assetsPlugin], then hooks: _ctx => ({ "assets:loaded": ({ alias, kind }) => { … } })).

Scripts

bun run build              # Build with tsdown
bun run lint               # Biome check + ESLint
bun run lint:fix           # Auto-fix lint issues (Biome --write + ESLint --fix)
bun run format             # Format with Biome
bun run test               # Run all tests (vitest run)
bun run test:unit          # Unit tests only
bun run test:integration   # Integration tests only
bun run test:coverage      # Tests with coverage

Requirements

  • Node >= 24 and Bun >= 1.3.14 — use bun exclusively (never npm/yarn/pnpm).
  • TypeScript in strict mode, with exactOptionalPropertyTypes and noUncheckedIndexedAccess.
  • @moku-labs/core — the micro-kernel the three-layer factory chain is built on (consumers go through createApp, never import it directly).
  • @moku-labs/common — provides ctx.log (logPlugin) and ctx.env (envPlugin) on every plugin context.
  • pixi.js ^8 — the rendering backend the renderer and assets plugins wrap.
  • Composable with @moku-labs/web — mount the Pixi canvas into a DOM surface when renderer.mount is a selector.
  • mcp adds @modelcontextprotocol/sdk, hono, and zod — the MCP server, HTTP transport, and tool input schemas.

Docs

License

MIT © moku-labs