User journey · verified · game-asset generation

Multi-Asset Sheet

The AI needs to draw ten game sprites that look like they belong together. How does one AI call produce a consistent art-style spritesheet, and how does each sprite end up cropped and transparent in the right place?

Last verified 2026-04-18 against code
Slug multi-asset-sheet
Declaration <multi-asset-sheet> HTML tag
Prewarm synchronous during AI turn

§1 What it is (and isn't)

player_idle
player_walk
enemy
coin

One sheet, many sprites, one style

Multi-asset sheet is one FAL API call that produces a single spritesheet image containing N sprites sharing a consistent art style. Gemini Vision then detects each sprite's bounding box, each sprite is individually background-removed, and each named asset is served as its own cached WebP.

Not grid-select. Not character variants. Not one-image-many-styles. It's: one visual call → a whole family of named sprites.

§2 How the AI declares a sheet

Two tags in the AI's HTML response. The declaration and each grab site.

<!-- 1. The declaration — once per sheet -->
<multi-asset-sheet
  id="main_sheet"
  assets="player_idle, player_walk, enemy, coin"
  style="16-bit pixel art"
  theme="forest adventure"
  requirements="all sprites face right, transparent bg"
  sheet_size="2x2"
></multi-asset-sheet>

<!-- 2. Each grab site — one per <img> -->
<img src="https://images.fuzzycode.dev/multi_ai?asset_sheet_id=main_sheet&grab=player_idle&resize=64x64" />
<img src="https://images.fuzzycode.dev/multi_ai?asset_sheet_id=main_sheet&grab=enemy&resize=32x32" />
The AI is forbidden from building URLs dynamically. Template-literal URLs (${playerName}) break the prewarm because the post-processor can only rewrite static strings. This rule is repeated four times in core_ai.py:82-86, 117-125.

§3 The pipeline — declare to served-pixel

client + ai
1. AI emits tags

AI generates HTML containing <multi-asset-sheet> + N <img ...asset_sheet_id=main_sheet&grab=...>

server · fuzzycode
2. Prewarm (synchronous)

prewarm_multi_asset_sheet_urls() at html_processing.py:382-482. Parses all sheets, resolves spritesheet_ref dependencies.

server · imagebuddy
3. Generate + slice

Single FAL call for the sheet image. Gemini Vision detects bounding boxes per named asset. Each sprite bg-removed and cached.

server · fuzzycode
4. Rewrite + inject

Every asset_sheet_id= URL rewritten to sheet_key=<hash>. Tags receive server_sheet_key, server_sheet_hash, server_sheet_dt attrs.

browser
5. Per-asset GETs

Browser fetches multi_ai?sheet_key=<hash>&grab=<name>&resize=WxH. ImageBuddy serves the pre-sliced WebP.

Blocking step: step 2 runs synchronously during the AI turn — the user sees no extra loading; they just see the AI "thinking" for a bit longer. The sheet key is embedded in HTML before it reaches the browser, so <img> fetches in step 5 are cache hits.

§4 The cache-key inputs — what counts as "a different sheet"

The hash that becomes sheet_key is computed over this set. Change any input and a whole new spritesheet is generated, and all previous visual art is discarded.

assets

comma-separated list of named sprites

style

"16-bit pixel art", "watercolor", etc.

theme

world / setting descriptor

requirements

free-text constraints (facing direction, transparency)

sheet_size

grid dimensions like 2x2

model

AI model id if overridden

spritesheet_ref

id of parent sheet for style consistency (optional)

Existing sheets should be treated as frozen. The AI is instructed "Treat existing multi-asset-sheet definitions as frozen" (core_ai.py:109) — because changing any hash input regenerates everything, losing the user's existing sprites. There is no UI enforcement; if the AI disobeys, the user silently loses art.

§5 spritesheet_ref — art style consistency across sheets

When a user wants to add new sprites that match the art style of an existing sheet, the AI can declare a second sheet with spritesheet_ref="main_sheet". The parent sheet's generated image is passed as a reference to FAL flux-2/edit, keeping style aligned.

flowchart LR classDef sheet fill:#fff4d6,stroke:#683c06,color:#111 classDef gen fill:#eaf3ff,stroke:#1d58b1,color:#111 classDef out fill:#e8f6ec,stroke:#2f7a3e,color:#111 A[main_sheet
assets: player_idle, player_walk, enemy, coin
style: 16-bit pixel art]:::sheet A --> GA[FAL: text-to-image
1 sheet produced]:::gen GA --> I1[sheet image · cached]:::out I1 --> S1[sliced: 4 WebPs
by Gemini Vision]:::out B[boss_sheet
assets: boss_idle, boss_attack
spritesheet_ref: main_sheet]:::sheet B --> GB[FAL: flux-2/edit
with main_sheet image as reference]:::gen I1 -.->|ref image| GB GB --> I2[boss_sheet image · cached]:::out I2 --> S2[sliced: 2 WebPs
by Gemini Vision
same art style]:::out

spritesheet_ref resolves transitively — a third sheet can ref boss_sheet which refs main_sheet. Transitive resolution uses a pending-queue algorithm (html_processing.py:425) with a self-reference deadlock guard (line 428).

§6 Failure modes (what the user might see)

FailureDetectionUXEvidence
AI omits server_sheet_key after prewarm checkUnprocessedAssetSheets walks the HTML Yellow toast: "Found N asset sheet(s) without server_sheet_key. Run an AI change to prewarm these assets." static/script.js:1509-1532
AI hallucinates an asset_sheet_id that doesn't match any declaration validateMultiAiUrls cross-references ids to sheets Yellow toast: "asset_sheet_id X doesn't match any declared sheet" with offending URL script.js:1534-1655
spritesheet_ref deadlock (self or cyclic) Pending-queue guard in prewarm Sheets without resolved refs remain unprocessed; surfaces via the toast above html_processing.py:428
Gemini Vision miscount (fewer labels than declared assets) ImageBuddy returns the sheet with fewer slices than requested Missing <img>s will 404 at browser; broken-image icon — no specific UI signal ImageBuddy side (not in this repo)
Prewarm timeout (long wait) AI turn blocks on prewarm User sees main Send/Swipe button spinner for the combined duration (AI + prewarm) core_ai.py:1050
Dynamic URL interpolation by the AI (${name} style) Post-processor cannot rewrite; URL reaches browser with raw asset_sheet_id Broken image (server sees unhashed id) AI rule: core_ai.py:82-86, 117-125

§7 Doc vs code cross-check

The existing doc at docs/MULTI_ASSET_SHEET_CURRENT_STATE.md matches code for phases 1-7 as verified. One section is explicitly labelled unimplemented and is confirmed absent:

"Auto-splitting large asset lists" (doc §at line 488-511) is marked "Not yet implemented". Verification: no such logic exists in html_processing.py. The doc is accurate about its own gaps — a rare and welcome property.

§8 Verification pointers