Spawn

Make Games with Words

Explore or make your own

spawn / swhat we're building

pinned

start herewhat spawn isfaqfrequently asked questionsthe betthe spawn bet

updates

engine v4.5Surface Tension2 daysengine v4.4Solid1 weekengine v4.3Groovy1 weekengine v4.2Continuum2 weeksengine v4.1Foundations3 weeksengine v0.1Genesis3 weeks
← All posts

engine v4.1.0

Foundations

May 4, 2026

First big engine update. Faster, smoother, smarter — and a small mountain of fixes.

what's new

Spawn's first big engine update. Faster, smoother, smarter — and a small mountain of fixes.

  • Huge engine perf upgrade. The whole renderer was rebuilt, terrain streams better, and big scenes draw faster and smoother.
  • Savi's tools are way more reliable, and she can now see and poke at your game's UI — huge unlock for UI-heavy games (Discord-style chat games, dashboards, card games, etc.).
  • Multiplayer feels much smoother — camera, controls, and effects (vignettes, particles, music swells) all stay in sync now instead of glitching during rollback.
  • Hundreds of fixes — actually hundreds. Screen flashes, object juice, screenshots, voxel marks, query ordering, and a long tail of small things that used to glitch now just work.
  • New animation stuff! Savi can now tween any property on any object — bounce a chest open, pulse a crystal, fade things in, flash red on hit. More animation features coming soon.
