The directive was that the demo scene become a project template you can make
new worlds from and edit by voice. This is the blocker gone: an authored
splash file can now build the world the demo builds.
`apps/arcade/resources/template.splash` is that world — sky, sun, terrain,
crossroads, two rows of houses, six distinct drivable cars, a bearded hero,
wandering townsfolk and the spiral climb — entirely in `game.*` verbs. It
loads: "eval ok, 28 entities, 5 skinned characters".
Two engine gaps had to close first, and both were the same shape: arcade
owned something the script could not reach.
**The script could not see the asset library.** `ScriptHost::set_assets`
existed and arcade never called it, so every asset verb — find_model, model,
kits, cast — reported "no stock library on this device" on a machine with
4,700 models on disk. arcade built an index for its own Rust demo and kept
it. An authored game therefore could not use any of the art the demo is made
of, which is most of the reason the demo had to stay in Rust. The index is
now built once, lazily, and shared with the host.
**Script characters had no bodies.** The skinned draw path walks
`self.villagers`, which only the built-in Rust world ever filled, so every
character in an authored game rendered as its bare collision box — which is
what a splash world looked like next to the demo. `sync_script_characters`
now builds that list from `blocks.characters` after each eval, matching the
model id the script asked for against the loaded cast and round-robining on a
miss. A wrong-looking character beats an invisible one.
Not yet done, and the template is honest about which parts are which: the
climb's moving lifts and the fireworks still have no verb, and the world is
written out longhand where the Rust version used loops.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE FINDING THAT CHANGES THE PREMISE: "1 rigged model in 4,442" was measured
with the BROKEN GLB probe (the gLUF magic bug). With the probe fixed the
library holds 36 rigged models across three rigs:
41 joints — 9 KayKit heroes + undead, up to 95 clips
7 joints — 22 KENNEY civilians (male/female a-f, orc, human, archer, shop
employees, skaters, soldiers), 32 clips
6 joints — 5 Kenney platformer characters, 25 clips
So a village can be populated with 22 visually distinct civilians TODAY, with
no third-party pack at all. Every conclusion drawn from that probe before it
was fixed needs re-checking, not just this one.
KayKit: 9 characters fetched (Adventurers + Skeletons), pinned by commit +
sha256, 37 MB, gitignored. CC0 verified by READING LICENSE.txt at each pinned
commit, recorded in the script header and CREDITS.toml.
THE SHARED RIG HOLDS ACROSS PACKS, proven rather than assumed: hashing the
joint-name list of all nine files yields the SAME digest — 41 joints, same
names, same order — despite two separate repositories. Skeleton clips are a
strict superset (95 = the adventurers' 76 + 19 undead extras: awaken,
resurrect, spawn, taunt). So a clip authored for the knight plays on the
skeleton warrior and one animation path drives the cast. A test pins this,
including that both packs are present, so a version bump cannot silently break
it.
The texture trap that cost the Kenney fetch three attempts does NOT apply:
KayKit GLBs EMBED their atlas (image/png in a bufferView), verified by parsing
all nine.
tests/rigged.rs parses all 36 rigged models through makepad_game_render::skin
— the loader the app actually runs — and asserts the index's joint and clip
counts match it. Deliberate: the index's own probe was wrong for the entire
library once and survived because the fixture encoded the same error.
find_cast groups by JOINT COUNT rather than pack, because the valuable fact is
cross-pack interchangeability. Cast states are the INTERSECTION, not the union
— advertising a state one member cannot perform is worse than a shorter list.
Added the state words the skeletons needed (spawn/resurrect/taunt/use):
Skeletons_Awaken_Floor previously matched nothing, so "an undead that rises
from the ground" was unfindable.
One bug found in its own work: casts_to_json emitted a doubled closing brace —
malformed JSON that still looked fine in a log. Fixed with a structural test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
52 packs, 4442 GLB models, 136 MB on disk — fetched sequentially with resume
(a hash-valid pack is skipped, so an interrupted run costs nothing) via
kenney.nl's content-hashed URLs with per-zip sha256. MIRROR.toml records
every pack's canonical URL, sha256, size and file count, so a mirror is
reproducible; --mirror=/ARCADE_ASSET_MIRROR redirects the base URL and fetch()
verifies the digest identically whatever host served the bytes — a mirror we
control is never trusted more than upstream. --packs= keeps a fresh clone from
being forced to pull everything.
Aliases restructured to survive the scale: per-pack theme rows (55) so every
model in a pack inherits its setting, filename-token parsing with variant-
marker stripping as the workhorse, and ~240 hand-curated query-time synonyms —
the layer whose curation compounds across the whole catalogue. 82-query suite
reports misses instead of being tuned green; the list is down to 2, both
defensible (a floor IS somewhere to stand; a bell IS a metal clang).
Three ranking bugs root-caused, not patched:
- No stemming, so "smashing" never reached the alias "smash" and "glass
smashing" returned glass PIPES. Added a conservative stemmer probed at
synonym strength (only ever adds matches), which refuses to mangle
glass/grass/class and routes "trees" to "tree", not "tre"
- An overreaching alias: `spaceship` sat on four spaceEngine SOUND families.
An engine hum is not a spaceship. Removed; "spaceship engine" still resolves
- Kind confusion on ties: spacecraft models tied with spaceTrash sounds and
lost the alphabetical tie-break. Added kind-aware tie-breaking driven by
query intent — deliberately a TIE-BREAK, not a score bonus, so it cannot
drag a weak model above a strong sound (laser gun / explosion / coins scores
verified unchanged)
Repo-policy violation fixed: all three asset .gitignore files were deny-lists
covering only .glb/.png/.jpg, leaving 302 .gltf files from 3d-road-tiles fully
committable. Converted to allow-lists — 4,744 asset files are now unstageable
by accident.
Scale at 4,999 entries: build 120 ms, search ~0.2 ms, 2.1 MB heap, and the
prompt summary still 479 chars — flat as the catalogue grows, which is what
keeps it affordable in every AI turn.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
75 models (5 KenneyNL starter kits, .glb, pinned commits + sha256, 2.8MB)
and 556 sounds (7 packs via kenney.nl content-hashed URLs, each zip
sha256-verified, 13MB). Nothing large enters git: dirs are gitignored, only
CREDITS.toml and .gitignore files are tracked.
The index is the point — an AI cannot use a library it can't name:
- id is kenney/racing/vehicle-truck-yellow, anchored to where the file
LIVES, not its category, so retuning the category tree never invalidates
a saved game
- Filename tokens are the floor; the value is two hand-curated alias tables
(76 model rows, 116 audio FAMILY rows — Kenney's footstep_wood_000..004
collapse to one family, so 556 files stay maintainable) spanning
synonyms, kid vocabulary and misspellings (vehical, hosue, motercycle),
function over identity ("something to hide behind"), colour/size/
material, and theme, plus ~190 query-time synonym expansions
- AssetKind model/sound/music so a 30-second track can't be returned as a
hit sound; GLB probe reads skins -> rigged, animations -> animated
- FIND_MODEL tool descriptor (provider-neutral plain data) + compact
results; library_summary() is 469 chars for 632 entries and provably
doesn't grow with the catalogue; resolve_or_explain() rejects
hallucinated ids with near-misses; local_spawn() gives the local
librarian a best match plus a confidence blending strength with margin
HONEST GAP: every Kenney audio pack is Ogg Vorbis only — no WAV exists
upstream — and this tree has no vorbis decoder. Sounds are indexed and
searchable but NOT playable: entries carry decodable:false, the agent JSON
emits playable:false so a game cannot fire a silent sound, and
--transcode converts via ffmpeg when present. A real decoder is the fix.
Three bugs found by testing, all fixed: sci-fi-sounds.zip ships a directory
with no owner-write bit so that pack alone silently extracted 0 of 73 files;
prepositions matched phrase aliases ("...at the roadworks" hit "something to
shoot at"), so function words must be dropped, not down-weighted; and exact
names lost to incidental aliases (a coin SOUND outranked the coin MODEL).
Miss list left visible at 2/52 rather than tuned away — both are defensible
answers against over-narrow expectations.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- libs/game/render/skin.rs: GLB container + owned-JSON parser, dense
accessors, multi-primitive skinned meshes, skin + inverse binds, full
node-hierarchy palette, T/R/S clips (linear/step), nlerp blending.
Hermetic tests via an in-code 2-joint GLB; real-asset test skips with a
hint when the download hasn't run
- DrawGameSkinned shader (PbrVertex + albedo texture, terrain-style
lighting); skinned batch draws between opaque and alpha passes.
CPU skinning for now (sub-ms at 3716 verts): the uniform_buffer GPU path
has zero in-tree runtime consumers — SkinnedModel::palette is the seam
for the GPU swap
- apps/arcade: download_assets.sh (KayKit pinned commit + sha256,
idempotent, dir gitignored, CC0 license note in-repo); Knight patrols
the demo world with idle/walk blending, yaw follows path; captures
verified (pose advances, depth-occludes correctly)
- renderer binary_search sites use the shared sorted-id helper from M0r
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>