A source-available, deterministic RPG simulation core — one Rust engine, five language surfaces, same seed, same result.
The engine resolves the dice and owns the state; an AI narrates the outcome but can never author it. The same seeded simulation runs in TypeScript (npm), Python (PyPI), Rust (crates.io), WebAssembly, and a C ABI and produces byte-identical output on every surface — the basis for replay, server-authoritative anti-cheat primitives, and honest AI-narrated play.
same input → same 1d4 roll → same state hash, on every surface:
tickFrame({ worldId:"arena", frameNumber:1,
commands:[{ playerId:"p1", seq:1, actionId:"move" }], ... })
→ x = 4
→ state_hash = cea43ee25ad95f845260985846936bd81f2b6d1aa735102cfd001295654b0a54
That exact hash is reproduced by npm, the Python port, the Rust crates, the WASM build, and
the C ABI - pinned by a shared golden-vector suite (4,259 tests) and enforced in CI by the
cross-language determinism gate
(.github/workflows/determinism-gate.yml).
npm install loom-engine # TypeScript / browser / Node
pip install loom-engine-rpg # Python - pure-Python port (the native PyO3 wheel is not on PyPI yet; build it from rust/loom_py with maturin)
cargo add loom_frame # Rust — the deterministic cratesLive playground: theworldtable.ai/engine ·
API docs: loom-engine.pages.dev ·
Repo: sadhaka/loom-engine ·
Reproduce the proof: examples/same-seed
flowchart TD
CORE["One deterministic Rust core<br/>loom_math · loom_events · loom_snapshot<br/>loom_ruleset (AST) · loom_epoch · loom_session · loom_frame"]
TS["loom-engine (npm)<br/>TypeScript reference"]
CORE -->|wasm-bindgen| WASM["WASM"] --> B["Browser client"]
CORE -->|PyO3 / maturin| PY["loom_py binding (PyPI wheel planned)"] --> SV["Python game server"]
CORE -->|C ABI| CA["C ABI .dll/.so/.a"] --> NT["Unity · Godot · Go"]
TS -. "byte-identical, pinned by golden vectors" .- CORE
B --> AI["AI narrates — never authors the dice or the state"]
SV --> AI
NT --> AI
Source-available under BUSL-1.1 — not OSI "open source." It is licensed this way from day one (not relicensed after the fact), and converts automatically to Apache 2.0 — each version four years after its release (Change Date 2030-05-08). You may self-host, modify, contribute to, and build on it for internal or non-competing commercial use today; the single restriction protects a solo developer from a hyperscaler turning the work into a competing hosted service before that conversion. No further restrictions are planned, and feedback on the terms is welcome. It's the deterministic core under a solo-founded commercial product (TheWorldTable.ai).
- ✅ A deterministic simulation core: a seeded PRNG, a rules AST (d20-style delta / derived checks plus the AST v2 system families - PbtA-style 2d6 moves and d100 roll-under checks - all as data, no untrusted-code execution), a tamper-evident HMAC event chain, snapshot + replay, and a real-time command-frame + client-rollback netcode primitive.
- ✅ Server-authoritative primitives for anti-cheat: the engine owns the dice and the state, so an AI — or a client — cannot author a mechanical outcome. (Anti-cheat is a system you build on these; this is the honest referee, not a finished anti-cheat product.)
- ✅ 5e / PF2e-style adapters for action economy, conditions, and grid-free range bands (rules-style primitives, not official D&D / Pathfinder content).
⚠️ Determinism is proven byte-identical across the shared golden vectors for the supported APIs — not a proof over every possible program state.- ❌ Not a full VTT or tabletop app (no character sheets, maps, or campaign UX). It is the engine underneath one.
Shipped in production by LoomMaster (theworldtable.ai/loommaster.html), an AI Game Master for 5e + PF2e where every roll, degree-of-success, and HP change is resolved server-side on a seeded PRNG and HMAC-chained for replay — the LLM only narrates.
v3.1 brings 5e/PF2e content depth onto the deterministic core and hardens the persistence boundary, across five hand-closed release-audit rounds.
- Ruleset AST v2 - the homebrew interpreter gains richer closed-grammar node families (PbtA-style 2d6 moves, d100 roll-under), still no untrusted-code execution, still fail-closed before any RNG draw, NFC-guarded at every name boundary.
- 5e Action Pack - leveled spell slots, concentration, and the full RAW condition table, each an event-sourced, replay-verifiable primitive.
- Forge-proof persistence - the
.loombundle carries a v3 length-prefixed signature plus an ownership-binding digest, so a save cannot be re-pointed at another world or truncated undetected - rejected on every surface. - delve-mini - a full multi-room dungeon run reconstructed from the tape as a headless determinism proof, the TileMap stage folded into the fingerprint.
- Hardened - release-audit rounds 4-8 closed by hand: NFC parity on the seal, every seed digest, and the AST name guard across TypeScript / Python / Rust; the bundle-binding cross-surface fork shut everywhere; every version surface test-locked to one string. 4,270 tests pass; published to npm / PyPI / crates.io (core crates 0.1.2).
v3.0 makes the engine a deterministic, server-authoritative world engine. One Rust core, bound to four surfaces, resolves the same seed to byte-identical results everywhere - the basis for replay, anti-cheat, and honest AI narration in a shared persistent world.
- Any-System ruleset AST - express tabletop rules as DATA (a strict JSON interpreter; no untrusted-code execution). Roll-vs-DC -> degree -> mutations, integer-only expressions, floor_div toward -inf, a fail-closed validation pass. Covers d20-style delta / derived checks, and the AST v2 node families make PbtA-style 2d6 moves and d100 roll-under checks expressible as data too - byte-identical on every surface, pinned by shared golden vectors.
- Snapshot + replay - a pure content
state_hashreconstructs a world from a verified snapshot + the events after it (provably equal to replay-from-genesis). - Epoch world-tick - the world keeps moving between sessions: offline factions
act deterministically, fail-closed, bounded (the Veil-Ceiling guard). Primitive
shipped with unit-level golden vectors; the end-to-end
suspend -> offline epochs -> verified resume flow is proven by the
Plaza Persistent reference demo and soaked past
100 epochs by the v3.5 session-soak vectors in
npm test. - WorldSession suspend/resume - pack a world into a verifiable bundle that embeds the chain's structural seal plus a signed identity binding (bundle format v3); on resume, verify the binding first (a leading-truncated, cross-world-spliced, or rewritten-position bundle is rejected), verify the seal fail-closed (an end-truncated, seal-less, or forged-seal bundle is rejected), replay the HMAC chain tail, reject time-travel, fast-forward.
- Real-time multiplayer core - server-authoritative command frames, client
rollback reconciliation (predict, then reconcile to the authoritative frame - you
can never forge an outcome), and region hashing (a partial-sync client verifies
only its own region + the Merkle root). The region-hash partial sync ships BOTH
halves: the server-side leaves/root primitive AND the fail-closed client consumer
(
partitionRegions/diffRegionLeaves/applyPartialSync), proven end-to-end by the Plaza Persistent reference demo.
Every surface, one core. A Rust workspace (loom_math / loom_events /
loom_snapshot / loom_ruleset / loom_epoch / loom_session / loom_frame) binds
to WASM (browser), a native PyO3 binding (Python server; built from source with
maturin - the wheel is not yet published to PyPI), and a panic-guarded
C ABI (Unity / Godot / Go) - each verified against the same golden vectors as the
TypeScript reference.
The 2.3.0 milestone extracts the deterministic tabletop primitives into a cross-language core: the same rules resolve byte-identically in TypeScript, Python, and Rust, so a server-authoritative result and a browser one can never disagree - the basis for replay, anti-cheat, and honest AI narration.
New rules modules: grid-free range bands (Engaged / Near / Far), the
per-round reaction economy ceiling, the narration contract
(findInventedNumber - reject prose that states a mechanics number the engine
never produced, numerals and number-words), and ruleset adapters for
D&D 5e + Pathfinder 2e (action economy, initiative with a numeric-aware
tiebreak, conditions). SRD 5.1 (CC-BY-4.0) + PF2e Remaster (ORC) attributed in
NOTICE.md.
One core, every surface. A Rust deterministic core (loom_math PCG32 +
integer math, loom_combat, loom_events HMAC chain) binds to:
- WASM (wasm-bindgen) - for TS / browser / edge,
- a native Python extension (PyO3),
- a C ABI (cbindgen) - for C#/Unity, Godot/GDExtension, and Go/cgo.
A pure-Python port ships alongside: pip install loom-engine-rpg then
import loom_engine (the bare loom-engine name is taken on PyPI). Cross-language
byte-parity is enforced by a shared golden-vector suite that the TypeScript,
Python, and Rust test harnesses all assert against. 4131 / 4131 TS tests pass;
the release was hardened by a full external security + cross-language determinism
audit. 2.3.0 is the current npm latest (npm install loom-engine).
The 2.2.x line adds EventChain, a tamper-evident HMAC-SHA-256-chained
event log - the integrity-bearing sibling of EventLog. Every record is
signed and folds in the previous record's signature, so the chain catches
field tampering, record deletion, reordering, and (with seal()) tail
truncation. Use it for audit trails, anti-cheat event tapes, and economy
/ ledger logs. Four independent security-audit rounds hardened the
canonical encoding (length-prefixed + domain-tagged, fail-closed
canonicalization, deep-clone isolation + bounded recursion + transactional
snapshot at every trust boundary); the round-4 audit is GREEN with no
CRITICAL/HIGH/MED findings. 4087 / 4087 tests passed at 2.2.5.
The 2.0.0 drop closes the Trinity Mainframe ingestion - 14 new pure-logic kernels that extend the Canvas2D / ECS base with deterministic core primitives for persistent-world and multiplayer systems. These are standalone modules, not an integrated MMORPG runtime - there is no zone streaming, instancing, or sharding layer. Each kernel is the safe single-thread / single-owner core intended to drive a deferred WebGPU / WebTransport / WebCrypto / WASM-SIMD / SQLite-WAL integration layer. All Codex hardening gates enforced inline; all non-negotiable engine gates (no RNG, no wall clock, no Atomics, fixed-capacity, every input bounds-checked) honoured across the board. 3984 tests pass; see CHANGELOG.md for the per-component breakdown.
New components: SonicSync, LoomVerify, NeuralMaterial, InferenceOrchestrator, LoomPulse, LoomFlow, NeuralAnimationSystem, VoxelComputeSystem, AetherGrid, LoomFSR, SealedAssetRegistry, LoomForgeBridge, GlobalStateLedger, LoomStudioOrchestrator.
npm install loom-enginePre-alpha. ESM-only, browser-first. TypeScript types ship in the
package (dist/index.d.ts). Node 18+ for the build toolchain;
the runtime targets evergreen browsers (Canvas2D + Web Audio +
EventSource).
API reference (TypeDoc) - generated from the public surface in
src/index.ts on every push to main:
https://loom-engine.pages.dev/
Build it locally with npm run docs (writes to ./docs/).
See Docs deploy for the hosting chain and one-time activation steps (Cloudflare Pages, since GitHub Pages is unavailable on private repos for free user plans).
// 1. Install
// npm install loom-engine
import {
Engine,
SpriteRenderSystem,
InputSystem,
VeilBudgetSystem,
SYSTEM_PHASE_INPUT,
SYSTEM_PHASE_RENDER,
} from 'loom-engine';
// 2. Attach to a canvas. Engine.create wires Canvas2DDevice, World,
// TransformPool, SpritePool, Time + Camera resources, and the
// default SpriteRenderSystem in SYSTEM_PHASE_RENDER.
var canvas = document.querySelector('canvas');
var engine = Engine.create({ canvas: canvas });
// 3. Register the systems your game needs. Order within a phase is
// deterministic; phases run INPUT -> LOGIC -> PHYSICS -> ANIMATION
// -> RENDER -> POST_RENDER per frame.
engine.world.addSystem(new InputSystem(), SYSTEM_PHASE_INPUT);
engine.world.addSystem(new VeilBudgetSystem(), SYSTEM_PHASE_INPUT);
// 4. Drive the frame loop. engine.tick advances Time, beginFrame on
// the device, world.update across all phases, endFrame.
function tick(now) {
engine.tick(now);
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);SSEDirectorBridge and SnapshotRecoveryHelper send credentials with
their network requests by default. The default eventSourceFactory
constructs new EventSource(url, { withCredentials: true }) and
SnapshotRecoveryHelper calls fetch(url, { credentials: 'include' }).
This is the right default for the embedded TheWorldTable.ai
same-origin use case (cookies + auth headers flow with the request
to the same origin), but a third-party consumer pointing the bridge
at a URL configured from user input could end up sending their own
site's credentials cross-origin (the browser still requires the
target server to opt in via Access-Control-Allow-Credentials: true
plus a specific Access-Control-Allow-Origin, so this is not a
one-sided SSRF; it requires attacker control of the target server's
CORS policy).
If you do not want credentials to flow with director-bridge requests, override the seams the engine already exposes - no engine code change needed:
import {
SSEDirectorBridge,
SnapshotRecoveryHelper,
} from 'loom-engine';
// Credential-free SSE subscription.
var bridge = new SSEDirectorBridge({
baseUrl: directorUrl,
characterId: characterId,
eventSourceFactory: function(u) {
return new EventSource(u, { withCredentials: false });
},
});
// Credential-free snapshot recovery.
var recovery = new SnapshotRecoveryHelper({
baseUrl: snapshotUrl,
characterId: characterId,
fetchImpl: function(input, init) {
var safeInit = Object.assign({}, init, { credentials: 'omit' });
return fetch(input, safeInit);
},
});The override hooks have always existed; 0.10.1 documents them. Internal security audit references are kept in the repository, not shipped with the npm package.
Pre-alpha, productized as of 0.10.0 (Phase 11B.3 - npm publish under MIT). Phases 0 through 9.3 + 11A.2 are shipped; the engine runs the public TheWorldTable.ai pre-alpha. Productization is a fund-raising and distribution decision, not a stability claim - the public API surface will evolve until 1.0.
| Phase | Status | Surface |
|---|---|---|
| 0 | shipped | scaffolding, package.json, tsconfig, PRIOR-ART log |
| 1 | shipped | Canvas2D iso renderer, camera, transform pool (SoA) |
| 2 | shipped | ECS World, system scheduler, resource registry, Engine facade, asset pipeline |
| 3 | shipped | clip-aware sprite-sheet manifests, AnimationStatePool, AnimationSystem |
| 4 | shipped | particle pool, emitter component, three-system VFX pipeline, additive blend |
| 5 | shipped | Web Audio bus mixer with VE-budget gating, unified keyboard / mouse / touch input |
| 6 | shipped | Director-bridge: SSE event-stream subscription, eventSourceFactory hook, snapshot-recovery |
| 7 | shipped | Survivor combat layer (projectile pool, hit resolution, damage application) ported onto Loom Engine |
| 8 | shipped | 2.5D ARPG hub-and-spoke per LOOM-CLASS-SYSTEM-SPEC, plaza narrator, mobile + touch input (virtual D-pad, tap-to-walk) |
| 9.1 | shipped | perf pass: alloc-churn fixes + bench harness |
| 9.3 | shipped | TypeDoc public-API site with auto-deploy |
| 11A.2 | shipped | docs hosting migrated to Cloudflare Pages |
| 11B.3 | shipped | MIT license + npm publish posture (this release) |
| 12.4 | shipped | License pivot from MIT to BUSL 1.1 with $1M revenue cap |
| 13.2 | shipped | Engine hardening: 12.6 audit lows L-08..L-12 closed |
| 14.1 | shipped | WebGL2 instanced sprite batcher |
| 15.1 | shipped | Multiplayer presence: pluggable bridge (SSE / Mock), peer pool with per-peer linear interpolation, render system (this release) |
See LOOM-ENGINE-SPEC.md Section 7 for the full phase plan with effort estimates.
Two backends ship behind the same IGraphicsDevice contract:
| Backend | Status | When to use |
|---|---|---|
canvas2d |
default, stable | Hundreds to ~2k sprites; broadest browser coverage |
webgl2 |
0.12.0+, opt-in | Thousands of sprites; instanced batching with atlas grouping |
Existing consumers do not need to change anything - Engine.create({ canvas })
keeps the Canvas2D path it has always had, with byte-for-byte
compatibility on every public API.
import { Engine, WebGL2Device } from 'loom-engine';
// Importing WebGL2Device side-effect-registers the 'webgl2' backend
// factory. The string-based form then works:
var engine = Engine.create({ canvas: myCanvas, backend: 'webgl2' });Or inject a pre-built device for absolute control over construction (useful when sharing the GL context with another renderer):
import { Engine, WebGL2Device } from 'loom-engine';
var device = new WebGL2Device(myCanvas);
var engine = Engine.create({ canvas: myCanvas, device: device });Every drawSprite / drawTile / drawParticle / drawText call
flows through SpriteBatcher, which accumulates per-instance data
(screen-space origin + size, atlas UV rect, RGBA tint) into a single
Float32Array. A flush triggers when the next call uses a different
atlas or blend mode, and at endFrame. Each flush issues exactly
one drawArraysInstanced for the batch.
For maximum throughput, group draws by atlas at the system level
(e.g. render all hamlet props from one atlas, all NPCs from another).
The default SpriteRenderSystem already sorts globally by iso depth
key; submission order within an atlas-bounded run is preserved
through the batcher.
engine.ts deliberately does not statically import
WebGL2Device. A consumer that only uses Canvas2D never pulls
WebGL2-specific code into their bundle - the only way the WebGL2
path enters the dependency graph is via an explicit
import { WebGL2Device }, which also triggers backend registration
through a /*#__PURE__*/-marked side effect.
- WebGL2 contexts can be lost (GPU crash, long backgrounded tab,
extension hijack). The device handles
webglcontextlost/webglcontextrestoredautomatically: every atlas re-uploads from its cached source image; frames during the lost interval no-op. - Browsers without WebGL2 (Safari < 15, very old Chrome/Firefox)
throw at
WebGL2Deviceconstruction. Wrap the upgrade in a feature check and fall back to Canvas2D:function pickBackend() { var probe = document.createElement('canvas').getContext('webgl2'); return probe ? 'webgl2' : 'canvas2d'; } var engine = Engine.create({ canvas: myCanvas, backend: pickBackend() });
- Performance characterization lands in phase 14.3 (synthetic 5k+ sprite bench, frame-time histograms, pareto-front against Canvas2D). Until then, treat WebGL2 as opt-in for scale-bound scenes only.
npm install
npm run build # tsc src/ -> dist/
npm run build:demo # tsc demo/*.ts -> demo/*.js
npm run build:all # both
npm run watch # rebuild src on change
npm run test # tsx tests/*.test.ts
npm run clean # remove dist + compiled demonpm run build:all
python -m http.server 8765
# browse http://localhost:8765/demo/http://localhost:8765/demo/ is the gallery index. The same tree is
published to loom-engine.pages.dev/demo/
on every push to main.
Three minimal, copy-paste-ready starters live under demo/. Each is
roughly 150 lines of TypeScript, imports from loom-engine
(resolved via importmap to the local engine bundle), and runs in
the browser without a build step on the consumer side.
- Survivor Mini - 100-line autobattler.
Player at center auto-fires at the nearest mob; mobs spawn from
random screen edges and pursue. Showcases ECS pools (Transform /
Sprite / Health / Pursue / RangedAttack),
MOB_CATALOG, projectile physics, system-phase ordering. - Plaza Mini - walkable iso plaza wired to
a mock Director bridge. WASD to walk; the narrator overlay below
the canvas pulses every five seconds with a synthetic
narrator.lineevent drained fromMockDirectorBridge. Demonstrates iso projection, input snapshot, the bridge / event-log / DOM-overlay boundary. - Dialogue Mini - branching dialogue
tree, no movement, no combat. Click a choice or press 1 / 2 / 3.
Demonstrates that the same ECS / resource model that runs the action
demos also fits a UI-only game: custom
Resource, customSystemreading bothInputSnapshotand DOM events, DOM as the primary UI. - Plaza Multiplayer - walkable iso
plaza with three synthetic peers wandering randomly, driven by a
MockMultiplayerBridge. WASD to walk; the local player broadcasts position at 10 Hz and the three peers (Alice / Bob / Carol) lerp smoothly between presence updates. Demonstrates the pluggable multiplayer bridge,PeerPoollinear interpolation, and thePeerPresenceSystem/PeerRenderSystempipeline. See the Multiplayer section below for the wire protocol.
The legacy reference demos (Phase 6 director, Phase 7 combat, Phase 8 ARPG slice) stay accessible from the gallery index.
Controls in the legacy director demo (demo/director.html):
- Arrow keys / WASD: pan camera
- Click: burst 24 particles + play SFX chirp (after first click, AudioContext unlocks)
- Hover: stats panel shows the iso tile under the cursor
Phase 15.1 adds a thin presence layer for showing other players in
real time on the same world. The transport is pluggable: the engine
ships an SSEMultiplayerBridge (server-sent events) and a
MockMultiplayerBridge (in-process; tests + offline demos), and the
IMultiplayerBridge interface is small enough to swap in WebSocket
or WebRTC without touching anything above it. No CRDT - peers carry
position only, and conflict resolution is "last write wins" at the
server. Shared state beyond position is deferred to a later phase.
The bridge layer hides this from gameplay code, but for anyone implementing a server (or a custom transport) the contract is:
Server -> client (SSE event types):
presence.snapshot{ peers: [{ character_id, x, y, zone, ts_ms, name? }] }emitted once on connect with the full current peer roster. The client treats this as authoritative and drops any peer not in the snapshot.presence.update{ character_id, x, y, zone, ts_ms, name? }emitted as peers move.presence.depart{ character_id }emitted when a peer disconnects.
Client -> server (HTTP POST):
POST <broadcastUrl>{ character_id, x, y, zone, ts_ms }the engine rate-limits to 10 Hz (BROADCAST_HZ); excess calls tobroadcastPosition()are silently dropped and counted inbridge.stats().rateLimitedDrops. Calling once per frame is fine.
ts_ms is the wall clock at which the position was true. The
PeerPool uses it to interpolate between successive samples so peers
don't jitter at the network rate. Acceptable lag is ~150 ms (one
update interval at 10 Hz), imperceptible at walk speed.
import {
Engine,
// Multiplayer
MockMultiplayerBridge, // or SSEMultiplayerBridge
PeerPool,
PeerSpritePool,
PeerPresenceSystem,
PeerRenderSystem,
POOL_PEER_SPRITE,
RESOURCE_MULTIPLAYER_BRIDGE,
RESOURCE_PEER_POOL,
SYSTEM_PHASE_INPUT,
SYSTEM_PHASE_RENDER,
} from 'loom-engine';
const engine = Engine.create({ canvas });
// 1. Create a bridge. Production code uses SSEMultiplayerBridge:
//
// const bridge = new SSEMultiplayerBridge({
// baseUrl: '/api/v1/loom/presence/events',
// characterId: 'me',
// zone: 'plaza',
// });
//
// Tests + offline dev use the in-process mock:
const bridge = new MockMultiplayerBridge();
bridge.connect();
// 2. PeerPool stores all known peers + their interpolated positions.
// Self-filter: tell the pool which character_id is the local player
// so it isn't rendered as a ghost when the server echoes it back.
const peerPool = new PeerPool();
peerPool.setLocalCharacterId('me');
// 3. PeerSpritePool maps character_id -> rendering hint. A default
// atlas + frame is enough for an undifferentiated demo; setOverride()
// lets you assign per-class sprites or cosmetic shards.
const peerSprites = new PeerSpritePool({ defaultAtlas: peerAtlas });
engine.world.resources.set(RESOURCE_MULTIPLAYER_BRIDGE, bridge);
engine.world.resources.set(RESOURCE_PEER_POOL, peerPool);
engine.world.registerPool(POOL_PEER_SPRITE, peerSprites);
// 4. Wire the systems. PeerPresenceSystem drains the bridge each
// frame; PeerRenderSystem submits a drawSprite + name label per
// peer at their interpolated position.
engine.world.addSystem(new PeerPresenceSystem(), SYSTEM_PHASE_INPUT);
engine.world.addSystem(new PeerRenderSystem(), SYSTEM_PHASE_RENDER);
// 5. Inside your walk-system, call broadcastPosition() each frame.
// The bridge enforces the 10 Hz wire rate.
bridge.broadcastPosition(playerX, playerY, 'plaza', Date.now());The bridge interface is just five methods (connect, disconnect,
status, pollMessages, broadcastPosition, plus stats). To use
WebSocket or WebRTC, implement IMultiplayerBridge with the same
PresenceMessage shape (update / depart / snapshot); none of
the systems above the bridge change.
loom-engine/
src/
util/ math, color, typed-arrays
components/ transform, sprite, particle-emitter
renderer/ graphics-device, canvas2d-device, camera, iso-projection
animation/ animation-clip, animation-state-pool
asset/ sprite-sheet-loader
audio/ audio-bus
input/ input-manager
systems/ sprite-render, animation, particle-{simulation,emitter,render}, input, veil-budget
vfx/ particle-pool
entity.ts entity allocator (32-bit handle, generation guard)
world.ts ECS World class
system.ts System interface + phase constants
resources.ts ResourceRegistry + Time + VeilBudget
engine.ts Engine facade
index.ts public API barrel
demo/ browser demo (one tile + animated knight + sparkles + click-to-burst)
tests/ node-based smoke tests (tsx --test)
assets/ placeholder game assets (knight walk-cycle PNG + JSON)
tools/ helper scripts (gen-knight.py - Pillow generator)
PRIOR-ART.md cumulative inspirations log (clean-room defense)
package.json tsc + tsx as only dev deps
tsconfig.json ES2022 strict + noUncheckedIndexedAccess
dist/ tsc output (gitignored)
node_modules/ npm install output (gitignored)
- ECS over god-object scene graph - entities are 32-bit handles, components live in pools indexed by entity index
- Structure-of-arrays for hot data (TransformPool, SpritePool, ParticlePool, ParticleEmitterPool, AnimationStatePool) - tight iteration over Float32Arrays, no per-entity object allocation
- IGraphicsDevice abstraction with Canvas2D primary backend (WebGL2 reserved for Phase 2+ if profiling demands)
- 6-phase scheduler - INPUT -> LOGIC -> PHYSICS -> ANIMATION -> RENDER -> POST_RENDER, deterministic registration order within each
- VeilBudgetResource - the patent-defensible novelty hook. Single
resource with
particleBudget,audioBudget,shaderBudget,eventBudget. VeilBudgetSystem propagates updates to ParticlePool, AudioBus, etc. Director-bridge mutates the budget; subsystems read - Frame loop -
engine.tick(now)runs in this order:- compute dt (clamped to 1/30s)
- advance Time resource
- device.beginFrame
- world.update (walks all phases)
- device.endFrame
import {
Engine,
// ECS
POOL_TRANSFORM, POOL_SPRITE, POOL_ANIMATION, POOL_PARTICLE,
POOL_EMITTER,
TransformPool, SpritePool, AnimationStatePool, ParticlePool,
ParticleEmitterPool,
SYSTEM_PHASE_INPUT, SYSTEM_PHASE_LOGIC, SYSTEM_PHASE_PHYSICS,
SYSTEM_PHASE_ANIMATION, SYSTEM_PHASE_RENDER, SYSTEM_PHASE_POST_RENDER,
// Default systems
AnimationSystem, SpriteRenderSystem,
ParticleEmitterSystem, ParticleSimulationSystem, ParticleRenderSystem,
InputSystem, VeilBudgetSystem,
// Resources
RESOURCE_TIME, RESOURCE_CAMERA, RESOURCE_DEVICE,
RESOURCE_VEIL_BUDGET, RESOURCE_INPUT, RESOURCE_AUDIO_BUS,
// Renderer
Canvas2DDevice, ISO_TILE_WIDTH, ISO_TILE_HEIGHT,
// Asset
loadSpriteSheet, computeFrameIndex,
// Audio
AudioBus, AUDIO_BUDGET_AMBIENT_FLOOR, AUDIO_BUDGET_ESSENTIAL_FLOOR,
// Input
InputManager,
// Math + color
vec2, vec3, rect, clamp, lerp,
hexToRgba, rgbaToCssString,
COLOR_KNOT_STR, COLOR_KNOT_DEX, COLOR_KNOT_INT, COLOR_KNOT_CENTER,
// Iso
tileToIso, worldToIso, isoToTile, isoDepthKey,
} from 'loom-engine';
const engine = Engine.create({ canvas });
engine.world.addSystem(new InputSystem(), SYSTEM_PHASE_INPUT);
engine.world.addSystem(new VeilBudgetSystem(), SYSTEM_PHASE_INPUT);
// ... game systems ...
engine.world.addSystem(new SpriteRenderSystem(), SYSTEM_PHASE_RENDER);
function tick(now: number) {
engine.tick(now);
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);The engine's defensible novelty is in the Loom integration layer, not the rasterizer. Director-driven scene state, Veil Essence economy gating render budget, knot-aware encounter generation, event-sourced rendering. The renderer underneath uses public-domain techniques (sprite batching, isometric projection, ECS) implemented from scratch.
See PRIOR-ART.md for the cumulative inspirations log (public talks, papers, OSS architecture - took / declined per source).
Every architectural commit names its inspirations in plain text. No copy-paste from any external engine source. PRIOR-ART.md is the audit trail any future productization or patent dispute would lean on.
4259 / 4259 tests pass via tsx --test, spanning 220+ test files in
tests/. Core suites include:
smoke.test.ts- public API barrel, version stampworld.test.ts- ECS world, system scheduling, sprite pool, sprite render, timeasset-loader.test.ts- sprite-sheet manifest, frame stepper, error discriminatoranimation.test.ts- animation clip math, state pool, AnimationSystem end-to-endvfx.test.ts- particle pool, emitter pool, simulation, emitter system, veil budgetaudio-input.test.ts- audio bus + ducking, input manager, input system, budget propagationdirector.test.ts- SSE bridge, eventSourceFactory hook, scene-state derivationcombat.test.ts- hit resolution, damage application, knockbackprojectile.test.ts- projectile pool, lifetime, collisionarpg.test.ts- ARPG hub-and-spoke, plaza narrator, encounter schedulingsnapshot-recovery.test.ts- SnapshotRecoveryHelper for Director reconnecttouch-input.test.ts- virtual D-pad, tap-to-walk, multi-touch arbitration
Run via npm test. Each suite is fully node-based; no DOM dependency.
Browser-only paths (Canvas2DDevice rasterization, AudioContext
unlock, DOM event listeners) are exercised via the demo's preview
verification, not unit tests.
The TypeDoc site at https://loom-engine.pages.dev/ is served by
Cloudflare Pages from the gh-pages branch of this repo. The chain:
- Push to
maintriggers.github/workflows/docs.yml - Workflow runs
npm ci,npm test,npm run docs:ci, then publishes./docs-build/to thegh-pagesbranch viapeaceiris/actions-gh-pages - Cloudflare Pages watches the
gh-pagesbranch and auto-deploys on every push, typically within 1-2 min
GitHub Pages itself is not used: the repo is private and free user
plans do not include Pages on private repos. The 422 error from the
Pages create API is the canonical signal: "Your current plan does not support GitHub Pages for this repository."
If the Cloudflare Pages project is ever deleted or the repo is forked to a new owner, re-activate as follows:
- Cloudflare dashboard -> Workers & Pages -> Create -> Pages -> Connect to Git
- Authorize Cloudflare on the GitHub account that owns the repo (only the engine repo needs to be granted access)
- Select
loom-engine, name the projectloom-engine(default URL becomesloom-engine.pages.dev) - Production branch:
gh-pages - Build command: leave empty (the gh-pages branch is already a built static site)
- Build output directory:
/(root) - Save and deploy. First deploy reads whatever is currently on
gh-pages; subsequent deploys auto-trigger on push to that branch - Optional: assign a custom domain (e.g.
engine.theworldtable.ai) under the project's Custom domains tab. CF DNS fortheworldtable.aiis already on the same account, so this is a one-click CNAME add
If the workflow ever stops updating gh-pages (CF Pages will keep
serving the last successful build but go stale), check
gh run list --repo sadhaka/loom-engine --workflow=docs.yml.
Versions 0.11.0 and later are licensed under the
Business Source License 1.1 - SPDX identifier BUSL-1.1,
declared in package.json. Copyright (c) 2026 Misha Mitiev.
GitHub's repo header shows "Other" / "NOASSERTION" for this repo: its license
detector (licensee) only recognizes the open-source licenses catalogued on
choosealicense.com, and BUSL-1.1 is source-available, not in that set - so every
BUSL project displays this way. It is cosmetic; the authoritative license is the
LICENSE file plus the BUSL-1.1 SPDX id in package.json.
For a one-page plain-English overview of the terms (what you can do for free, what requires a commercial license, the Change Date conversion to Apache 2.0), see docs/LICENSE-HEADER.md.
- Free for use below USD $1,000,000 annual gross revenue from any product, game, or service that incorporates this engine. Personal projects, learning, prototyping, and indie games well under that threshold all qualify.
- Commercial license required above the threshold. Contact
licensor@theworldtable.ai. Standard terms include a 5% royalty on excess revenue; lump-sum buyouts and equity-for-license arrangements are negotiable. See COMMERCIAL_LICENSE_TERMS.md. - Auto-converts to Apache 2.0 on 2030-05-08 (4-year window per BUSL spec). After that date, all 0.11.0+ versions become permissive.
- Patent strategy: novelty claims documented in PRIOR-ART.md are independent of the source-code license and apply to all versions regardless of license phase.
Version 0.10.0 (the only previously-published release) remains
permanently licensed under MIT for backwards compatibility. Projects
pinned to 0.10.0 are unaffected by the license change but will not
receive future updates without accepting BUSL-1.1.
Tagged releases publish to npm via
.github/workflows/npm-publish.yml.
The workflow runs npm test and npm run build, then
npm publish --access public, when a tag matching v* is pushed to
main. It needs the NPM_TOKEN repo secret to authenticate.
Manual publish from a local checkout:
npm login # one-time, npm account named sadhaka
npm test # 208/208 must pass
npm run build # tsc -> dist/
npm publish --dry-run # inspect tarball contents first
npm publish --access public # scoped packages default to private; flag is requiredprepublishOnly in package.json re-runs npm test && npm run build
before any publish, so the dry-run and the final publish always rebuild
from a clean source tree.
This is a single-author project (Misha Mitiev) for TheWorldTable.ai.
The BUSL-1.1 license permits forking and modification under the revenue
cap (see License above); pull requests are
welcome but not actively triaged - the canonical roadmap is the spec
file (LOOM-ENGINE-SPEC.md in the parent repo) and capacity is
limited. For bug reports, file an issue with a minimal repro.