| name | yume-content-designer |
| description | Entities + initial state designer for Yume games. Translates GDD + world plan into entity definitions (entities/*.json), initial placements (per-level entities.json), and world initial state (world/state.json). Picks tag names, state field names, default values, positions. Per ADR 0009 — narrowed scope: rules are NOT this skill's job. World physics rules go to yume-systems-designer; game rules go to yume-game-rules-designer. This skill owns the entity vocabulary and where things are at level start. |
/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//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-game-rules-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-game-rules-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-game-rules-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
-
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 / game-rules-designer.
-
Read .claude/rules/data-demo.md first. Schema rules apply:
no semantic effect types, formula whitelist, tag conventions,
cross-renderer coordinates.
-
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
-
Write entity definitions under entities/ (per-def files) or
entities.json (single-file):
{
"definitions": [
{
"id": "<def_id>",
"tags": [<membership tags>],
"properties": { },
"state_init": { },
"visual": {
"shape": "<from data/shapes.json>",
"params": { }
}
}
]
}
- 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:
{
"initial_instances": [
{ "def": "<def_id>", "id": "<unique_id>", "position": [x, y] }
],
"initial_relations": [
{ "type": "<relation_type>", "from": "<id>", "to": "<id>" }
]
}
- Write
world/state.json with the initial values for any
world.X bindings the rules will read (e.g. score, current_level,
phase):
{
"_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). yume-game-rules-
designer owns 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.
- 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:
{
"trigger": {"type": "contact"},
"query": {
"a": {"tags_all": ["fish"]},
"b": {"tags_all": ["plant", "mature"]},
"radius": 50
},
"effect": [...]
}
{
"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.
-
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 Python-style ternary
a if cond else b — NOT
C-style cond ? a : b. Godot Expression doesn't parse C-style.
(Note: Godot 4.6.1's Expression parses ternary but always returns
the IF branch — see .claude/rules/data-demo.md for the clamp-step
workaround.)
- All entity ids in
initial_instances and initial_relations
are unique
-
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:
{"def": "boss_spawn_marker", "id": "boss_marker", "position": [0, 0, 16]}
Pattern entries → patterns block (engine expands at load):
{"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 — game-rules-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 — game-rules-designer
├── level_2/
└── ...
game/flow.json (game-rules-designer writes this; included for context):
{
"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):
{"type": "transition_level", "target": "next"}
{"type": "transition_level", "target": "level_5"}
"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):
{
"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:
{
"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."
}
{
"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 | Try parsing — most syntax errors visible by eye; use Python-style ternary |
| 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:
- Open
godot/data/meshes.json, find the mesh referenced by
visual.mesh. Note its visible footprint (width × height ×
depth).
- Compare to
aabb_extents: [hx, hy, hz] (HALF-extents — full
width is 2*hx).
- 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.
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's state_init MUST include either
zero_velocity_pretick: true or drag > 0. Without one of these,
mouse-turning mid-walk produces the facing-lag "pulling" bug
(velocity accumulates in OLD facing direction; this tick's add in
NEW facing direction; clamp lands between them; player feels
something is pulling them back from where the camera is pointing).
| Scene's camera_mode | Required player state_init |
|---|
top_down_3d, third_person_3d | n/a (velocity_set per-axis is fine) |
isometric_3d | zero_velocity_pretick: true (recommended) OR drag > 0 |
first_person_3d | zero_velocity_pretick: true (tight FPS feel) OR drag > 0 (momentum feel) |
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 / game-rules-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
- C-style ternary
cond ? a : b (use Python-style instead)
- 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.
Quick command:
godot --path C:/.../YumeTemplate scenes/<game>_3d.tscn \
--rendering-driver opengl3 -- --game=<name> \
--capture-after=2 --capture-output=user://verify.png
Then Read("/mnt/c/.../verify.png") and verify your specific change
rendered as intended. See visual-qa.md for the full 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 res://...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.