Spawn
spawn / swhat we're building

pinned

start herewhat spawn isfaqfrequently asked questionsthe betthe spawn bet

updates

engine v5.1Connection2 weeksengine v5.0For RealJune 4, 2026engine v4.6AtelierJune 1, 2026engine v4.5Surface TensionMay 22, 2026engine v4.4SolidMay 15, 2026engine v4.3GroovyMay 13, 2026engine v4.2ContinuumMay 9, 2026engine v4.1FoundationsMay 4, 2026engine v0.1GenesisApril 29, 2026

pinned

what spawn isstart herefrequently asked questionsfaqthe spawn betthe bet

updates

Connectionengine v5.12 weeksFor Realengine v5.0June 4, 2026Atelierengine v4.6June 1, 2026Surface Tensionengine v4.5May 22, 2026Solidengine v4.4May 15, 2026Groovyengine v4.3May 13, 2026Continuumengine v4.2May 9, 2026Foundationsengine v4.1May 4, 2026Genesisengine v0.1April 29, 2026
← All posts
← All posts

engine v5.1.13

Engine v5.1.13

July 8, 2026

A patch in the Connection line.

what's new

  • When your game's music code hits a wrong note — an unknown drum name, a bad pattern — Savi actually hears about it now, with a helpful nudge toward the closest real instrument. And "tabla" just works.
  • Games open behind a smooth veil instead of flashing a half-assembled world for the first second — the reveal waits until the scene has actually come together.
  • Holding Ctrl while scrolling in god mode no longer flies the camera — Ctrl+scroll is reserved for your game's own bindings (and your browser's), so a zoom binding doesn't fight the editor.
  • Box colliders on scripted meshes now sit exactly where the mesh is — an off-center shape (like a car whose body reaches forward of its pivot) no longer leaves an invisible wall behind it or lets players walk through its front.
  • Testing in your dev room no longer touches your players' real saves — dev gets its own save space (starting fresh), while your published game's saves stay exactly as they are. Shared world data (terrain edits, shops, chat) still flows from build to publish like before.
  • If god mode gets switched off in your game while someone is still flying around in it, they can now always land back into normal play — no more being stuck as a ghost until they reload.
  • The god-mode help overlay now tells you Ctrl is sprint.
  • Games can now have real, permanent leaderboards! Scores survive server restarts and rank players across every room and session — all-time high scores, fastest laps, daily bests. Time trials work too: mark a board as lowest-wins and the best time comes out on top.
  • Liquid marks (paint-a-pond terrain) no longer leave invisible ghost objects piling up on the server for every player who joins — and rooms clean up the ghosts left behind by older sessions on their own.
  • When a 3D model can't download, Savi now gets a clear "model failed to load" message naming the asset — instead of the game silently showing a placeholder all day with nothing in the logs.
  • Games that build asset paths with ${…} templates no longer try to download those half-written paths behind the scenes — the phantom fetch errors that spammed logs every session are gone.
  • AI-generated models with layered shells (hollow casings, double-walled builds) stop coming out with scorched-black patches — ambient shadows now bake from the surface you actually see.
  • Game scripts can now ask which room they're in: api.getRoomMode() returns "dev" in your build room and "live" in your published game — handy for keeping test data (scores, shop stock) separate from your players' while world data keeps flowing from build to publish.
  • When Savi looks at your game, she now sees exactly what players see — your color grading and glow included — so her verdicts on how things look are finally trustworthy.
  • Scripted materials that failed to arrive after joining a room used to silently show up as plain default surfaces forever — now the engine retries the handoff on its own and tells Savi exactly what went missing.
  • Editing your scene reuses already-built materials instead of rebuilding all of them, so edits land faster and smoother.
  • Players on slower machines no longer get trapped in a rebuild-the-world loop that ends in a disconnect — if the world falls behind, the engine recovers once and then backs off instead of thrashing.
  • When a terrain texture can't load, that layer now shows a neutral grey instead of hanging invisible or wrongly-colored — and Savi gets told exactly which layer failed and why. If the texture recovers later, the real look comes back on its own.
  • Transparent generated shapes (glass, paper lanterns, light shafts) render both sides again — translucent builds that turned inside-out (caps floating, fins splayed) snap back to their assembled look.
  • When a game hits its voice-generation limit, the engine now waits exactly as long as the server asks before trying again — instead of hammering the same request every few seconds for hours.
  • Savi can read each device's live render-quality state again (api.getClientHealth()), engine-bug reports include per-device health instead of nothing, and api.resetQualityLanding() actually resets a stuck quality landing — all three had been silently dead since mid-June.
  • Setting a rotation hinge on a character mid-game no longer makes them jump sideways — the object stays exactly where it is when a script sets or clears its pivot, and only turns around the new hinge from then on.
  • Pixel-art sprites no longer get permanently stuck broken when the network rate-limits during loading — no more whole sprite sheets squeezed into one tile, wrong-size buildings, or characters that ignore the lighting. The engine now keeps retrying politely until the art arrives.
  • Characters no longer blink invisible for a moment when turning or changing animation in 3D games with pixel-art sprites.
  • Fixed 2D characters turning invisible after walking between places — leftover 3D animation names can no longer hijack a sprite's animation and point it at an empty image.
