Skip to content
Back to skills

Yume Map Author

ASecurity

Author a level's entities.json fragment from a semantic top-down map image: only catalog defs, positions from pixel bbox via the coord transform, patterns from scatter presets. Use to add or edit a level from a map sketch.

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

Works with

  • api

Security analysis

A100/100

Scanned October 18, 2026

npx -y skills add kamwoh/yume --skill yume-map-author --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Yume Map Author?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Yume Map Author
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/kamwoh-yume-map-author/badge)](https://www.skillsdirectory.com/skills/kamwoh-yume-map-author)

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-map-author
description: >-
  Author a level's entities.json fragment from a semantic top-down map
  image: only catalog defs, positions from pixel bbox via the coord
  transform, patterns from scatter presets. Use to add or edit a level from
  a map sketch.
---

# /yume-map-author

You are the **level / map author** for Yume. You read a semantic
top-down map image and produce a level's `entities.json` — meaning
the in-game level matches the map (modulo procedural-scatter
variance for zones).

Sibling of `yume-hud-author` and `yume-screen-author`. Same harness
pattern; the surface this time is a 2D spatial world, not a 16:9
viewport. Deterministic surface lives at
`tools/visual_layout/wireframe_to_map.py`.

> **⚠ CURRENT PIPELINE (2026-05-27).** The LLM-parses-the-map-PNG flow
> below (wireframe_to_map) places entities by reading the map via
> vision against a per-game entity catalog. It has been largely
> SUPERSEDED for FULL-WORLD generation by
> `tools/visual_layout/compose_world.py`, which extracts placements
> DETERMINISTICALLY from the semantic map via
> `data/lib/extraction_strategies.json` + `lib_extract_dispatch` (connected
> components, PCA fit-to-mask, variant buckets, kit meshes), and emits
> entity defs + biome ground + water + roads. A separate
> `compose_shell.py` adds camera/player/input/lighting. This skill
> still applies when authoring a level by hand-placing catalog entities
> against a wireframe (the fit-fit LLM-parser path), but
> text-to-WORLD generation now goes through compose_world/compose_shell
> (the active 3D map pipeline; see `.claude/rules/pipeline-stability.md`).

## Why this skill exists

The earlier `compose_map.py` chain used opencv k-means + skimage
skeletonization to parse semantic maps. Same fragility as the
CV-based HUD pipeline: borderline-cropped color regions, soft
JPEG edges, ambiguous zone/anchor calls.

Putting Claude at the parsing step gives:
- Reliable region identification via vision (not k-means)
- Semantic def_id picking ("this blob looks like a fire pit
  because it's small and at center" — informed by tags +
  asset_resolution hints)
- Better anchor-vs-zone judgment (a small distinct region is an
  anchor; a large irregular region is a zone)

Fit-fit: what's drawn on the map is what's placed. No clever
"densify the forest by 2x" or "shift the camp 10m east." The map
is the spec.

## When to invoke

- `compose_map.py` run that needs a wireframe → level entries
- Iterating on a level — re-runs replace the level's
  entities.json (with .bak)
- New level / new game — first time a level is authored

Do **not** invoke for:
- Single-entity tweaks ("move the well 2m north") — direct edit
- Pattern density adjustments — direct edit
- Adding a new entity def — that's content-designer

## Inputs

1. A semantic map PNG (or JPEG — gemini-3.1 with 1:1 aspect)
2. The game id (e.g. `demo_aldenmere`)
3. The target level id (e.g. `level_proto_village`)

## The procedure

### Step 1 — Run preprocess

```bash
python3 -m tools.visual_layout.wireframe_to_map preprocess \
    <game> <map.png> <level_id>
```

This writes `/tmp/_map_author_context.json` with:

- `map.width/height` — pixel dimensions
- `world_bounds.width/depth` — meters per axis (e.g. 80×80 = ±40m)
- `coord_transform.scale_x_per_px` / `scale_z_per_px` — pixel →
  world meters
- `coord_transform.formula` — deterministic position math
- `entity_catalog` — ALL existing def_ids in this game, with tags
  + key visual fields. This is your strict allow-list.
- `anchor_hints` — per-anchor-name → preferred def_id from the
  game's asset_resolution.json (e.g. `fire_pit` → `structure_fire_pit`)
- `zone_hints` — per-zone-name → preferred def_id + scatter_preset
  (e.g. `forest` → `prop_tree_oak` + `forest_dense`)
- `scatter_presets` — all available scatter density profiles
- `existing_level_summary` — informational; what was here before
  (you're REPLACING it, not preserving)
- `authoring_rules` — repeats the constraints below

### Step 2 — Read the map + context

`Read` on both. The PNG is parsed natively via vision; the context
gives you the deterministic surface.

### Step 3 — Identify each visible region

A semantic map typically has 3-15 visible regions. For each:

#### 3a — Classify as anchor / zone / path

- **Anchor** — small to medium distinct region (~3-8% of canvas),
  unique color matching an `anchor_hint` name. Examples: fire
  pit (yellow circle), hut (red-brown square), market stall
  (orange square), well (grey circle).
- **Zone** — large irregular region (>10% of canvas), color
  matching a `zone_hint` name. Examples: forest (dark green),
  grass clearing (light green), water (blue), rocks field (grey
  scatter color).
- **Path** — thin elongated band connecting anchors (tan/brown).
  DEFERRED — emit nothing for path regions; engine does not
  consume them yet.

If a region's color doesn't match any hint, look at its size +
shape:
- Small + distinct → likely an anchor; check `entity_catalog`
  tags for a fit
- Large + irregular → likely a zone; check tags + scatter_presets

If you can't confidently classify, emit nothing and FLAG.

#### 3b — Pick the def_id strictly

- **Anchors**: prefer the `anchor_hints[name].def` if the region
  color matches a hinted name. If the map shows something outside
  the hints, scan `entity_catalog` for a def whose tags match the
  visual intent. **Never invent a def_id**; if no fit, flag and
  skip.
- **Zones**: prefer `zone_hints[name]`. Same fallback discipline.

#### 3c — Compute world position deterministically

For an anchor centered at pixel (px_cx, px_cy):

```
wx = (px_cx - image.width / 2)  * coord_transform.scale_x_per_px
wz = (px_cy - image.height / 2) * coord_transform.scale_z_per_px
wy = 0.0   (floor-anchored)
```

`wy` is the mesh's BASE height — meshes are base-anchored (bbox base
→ y=0), so `0.0` means "standing on the floor". NEVER emit
y = height/2 (center-pivot semantics): that floats the prop by half
its height; `validate_center_pivot_y.py` flags it (empirical
2026-06-12, autorace furniture).

Round to 1 decimal place. Position MUST satisfy:
- `|wx| <= world_bounds.width / 2`
- `|wz| <= world_bounds.depth / 2`

#### 3d — Emit instance OR pattern

**Anchor → `initial_instances[]` entry**:

```jsonc
{
  "_source_region": "fire_pit yellow circle at px (512, 380)",
  "def": "structure_fire_pit",
  "id": "fire_pit_00",
  "position": [0.0, 0.0, -10.3]
}
```

If `anchor_hints[name].count_policy == "each"` (e.g. berry_bush,
rock), emit ONE instance per visible region of that color. Apply
per-instance scale/yaw jitter via state overrides:

```jsonc
{
  "def": "food_berry_bush",
  "id": "berry_bush_00",
  "position": [-13.3, 0.0, -24.9],
  "state": {"scale": 0.92, "yaw": -1.78}
}
```

**Zone → `patterns[]` entry**:

```jsonc
{
  "_source_zone": "forest dark-green ring around the clearing",
  "pattern": "scatter",
  "def": "prop_tree_oak",
  "id_prefix": "forest_prop",
  "count": 120,
  "min_r": 25,           // REQUIRED for count > 10
  "max_r": 38,           // REQUIRED for count > 10
  "min_spacing": 1.5,
  "scale_min": 0.8,
  "scale_max": 1.4,
  "yaw_jitter": 3.14,
  "origin": [0, 0, 0]
}
```

`count` = `floor(area_m2 * scatter_preset.density_per_m2)`, capped
at the preset's `max_count` (default 200, postprocess caps at 500
hard). Compute area as:

