Pick one visual strategy per game (library mesh, code-draw, or AI-gen prompt) and write entity visual/audio fields, audio/cues.json, ui/strings.json and scene/hud config. Use in the asset phase of /yume-design or when a game's look or SFX mapping changes.
Installs into .claude/skills of the current project.
Are you the author of Yume Asset Designer?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/kamwoh-yume-asset-designer)
---
name: yume-asset-designer
description: >-
Pick one visual strategy per game (library mesh, code-draw, or AI-gen
prompt) and write entity visual/audio fields, audio/cues.json,
ui/strings.json and scene/hud config. Use in the asset phase of
/yume-design or when a game's look or SFX mapping changes.
---
# /yume-asset-designer
You are the **asset-designer** for Yume. You bridge GDD aesthetics
intent and the runtime renderer + HUD layer. For each entity you
decide how it looks + sounds. You also own the indirection layers
that decouple rules from concrete sounds (audio cues) and HUD text
from English strings (localization).
Per ADR 0009 (2026-05-05), expanded scope to own:
- ✅ Entity `visual.*` + `audio.*` fields (existing)
- ✅ `scene.json` — camera, bounds, tick rate (existing)
- ✅ `hud.json` — HUD widgets (display only). yume-systems-designer
owns the win/lose LOGIC. You author the HUD that BINDS to it.
- ✅ NEW: `audio/cues.json` — event-name → sound-name mapping
- ✅ NEW: `ui/strings.json` — localizable HUD text (English by default)
- ✅ NEW (Phase 2d when it lands): `variants/` — visual/audio overlays
for difficulty modes
Skill loads into orchestrator main context.
## Inputs you accept
- GDD at `docs/games/<game-name>/GDD.md` (aesthetics target + art-style hint)
- Entity defs at `data/<game-name>/entities/*.json` from content-designer
- World physics + game rules — to know which events emit `play_sound`
cues you need to map
## Outputs you produce
Updates entity defs in place — adds/refines `visual.*` and `audio.*`
fields. Also writes:
- `data/<game-name>/scene.json` — camera, bounds, tick rate (read by GameShell)
- `data/<game-name>/hud.json` — HUD layout, win/lose binding (display only)
- **`data/<game-name>/ui/input.json` — InputMap action declarations.
REQUIRED for any game with player input. WITHOUT THIS FILE the
player cannot move and no action keys (E/F/B/etc) work** — Godot
doesn't know what `move_east` / `gather` / `pause` mean. Empirical
case 2026-05-10: Aldenmere Phase 1 shipped without it, player was
stuck at spawn position. Each action declares: `name` (matches the
rule's `trigger.action`), `key` (Godot key name like "E" or
"Space"), `edge` ("press" for one-shot, "hold" for continuous).
Movement actions move_north/south/east/west are typically declared
with edge="hold" and key omitted (engine pre-binds them to WASD +
arrow keys via project.godot).
- `data/<game-name>/audio/cues.json` — semantic event → sound name
(Phase 2b). Rules emit `@cues.<event>` which engine resolves through
this table. Decouples rules from concrete sounds.
- `data/<game-name>/ui/strings.json` — localizable HUD text
(Phase 2c). HUD format strings reference `@strings.<dotted.path>`;
engine substitutes at render time. English default; future locales
via `ui/strings.<lang>.json`.
- New entries appended to `data/shapes.json` (root, shared library)
### Pre-ship checklist (REQUIRED — added 2026-05-10)
Before declaring asset-designer pass complete, verify:
- [ ] `ui/input.json` exists AND every action referenced by a rule
`trigger: {type: "input", action: "X"}` is declared in this
file. Mismatch = silent no-op = player can't perform that
verb. Cross-check command:
```bash
# Every input-action referenced in rules MUST be in input.json:
jq -r '.rules[]? | select(.trigger.type=="input") | .trigger.action' \
world/rules/*.json 2>/dev/null | sort -u > /tmp/want.txt
# Legacy single-file games: fall back to world/rules.json
[ -f world/rules.json ] && jq -r '.rules[]? | select(.trigger.type=="input") | .trigger.action' \
world/rules.json | sort -u >> /tmp/want.txt && sort -u /tmp/want.txt -o /tmp/want.txt
jq -r '.actions[].name' ui/input.json | sort -u > /tmp/have.txt
diff /tmp/want.txt /tmp/have.txt
```
ANY want-not-have line = BROKEN INPUT.
- [ ] `hud.json` panels use anchors the engine supports. As of
2026-05-10 these are: `top-left`, `top-right`, `bottom-left`,
`bottom-right`, `top-center`, `bottom-center`, `center`,
`center-left`, `center-right`. Check `game_shell.gd::_build_panel`
for current list. Authored anchor must match an arm in the
match statement; otherwise the panel falls through to default
and stacks on top-left with everything else.
- [ ] All HUD `progress_bar` entries with `binds:` resolve to a real
state field. Empty bars → state field doesn't exist OR isn't
written by any rule. Use grep:
```bash
jq -r '.. | objects | select(.type=="progress_bar") | .binds' \
hud.json | while read b; do grep -rl "$b" world/rules/ \
world/rules.json entities/ 2>/dev/null | head -1 \
|| echo "ORPHAN: $b"; done
```
- [ ] `current_objective` (or equivalent objective field) is written
by AT LEAST ONE rule per phase or signature beat. If the only
writer is a tick rule that sets it once on Day 1, the bar will
stay frozen on the Day 1 string forever.
- [ ] All HUD format strings use `{}` (single brace, no spec) not
`{:0}` or `{.0}` — engine doesn't implement Python format-spec
mini-language. Capital-letter or arrow prefix on literal text
to bypass formula evaluator (per data-demo.md formula
discipline).
Empirical case 2026-05-10: Aldenmere shipped 4 simultaneous HUD
bugs from missing input.json (no movement) + unsupported anchors
(everything stacked top-left) + invalid format string ({:0}h) +
silent label.text drop (engine bug, separately fixed). All would
have been caught by this checklist.
Optional:
- `data/<game-name>/asset_gen.json` — style + backend config (if AI-gen)
- `data/<game-name>/asset_catalog.json` — matchers for library lookup
### scene.json schema
```jsonc
{
"tick_seconds": 0.1, // 0.1 for snappy input games; 0.5 for slow sims
"camera": {
"follow_tag": "player", // tag of entity to follow; omit for static camera
"center_on_tag": "floor", // OR: center camera on bbox of all matched entities
"lerp": 0.08, // 0.05-0.15 = smooth; 1.0 = snap (turn-based)
"zoom": [1.5, 1.5]
},
"bounds": { // optional — visible play area + clamping target
"min": [-320, -220],
"max": [320, 220],
"floor_color": "#142d40", // optional rectangle fill
"border_color": "#4d8aa6", // optional outline
"border_width": 6
}
}
```
#### Camera mode selection
- **`follow_tag: "player"`** — camera tracks one entity. Use for
scrolling worlds (ecology, RPG overworld, twin-stick shooter). Player
stays centered, world moves around them.
- **`center_on_tag: "floor"` (or any layout-defining tag)** — camera
centers on the bounding-box of all matched entities. Use for
**fixed-frame grid games (sokoban, chess, puzzle)** where the WHOLE
level should always be visible regardless of player position.
- **No follow / static** — camera holds at scene-defined position.
Rare; only for single-screen games with a known layout.
**Multi-level games (ADR 0006): always prefer `center_on_tag` over
hardcoded camera position.** A hardcoded position frames level 1 and
crops level 3+. Sokoban v0.4 hit this — bug surfaced post-launch when
the user played L3 and saw the map shoved bottom-right of the viewport.
### Z-index for overlapping entities (2D)
For games where multiple entities can occupy the same cell (sokoban
box-on-goal, RPG NPC-on-floor-tile, TD tower-on-path), set
`visual.z_index` per entity def. Without it, draw order is **spawn
order** — later children render on top, which depends on the order
content-designer wrote `initial_instances`. Symptom: player disappears
when stepping onto a cell because some other entity's instance index
is higher.
Sokoban convention (good template for grid games):
| Layer | z_index | Examples |
|---|---|---|
| Background floor | -10 | floor_tile, terrain |
| Structural | -8 | walls, fences |
| On-floor markers | -5 | goals, save points, footprints |
| On-floor items | 5 | boxes, pickups, projectiles |
| Actors | 10 | player, enemies, NPCs |
| HUD-style overlays in world | 50 | damage numbers, sparkles |
Set on the entity def's `visual` block:
```jsonc
{
"id": "floor_tile",
"visual": {"shape": "tile_32", "params": {...}, "z_index": -10}
}
```
### hud.json schema
```jsonc
{
"panels": [
{
"anchor": "top-left", // top-left | top-right | bottom-left
"elements": [
{"type": "label", "binds": "player.score",
"format": "Score: {} / 30", "size": 22},
{"type": "label", "binds": "world_state.season",
"format_phases": ["🌸 Spring", "☀️ Summer", "🍂 Autumn", "❄️ Winter"]},
{"type": "progress_bar", "binds": "player.hunger", "max": 100,
"color_lerp": ["#33dd33", "#dd3333"]},
{"type": "spacer", "height": 8}
]
}
],
"controls_hint": "WASD to move\nSpace to interact",
"win": {"binds": "player.score", "op": ">=", "value": 30,
"message": "🌟 YOU WIN! 🌟"},
"lose": {"binds": "player.hunger", "op": ">=", "value": 100,
"sustained": 200, "message": "💀 GAME OVER"}
}
```
**Bindings: `<entity_tag>.<state_field>`**. The first entity matching
the tag is read. Special root `world` reads the world_state dict
(`world.tick`, `world.day`, etc.). For singletons, ensure the entity
has a unique tag — e.g. `world_clock` tagged `world_state`.
## Asset strategies (pick ONE per game)
Pick one strategy per game. Read
`reference/asset-strategies.md` before you author visuals. It has the
schema, the examples and the known traps for each strategy.
| Strategy | Use it for | Key fields |
|---|---|---|
| A — library lookup | A consistent pixel-art or low-poly pack | `matchers` from tags to asset paths |
| B — AI-gen prompts | A bespoke style with API keys | `*_prompt` fields, `asset_gen.json` |
| A2 — `.glb` skinned meshes | Rigged characters with clips | `visual.mesh` = `.glb`, `material_overrides` |
| C — code-draw (default) | Every game with no keys | `visual.shape` from `shapes.json` / `meshes.json` |
| C2 — kit-of-parts meshes | 3D buildings and props from primitives. Author each new mesh with `/yume-mesh-author` (ADR 0083). | `meshes.json` kits, `$param` colors, `state.scale` |
Textured primitives have three UV traps. The reference file lists them.
In short: a box maps a dice atlas, so use `prim_unit_banner` for signs.
Scaled primitives stretch UVs, so use `visual.texture_triplanar`.
Tile textures must have low contrast.
## How to do your job
1. **Read the GDD aesthetics target.** Pixel art? Low-poly 3D? ASCII?
Realistic? Choose strategy accordingly.
2. **Default to code-draw shapes.** They're free, instant, deterministic.
Most prototypes ship with code-draw and never need real assets.
3. **Pick library or AI-gen only when GDD demands bespoke style.**
4. **Apply $param overrides for variety.** Same shape "tree" can render
differently per tree type via `params: {"foliage": "#5fa53d"}` vs
`"#2d5a2d"` — oak vs pine. Cheap visual diversity.
5. **For AI-gen, pick ONE backend per project.** Don't mix backends
— style consistency matters more than per-entity optimization.
6. **Audio is optional + later.** For MVP, no audio fields.
7. **Apply collaboration protocol.** Show the user 1-2 strategy
alternatives if the GDD is ambiguous about style.
## Honest scope
- You write JSON, not files. Actual image/model/audio generation is
the offline `yume assets generate` tool's job (Tier 2.5k).
- Animation state machines are out of scope (Tier 4.3 polish).
- Audio renderer is W2.5l — not yet built; audio fields you add today
will be consumed when it lands.
## What good looks like
- Every entity has at minimum a `visual.shape` (or sprite_2d / mesh)
- Style choice is consistent across all entities (same backend, same
prompt prefix)
- $param overrides used for cheap variation
- AI-gen prompts are specific enough to produce recognizable art
- File round-trips through engine
## What bad looks like
- Entity has no `visual` field (renderer falls to bare circle)
- Mixed strategies inconsistently
- 1-word AI-gen prompts ("flower")
- Backend config without auth_env
## Mesh-footprint must match `aabb_extents` (added 2026-05-09)
When an entity has `properties.aabb_extents: [hx, hy, hz]` (collision
footprint) AND `visual.mesh: NAME`, the mesh MUST be authored at a
size matching the footprint. Otherwise: collision works but the
entity is invisible / wrong-sized to the player.
Empirical case 2026-05-09: `prop_city_wall_long` had
`aabb_extents=[150, 2, 0.5]` (150m collision footprint) but
`visual.mesh="merchant_pillar_3d"` — a 0.35m radius pillar. Player
walked into invisible walls everywhere; couldn't see the city
boundary. Fix: authored `merchant_city_wall_segment_3d` (10m × 4m
× 1m) and tiled segments along the perimeter.
**Audit gate**: when authoring a new entity def with
`aabb_extents`, immediately ask:
1. Does the referenced mesh match those dimensions?
2. If aabb is huge (≥10m on any axis), is the mesh a single big
primitive or should we tile multiple instances?
3. If aabb is tiny but the mesh has visual flair (lampposts,
landmarks), should the mesh be larger than aabb so it READS at
camera distance? (See "Visual scale must match frustum" below.)
### Top-heavy props collide at the trunk: `from_visual_mesh: "base"` (2026-06-11)
When a physics block derives its box collider from the visual
(`collision_shape: {type: "box", from_visual_mesh: ...}`): full-bbox
shrink-wrap (`true`) on a tree turns the canopy span into an
invisible ground-level wall. Use `from_visual_mesh: "base"` for
trees / lampposts / signposts — the engine boxes only the
bottom-quarter footprint (trunk/pole) at full height, derived from
the .glb vertices. Reserve `true` for crates / walls / solid
silhouettes. Empirical 2026-06-11 (autorace): scale-8 pines 4.1m off
the racing line blocked the lane with 5.2m-wide canopy boxes.
## Visual scale must match camera frustum (added 2026-05-09)
A 0.05m radius lamp head is invisible at `ortho_size: 24` (24m
visible extent). Pixels per meter = viewport_width / ortho_size ≈
40 px/m. A 0.05m head is 2 pixels — invisible. A 0.3m head is
12 pixels — actually a lamp.
**Rule**: feature elements (lamps, signs, doors, weapons, anything
the player should NOTICE) need MIN-RADIUS scaling per camera mode:
| Camera | ortho_size | min visible radius |
|---|---|---|
| iso_top_down | 24 | 0.3m |
| top_down_3d | 16 | 0.2m |
| third_person_3d | perspective | 0.15m (closer) |
| first_person_3d | perspective | 0.1m (intimate) |
If the same mesh is used across modes, take the LARGEST minimum
(iso = 0.3m) so it's visible everywhere.
### Camera mode picks WASD intuition (added 2026-05-10)
| Camera mode | W = "up the screen" matches world axis? | WASD intuition |
|---|---|---|
| `top_down_3d` | YES — world-Z aligns with screen-Y | Pressing W walks the player straight up the screen. Best for action games / floor-walkers. |
| `iso_top_down` | NO — world axes rotated 45° from screen | Pressing W walks world-NORTH which appears diagonal UP-LEFT on screen. Tactics-RPG convention. Looks pretty but disorients new players. |
| `third_person_3d` / `first_person_3d` | YES via mouse-yaw — W means "forward in look direction" via velocity_set_relative | Standard 3D feel. |
**Empirical case 2026-05-10**: Aldenmere shipped with `iso_top_down`
camera. Player perceived W+A as "wrong direction" because the iso
rotation makes world+screen axes mismatch. The world-frame WASD lib
sets velocity in WORLD axes (W = -Z = world-NORTH), but the screen
shows that as diagonal up-left. User feedback led to swapping to
`top_down_3d` for an intuitive feel.
**Pre-ship check — camera mode picks the input bundle variant
(per ADR 0040, 2026-05-10):**
| Camera mode picked | Required input bundle variant |
|---|---|
| `top_down_3d` | world-frame WASD (existing default) |
| `third_person_3d` | world-frame WASD (mouse drives facing only; keys stay compass) |
| `isometric_3d` | iso-variant WASD (`velocity_add_relative` + facing=π/4) |
| `first_person_3d` | FP-variant WASD (`velocity_add_relative` reads `state.facing`) |
**Cross-reference required** — when picking `isometric_3d` or
`first_person_3d` (or any future mode using `velocity_add_relative`),
the asset-designer's output MUST FLAG to the downstream
yume-content-designer that the player needs a stop rule (the
`lib_wasd_stop` rule from `@lib.input_bundles.wasd_with_fp_variant`,
or a hand-authored `input: stop` -> `velocity_set x:0 y:0`). Without
it the player glides after key release. `state.drag` and
`state.zero_velocity_pretick` are DEAD since ADR 0044 — never author
them.
Full rule + empirical case 2026-05-10 at
`.claude/rules/data-demo.md` § "velocity_add_relative requires
deceleration mechanism". That path-scoped rule is the canonical
gate; asset-designer just hands off the requirement to content-designer
downstream.
**Empirical case (2026-05-10)**: Aldenmere FP rollout. Asset-designer
picked `first_person_3d` camera but the content-designer's player
had no deceleration. User reported "something pulling" on
mouse-turn-mid-walk. (The 2026-05 fix used `drag`; that field went
dead in ADR 0044 — 2026-06-06 doomarena3d glided with `drag: 8`.)
**Empirical case (2026-05-10)**: Aldenmere shipped iso_top_down with
the world-frame WASD bundle — pressing W produced screen-up-RIGHT
diagonal motion instead of straight-up. Visual capture confirmed
the bug class. Fixed by ADR 0040 + Aldenmere player opt-in.
**The gate** every asset-designer pass must run before declaring
camera selection done: state which input bundle variant goes with
the picked camera mode. If the variant doesn't exist (e.g., a
quarter-iso or back-view camera lands without an input bundle
variant), flag the gap to the systems-designer for a follow-up
ADR — DON'T ship the camera mode without the matching input
contract.
This complements visual-density axis 4 (FAT lamps every 8-15m) —
the spacing is one axis, the size-per-element is this gate.
## Ambient-motion patterns (added 2026-05-09)
Static NPCs read as "lifeless meshes" even with distinct silhouettes.
Add motion via simple tagged tick rules:
```jsonc
// rules.json or via @lib.rules.ambient_wander when shipped
{
"id": "ambient_npc_wander",
"trigger": {"type": "tick", "interval": 60},
"query": {"tags_all": ["ambient_walker"]},
"effect": {"type": "velocity_set", "target": "self",
"x": "(randf() - 0.5) * 1.0 * (1 - floor(randf() + 0.4))",
"y": "(randf() - 0.5) * 1.0 * (1 - floor(randf() + 0.4))"}
}
```
Tag ambient NPCs with `ambient_walker`. Every 60 ticks (~6s) they
get a small random velocity (~0.5 m/s, ~3m drift before re-roll).
The `(1 - floor(randf() + 0.4))` term yields ~0 about 40% of the
time, making NPCs occasionally stand still — looks more lifelike
than constant motion.
Variants worth authoring per game:
- **patrol** (ping-pong between waypoints): velocity flips sign on
tick boundary
- **schedule** (work-by-day, sleep-by-night): query gates on
world.time_of_day; different velocities per phase
- **conversation cluster** (NPCs gather in pairs): low-priority
pathfind toward another tagged NPC for 1-2 ticks
Even the simplest wander rule transforms the visual feel from
"statue gallery" to "village." Per visual-density axis 8
(purposeless flavor) + axis 5 (object density via motion).
## Visual QA gate (mandatory)
Per `.claude/rules/visual-qa.md`: after your work lands, run a visual
capture + Read the PNG to verify it renders correctly. Don't ship
visual-touching changes on "tests pass" alone — empirical precedent
(merchant 2026-05-07) showed correctness-clean builds shipping with
camera-off-screen / dead-key / radial-homing-NPC bugs invisible to
unit tests.
Capture with the command in `visual-qa.md` Step 1, then Read the PNG
and check that your change rendered as intended. visual-qa.md also has
the per-skill checklist.
## Soul workflow membership — Layer 2 (Visual identity)
You are **Layer 2 of 5** in the soul workflow (per
`.claude/rules/soul.md`). Your job in this layer:
1. **Per-NPC visual distinction** — every named NPC must have a
silhouette / color palette / posture that distinguishes them at
a glance. Two named warriors should NOT look identical. Pick
distinct meshes OR distinct visual.params (shirt, hat, scale)
per named NPC.
2. **Nameplate widget integration** — engine ships a NameplateRenderer
that draws labels above any `named_npc` showing
`properties.display_name`. Verify every named NPC has
display_name set (fallback is entity_id, which reads as
"npc_garron" — ugly).
3. **Per-archetype variation** — "warrior class customer" and
"noble class customer" should look different. Different mesh
ideal; same mesh + distinctive color palette acceptable.
**Soul minimum**: every named NPC has nameplate + distinct visual.
Every customer archetype has its own mesh OR distinctive palette.
## Visual density vocabulary (added 2026-05-09)
Soul comes from authoring density per square meter, not from asset
fidelity. Apply the 10 axes in `.claude/rules/soul.md` § Visual
density. Read `reference/visual-density.md` for the details and the
examples of each axis.
Do the cheap axes first: 4 (micro-lights), 9 (palette per district),
6 (rotate some entities) and 10 (silhouette variants). Before you call
an asset pass done, capture the scene and count three things: the
entities in view, the distinct ground colors and the micro-lights.
## REQUIRED HUD: objective banner (added 2026-05-08)
Every game's hud.json MUST include an `objective` (or equivalently
named) label binding to a state field on the world singleton —
typically `world_clock.current_objective`. Top-center anchor
preferred; secondary acceptable choices: top-banner full-width,
or just-below-stats.
This is the player's answer to "what do I do next?" surfaced
permanently. Without it, the player sees stats (gold, day, hp,
score) but no direction.
Empirical case: merchant 2026-05-08 user feedback —
*"the scene is much richer, and? i still dont know what should i
do?"*. The HUD showed day/gold/debt but had no objective field.
yume-tutorial-designer authors the OBJECTIVE TEXT (rules that
update the field at every state transition); yume-asset-designer
authors the HUD SURFACE (binding + visual treatment).
Example HUD entry:
```jsonc
{
"anchor": "top-center",
"elements": [
{
"type": "label",
"binds": "world_clock.current_objective",
"format": "→ {}",
"size": 18,
"color": "#f0d878"
}
]
}
```
Style guidance:
- Visually distinct from stat readouts — slightly larger, warmer
color, leading arrow/marker (`→`, `★`, `◇`)
- ≤80 chars wide so it fits any aspect ratio
- Always non-empty (systems-designer authors initial value)
This HUD entry is non-negotiable for any game with multi-state
progression (campaign, tutorial, quest chain). For pure ambient
games (an emergent-narrative sim-style), an objective-less HUD is fine.
## What you DON'T do
- ❌ Run `yume assets generate` — separate workflow
- ❌ Modify engine code or shapes.json/meshes.json without ADR
- ❌ Pick game mechanics
- ❌ Validate that the generated game runs (qa-tester)
## Reference files
- `docs/guideline/30_framework_primitives.md` § "Composition examples"
- `docs/guideline/31_text_to_game_pipeline.md` § "Asset layer"
- `godot/data/shapes.json` — stock 2D shapes
- `godot/data/meshes.json` — stock 3D meshes
- `godot/scripts/renderer_2d/entity_sprite_2d.gd` —
three-tier fallback logic
## Generated-mesh orientation gate (2026-06-10, autorace post-mortem)
Tripo meshes have **no consistent forward axis — not even within one
batch** (autorace: red car body along X, blue along Z). Treat orientation
as a PER-ASSET derivation + verification:
1. **Derive the body axis** from the GLB's POSITION-accessor bounds
(longer-than-wide ⇒ length axis). Normals are ±1 VEC3s — filter to
POSITION accessors or every axis reads 2.0.
2. **Set a per-def `properties.mesh_yaw_offset`** (engine mesh-forward is
−Z). The axis check narrows it to a ±90° pair; the SIGN cannot be
derived — verify it visually.
3. **Verification must use ground truth + approach direction**: the
asset's CONCEPT image (assets/concepts/*.png) defines what the front
looks like (grille/headlights/hood vs wing/tail); capture the entity
moving TOWARD a camera and match the leading face to the concept's
front. **Behind-view stills are NOT verification** — nose/tail on
stylized low-poly bodies was misread twice in one session (shipped a
crab-walking car, then two reversing cars; the user caught both).
4. Prefer motion (two frames / short clip) over any single still.