›technical notes
  • Vibe audio runtime errors reach Savi again across the worker boundary (#8349, merge 4e113a7018, ledger 1018): the error drain was structurally dead in prod — errors are born in the WebAudio renderer on the MAIN thread, but the drain consumer (AudioPrepSystem → sendClientBehaviorFault → engine-diagnostic rail) runs in the runtime worker where the renderer handle is realm-local null. Batches now relay across the existing audio seam (audio.vibeErrors, same lane as audio.completed), and prep consumes both the realm-local handle and the relayed queue. Plus: _suggestBankName speaks bank-level vocabulary (a vsco_cellos_pizz miss suggests a vsco_cellos* bank, not a per-pitch group member), and tabla aliases to tabla_na so the bare drum name resolves like bd/sn/hat.
  • TransitionVeil engages on cold boot — the raw-world first-frame flash dies (#8353, merge f6a9c8dd6a, ledger 1021): the session's first renderer frame starts in the veil's holding state, so the first present is black instead of the assembling world (assets streaming, materials compiling) that painted ~1s before the game's HTML UI could show its loading screen. Boot readiness rides sceneAssembled: first authoritative snapshot applied AND world-residency's streaming lanes quiet this frame (new un-latched quietNow verdict); at boot the flip signals are vacuously true. Same VEIL_MAX_HOLD_MS cap, same reveal fade; ECS still reflects spec instantly — only the visual reveal is held. 2D-place boots get the same dimension-agnostic engage.
  • Chorded scroll belongs to authored bindings — ctrl+scroll no longer dollies the god-mode camera (#8368, merge d4f545c617): the god-mode wheel dolly now suppresses itself when modifiers.ctrl is held, alongside the existing over-UI / coarse-pointer / brush-gesture suppressions. Bare scroll keeps the dolly.
  • Box colliders derived from bespoke-mesh bounds carry bounds.center as an optional entity-local translation on PhysicsColliderBox (ledger 1001; deriveColliderFromBounds previously dropped the center, so any off-center collider() mesh got a box snapped back to the entity origin — the iso-town car's ~1 m phantom wall behind / walk-through front). y rides the body origin via computeFeetOffset, x/z land as the collider's local shape offset in both engines (rapier setTranslation, mantle localPos); carried through sanitize, the wire codec, collider signatures, AOI radius, NPC nav/perception AABBs, the collider debug view, and trace. Not breaking: the translation is omitted whenever the bounds center equals the implicit default (x/z-centered, feet-anchored — every builtin primitive), so already-correct derivations keep identical component values and collider signatures; only previously-wrong off-center shapes move, and they move onto their visuals.
  • Dev rooms get their own player-save namespace; live saves untouched; world data still shared dev↔live (ledger #995). Dev and live are room modes of one variant and the documents API is mode-blind, so a dev-room script writing user/<uid>/save previously hit the same document as the live room — player saves alternate-clobbered whenever both containers were up. Now both storage rails (the pool-worker rail in jobs/worker-env.ts and the lifecycle rail in room-runtime's LifecycleContext, applied in object-api's fast path — mapping shared via engine/runtime/jobs/storage-namespace.ts) mode-segment PLAYER-scoped script keys only: a dev-room user/... read/write/list/lock resolves to document jobs/dev/user/..., while live/published rooms keep the bare jobs/user/... names — zero migration, every existing live save untouched; dev starts empty. World-scoped keys (everything not under user/ — terrain-edits, market/state, chat/state) stay shared across modes by design: the build→publish workflow depends on world data riding the mode-blind lane. Absent/unknown room modes fail toward the old behavior (bare names, shared with live). forwarded-storage-namespace.test.ts pins dev↔live disjointness for user/ keys, continued world-key sharing, and that both rails resolve identically.
  • God-mode exit is always allowed — the spec.godMode.enabled === false guard now gates entry only (#8341, merge ebf2c5ae96, ledger 1006). toggleGodModeForPlayer ran the spec-enabled guard before the exit branch, so a mid-session spec flip to disabled stranded an in-god creator ("God mode disabled in spec" on every exit) until disconnect/reload. Both authorities share the fix (server command + singleplayer worker call the same core); pinned in godmode-spec-update.test.ts, and the #8326 posture characterization updated to the new semantics (mid-session disable → atomic exit posture; blocked re-entry's god:error still never mutates posture).
  • God-mode controls overlay adds a "Ctrl — sprint" row to the starter shortcut legend (#8351, merge 0eeed3ef5f).
  • New builtin job leaderboard:submit — platform-durable score submission from creator scripts: objectApi.job("leaderboard:submit", { leaderboardId, score, userId, direction? }, cb). Posts server-side to kiln's SDK leaderboard submit route with the room's SDK identity (the API key lives in a worker-env capability closure — env.leaderboard, built at pool-worker spawn from workerData — and is never readable from script space, args, or results). direction: "lower" (time trials) negates the score on submit to fit the GREATEST-hardwired platform tables; scores are rounded to integers and rejected outside ±2^52. Soft-fail contract: the job DATA always resolves { ok: true, rank?, improved? } or { ok: false, error } (missing_user, user_not_in_room, invalid_score, rate_limited, …) — never throws for runtime failures, so fire-and-forget if (!result.data.ok) checks are safe.
  • Room-trust gating at the two builtin-job seams (tome/job-dispatch on the authoritative server world, tome/job-forward-server for forwarded singleplayer/client-auth submissions): the target userId must be a connected player (presence stamped into args server-side, overwriting anything script- or wire-supplied; the handler fails closed without the stamp), plus an in-room cadence cap of one submission per (userId, leaderboardId) per 2s. Server submissions also opt in to kiln's DB-side rate limit (enforceRateLimit: true).
  • Skill docs: leaderboard gains a "Durable Leaderboards (platform)" section; jobs-and-storage lists the new builtin.
  • Mark-liquid surfaces stop minting server husks (#8354, merge 0f788c664b, ledger 1020): TerrainMarkLiquidSystem (mode client) derives liquid surfaces locally on every client, but on a place host the entities satisfied isHostSimulatedEntity and published as hosted-remainder state — the server stripped the replicate-never geometry and kept a permanent geometry-less husk per surface, fanned to every joiner, with signature drift stranding old husks forever. Three walls: (1) isHostSimulatedEntity excludes engine-derived ClientEntity locals (covers the upload view, the reset stray sweep, and the rail flush in one predicate); (2) the server's upload create gate refuses terrain/mark-liquid/* rows so stale-engine hosts can't re-mint (create-only — deletes stay host-writable); (3) as live host, the system reaps stale-signature husks for mark keys it owns and publishes the deletes through the new ClientAuthHostDespawnPublishResource, re-arming on host claim between rebuilds.
  • model-load-failed joins the allowlisted engine-diagnostic codes (#8359 kernel leg, merge 043b479c3f, ledger 1025): the model sibling of texture-load-failed. Neither of the model rails covered a failing FETCH — the contentless report fires only on successful-but-empty loads and the unready-asset note rides scene-view only — so a 5xx-looping model (e.g. a 24h magic-cdn tombstone) held its "Couldn't spawn" placeholder all day while Savi's log rail stayed clean. Same emitter, same once-per-asset-per-episode cap. The kiln legs of ledger 1025 (429/502 propagation on the serving routes) ride kiln's own lane, not this kernel build.
  • asset-preload-sweep skips template placeholders (#8347, merge fd67a70350, ledger 1017): SCRIPT_ASSET_REF_PATTERN string-scans raw script source including backtick template literals, so refs like `/cdn/${SLUG}/tracks/theme.mp3` were prefetched as literal URLs (~9 phantom URLs × 3 retries × every session on the specimen app, ~311 iframe errors/2h). Any scanned ref containing ${ is now skipped — a template placeholder is by definition not a fetchable literal. Literal refs in the same source still preload.
  • Remesh GPU bake casts AO from the outside-visible sheet on nested shells (#8352, merge ef67ac807d, ledger 1013 step 1): Trellis raws carry nested same-facing onion shells, and the nearest-agreeing texel search landed on the inner enclosed sheet wherever the decimated surface drifted inward — the AO hemisphere then measured the inside of the shell stack (occlusion ~1, scorched black; the y2k/frutiger mottling class). Before the AO loop, one BVH scan walks +N to the cage-clamp ceiling and relocates the AO origin to the outermost same-facing crossing below the nearest opposing crossing; single-sheet texels keep the classic origin bit-for-bit, and albedo/normal/roughness/metal transfer is untouched. Ships in packages/remesh (the remesh worker image), not the kernel tarball — aboard for every remesh regardless of pinned engine version.
  • Scripts can read the room's mode: api.getRoomMode() returns "dev" | "live" in behaviors, lifecycle hooks, crons, and run_script. The value is mirrored from the room's SDK identity (TomeRoomModeResource, written by room-runtime from the same getRoomModeIdentity() both storage rails key their mode segmentation on — storage-namespace.ts) so the read can never disagree with where a user/ save actually lands; absent/unknown modes read as "live", exactly how modeSegmentStorageKey treats them (ledger #996, follow-on to #995).
  • Dev-room shared-key FYI for Savi: when a game script in a dev room writes (storage:set/storage:del) a world-scoped key — anything not under user/ — the engine records one informational runtime-log line (the getLogs channel) per key per room session, naming the by-design dev↔live sharing and the deliberate mode-split options (user/ keys, or api.getRoomMode() in the key) without instructing a rewrite. The hook sits on the ObjectAPI job() seam, so the lifecycle fast path and the queued/pool lane both pass through it while engine-internal storage lanes (terrain-edit persistence submits straight to the JobQueue) stay silent. Live rooms and unknown-mode worlds never warn.
  • view_live_scene camera/frame captures composite the active look + bloom (#8344, merge 32684f98c4, ledger 1009): captures previously skipped the whole post chain (no atmosphere.look grading, no bloom), so Savi graded look-dependent work against pixels the player never sees. The live post graph can't be re-driven from a capture pose (scene pass, bloom pyramid, TAAU history are main-camera-keyed and accumulate across frames), so captures run a capture-owned chain built from the same pieces: scene → linear HDR (+ capture-pose depth) → the engine's dualFilterPyramidBloom driven by the same resolved-look dials and quality-governor skip → ACES → the same LookPass class (scripted graph + vocabulary + sRGB + dither) when a look is active → the capture's output target.
  • Scripted-material library join failure gets a loud diagnostic + a join watchdog retry (#8342, merge f98c0073f3, ledger 1014): on some joins the derived MaterialScripts library (replicate-never, forward-always) never reached the renderer worker, so every scripted intent built as its Std/PBR fallback — silently (the report was gated on libraryReceived) and permanently (only a source-hash edit or renderer restart refreshed). Now: a scripted-material-library-missing diagnostic (reportOnce per ref per session, distinguishing never-received from received-empty), a renderer-local join watchdog (~10s after boot-drain latch, one re-check, never a poll loop), and the server-side engine-diagnostics dedupe gains a TTL so later joins' byte-identical reports aren't swallowed room-lifetime. Fallback behavior itself unchanged.
  • Standalone-primitive material mints resolve through a scene-scoped refcounted content-identity registry (#8315, merge 8b13fa8fa0, perf-apex L16, ledgers 900/952/959): same mint inputs → the same already-compiled material instance across wearers and across edits; changed content mints new, and a replaced identity disposes exactly when its last wearer swaps off. Per-entity-mutable mints (textured leases, draw/effect carriers, active flash, appearance-owning water/shockwave/slash) stay per-entity; patches on shared identities settle first (sole wearers re-home in place, divergers split before any uniform write can bleed).
  • The stale-path reset↔recompile strobe is capped with the bespoke branch's epoch budget + backoff (#8361, merge f78ff89b14, ledger 1026): the stale branch of prepareRenderBacklogRecovery fired writeRecoverySnapshot unbounded — on a cold-shader client the recovery snapshot's own respawn mints all-new batches/materials, the compile storm puts the renderer behind again, and the trigger re-arms from its own hangover (observed 60s→29s cycles, 379 programs on 25 draws, 691ms renderer frames, until connection death). Now: a sim-side epoch that survives the reset, first recovery immediate, then doubling backoff (30s/60s/120s…) with the strobe declared via one warn once the rapid budget (3) is spent. One deliberate divergence from the bespoke sibling: the backoff clamps at 300s instead of refusing forever (the stale reset is also the sim-side backlog's memory-relief valve), and the epoch dies after 120s of quiet so a later real staleness event starts fresh.
  • Terrain splat-layer texture fetch failures get a neutral-albedo fallback + a Savi-visible diagnostic naming the layer (#8345, merge aed167474f, ledger 1008): the layer atlas only subscribed to load successes, so a failed fetch (the staging moodboard KTX2 401 family) left the layer waiting invisibly forever and Savi never heard the layer's name. renderer-asset-service gains subscribeTextureLoadFailures (the failure twin of subscribe(), fed from the retry ladder's verdicts; 202 still-generating and non-exhausted auth parks stay silent); a SETTLED failure flips the layer's tint row to a neutral mid-grey through the authored tint pipeline (never black, never a stale full-strength tint) and reports material + texture + HTTP status through the existing allowlisted terrain-atlas fallback codes. The success subscription stays armed — a late heal re-textures the layer and restores the authored tint.
  • #8345 review follow-ups (#8348, merge a0a5aa5dba): the heal path's authored-tint restore is un-gated from the join verdict (a healed-but-unjoinable texture takes its authored tint back); the failure channel gains a settled-verdict query (getTextureLoadFailure) so an atlas built after a terminal settle still learns of it; dead-asset fix text is auth-flavored.
  • Transparent bespoke-mesh material leases default to THREE.DoubleSide, completing the one-winding-convention pairing (ledger 977; the original rescue in createMaterialLease was scoped !willRenderTransparent, which left transparent one-sided bodies — exhibit-level transparent: true or vertex-alpha-forced — rendering the flipped side of ctx-authored faces). One default for every bespoke lease now: DoubleSide unless the author sets side or the material owns its graph. Cost is three's transparent+DoubleSide two-pass (BackSide then FrontSide), which buys correct within-mesh self-ordering and lands only on the rare standalone transparent bespoke population; it also matches the batched transparent lane (always DoubleSide) and what a live opaque→translucent geometry patch already produced (applyVertexAlphaTransparency flips transparent and leaves the lease's DoubleSide in place). The alpha-split draw grouping (ledger 956) is untouched — translucent slots keep their own groups, lanes, and shadow flags byte-for-byte; only the lease side default changes.
  • Magic-cdn probe honors Retry-After per filename (#8355 kernel leg, merge e1b641342e, ledger 1023): a dialogue-heavy game that exhausted the per-app speech cap kept re-requesting capped assets ~3.2/min for hours (937 rate-limit rejections/6.5h). A 429 with a quota reason now remembers the server's own Retry-After in a per-filename tombstone (conservative 5-min fallback, 1h ceiling); re-asks during the window get the same MagicCdnFetchError with zero network round-trip, and expiry re-admits a real probe. Per-URL-identity (other assets keep serving), bounded map (512 entries, oldest-out). The kiln legs of ledger 1023 (single-designer voice claims) ride kiln's own lane, not this kernel build.
  • The engine.clientHealth snapshot producer is restored (ledger 1033; deleted by the #6891 runtime-worker rewrite, which left CLIENT_HEALTH_COMMAND a server terminus with no sender and recordLocalClientHealth with zero callers). A factory module (client-health-report.ts, the renderer-perf-rollup pattern) taps the raw quality-governor block off each renderer.perfSample, and reports on the 15s perf-rollup cadence plus a throttled edge report on rung transitions — writing the local mirror (singleplayer api.getClientHealth(), engine-bug dump health blocks) and the client command outbox (multiplayer api.getClientHealth() via the server mirror, still re-validated + rate-limited server-side). The transition-trigger allowlist widens with the kernel's GovernorTrigger vocabulary (pacing-engage/pacing-release, the #8314 pair). The quality.landing.reset client control case and the singleplayer reset transport (setClientQualityResetTransport) — deleted by the same rewrite — are re-wired, so api.resetQualityLanding() reaches the renderer again in both realms.
  • Runtime pivot writes (setProperty("pivot") / setLocalPivot) are now pose-preserving (ledger 1038). The pivot fold T(F + S·P − R·S·P)·R·S nails the pivot's fixed point at its identity-rotation world location, so a runtime set on an already-rotated entity teleported the folded pose by (I − R)·S·P that frame — and the clear snapped it back (Rabies Bear's rear-up: 1.40m lunge at yaw 180, 0.99m diagonal at yaw 90/270, collider riding the fold). The property writer now rebases the authored local position by the fold-offset delta at write time (writeLocalPivotPreservingWorldPose, tome/pivot.ts): the entity doesn't move on set or clear, and rotation changes after the write hinge at the pivot's world location at write time. The rebase reads only replicated authored state (Local*, LocalPivot, renderable bounds — never a sampled world pose), is exactly zero at identity rotation, and works identically on roots and parented children. Spec-frame surfaces keep composing the authored fold with no rebase, via the new applySpecPivot: boot + spawn (interpreter applyObjectProps, applySpawnProperties) and property patches (applyPropertyPatchToWorld — updateObjectSpec, undo/redo envelopes, where a mixed {feetPosition, pivot} patch must land exactly where it says).
  • REMINT (#8403, merge 5ad2155ea6, ledger 1050 — the 5.1.13 remint blocker): 429s are transient, not dead — sprite atlas/meta hydration un-starves. The #8274 boot pre-warm floods magic-cdn in the boot window, and the #8259/#8270 give-up ladders classified any 4xx as a dead asset (status >= 400 && status < 500), so a staging 429 storm latched permanent give-up after 8–10 attempts: sprite atlas metadata starved (resolveSpriteUv → UNIT_UV — a whole sheet rendered into one quad), ?normals= companions starved (unlit lit-sprites), and spawn.meta world-height starved (default-size sprites). Dead is now isDeadAssetHttpStatus (4xx except 429/408/425) in both give-up lanes (renderer texture/model retries + sprite metadata hydration); transient statuses retry indefinitely like 5xx/network, honoring the server's Retry-After (the #8355 park doctrine). The renderer's atlas-hydration lane carries HTTP status through the meta fetchers instead of folding every failure to null — give-up latches only when EVERY failed source answered a dead 4xx, and served-but-no-meta settles authoritatively in one probe. A KTX2 kvd-block fetch failure is a retryable failure now, not a silent header-only fallback that latched fps=12 forever.
  • The #8277 auto-size gate, narrowed to its stated purpose: an authored collider2d no longer pins an UNSIZED sprite to [1,1] (Village Green's t2-cottage). Authored sprite.size still beats hydrated size (art and hull stay glued); the auto-anchor gate (#6713) is untouched.
  • 3D-place batched sprites no longer blink invisible on facing/clip texture swaps (ledger 1041, tiger's iso-town walk trace). Texture identity is the batch key, so a ?facing=X&animated=CLIP swap re-keys the visual across batches — and the re-key hold ghost used to release when the destination's COMPILE settled, while a destination batch still waiting on its texture lease is mesh-wide hidden by the anti-flash gate and the async compiler drops invisible objects at drain time without compiling them: "no compile pending" released the ghost into a batch that had never drawn, and the sprite rendered nothing for the fetch/transcode window (~230ms+ in the trace). The hold now arms and releases on PRESENTABILITY (spriteBatchCanPresent: texture lease bound or the mesh already past the visibility gate, plus compile actually settled — the release probe re-queues dropped compiles until the first visible pack lets them land).
  • The recurrence engine dies with it: the idle-dispose sweep (ageIdleSpriteBatches) now spares empty batches whose texture BASE (spriteTextureBaseOf — the URL before its variant query) is worn by a live visual, so a walk-kit hero's 16 facing/clip variant batches stay warm (compiled material + bound lease) once seen instead of re-fetching and re-transcoding the KTX2 on every 5s-idle revisit. The whole family disposes on the sweep after its last wearer despawns. The standalone (ySort) lane and 2D pens are untouched.
  • Sprite mixer selection never composes a non-storyboard clip name into a ?animated= variant URL anymore (ledger 1047, the zoo 2d-side invisible player). A place transition can strand stale 3D locomotion channels (idle: "Idle" at weight 1 — the a3dc vocabulary) in a sprite hero's mixer when the skin script's one-shot setProperty('mixer', …) replace is lost across the transition write-window; the stale channel won selection, composed hero.png?facing=right&animated=Idle-loop, and magic-cdn answers such URLs with a renders-nothing placeholder — an invisible player that only a refresh (fresh onSpawn skin derivation) healed. The composability rule is shared (isStoryboardClipSlug in sprite-appearance-rules — lowercase kebab slugs, the 2d-mode skill's taught vocabulary): selection skips non-slug channels (the next declared 2D clip wins; all-invalid mixers hold the visible base frame), sprite clip timing reports the nominal ~1.3s fallback for them immediately (no bogus hydration fetches, and no permanent duration: null — getChannel().finished keeps firing on wall-clock and atlas probes read the taught failed-cook signal), and the warm derivation stops minting their variants (?animated=Idle-loop warm refs were real: the scraper crossed a script's 3D-branch clip literals with its sprite textures). The skip teaches through the Savi-visible diagnostic rail (sprite-clip-not-storyboard, once per entity+clip — getLogs + one DM, like sprite-atlas-missing) plus one console warn per entity, same lane as the blank-clip skip.