```
area_m2 ≈ (region pixel area) * scale_x * scale_z
       ≈ (px_w * px_h) * scale_x * scale_z   for rect-ish regions
```

For irregular regions, eyeball the area (rough is fine — the
engine scatter picks random points; small density errors are
imperceptible).

**`min_r` and `max_r` are MANDATORY for count > 10.** They define
the annulus (ring) around `origin` where scatter happens. Engine
defaults are min_r=0, max_r=5 — a 5-meter disk. With min_spacing
≥1m, you can fit ~30 entities max in that disk; everything beyond
silently fails to place. Empirical case 2026-05-20: a 200-tree
forest pattern shipped without min_r/max_r and the engine spawned
only ~30 trees clumped at origin.

Pick min_r/max_r from the wireframe:
- **Forest ring outside a camp clearing**: min_r = camp half-width
  + 1-2m margin; max_r = world half-extent − 1-2m margin.
- **Rocks/forageables scattered within a clearing**: min_r = 0
  (or just outside the very center), max_r = clearing half-width.
- **Sparse decoration across whole world**: min_r = 0, max_r =
  world half-extent.

Convert from pixel-radius in the wireframe to world meters via
`coord_transform.scale_x_per_px`.

### Step 4 — Write the draft + run postprocess

Write to `/tmp/_map_draft.json`. Shape:

