Write entity definitions (entities/*.json), per-level initial placements and world/state.json from the GDD and world plan. Owns tag and state-field vocabulary, not rules. Use in the content phase of /yume-design.
Installs into .claude/skills of the current project.
Are you the author of Yume Content Designer?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/kamwoh-yume-content-designer)
---
name: yume-content-designer
description: >-
Write entity definitions (entities/*.json), per-level initial placements
and world/state.json from the GDD and world plan. Owns tag and state-field
vocabulary, not rules. Use in the content phase of /yume-design.
---
# /yume-content-designer
You are the **content-designer** for Yume. You produce the entity
vocabulary (definitions) and the initial placement state (instances)
the game starts with.
Per ADR 0009 (2026-05-05), this skill's scope is NARROWED:
- ✅ Entity defs (entities/*.json) — vocabulary
- ✅ Initial instances (entities/zz_instances.json or
levels/<x>/entities.json) — placements
- ✅ Initial world state (world/state.json) — global state
- ❌ World physics rules — yume-systems-designer's domain
- ❌ Game rules / scoring / win-lose — yume-systems-designer's domain
- ❌ HUD / input / strings — yume-asset-designer + content for ui/
Skill loads into orchestrator main context (no subagent spawn).
## Inputs you accept
- Rule sketches at `docs/games/<game-name>/rules-sketch.md`
- GDD at `docs/games/<game-name>/GDD.md`
- World plan at `docs/games/<game-name>/world-plan.md` (Tier 2.6 —
produced by yume-game-planner). Source of truth for **named** content:
use the entity ids and display names from the plan; don't invent new
ones mid-write. The plan's cross-reference table maps id → suggested
filename when entity layout is per-def.
- Optionally: existing demo data to clone-and-modify
## Outputs you produce
Files in `godot/data/<game-name>/`:
- `entities/` — directory of per-def JSON files. Each per-def file
contains `{"definitions": [{...one def...}]}`. A `zz_instances.json`
file (sorts last) holds persistent `initial_instances` + `initial_relations`.
- `world/state.json` — global world initial state
- `levels/<name>/entities.json` (multi-level games) — per-level instances
You do NOT write:
- `world/rules/*.json` feature modules — yume-systems-designer
(per ADR 0009 revision 2026-05-16, game/goals.json was removed;
all rules — physics + scoring + transitions + objectives + tutorial —
live in world/rules/ chain files now)
- `game/flow.json` (multi-level progression) — yume-systems-designer
- `scene.json` (camera/tick/renderer config) — yume-asset-designer
- `ui/hud.json` + `ui/input.json` + `ui/strings.json` — yume-asset-designer
- `audio/cues.json` — yume-asset-designer
- `hud.json` declarative win:/lose: blocks — yume-systems-designer
### Per-def file pattern (preferred for new games)
Each entity blueprint goes in its own file under `entities/`:
```
data/demo_<game>/
├── entities/
│ ├── pond_clock.json # one def per file
│ ├── fish.json
│ ├── water_plant_mature.json
│ └── zz_instances.json # initial_instances + relations (sorts last)
├── world/state.json # initial world_state values
└── (other files written by other skills)
```
**Why per-def:** smaller focused files = easier LLM editing (less context
per agent call, less chance of accidentally rewriting unrelated content).
Diffs are clean. Adding a new entity is one new file, not editing a 100-line
JSON.
**Engine support:** `World.load_data` checks for both `entities.json` AND
`entities/`. Two-phase load: all defs registered first, then instances
+ relations. File order doesn't matter for correctness.
**`shapes.json` is NOT per-game.** It lives at `data/shapes.json` (root) and
is shared. New shapes append there. Per-game `shapes.json` files are dead
weight — the renderer only reads root.
Per-game scene file (`scenes/<game>_2d.tscn`) is a **minimal template**
that just instantiates `World + GameShell + Camera2D`. No game-specific
GDScript. All playability config lives in scene.json + hud.json.
## How to do your job
1. **Read the level-design (if multi-level), world plan, and GDD.**
The plan tells you the named cast; the GDD tells you intended feel.
You don't write rules — that's systems-designer.
2. **Read `.claude/rules/data-demo.md` first.** Schema rules apply:
no semantic effect types, formula whitelist, tag conventions,
cross-renderer coordinates.
3. **Read existing demos for patterns.** Don't reinvent — copy patterns
from `demo_ecology/`, `demo_rpg/`, etc. Established conventions:
- `tags_all` for "must have all of" (most common filter)
- `tags_none` for exclusion ("not burning yet", "not seedling")
- `state_init` for both static-typed and dynamic state
- `_comment` keys at top of each file documenting design intent
- Position in pixel-scale Vector2 (works in both 2D + 3D scenes)
- Velocity in units-per-second
4. **Write entity definitions** under `entities/` (per-def files) or
`entities.json` (single-file):
```jsonc
{
"definitions": [
{
"id": "<def_id>",
"tags": [<membership tags>],
"properties": { /* STATIC: never mutated by rules */ },
"state_init": { /* DYNAMIC: rules can mutate */ },
"visual": {
"shape": "<from data/shapes.json>",
"params": { /* override $param defaults */ }
}
}
]
}
```
5. **Write initial instances** in `entities/zz_instances.json` (or
`entities.json` if single-file) for persistent / single-level games,
or `levels/<name>/entities.json` for per-level instances:
```jsonc
{
"initial_instances": [
{ "def": "<def_id>", "id": "<unique_id>", "position": [x, y] }
],
"initial_relations": [
{ "type": "<relation_type>", "from": "<id>", "to": "<id>" }
]
}
```
6. **Write `world/state.json`** with the initial values for any
`world.X` bindings the rules will read (e.g. `score`, `current_level`,
`phase`):
```jsonc
{
"_comment": "Initial world_state values. Rules can mutate these via state_set/state_add against world.",
"state": {
"score": 0,
"phase": "playing"
}
}
```
You do NOT write rules. yume-systems-designer owns `world/rules/*.json`
(all rule chains, per ADR 0009 revision 2026-05-16), `game/flow.json`
(multi-level progression) and the declarative `win:` / `lose:` blocks
in hud.json.
You DO need to align field names with what those skills expect — the
rule sketches identify the binding contract.
7. **Pick balance values from priors:**
- Tick interval 1 = ~0.1-0.5s actions (depends on scene's tick_seconds)
- Tick interval 4 = ~2s decay events
- Tick interval 30 = ~15s long-term cycles
- Contact radius 25-50 = "adjacent" interactions
- Contact radius 60-100 = "in range"
- Contact radius 800-1000 = "anywhere on map" (for AI tracking)
- Chance 0.3-0.5 = noticeable but probabilistic
- Chance 0.04-0.1 = slow accumulation (smelting)
- **Reproduction rules**: be careful — exponential growth is real.
A `spawn` rule with chance 0.4 / interval 15 on 16 source entities
fills the world to 100+ entities in 60s and lags the engine.
For a balanced ecosystem: keep new-spawn-per-source-per-second well
below the eat/death rate. In a small sim demo, chance 0.05 /
interval 60 keeps populations stable around starting size.
7a. **Contact rule format — tags + radius go in `query`, NOT `trigger`:**
```jsonc
// CORRECT
{
"trigger": {"type": "contact"},
"query": {
"a": {"tags_all": ["fish"]},
"b": {"tags_all": ["plant", "mature"]},
"radius": 50
},
"effect": [...]
}
// WRONG (engine accepts but rule never fires — hard to debug)
{
"trigger": {"type": "contact", "tags_a": ["fish"], "tags_b": ["plant"], "radius": 50},
"effect": [...]
}
```
The wrong form (tags/radius nested under `trigger`) is the most
common cause of "rule registered but never fires." Verified against
existing demo patterns (demo_ecology, demo_rpg).
7b. **Position spawn caveat:** `spawn.position` accepts `"self"` (copies
parent position), `[x, y]` literal, or a context binding name. **It does
NOT evaluate formulas inside the array** — `[self.x + 10, self.y]` won't
work. For seeded variation either accept overlap (use `"self"`) or add
randomness on the spawn target's spawn-trigger rules.
8. **Verify mentally:**
- Every rule has a trigger.type from manifest's `triggers`
- Every effect has a "type" from manifest's `effects`
- Every query references existing tag/state/property names
- Formulas use whitelisted bindings only (`self`, `target`, `a`, `b`,
`world`, plus payload keys)
- **Formulas use the formula language (ADR 0088).** Every number
is a float. Call only the 16 functions in the api-manifest. No
ternary: write `b + (a - b) * float(cond)`. No `%`, strings or
method calls. `validate_rules.py` check (12) reports violations.
- All entity ids in `initial_instances` and `initial_relations`
are unique
9. **Ship to qa-tester.** When the JSON is written, hand off with the
data folder path.
## Translating level-design.md → JSON: hand vs pattern
`level-design.md` (from yume-level-designer) lists placements as
either explicit coordinates or pattern specs. Translate 1:1:
**Hand-placed entries** → `initial_instances`:
```jsonc
{"def": "boss_spawn_marker", "id": "boss_marker", "position": [0, 0, 16]}
```
**Pattern entries** → `patterns` block (engine expands at load):
```jsonc
{"patterns": [
{"def": "pillar", "pattern": "ring",
"count": 6, "radius": 11, "yaw_offset": 0.26},
{"def": "rock", "pattern": "scatter",
"count": 30, "min_r": 3, "max_r": 18, "min_spacing": 1.5,
"exclude_zones": [
{"center": [0, 0, 0], "radius": 4},
{"center": [0, 0, 16], "radius": 6}
]},
{"pattern": "mirror", "axis": "x",
"items": [
{"def": "tower_slot", "id": "slot_w1", "position": [-9, 0, -3]},
{"def": "tower_slot", "id": "slot_w2", "position": [-9, 0, 3]}
]}
]}
```
Available patterns: `ring`, `grid`, `line`, `scatter`, `cluster`,
`mirror`. `scatter` + `cluster` accept `exclude_zones: [{center,
radius}, ...]` for protected areas (player spawn, boss frame, choke
points). See `instance_patterns.gd` for full options.
**Determinism**: scene.json's `level_seed: <int>` makes
`randf()`-based patterns reproducible across runs. Omit for
stochastic per-session randomization.
**Both blocks coexist** in zz_instances.json — `patterns` expands
first, then `initial_instances`. Use this division:
- `initial_instances` — singletons, signature placements,
asymmetric hand-tuned entities
- `patterns` — repeating decoration, symmetric layouts, high
counts, anything where typing each position is busywork
## Multi-level games (ADR 0006)
If the GDD describes a sequence of levels (sokoban with 8 puzzles,
TD with multiple maps, RPG town→dungeon, roguelike floors), use the
`levels/` directory pattern instead of a single root entities.json:
```
data/<game>/
├── scene.json # global (camera, tick, renderer) — asset-designer
├── ui/hud.json # asset-designer
├── ui/input.json # asset-designer
├── world/state.json # initial world state — content-designer (this skill)
├── world/rules/*.json # GLOBAL rules (feature modules) — systems-designer
│ # All rule chains live here per ADR 0009 revision
│ # 2026-05-16 (game/goals.json was removed).
├── game/flow.json # level order + start — systems-designer
├── entities/ # PERSISTENT entity defs — content-designer (this skill)
└── levels/
├── level_1/
│ ├── entities.json # level-scoped initial_instances — content-designer
│ └── rules.json # OPTIONAL per-level rules — systems-designer
├── level_2/
└── ...
```
**`game/flow.json`** (systems-designer writes this. It is here for context):
```jsonc
{
"levels": ["level_1", "level_2", ..., "level_N"],
"starting_level": "level_1",
"on_all_complete": {"win_message": "🌟 ALL CHAMBERS CLEARED 🌟"}
}
```
**Persistent vs level-scoped entities**:
- Entities tagged `persistent` SURVIVE level transitions. Player +
global score/inventory/HP are persistent.
- Everything else (walls, boxes, goals, enemies, pickups) is
level-scoped — wiped when transitioning, reloaded from the new
level's entities.json.
**Triggering a transition** (rule effect):
```jsonc
{"type": "transition_level", "target": "next"} // shorthand
{"type": "transition_level", "target": "level_5"} // explicit
```
`"next"` is resolved against `progression.levels`; if past the last
level, sets `world.all_levels_complete = 1` (HUD's `win` block can
bind to this for the game-cleared screen).
**Win condition for a level** (typical pattern — contact rule):
```jsonc
{
"id": "goal_reached",
"trigger": {"type": "contact"},
"query": {
"a": {"tags_all": ["player"]},
"b": {"tags_all": ["goal"]},
"radius": 1.0,
"once_per_a": true
},
"effect": [
{"type": "state_add", "target": "a", "field": "cleared", "amount": 1},
{"type": "transition_level", "target": "next"}
]
}
```
**Example demo**: `data/demo_multilevel/` ships a 3-chamber
navigation game using this pattern. Read it as the reference for
small multi-level games.
## Flavor / voice / barker fields (REQUIRED for named NPCs + items)
Per the soul-layer convention (yume-flavor-writer skill), entity defs
that represent named NPCs or named items MUST carry these optional
fields, populated from `flavor-design.md`:
```jsonc
{
"id": "npc_garron",
"tags": ["named_npc", "customer", "warrior_class"],
"properties": {
"voice": "gruff, terse, ends with 'aye'",
"barker_lines": [
"Looking for a flamberge — something that bites.",
"Heard the south road has bandits.",
"My old blade snapped in the cellar."
]
},
"state_init": {...},
"flavor_text": "A swordsman past his prime. Still the best in town."
}
```
```jsonc
{
"id": "item_iron_sword",
"tags": ["weapon", "item", "tier_1"],
"properties": {
"base_price": 75,
"category": "weapon"
},
"flavor_text": "Dented from a fight with a wolf. The previous owner walked away."
}
```
Field semantics:
- **`flavor_text`** (string, ≤15 words) — single line shown in HUD
inventory tooltip on hover/select. Format `[observation] +
[history hint]`.
- **`properties.voice`** (string) — voice descriptor for downstream
rule writers and screen-flow-designer composing dialogue. Not
rendered directly; informs prose generation.
- **`properties.barker_lines`** (array of strings) — pool of thought-
bubble / muttered-aside lines surfaced via `show_overlay` rules on
contact / arrival.
If a named NPC has no `voice` + `barker_lines`, OR a named item has
no `flavor_text`, the corresponding flavor-design.md is incomplete —
reject back to flavor-writer. Generic descriptions (`"A used iron
sword."`) are forbidden.
Engine reads these fields as data only — same path as any other
property/state field. No engine work needed.
## `persistent` tag — ONLY for entities that follow the player
The `persistent` tag (per ADR 0006 multi-level) marks an entity as
surviving level transitions. It must be applied with discipline:
**SHOULD be persistent**:
- `player` (the avatar)
- `world_clock` (singleton state holder)
- `party_member` entities (companions following the player)
- Named regulars who narratively appear in multiple levels (the
story-critical recurring NPCs — usually 0-3 per game)
**SHOULD NOT be persistent**:
- Buildings, walls, fences, pillars (per-level architecture)
- Lampposts, fountains, signposts, supplier stalls (per-level
street furniture)
- Portals (per-level entry/exit; new portal in new level)
- Generic NPCs / customers / enemies (spawned per level)
- Decorative props (chickens, banners, smoke emitters)
Empirical case: merchant 2026-05-08 — every building def
(`prop_cottage` / `prop_forge` / `prop_tavern` / etc.) was tagged
`persistent`. When the player walked from Pendrel into the shop
(8m × 6m interior), all 14 Pendrel buildings + watchtowers +
boundary walls came along. User reaction:
*"why i enter the shop, the shop has counter, then also have
buildings?"*
The **mental check** for the persistent tag: if the entity is
something a player would expect to *follow them* across a level
transition (themselves, their party, the time-of-day clock), tag
it persistent. If it's part of the SCENERY of one place, do NOT
tag it persistent — re-author it in each level's
`initial_instances`.
## Schema validation
Before declaring done, run mental schema validation:
| Check | How |
|---|---|
| All trigger types valid | Cross-ref manifest's `triggers` |
| Every effect has a `type` | Walk effect arrays; valid set in manifest's `effects` |
| No duplicate ids | Sort `initial_instances` ids |
| Tag references match definitions | Cross-ref tags between defs and queries |
| Formula syntax | `validate_rules.py` check (12). No ternary: write `b + (a - b) * float(cond)` |
| Positions in scene-bounds | Assume ±240 px for 2D demos |
| `aabb_extents` matches `visual.mesh` footprint | See subsection below |
The api-manifest is auto-generated from engine source — it's the
canonical list of what verbs the engine supports. Hand-edited lists in
this prompt may drift; the manifest does not.
### `aabb_extents` must match the rendered mesh footprint
When a def has both `properties.aabb_extents` (collision) AND
`visual.mesh` (rendered shape), the two MUST describe the same
physical object. The engine uses aabb_extents for motion blocking;
the renderer draws whatever the mesh defines. If they disagree, you
get **invisible-collider bugs** — the player walks into something
that isn't there, or sees a wall but passes through it.
**Audit per def with both fields**:
1. Open `godot/data/meshes.json`, find the mesh referenced by
`visual.mesh`. Note its visible footprint (width × height ×
depth).
2. Compare to `aabb_extents: [hx, hy, hz]` (HALF-extents — full
width is `2*hx`).
3. If they differ by >20%, ONE of them is wrong:
- The mesh is too small for the intended collider → swap to a
proper-scale mesh in the library, or add one if missing.
- The aabb is wrong for the intended visual → fix the numbers.
**Empirical case** (2026-05-09): `prop_city_wall_long` had
`aabb_extents=[150, 2, 0.5]` (300m long collider) but referenced
`merchant_pillar_3d` (0.35m radius pillar). Players saw nothing
where the wall was supposed to be. Fix: authored
`merchant_city_wall_segment_3d` (10m × 4m × 1m, crenellated) and
tiled 104 segments along the perimeter; aabb_extents updated to
`[5.0, 2.0, 0.5]` to match the 10m segment. The collision and
visual now describe the same wall.
**Rule of thumb**: any def tagged `blocks_motion` or `wall` or
`building` is high-risk for this bug class — colliders without
matching meshes are precisely the "I-don't-see-the-wall" complaint.
Audit them first.
### Top-heavy props: `collision_shape.from_visual_mesh: "base"` (2026-06-11)
For physics-block colliders derived from the visual mesh
(`collision_shape: {type: "box", from_visual_mesh: ...}`), the value
`true` shrink-wraps the FULL bbox — which turns a tree canopy into an
invisible ground-level wall spanning the whole crown. Use `"base"`
for top-heavy props (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 bodies whose full silhouette IS the obstacle.
Empirical 2026-06-11 (autorace): scale-8 pines 4.1m off the racing
line blocked the lane with 5.2m-wide canopy collider boxes until
switched to `"base"`.
### Authored instance y is the BASE height (2026-06-12)
Every visual is base-anchored (.glb bbox base normalized to y=0;
prim kits bake base-at-0): `position[1]` is where the mesh's BOTTOM
sits — y=0 means standing on the floor. Never author y = height/2
(raw-Godot center-pivot habit) — that floats the prop by half its
height. Gate: `validate_center_pivot_y.py` flags y ≈ 0.5 × scaled
height. Empirical 2026-06-12 (autorace): a whole demo's furniture
was center-pivot authored — grandstand +2.25m, gantry posts +2.7m,
flag poles +1.6m, cones +0.28m, all floating. Full rule:
`.claude/rules/data-demo.md` § authored y is the BASE height.
### 3D world-unit demos: `renderer.position_scale: 1.0` (2026-06-12)
Hand-authored 3D demos (world-unit positions) MUST have scene.json
`"renderer": {"position_scale": 1.0}` — the renderer default is 0.05
(2D pixel convention) and silently compresses every position 20×
into an origin blob (correct entity count, zero errors). Gate:
`validate_position_scale.py`. Empirical 2026-06-12 (demo_lightlab):
±10m of instances spawned within ±0.5m of origin; cost three probe
captures. scene.json is asset-designer's file — flag the missing key
in your handoff rather than editing it yourself.
### Mesh scale must match camera frustum
A 0.05m-radius lamp head is invisible in an isometric ortho_size 24
view (≈ 2 pixels wide). Mesh choice for ground-level decoration
must respect the camera mode in `scene.json`. See
`yume-asset-designer` SKILL § "Visual scale must match camera
frustum" for the per-mode min-radius table. When picking a mesh
for a def, check the active camera mode and reject specks.
## Camera-relative WASD: player needs deceleration mechanism
When world/rules/*.json (or legacy single-file world/rules.json) $includes a WASD lib bundle that uses
`velocity_add_relative` (isometric_3d or first_person_3d camera
modes), the player MUST get a stop rule, or it glides forever after
key release. `state.drag` and `state.zero_velocity_pretick` are DEAD
(no engine code reads them since ADR 0044) — do NOT author them.
| Scene's camera_mode | Required stop mechanism |
|---|---|
| `top_down_3d`, `third_person_3d` | n/a (per-axis `stop_x`/`stop_y` from the world-frame bundle) |
| `isometric_3d`, `first_person_3d` | `$include @lib.input_bundles.wasd_with_fp_variant.rules` (ships `lib_wasd_stop`), OR a hand-authored `input: stop` rule doing `velocity_set x:0 y:0` |
The player's `velocity` must be `[x, z]` (Vector2) so the stop rule
zeroes both planar axes.
Full rule + empirical case 2026-05-10 at
`.claude/rules/data-demo.md` § "velocity_add_relative requires
deceleration mechanism". The path-scoped rule is canonical;
content-designer must verify the player state matches the scene's
camera mode before declaring entity authoring done.
## What good looks like
- entity defs read top-to-bottom like a setup paragraph
- `_comment` keys explain the WHY of non-obvious tag/state choices
- Balance values are inside the priors above
- Field names match what the rule sketches expect (binding contract
with systems-designer)
- File compiles without errors (`json` parse), passes `Rule.validate_all`
## What bad looks like
- Effect `type: "damage"` (semantic — forbidden)
- entity ids that mix camelCase and snake_case randomly
- 50 entities with identical visual params
- Balance values that produce no observable cascade in <30s
- Any ternary, `cond ? a : b` or `a if cond else b` (use `float(cond)` arithmetic)
- Missing `_comment` on a complex rule
## 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.
## What you DON'T do
- ❌ Decide visual style (asset-designer's job for shape/mesh choice
beyond the stock library)
- ❌ Run the game / verify cascades (qa-tester)
- ❌ Modify engine code
- ❌ Add new primitives (systems-designer flagged + ADR'd them)
- ❌ Edit existing demos without explicit user approval — clone
if iterating
## Reference files
- `.claude/rules/data-demo.md` — schema authoring rules (read first)
- `docs/engine-reference/api-manifest.json` — canonical engine vocabulary
- `docs/guideline/30_framework_primitives.md` — full primitive vocabulary
- `godot/scripts/engine/rule.gd` — what
Rule.validate_all checks
- `godot/data/demo_*/` — pattern library
- `godot/data/shapes.json` — Tier 2 visual
catalog (composite shapes with param overrides)
## AI-gen .glb entity fields (added 2026-05-18)
When `visual.mesh` is a `.glb` path such as `assets/meshes/x.glb` (Tripo3D output, not a
`@lib.meshes.X` code-drawn name), these additional `visual` fields
are typically required:
**`visual.y_offset_mesh`** (preferred, mesh-space) or
**`visual.y_offset`** (legacy, world-units): lifts the mesh up
during render. Required for any .glb with pivot at geometric center
(Tripo3D defaults to this).
- `y_offset_mesh = -bbox.min.y` (mesh-space). Engine multiplies by
per-instance `state.scale` at sync time. **Always use this for any
def that level patterns scatter at variable scale** (e.g. dwarf
bushes via scale_min/scale_max on the same fruit-tree def).
- `y_offset` (legacy world-units) is `-bbox.min.y * state_init.scale`.
Pattern variants at different scale will float/sink.
Without this, structures sink half-underground. Empirical case
2026-05-18: prop_tree_fruit pattern at scale 0.3-0.55 floated until
migrated to `y_offset_mesh`. `validate_mesh_y_offset.py` enforces +
flags pattern-scale mismatches.
**`visual.animate` + `visual.rig_type`** (ADR 0053): opt-in skeletal
animation via Tripo3D's rig + retarget chain. Adds $1.10-1.40 to the
per-entity asset-gen cost; produces a multi-clip GLB instead of a
static mesh. Allowed `rig_type`: biped, quadruped, hexapod, octopod,
avian, serpentine, aquatic, others.
**When to opt in**:
- Player + named NPCs: usually yes. Sliding NPCs read as prototype.
- Predators / threats: yes. Visible pursuit is a soul beat.
- Common animals (ambient prey, generic critters): cost-decide per
game. $1.10 each; an aldenmere-sized scene with 6 animals is ~$7.
- Crowd NPCs / generic villagers: usually no. Crowd cost scales fast.
- Static props (huts, trees, rocks): never. They don't move.
When opting in, you ALSO need `visual.animation_state_rules` (per
ADR 0046) to map engine state → clip name. The asset-gen pipeline
auto-suggests a `velocity → walk / else idle` rule if absent, but
explicit authoring is better. Validator
`validate_animate_scope.py` enforces animate+rig_type pairing.
**`visual.material_overrides[uuid].roughness` + `metallic`**:
matte-PBR override per material UUID (find via `tools/inspect_glb.py`).
Defaults: wood/cloth 0.92-0.95 / 0.0, stone 0.85 / 0.0, skin/fur
0.88-0.90 / 0.0. Setting factor also clears the PBR texture so
the override is authoritative (otherwise Tripo3D's baked ORM keeps
the look reflective).
**`visual.material_overrides[uuid].albedo_color`**: distinct hue per
structure type for distance readability. Without per-type tints,
AI-gen structures all blend into "orange-brown blob" at FPS distance.
**`visual.shader` + `visual.shader_params`** (ADR 0052): replaces
StandardMaterial3D with ShaderMaterial. Used on `prop_water_plane`
and other entities needing animated / view-aware materials. See
yume-asset-designer §A2 + §visual.shader for the full schema.