WowCube Asset Builder
This skill is Stage 3 of the
cube_orchestratorpipeline. The orchestrator invokes it (via the Skill tool) when the asset manifest exists butassets/packed//src/app_<game>_ids.hdo not. It is not a user-facing entry point — the user enters throughcube_orchestrator, which routes here.
Drive the asset pipeline from a structured manifest. This skill never writes
game code and never creates prompts — it exists solely to turn a validated
<game>_assets.json into a runnable set of packed sprites and sound files that
cube_orchestrator's coder agents can reference. The packed assets are the
Stage 3 artifact; cube_orchestrator checkpoints them with the user and then
runs Stage 4 (implementation).
Sprites are generated by AI: each sprite's gen_prompt (written by
technical_prompter, Step 4a) is sent to the OpenRouter image model via
scripts/genimg.py, then resized to the manifest's exact size. Sounds are
deterministic synthesised placeholders written as mono PCM16 WAV (no
external encoder), then — when ffmpeg is on PATH — additionally encoded to
the exact format the beta engine decodes: 22050 Hz mono CBR 32k mp3, no
Xing header, no metadata. The pack stage copies those mp3s into
app_<game>/sound/assets/ and records one KIND_SOUND entry per mp3 in
index.bin. AI sprite output is not reproducible across runs; sounds
(both the WAV and the mp3 encode) are byte-identical across runs.
Sizes are authored (pre-upscale) pixels — ≤ 120×120. The engine upscales every sprite ×2 at draw time, so the manifest
sizeis HALF the on-screen size (full screen =[120, 120], never[240, 240]). This skill generates and resizes to whateversizethe manifest carries — it does not invent sizes — so a correct manifest fromtechnical_prompteralready encodes this. If you ever see a spritesize> 120 (or a size that reads like on-screen pixels), it is a manifest sizing error: delegate it back totechnical_prompterrather than packing it. The one exception is sprites withflags.fullsizeset — any color (see the tier table below) — those are authored at native resolution up to 240×240.
colorpicks the sprite's encoding —"palette"(default) or"full"— and together withflags.fullsizeselects one of three art tiers:
Tier Manifest size(authored)Colors / transparency Drawn at Use for Palette (default) color: "palette"≤ [120, 120](= on-screen ÷ 2)shared quantized palette, index 0 transparent ×2 upscale characters, icons, HUD — anything needing transparency Palette fullsize color: "palette"+flags.fullsizenative, up to [240, 240]shared palette, transparency KEPT 1:1 native-resolution art that still needs alpha Full-color fullsize color: "full"+flags.fullsizenative, up to [240, 240]RGB565, 2 bytes/texel, no transparency 1:1 opaque backgrounds, tiles, photographic full-screen art
flags.alphais forbidden on"full"sprites (0x0000 is opaque black, not transparent); palette sprites keep index-0 transparency in both tiers. A"full"sprite withoutflags.fullsizestill draws at ×2 and is capped at 120, same as a default palette sprite. Full-color sprites also acceptdither: true— Floyd–Steinberg dithering to the RGB565 lattice, applied during conversion, before the lossless RLE encode. Recommended for photographic art and smooth gradients (kills banding); pointless for flat-color art, and it costs a somewhat larger RLE payload (dither noise breaks up runs). Prefer the cheapest tier that does the job (the table is ordered cheapest-first): there is no hard packer-side cap on pack size — only the cube's flash software region (contiguous free 512 KB cells) bounds it — but lean packs are good practice.The canonical MAXIMUM-QUALITY recipe (no compromises) is exactly:
color: "full"+flags.fullsize+dither: true. The pipeline applies nearest-level RGB565 rounding + Floyd–Steinberg dithering, then the plain lossless RLE — and nothing else. No smoothing, no pre-filtering, no lossy "optimizations" of any kind: the only loss between the source PNG and the cube's screen is the display's own 16-bit format. When the user asks for "maximum quality", this recipe IS the answer — never add smoothing (smooth_rows-style texel snapping exists inpack_beta.pystrictly as an opt-in size tool for noisy video sources and is NEVER applied by default or in the name of quality), never trade fidelity for pack size unless the user explicitly asks to shrink the pack.Why this is absolute: a smoothed/degraded full-color sprite looks about the same as a 256-color palette fullsize sprite — at twice the bytes. Degrading tier 3 collapses it into tier 2 and makes it pointless. So the three tiers are really three commitments: fast (palette + ×2 upscale — cheapest to render and store), mid (palette fullsize — native sharpness, 1 byte-class payload, keeps alpha), fat (full-color fullsize — uncompromised color, 2 bytes/texel). Pick by the art's needs; once picked, deliver the tier's full promise.
Core principle: every asset name that appears in a prompt must exist as a
file after this skill runs. The manifest is the contract. No placeholder text
like "requires asset: X" is ever produced here; either the asset is in the
manifest and gets generated, or the user is asked to fix the manifest. Every
sprite MUST carry a non-empty gen_prompt — generation fails loudly otherwise.
When to Use
cube_orchestratorrouted here because the manifest exists butassets/packed/orsrc/app_<game>_ids.his missing.- Resuming a partially-completed asset build after a user requested regeneration.
Do not trigger this skill directly for "generate assets" / "build assets"
requests — those are owned by cube_orchestrator, which routes here as Stage 3.
When NOT to Use
- No manifest exists → the orchestrator will route to Stage 2 (
technical_prompter). - No GDD exists → the orchestrator will route to Stage 1 (
cube_game-designer). - The user wants to modify game code → this skill does not touch
src/app_<game>.h.
Prerequisites
| File | Source | Required |
|---|---|---|
plans/<game>/<game>_assets.json OR plans/<game>_assets.json |
technical_prompter |
Yes |
A non-empty gen_prompt on every sprite in the manifest |
technical_prompter (Step 4a) |
Yes |
build_psd.py (at repo root, its scripts/, or next to scripts/) — internal: the pack stage uses it to atlas the sprite PNGs into a throwaway assets.psd; no hand-authored PSD is ever needed |
Project | Yes |
pack.py (at repo root, its scripts/, or next to scripts/) |
Project | Yes |
Pillow, numpy, requests, pytoshop, psd-tools |
pip install |
Yes |
OPENROUTER_API_KEY environment variable |
User (OpenRouter account) | Yes (generate stage) |
ffmpeg on PATH |
Windows: winget install Gyan.FFmpeg. Linux: distro package. |
Yes (beta mp3 encode) |
If any prerequisite is missing, print the gap and stop. Do not attempt to
generate partial output. The AI sprite generation needs network access and a
valid OPENROUTER_API_KEY; the key is read from the environment and never
hardcoded. ffmpeg is only needed to encode the synthesised WAVs down to the
beta mp3 format — without it the generate stage still writes the WAVs, it
just skips the mp3 step (pass --mp3 to make a missing ffmpeg a hard error
instead); a missing ffmpeg can also be worked around by dropping
pre-encoded mp3 files straight into sound/assets/.
Workflow
Step 1: Locate the manifest
Look in this order:
plans/<game>/<game>_assets.jsonplans/<game>_assets.json
If neither exists, stop and return control to cube_orchestrator — the manifest is a Stage 2 output, so the orchestrator will route to Stage 2 (technical_prompter) to produce it.
Step 2: Run the generate stage
Invoke the pipeline driver:
python OCT_wowcube-agent-skills/scripts/build_pipeline.py \
generate --manifest <manifest-path> --workspace assets
The generate stage AI-generates a PNG for every sprite (from its gen_prompt,
resized to the manifest size) into assets/art/, and synthesises a WAV for
every sound into assets/wav/.
Exit codes:
0— success.2— manifest invalid (errors printed to stderr; relay verbatim to user).3— missing dependency (Pillow / numpy / requests / pytoshop / psd-tools) or missingOPENROUTER_API_KEY(message names which).6— sprite generation failed (OpenRouter/network error, or a sprite has nogen_prompt); relay stderr to the user.- other — unexpected; surface stderr to user.
On success, the driver prints a summary: PNG count, WAV count, group names.
Step 3: User checkpoint — MANDATORY
STOP. Do NOT proceed to the pack stage. Print the summary and wait for user input. Offer these options verbatim:
Generated assets are in
assets/art/*.pngandassets/wav/*.wav. Review visually and by ear, then reply:
okorcontinue— pack and hand off to orchestrator.regen <group>— regenerate a single group (others untouched). For sprites this re-runs AI generation, so the art will differ from the previous attempt even with the same prompt.swap <name>— drop a PNG or WAV intoassets/art/orassets/wav/with that name, then replyok.edit <name> prompt <text>— edit the sprite'sgen_promptin the manifest, then I'll regenerate that sprite.edit <name> size <WxH>— edit the manifest, then I'll regenerate that sprite. (Sizes are authored, pre-upscale pixels — keepWandH≤ 120; full screen is120x120, since the engine upscales ×2 on draw.)
Wait for an explicit reply. Never auto-continue.
For regen <group> — re-run the generate command with --group <name>.
For edit <name> size <WxH> or edit <name> prompt <text>:
- Load the manifest JSON, find the entry, update
size(orgen_prompt). - Save the manifest.
- Run
build_pipeline.py generate --manifest <manifest> --workspace assets --group <group>(where<group>is the sprite's group or derived group). - Return to Step 3.
Step 4: Run the pack stage
After the user replies ok:
python OCT_wowcube-agent-skills/scripts/build_pipeline.py \
pack --game <game> --workspace assets --src-dir src \
--app-dir <workspace>/app_<game> --manifest <manifest-path>
--app-dir and --manifest are what make this the beta pack: without
--app-dir you only get the legacy assets/packed/*.png + pal.png
intermediates, not the container the simulator/.oct actually load.
--manifest is what lets color: "full" sprites be RAW565-encoded into the
container instead of run through the palette codec, and what carries each
palette sprite's manifest flags (fullsize/additive/bg) into its packed
header. Under the hood this
drives scripts/pack.py --export --build-palette --build-ids --emit-raw --beta-app-dir <app-dir> --app-name app_<game> --manifest <manifest-path>.
Full-color-only apps can skip the palette pipeline entirely. When every
sprite in the manifest is color: "full" (e.g. a photo/video app packing
pre-made PNGs, nothing PSD-authored to export or palette-encode), drop
--export and --build-palette from the pack.py invocation — the legacy
export/palette phases self-skip when there's nothing for them to do, and only
the beta container gets emitted:
python scripts/pack.py --build-ids \
--beta-app-dir <app-dir> --app-name <app-name> \
--manifest <manifest-path> --icon <icon.png>
--build-ids is still required (it's what lets the no-palette legacy phase
exit cleanly instead of erroring on a missing pal.png); the pre-made sprite
PNGs must already sit in --exported-dir (default exported/) since there's
no --export step to populate it.
Palette sprites from PNGs need NO hand-authored PSD — and no PSD knowledge
at all. The pack stage reads one <name>.png per manifest sprite from
<workspace>/art/ (default assets/art/ — the same folder the generate
stage writes into; PNGs made or edited outside the generate stage go there
too: RGBA, transparent background, sized to the manifest size). The driver
then atlases those PNGs into a throwaway assets.psd internally
(build_psd.py — this is the only reason pytoshop is a dependency) and
packs from its export; nothing is ever authored in Photoshop. So the
build_pipeline.py pack command at the top of this step is the complete,
canonical palette invocation — run it from the workspace root (every path it
takes is cwd-relative), copy-pasteable form:
python OCT_wowcube-agent-skills/scripts/build_pipeline.py \
pack --game <game> --workspace assets --src-dir src \
--app-dir app_<game> --manifest plans/<game>_assets.json
The equivalent direct pack.py call — mirroring the full-color-only recipe
above, for when the PNGs already sit in an exported dir — keeps
--build-palette and drops --export:
python scripts/pack.py --build-palette --build-ids \
--exported-dir <png-dir> --packed-dir <out> --output-dir <out> \
--assets assets --ids-output <out>/ids.h \
--beta-app-dir <app-dir> --app-name app_<game> \
--manifest plans/<game>_assets.json --icon <icon.png>
Never add --export to a PNG-only invocation. --export means "rebuild
the exported dir from --art-dir PSD/FNT sources" — it deletes every
pre-placed PNG in --exported-dir first (it warns loudly and the pack then
fails with exit 1, but the PNGs are already gone). Use --export only for
PSD-authored art such as the template's assets.psd.
On success the driver prints:
- Path to
assets/packed/pal.pngand the packed PNGs (legacy intermediates). - Count of
assets/packed/*.raw— the decoded-RGBA asset bitmaps (emitted bypack.py --emit-raw; pure Python, noutils.exe/psd.exe). - Path to
src/app_<game>_ids.hwith the BMP_* constant count. - The beta container written to
--app-dir:index.bin(kind-tagged asset index — record index == asset id),art/packed/*.raw(sprites/maps) +art/packed/*.pal(palettes),sound/assets/*.mp3(copied in from thegeneratestage), and the kind-awaresrc/app_<game>_ids.h(BMP/MAP/SND enums plus animation aliases) written straight into the app.
Step 5: Hand-off
Print:
Asset build complete.
app_<game>/index.binready (sprites, palettes, maps, sounds).app_<game>/art/packed/*.raw+*.palready.app_<game>/sound/assets/*.mp3ready (beta mp3, 22050 Hz mono CBR 32k).src/app_<game>_ids.hready with N BMP_*/MAP_*/SND_* constants.
Then return control to cube_orchestrator. Do NOT start implementation
yourself. The orchestrator will run the Stage 3→4 boundary checkpoint with the
user and begin the implementation workflow (coder/verifier/fixer subagents) when
approved.
Constraints
- Sprites are AI-generated, sounds are deterministic. Sprite PNGs come from
the OpenRouter image model (
scripts/genimg.py) and differ run-to-run; sound WAVs are pure synthesis and byte-identical across runs, and so is their mp3 encode. Sounds never ship as WAV — the WAV is an intermediate thegeneratestage encodes (viaffmpeg) to the beta mp3 the engine actually decodes (22050 Hz mono CBR 32k); only the mp3 is copied intosound/assets/and indexed inindex.bin. Either way the user must approve before packing. Never silently replace a file the user provided by hand unless they explicitly saidregen. - Per-sprite
gen_promptis the source of the art.technical_prompter(Step 4a) writes a ready-to-run image-generation prompt into every sprite entry; thegeneratestage sends it togenimg.pyand resizes the result to the manifestsize. A sprite with nogen_promptaborts generation (rc=6) — do not invent one here; send the user back totechnical_prompter. Sounds have nogen_prompt. OPENROUTER_API_KEYis read from the environment, never hardcoded. If it is unset the generate stage stops at the dependency check (rc=3). Tell the user toexport OPENROUTER_API_KEY=sk-or-...and retry.- Never edit
_ids.hby hand. It is produced bypack.py. - Never edit
src/app_<game>.h. That iscube_orchestrator's territory. - Never invent asset names. Every sprite/sound name comes from the
manifest. If a required asset is missing, stop and ask the user to add
it to the manifest (and run
technical_prompteragain if appropriate). - 2-second sound cap and mono PCM16 WAV are enforced by the generator. Do not tell the user to set different values — the validator will reject out-of-range manifests.
Error Handling
| Symptom | Action |
|---|---|
Manifest invalid (rc=2) |
Relay all stderr lines to the user; wait for them to fix the manifest (or delegate back to technical_prompter). |
Missing dependency or API key (rc=3) |
Print the install hints / export OPENROUTER_API_KEY=... hint printed by the driver; stop. |
Sprite generation failed (rc=6) |
Relay stderr. If it is a network/OpenRouter error, retry (optionally --group for just the failed group). If it names a sprite with no gen_prompt, send the user back to technical_prompter to add it. |
mp3 encode failed (rc=7) |
The mp3 encode is attempted whenever ffmpeg is on PATH — even without --mp3 — and --mp3 additionally forces the encode when ffmpeg is missing (a hard error instead of a skip). Either way, rc=7 means ffmpeg was found but the encode itself errored — relay stderr. Install/repair ffmpeg via winget install Gyan.FFmpeg (Windows) or the distro package (Linux), or drop pre-encoded mp3s straight into sound/assets/ and retry. |
ffmpeg not found (no rc=7, mp3s just silently absent) |
Without --mp3, a missing ffmpeg degrades the generate stage to WAV-only instead of failing. If beta mp3s are needed, install ffmpeg (winget install Gyan.FFmpeg on Windows) or supply pre-encoded mp3 files in sound/assets/ before packing. |
build_psd.py failure during pack |
Show the failing filename from stderr. Ask the user to inspect assets/art/<name>.png. |
pack.py exit 1: no palette groups exist, but N PNG(s) need packing |
The exported dir held no sprites when the palette build ran. Almost always: --export was passed on a plain-PNG workflow and deleted the pre-placed PNGs (the export phase prints a WARNING: --export cleaned ... line when this happens). Restore the PNGs and re-run without --export — or use build_pipeline.py pack with the PNGs in <workspace>/art/, which never needs --export by hand. |
pack.py palette overflow |
Suggest --target-colors 64; the driver currently uses default grouped palette — add the flag if this becomes common. |
Empty _ids.h after pack |
Likely build_psd.py produced an empty PSD. Diagnose by listing assets/exported/ contents. |
index.bin missing after pack |
The pack command omitted --app-dir (which maps to pack.py --beta-app-dir) — only the legacy assets/packed/ intermediates were produced. Re-run build_pipeline.py pack with --app-dir <workspace>/app_<game> (or invoke pack.py directly with --beta-app-dir <app-dir> --app-name app_<game>). |