```jsonc
{
  "_comment": "Authored by yume-map-author from <map.png> on YYYY-MM-DD.",
  "initial_instances": [...],
  "initial_relations": [],
  "patterns": [...],
  "_scene_patch": {
    // Optional — only if context.biome_config is non-empty (ADR 0055).
    // Sparse override; merged over the game-level scene.json on level
    // load via GroundRenderer.rebind_shader_params(level_id).
    "ground": {
      "mesh": {
        "shader": "<context.biome_shader_path>",
        "shader_params": {
          "biome_map": "<map.asset_path>",       // THIS level's map
          "biome_color_dirt":   [r,g,b,1.0],     // from context.biome_config.<name>.color_uniform
          "biome_color_grass":  [r,g,b,1.0],
          "biome_color_water":  [r,g,b,1.0],
          "biome_color_path":   [r,g,b,1.0],
          "biome_color_forest": [r,g,b,1.0],
          "albedo_dirt":   "assets/...",         // from context.biome_config.<name>.albedo
          "albedo_grass":  "assets/...",
          "albedo_water":  "assets/...",
          "albedo_path":   "assets/...",
          "albedo_forest": "assets/...",
          "uv_tile": 30.0,
          "blend_softness": 0.5
        }
      }
    }
  }
}
```

### Step 4a — Biome scene_patch authoring (ADR 0055)

If `context.biome_config` is **empty**, skip this step — the level
uses the game-level ground (whatever scene.json declares).

If `context.biome_config` is **non-empty**:

1. Identify which biomes from `biome_config` are visible in the
   wireframe (by hex color match).
2. For each of the shader's 5 fixed slots (`dirt`, `grass`, `water`,
   `path`, `forest`), set `biome_color_<slot>` to either:
   - the `color_uniform` from biome_config (if that biome is in
     biome_config), OR
   - the dirt slot's color (for unused slots — safe fallback)
3. For each slot, set `albedo_<slot>` to either:
   - the `albedo` path from biome_config (if present), OR
   - the dirt slot's albedo (for unused slots)
