Skip to content
Back to skills

Yume Content Designer

ASecurity

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.

  • 25 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 18, 2026
developmentrustgoshellreactawsapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 18, 2026

npx -y skills add kamwoh/yume --skill yume-content-designer --agent claude-code

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.

Security grade badge for Yume Content Designer
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/kamwoh-yume-content-designer/badge)](https://www.skillsdirectory.com/skills/kamwoh-yume-content-designer)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.