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.4.0

Solid

May 15, 2026

Things stack right, worlds load clean, and clicks land where you point.

what's new

  • Stacking just works. A mug on a chair on a house keeps its size. Ask Savi to put something on top of something else and she'll land it first try — huge for any game where things sit on shelves, stack into towers, or get carried around.
  • Worlds load clean. Things appear at the right size right away. No more pop-in where stuff suddenly grows or shrinks as the level comes in.
  • Clicks land where you point. The sky doesn't steal clicks anymore, and big crowds of repeating things (trees, rocks, props) can be clicked one by one. Shooters, click-to-place, and pick-up-anything games all feel sharper.
  • Effects stick to surfaces the right way. Bullet holes, splats, dust puffs and decals face the wall they hit instead of floating sideways.
  • Camera stays still when you open the menu — no more drift while you're trying to read.
  • Heads up: a few old scenes might look a little different where you'd attached one thing to another. Take a peek and tweak if anything looks off.
›technical notes

Scene graph + layout (breaking)

  • BREAKING: scene graph rewrite (#6480). Replaces Scale + LayoutScale + RenderScale + LayoutScaleFeature + RenderScaleFeature + the multi-stage hierarchy composition with three explicit components, each with one job:
    • LocalScale / LocalRotation / LocalFeetPosition / LocalPivot — authored inputs.
    • WorldScale / WorldRotation / WorldFeetPosition — solved hierarchy outputs (World* = parent.World* ⊗ Local*). Pure tree math, no asset awareness.
    • GeometryScale — per-asset fit factor derived from (DrawModel.rawBounds, TomeLayout). Computed for every entity, leaf-only at consumption, never inherited.
  • BREAKING: fixed layout cascade bug — a root's layout.maxExtents no longer shrinks every descendant. Renderer / physics now read WorldScale × GeometryScale per entity instead of a composed RenderScale that mixed asset-fit into the hierarchy. Regression test: tome/__tests__/mug-on-chair-on-house.test.ts.
  • BREAKING: fixed parented-clamp drop bug — a parented entity's own layout.maxExtents now applies. GeometryScale is written regardless of parenthood. Regression test: tome/__tests__/parented-layout-clamp.test.ts.
  • BREAKING: maxExtents is a ceiling (factor = min(1, maxExtents / rawBounds)), minExtents is a floor (factor = max(1, minExtents / rawBounds)), minExtents === maxExtents = exact fit.
  • Added api.getWorldBoundsBox(id): { min, max, size, center } | null. Backed by tome/api/world-bounds.ts; WorldBoundsBox type surfaces in Savi's prompt via the shared-schemas section.
  • Renderer reads WorldScale × GeometryScale uniformly. Model matrix = T(WorldFeetPosition) × R(WorldRotation) × S(WorldScale × GeometryScale). Feet alignment uses the combined factor.
  • Rapier reads WorldScale × GeometryScale so collider geometry matches visible geometry on every entity. Pose stays in pure graph-transform space; feet-to-body-center offset = (rawHeight/2) × WorldScale × GeometryScale.

Asset bounds prefetch

  • New server-side BoundsPrefetchFeature (#6490) discovers entities with DrawModel, submits Range-fetch jobs that parse GLB headers (~64KB per model) off the main thread, and writes results to GameSpecResource.assets.metadata. Supports KHR_mesh_quantization dequantization.
  • Bounds persist to Supabase via patchAssets, so subsequent room starts find them cached and avoid the refetch.
  • Hooks-driven, not per-tick (#6491): onComponentAdd/onComponentSet(DrawModel) + a one-time seed of existing entities. Steady-state cost is zero. Catches applySpec, api.spawnObject, and runtime setProperty("model", …) swaps without scanning script source.
  • Removed the now-unused collectModelUrls / MODEL_URL_INLINE_RE spec walker.

Raycast

  • Raycast results now include per-hit surface normals (packed + decoded) for both CPU and GPU paths (#6504).
  • CPU/GPU raycast behavior aligned for first-hit queries; both use snapped ray directions so results are consistent across paths.
  • Sky meshes are excluded from raycasts — click queries no longer hit the skybox before the world.
  • Raycast reset and distance handling hardened against invalid (NaN/Infinity) values.
  • Fixes raycast against IndirectBatchedMesh in the compute raycast path.

Input

  • handleMouseMove in engine/input/raw-capture.ts now gates on getInputMode() === "overlay" like the other input handlers (#6501). Pointer-lock mouse deltas no longer rotate the camera under an open overlay.

Skills (Savi-facing)

  • Removed six phantom API method names from the engine skill files (#6500). All were taught to Savi but didn't exist on ObjectAPI / TomeCameraAPI, causing tool-call failures or silent [Tome] patchTerrain() ignored… rejections:
    • world-composition.md: api.setSpec("...decorations") → api.patch("terrain", { decorations: … })
    • voxel-terrain.md: api.patchTerrain({…}) → api.patch("terrain", { addMaterials, addMarks, … })
    • voxel-terrain.md: objectApi.getPointerRay(input) → objectApi.getInputRay(input)
    • pointer-raycasting.md: getPointerRay / getAimOrigin → getInputRay / getProperty("feetPosition") + offset
    • pointer-raycasting.md: objectApi.raycastPhysics(…) → objectApi.raycast(…)
    • 3d-billboard-sprites.md: objectApi.getPosition() → objectApi.getProperty("feetPosition")
    • turrets.md: objectApi.damage(id, n) → getObjectState + patchObjectState({ health: … - n })
›migration notes

1. maxExtents is now a ceiling, not a target fit

Previously maxExtents scaled both up and down to fit the box. After 4.4 it only shrinks oversized models — undersized models stay at their authored size. To force a specific size, set minExtents equal to maxExtents.

For every object with layout.maxExtents but no layout.minExtents:

for each obj in spec.places.*.objects with obj.properties.layout?.maxExtents and !obj.properties.layout?.minExtents:
  via run_script:
    api.setObjectProperty(obj.id, "layout", {
      minExtents: obj.properties.layout.maxExtents,
      maxExtents: obj.properties.layout.maxExtents,
    });

Scatter templates follow the same pattern — carry minExtents forward from each template's maxExtents.

2. Scene graph rewrite fixes two old transform bugs

The spec format is unchanged, but the rendered result of an unchanged spec can differ in two specific shapes:

  • Layout cascade: a root's layout.maxExtents no longer shrinks its descendants. If you'd visually compensated for that bug, descendants now look bigger.
  • Parented clamp drop: a parented entity's own layout.maxExtents now applies. If you'd added a clamp that was silently ignored, the entity now shrinks.

Per game, find and fix:

for each place in spec.places.*:

  // Case 1: clamped root with any descendants
  for root in entities where layout.maxExtents and !parent:
    factor = min(maxExtents.{x,y,z}) / max(rawBounds.{x,y,z})   // the old cascade factor
    for descendant in subtreeOf(root):
      // either scale descendant.scale *= factor to keep the old look,
      // or do nothing (recommended) and accept the fix.

  // Case 2: parented entity with its own layout
  for entity in entities where layout.maxExtents and parent:
    // either remove layout.maxExtents to preserve the old look,
    // or accept the (intended) clamp.

A wisp can identify affected entities per game; the creator decides per scene whether to compensate or accept the fix.

3. New API: api.getWorldBoundsBox(id)

Returns { min, max, size, center } | null for the world-space AABB. Use it instead of multiplying getProperty("scale") by rawBounds when stacking or aligning objects. Returns null until raw bounds are loaded (resolves next tick).

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