4. Set `biome_map` to `map.asset_path` (this level's semantic map). Asset
   paths are relative to the game folder, or start with `@lib/` (ADR 0087).
   `validate_host_neutral.py` rejects `res://`.
5. Use `biome_shader_path` from context as the `shader` field.

**Why all 5 slots must be set**: the shader has 5 uniform slots; an
unset slot returns black, breaking the blend. Re-pointing unused
slots at `dirt`'s albedo + color is the safe fallback; the math still
blends correctly but no new biome surfaces visually.

Then:

```bash
python3 -m tools.visual_layout.wireframe_to_map postprocess \
    <game> /tmp/_map_draft.json
```

Postprocess validates:
- Every `def` (in instances + patterns) exists in entity_catalog
- No duplicate instance ids
- Every `position` in-bounds
- Pattern count ≤ 500 (hard cap)
- Only `pattern: scatter` (other patterns deferred)

If validation fails, fix the draft and re-run. Don't `--force`.

On success: backs up `levels/<level_id>/entities.json` →
`entities.json.bak`, writes the new content.

### Step 5 — Report

User-facing message:

1. **Regions identified** — N regions, type (anchor/zone) + def_id per
2. **Regions skipped** — anything you couldn't confidently
   classify, with reason
3. **Anchors emitted** — N instances, broken down by def_id
4. **Zones emitted** — N patterns, broken down by def_id + count
5. **Existing level was replaced** — show what was overwritten
   (from existing_level_summary)
6. **The applied entities.json path** — and the .bak

## Strict rules (the fit-fit invariants)

1. **Def_ids are catalog-only.** No invented defs.
2. **Positions are derived, never chosen.** Use the coord transform.
3. **Anchor count policy matters.** If hint says `single`, emit
   ONE instance (use the first/most-centered visible region).
   If `each`, emit one per visible region.
4. **Zones use scatter_presets.** Pick the preset by name; don't
   invent density values.
5. **Path regions are deferred.** Emit nothing; flag in report.
6. **Skip-with-flag beats guess.** A region you can't classify
   gets emitted nothing + reported. Better than a wrong def.

## What this skill does NOT do

- Does **not** decide what should be ON the map. The prompt /
  preset / wireframe captures intent; you translate it.
- Does **not** generate the map. `compose_map.py` owns the Gemini
  call.
- Does **not** edit entity defs — that's `yume-content-designer`.
- Does **not** wire game logic (rules, goals). Pure spatial layout.
- Does **not** validate gameplay (is this winnable? can the player
  reach the fire pit?) — that's `yume-qa-tester` / `yume-playtest`.
- Does **not** preserve existing placements. Your output REPLACES
  the level. If specific instances must survive, re-roll the map
  via `compose_map --edit` to include them visually.

## Empirical case (2026-05-20)

Built after the HUD and screen harnesses shipped. Same pattern,
spatial surface — anchor_hints + zone_hints from the per-game
`asset_resolution.json` give the LLM a strong prior on def picks;
the entity_catalog provides the strict allow-list; the coord
transform makes positioning deterministic. compose_map's prior CV
approach worked for clean cases but failed on borderline-cropped
regions and ambiguous color similarities. LLM-as-parser fixes
those.

## Reference files

- `tools/visual_layout/wireframe_to_map.py` — preprocess +
  postprocess
- `tools/visual_layout/legends/scatter_presets.json` — shared
  scatter density profiles
- `godot/data/<game>/visual_layout/asset_resolution.json` —
  per-game anchor + zone def mappings
- `godot/data/<game>/scene.json` — `ground.mesh.size` → world
  bounds
- `godot/data/<game>/entities/*.json` — entity_catalog source
- `docs/engine-reference/api-manifest.json` —
  map_authoring_concepts + map_coord_transform
- `.claude/skills/yume-hud-author/SKILL.md` — sibling skill
- `.claude/skills/yume-screen-author/SKILL.md` — sibling skill
- `.claude/skills/yume-level-designer/SKILL.md` — the spatial-
  intent skill that PRECEDES this one. Level-designer decides
  what should be on the map; map-author parses the resulting
  wireframe.

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.