›technical notes
  • Added channel-based animation mixer to ObjectAPI. Declarative setup via api.setProperty("mixer", { <channelName>: { clip, weight?, duration?, speed?, loop?, blendIn?, direction?, mask? } }). Runtime control via api.updateChannel(name, opts) (pass null to clear) and api.getChannel(name) returning { clip, weight, elapsed, duration, finished }. Channels blend by weight with optional per-bone masks ({ from: <bone> } or { bones: [...] }). Backed by new DrawMixer ECS component (replicate: AOI).
  • Added anime.js-style property tween API: api.animate(targetEntityId, { keyframes, duration, easing, delay, direction, loop }) and api.isAnimating(targetEntityId, dotPath?). Keyframes target dot-paths like "feetPosition.y", "material.emissive", "scale". Supports scalar tween values, value arrays ([a, b, c] evenly distributed), per-segment timing ([{ value, duration, easing }]), and relative deltas ("+=5" / "-=5"). Server-authoritative via new tween-evaluator system (order 105, after behavior-update); state stored in TweenState component (replicate: never).
  • Extracted shared easing.ts (easing curves + OKLCH color interpolation) used by both behavior builtins and the tween evaluator.
  • Added parseMixerChannel shared validator used by both the mixer property setter and updateChannel runtime call.
  • Added interpolation property (getter/setter) backed by DrawInterpolation; { teleportThreshold: number } or null to disable.
  • Note: the previously documented animated3DCharacter.action: { clip } shorthand never had a real implementation (the setter silently dropped the field). Use the mixer instead — declare locomotion + action as separate channels.
  • Camera authority split: render worker is now sole owner of yaw/pitch, integrating mouse deltas directly and publishing back to sim via a new CameraAngles SharedArrayBuffer channel. Spring-arm smoothing and physics-collision raycast moved from renderer into camera-behavior (sim), collapsing five per-frame orbit params into a single orbitDist the renderer lerps toward. Removes input-lag and stale WASD movement axes.
  • Renderer now also publishes its final pos+quat through the camera-angles SAB; sim consumes rendererTransform inside buildPointerRay() so click rays match what's on screen at >60 Hz refresh. View-state pos/rot intentionally left on sim-frame semantics so script-side camPos* is unchanged. Unit-norm guard rejects zero-initialized SAB state.
  • Compiled behavior scripts: Math.random() now delegates to a seeded RNG when one is installed (globalThis.__tomeSeededRng), making prediction/resimulation deterministic. New setTomeRng / clearTomeRng helpers in tome/resources.ts install the RNG into both the ECS resource and the global bridge consumed by compiler.ts's deterministic-Math injection. Wired through interpreter.ts, input-applier.ts, and behavior-update.ts.
  • TweenState now replicates with AOI + snap correction so animation tweens stay in sync across clients during rollback and resimulation.
  • ECS event component system restored (reverts the entity-based event experiment): queueEventAdd / drainEventAdds / injectTransient re-instated on the ctrl channel. Particle bursts (ParticlesBurstEvent) drained per tick and forwarded through the SAB render channel. Juice/audio dedup logic survives rollback.
  • Snap-frame application bypasses the prediction entity filter for replicate:"owner" components. Server-authoritative owner state (e.g. TomePlayerJuiceState) was previously dropped on predicted entities, breaking vignette, letterbox, music, and other effects in multiplayer.
  • ECS query-utils: query results are now sorted on every path (not only the index path) so behavior scripts iterating queries see a stable order frame-to-frame and across server/client.
  • New client-only DrawVisibilityOverride (replicate: never) lets camera-behavior hide the local player without clobbering the server's DrawVisibility. All render_v2 states honor overrideLayerMask with fallback to server layerMask.
  • SkipReplication removed from terrain streaming. Terrain entities now replicate normally and are protected from server despawn; event components on the client are fixed.
  • Camera API (tome/api/camera-api.ts) getProperty/setProperty/lookAt/setRotation read and write FeetPosition + Rotation directly instead of going through the now-removed InterpTransform blob. Script-visible shape and semantics are unchanged.
  • Replaced legacy renderer with render_v2: new RendererFeatureV2 (engine/render_v2/feature.ts) is the sole renderer wired by engine/client/engine-bootstrap.ts. Worker-side ECS sync, camera, smoothing, and decorations all flow through engine/render_v2/* instead of the deleted engine/render/main, engine/render/extractors, engine/render/commands, and engine/render/worker/worker-prep.ts paths.
  • Removed legacy render pipeline: RendererFeature, worker-renderer.ts, worker-prep.ts, render-worker-state-adapter.ts, transform-smoother.ts, the prep/extractor/command system, and ~75k lines of supporting tests/utilities are gone.
  • New SAB-backed RenderChannel (render_v2/render-channel.ts) replaces postMessage ECS sync. Adds a string table with generation-based eviction, u32 frame-header numOps, local-buffer overflow instead of dropping ops, growable arena/string-table SABs, and per-frame perf metrics + transport instrumentation.
  • Renderer now consumes ops directly — legacy intent conversion deleted. AppearanceIntentValue/LightIntent are gone; per-property Draw* writes (DrawModel, DrawSprite, DrawText, DrawSign, DrawMaterialOverrides, DrawAnimation, DrawVisibilityOverride, etc.) are written directly by the interpreter and replicated as their own components.
  • IndirectBatchedMesh primitive batching for cubes/spheres/etc., with shadow fixes: per-instance shadow override materials, follow-camera shadow frustum, cast/receive flags honored on model meshes.
  • Troika text vendored in-tree under render_v2/text/vendor/*. Forces the bundled Geist Pixel atlas, short-circuits FontResolver.resolveFallbacks, and drops the per-spec data.font path so the renderer worker never races on the troika unicode CDN. Outline/highlight bleeding fixed.
  • Screenshot capture rewired for v2: captureScreenshot is now exposed on RendererV2Handle and threaded back to Savi's view_game_canvas_screenshot tool; fixes a freeze and reprojects the selection beam to the crosshair on the render worker.
  • Renderer is now sole authority for camera yaw/pitch; spring-arm collision moved to sim, rendered transform sent back to sim each frame. New camera-smoother.ts, orientation.ts, and renderer-camera.ts under render_v2/camera/.
  • Entity smoother batches pos/rot ingest per tick, adds per-object interpolation component and flash effects. Transform/layout-scale now applied in render_v2 model and scene state.
  • ECS-driven outlines and selection visuals moved into render_v2. Skinned models, LOD, decorations, and water materials added. Voxel terrain pipeline rewritten on render_v2.
  • New sprite-node-material.ts, EnvironmentCore, FogCore, LightsCore, PostProcessingCore, RendererPipeline, RendererAnimation, and terrain-decoration-service.ts under render_v2/.
  • New run_ui_script Savi tool plus general client-RPC system (cf-studio-chat DO ↔ kiln ↔ iframe). Scoped to the active user's most-recent WebSocket; rejects responses from other tabs/connections.
  • view_game_canvas_screenshot now works under render_v2: wired captureScreenshot through the worker RPC, captures inside the render frame (WebGPU texture lifetime), bumped size limit (512KB→2MB) and timeout (500ms→2s). Switched final encode from transferToImageBitmap to convertToBlob so taking a screenshot no longer freezes the renderer.
  • Trimmed run_script and run_ui_script tool descriptions; removed duplicated API docs and don't-lists in favor of taste/footgun notes.
  • run_script result shape collapsed to { newVersion, return, logs? } / { ok: false, error, logs? }; surfaces all log levels with data args.
  • New wisp tools terminate_wisp and list_active_wisps (in wisp.ts, no longer monkey-patched in server.ts); 4-char hex wisp IDs; terminated wisps render as "Wisp terminated" in the footer.
  • Misc Savi-side polish: grep merges adjacent matches; inspect_versions summary-only mode; get_game_pulse resolves userId at invocation; str_replace_editor view shows line numbers; debug gizmo renders text tool results as pre-wrapped text; interpreter preserves TomeTerrainAnchor on spec-driven position updates.
  • Screen flash routed through DOM UI sink/transport (was ECS-only, broken in worker).
  • Object-motion juice now restores base position/scale on effect end.
  • Voxel structure mark bounds derived from template build() output.
  • SPAWN_AGENT.md is now the single source of truth for CLAUDE.md / AGENTS.md; generated by scripts/generate-agent-docs.ts. Stale AGENTS/engine/{component-mixin-timing,live-reload}.md removed.
  • Rewrote terrain streaming as non-replicated, client-driven. Chunk components flipped from replicate: "aoi" to replicate: "never"; clients now build their own chunks from the terrain definition instead of waiting on snap frames. Removes terrain entities from the replication budget entirely.
  • Split the monolithic terrain/systems.ts (~4800 LOC) into server-terrain-system.ts, client-terrain-system.ts, streaming.ts, and terrain-systems-shared.ts. Server runs request/ingest/streaming/rescue; client runs its own streaming + ingest + collider flush + mark-liquid + rescue.
  • Added voxel terrain pipeline: per-layer textures, packed layer-texture sharing across LODs, vertex colors, async texture loading, and AO. New terrain-tile-service.ts (render_v2) is the LOD/tile authority.
  • LOD transitions no longer use dithered noise — replaced with stable cross-fade so seams stop crawling at distance.
  • Voxel chunk builds: numeric greedy meshing, boundary cache, SharedArrayBuffer result slots for zero-copy worker→main transfer, deadline raised to 2000ms, deadline-failure recovery, and stale-voxel-lookup fix.
  • Server voxel builds capped to a 3×3 chunk authority window with retry backoff so cold starts and large worlds don't stall the tick.
  • Unified chunk entity ID prefixes under terrain/stream/…; cross-prefix despawn fix prevents leaked chunk entities.
  • 2D top-down places now skip terrain mesh/collider work entirely (heightmap startup gate + ocean shoreline restore).
  • Material rebuild thrashing eliminated; weight-texture checkerboard artifact fixed; sphere terrain positioning corrected; stale colliders invalidated with fallback anchors.
  • Removed SkipReplication component; terrain entities now protected from generic despawn paths instead.
  • F3 Advanced panel wired through worker-host with chunk-mesh lifecycle tracking.
  • Fixed _f32 ReferenceError when terrain generator scripts use float literals.
  • Script-facing API (api.getTerrainHeight, getTerrainNormal, getTerrainMaterial, getVoxelMaterial, isVoxelSolid, raycastVoxel, setVoxel, setVoxelState) is unchanged.
  • Internal: split monolithic AppearanceIntent ECS component into per-aspect Draw* components (DrawModel, DrawPrimitive, DrawSprite, DrawText, DrawSign, DrawMaterial, DrawVisibility, DrawInterpolation, DrawLight, DrawAnimated3DCharacter, TomeLayout). Public ObjectAPI.getProperty / setProperty keys (visible, model, primitive, material, sprite, text, sign, layout, animated3DCharacter, light) and their value shapes are preserved — scripts read and write the same things they did before.
  • New interpolation object property: { teleportThreshold: number } | null (or false to disable). Controls per-entity render-tick smoothing; sets DrawInterpolation.
  • New GameSpec.assets.metadata field (Record<string, AssetMetadata>) — engine-managed cache of CDN model bounds. New AssetMetadata type and patchAssets ScriptMutation kind for engine-internal writers.
  • Camera API (createCameraAPI) reads/writes FeetPosition + Rotation directly instead of InterpTransform. Public getProperty("feetPosition" | "rotation"), setProperty, lookAt, getControlTarget, and query shapes unchanged.
  • queryWorld now returns results sorted by entity id on both the index and full-scan paths (previously only the index path sorted). Iteration order in scripts that loop api.query() is now deterministic across the two paths.
  • Type re-anchoring (no runtime change, no spec-shape change): MaterialOverridesSpec now derives from engine DrawMaterialOverrides; SignSpec and ObjectProperties.text now derive from AppearanceSignValue / AppearanceTextValue; ObjectProperties.model is spelled string | { id: string; animation?: DrawAnimationValue }; ObjectProperties.sprite is spelled inline with the same fields.
  • Schema cleanup: removed unused exports RectangleSplineShapeSchema (use SplineShapeSchema) and TerrainMarkSchema (use HeightmapTerrainMarkSchema). Voxel structure mark bounds is now optional and auto-derived from build() output when a generator is supplied.

pinned

what spawn isstart herefrequently asked questionsfaqthe spawn betthe bet

updates

Surface Tensionengine v4.52 daysSolidengine v4.41 weekGroovyengine v4.31 weekContinuumengine v4.22 weeksFoundationsengine v4.13 weeksGenesisengine v0.13 weeks
← All posts