# Theriapolis — Tile Generation Handoff This is the working knowledge accumulated while populating `Content/Gfx/tactical/` with art generated through the Pixellab MCP service. Read this whole file before generating any new tiles — there are several non-obvious failure modes and the ones we already burned credits on have specific fixes documented below. --- ## 1. Where tiles live ``` Content/Gfx/tactical/ surface/ ← edge-to-edge ground textures, fully opaque grass_0.png grass_1.png grass_2.png (user manual) tallgrass_0.png tallgrass_1.png (user manual) dirt_0.png dirt_1.png (user manual) gravel_0.png gravel_1.png (user manual) sand_0.png sand_1.png sand_2.png (Pixellab) snow_0.png snow_1.png snow_2.png (Pixellab) rock_0.png rock_1.png rock_2.png (Pixellab — see §6 known issues) marsh_0.png marsh_1.png marsh_2.png (Pixellab — third reroll) mud_0.png mud_1.png mud_2.png (Pixellab) cobble_0.png cobble_1.png cobble_2.png (Pixellab) floor_0.png floor_1.png floor_2.png (Pixellab) wall_0.png wall_1.png wall_2.png (Pixellab) shallowwater_0.png … _2.png (Pixellab) deepwater_0.png … _2.png (Pixellab) troddendirt_0.png … _2.png (Pixellab — see §3.5) deco/ ← single objects with transparent background tree.png bush.png snag.png (Pixellab — forest set) flower.png crop.png reed.png (Pixellab) rock.png boulder.png (Pixellab) ``` Filenames are **lowercased enum names** matching `TacticalSurface` and `TacticalDeco` in [`TacticalTile.cs`](Theriapolis.Core/Tactical/TacticalTile.cs). The `_N` suffix is a variant index — the renderer picks one per cell from a deterministic per-tile `Variant` byte. If only `.png` exists (no suffix), every cell uses it. The atlas auto-loads anything in these folders; missing files fall back to procedurally-generated colored squares from [`TacticalAtlas.cs`](Theriapolis.Game/Rendering/TacticalAtlas.cs). **Provenance matters.** The user manually placed grass/tallgrass/dirt/gravel to set the visual style baseline. **Do not overwrite those.** Anything the user produced in the Pixellab web UI also takes precedence over MCP-generated tiles. When in doubt, ask before saving over an existing file. --- ## 2. The Pixellab MCP setup The MCP server `pixellab` provides programmatic access to PixelLab AI's generators. Tools we use: - `mcp__pixellab__create_tiles_pro` — surface tiles. Always returns 16 variations regardless of what you describe. Async (~30 s without `style_images`, ~7-8 min *with* style_images). - `mcp__pixellab__get_tiles_pro` — poll status + retrieve URLs. - `mcp__pixellab__create_map_object` — single decoration sprite with transparent background. Async (~30-90 s). - `mcp__pixellab__get_map_object` — poll/retrieve. Each call returns a job ID immediately; the work happens server-side. Use `ScheduleWakeup` with the job ID(s) in the prompt to come back when the work is likely done — don't poll in a tight loop. **MCP scope quirk.** When you run the Pixellab CLI installer, it adds the server entry to whichever directory you were `cd`-ed into at the time. If the project Claude Code sees doesn't have the entry, the tools won't appear. Fix by editing `~/.claude.json` directly — find the project entry under `projects[].mcpServers` and ensure `pixellab` is in there. A full Claude Code restart is required for changes to take effect. **Auth.** The bearer token in the config is reusable indefinitely until rotated. Don't leak it — rotate any token that gets pasted in chat or read into a tool result. --- ## 3. The generation workflow ### 3.1 Surface tiles — use `style_images` mode, ALWAYS Without `style_images`, `create_tiles_pro` bakes a 1–3 pixel **dark border frame** into every tile (the algorithm literally "draws tile shape outlines and has AI fill them with pixel art"). The border is sometimes pure black, sometimes a dark grey/purple, and the existing `TacticalAtlas.StripBorderPixels` only catches some of it. Don't rely on the strip; just bypass the bordering algorithm by providing a clean reference. The reference image has to be a **clean, edge-to-edge, fully-opaque 32×32 PNG with no border itself**. The user's `grass_0.png` works perfectly. Encode it base64 and pass it as `style_images`. ### 3.2 Avoid the baked-in shadow gradient `create_tiles_pro` defaults to `tile_view: "low top-down"` which adds ~30% pseudo-3D depth. The AI renders that depth as a **dark shadow on the bottom and right edges of every tile** (interior brightness drops by 50–90 points on those edges). The border detector misses this because the shadow color isn't black-uniform — it's just darker than the interior. Tiled together, every tile shows a diagonal grid of dark seams. Always set: ```jsonc "tile_view": "top-down", // not "low top-down" "tile_depth_ratio": 0, // force flat "style_options": { "color_palette": false, "outline": false, "detail": true, "shading": false } ``` `shading: false` in style_options also helps suppress the depth shadow. ### 3.3 Force opaque coverage in the prompt If you say "no plants no rocks no transitions" the AI sometimes interprets that as "make most of the tile transparent and put a small clump of content in the middle". This produced a `marsh-v2` batch where every tile was 30–80% transparent. Fix by explicitly demanding full opacity: > "A fully opaque solid 32x32 ground tile … every single pixel must be > opaque earth, no transparency anywhere, no holes…" ### 3.4 Decoration tiles — use `create_map_object`, not `create_tiles_pro` Decorations are individual props (trees, bushes, etc.) with **transparent backgrounds**. They composite over surface tiles. `create_map_object` is designed for this — it returns a single PNG with proper transparency. Standard params: ```jsonc { "width": 32, "height": 32, "view": "high top-down", "outline": "single color outline", "shading": "medium shading", "detail": "medium detail" } ``` ### 3.5 Avoid description traps that bake in directional features For `troddendirt` we said "wheel ruts" in the description. The AI obliged by drawing very prominent vertical stripes that read as wood planks when tiled. Subtle features → safe. Specific features → problematic, because the AI baked them centered/directional and they don't tile. Rule of thumb: if you want the texture to *feel* like a road but tile seamlessly in any orientation, leave directional language out of the prompt and let the natural variation in the 16 tiles give you variety. --- ## 4. The `tile-analyze` command Run it on any folder of 32×32 PNGs: ```bash dotnet run --project Theriapolis.Tools -- tile-analyze --dir /tmp/pixellab- --sheet /tmp/-sheet.png ``` Output is a per-tile table + an optional 4×-upscaled labeled contact sheet. Each label shows: - **`brd:N`** — number of edges (0–4) where ≥80% of perimeter pixels are dark-uniform "border" pixels (max channel ≤ 95, max - min ≤ 25). Should be 0 for a clean tile. - **`op:NN%`** — fraction of pixels with α ≥ 128. Should be 100% for a surface tile, 30–70% for a decoration sprite. - **`sh t/b/l/r`** — interior brightness minus edge brightness, per side. Positive = edge is darker than interior. **Anything > 30 is a baked-in shadow gradient** that will show as a grid of dark seams when tiled. This is the single most useful number for triage; the border detector misses shadows. Labels are GREEN if the tile passes everything, ORANGE if it fails any check. The full implementation lives in [`TileAnalyze.cs`](Theriapolis.Tools/Commands/TileAnalyze.cs); the detector methods (`CountBorderEdges`, `CountOpaqueFraction`, `ShadowScores`) are public so you can call them directly from any future Tools command. ### Spot-check existing saved tiles Same command works on `Content/Gfx/tactical/surface/`: ```bash dotnet run --project Theriapolis.Tools -- tile-analyze \ --dir Content/Gfx/tactical/surface ``` Use this to sweep for the shadow issue before assuming any saved tile is clean. The history of what's been spot-checked vs not is in §6. --- ## 5. End-to-end recipe: generating one new surface 1. **Read this whole document first.** 2. Verify Pixellab MCP is loaded (`/mcp` in Claude Code shows it). 3. Get the base64 of `Content/Gfx/tactical/surface/grass_0.png` (the reference image). The previous session has this cached at `/tmp/grass_0_b64.txt` if it survived. 4. Call `create_tiles_pro` with: - `description`: include "fully opaque … every single pixel must be opaque … edge-to-edge" language; avoid directional feature language. - `style_images`: `[{"base64": "...", "width": 32, "height": 32}]` - `tile_view: "top-down"`, `tile_depth_ratio: 0`, `style_options: {color_palette:false, outline:false, detail:true, shading:false}` 5. Note the returned `tile_id`. Schedule a `ScheduleWakeup` for ~480 s. 6. On wakeup, `get_tiles_pro(tile_id)`. If still processing, reschedule another 240 s. Once complete, download all 16 PNGs from the B2 URLs. 7. Run `tile-analyze --dir --sheet `. 8. Show the contact sheet to the user along with the table. Recommend 3 picks that: - Pass all detector checks (border 0, opaque 100%, shadow ≤ 30). - Don't have strong directional features (wheel ruts, plank lines, centered swirls — these tile poorly). - Aren't visually identical to each other (variant rotation needs real variety). 9. After user confirms, save the picks as `_0/1/2.png` in `Content/Gfx/tactical/surface/`. 10. Rebuild Desktop (`dotnet build Theriapolis.Desktop`) so the runtime `bin/Debug/net8.0/Gfx/` copy refreshes. --- ## 6. Known issues / deferred work - **`rock_0.png` has a -87 shadow on the right edge.** Detected in a spot-check but not yet rerolled. Use the §3.2 flat-shading params. `rock_1` and `rock_2` haven't been spot-checked at all — verify before rerolling so we know if all three need replacing. - **`mud_0` and `cobble_0` are borderline** (shadow scores in the high 20s). Just under the 30 threshold. Acceptable for now but candidates for a quality pass later. - **Marsh history.** The marsh surface took three rerolls: - v1: came back as ponds with reeds and grey borders (asked for "reedy growth" — AI placed reeds on a smaller central pond shape). - v2: came back as transparent decoration sprites (the prompt said "no reeds no plants" too forcefully — AI made everything except the moss patches transparent). - v3 (current): correct after explicit "fully opaque every pixel" wording. This is the canonical example of the failure modes in §3.3 + §3.5. --- ## 7. Quick reference — current generation params we know work ```python # Surface tiles (create_tiles_pro) { "description": "", "style_images": '[{"base64": "", "width": 32, "height": 32}]', "style_options": '{"color_palette": false, "outline": false, "detail": true, "shading": false}', "tile_view": "top-down", "tile_depth_ratio": 0 } # Decoration sprites (create_map_object) { "description": "", "width": 32, "height": 32, "view": "high top-down", "outline": "single color outline", "shading": "medium shading", "detail": "medium detail" } ``` These are the configurations that produced clean tiles after we worked through the failure modes. Deviate only when you have a reason and you've re-checked with `tile-analyze`. --- ## 8. Architecture context The tactical render path that consumes these tiles is: 1. [`TacticalChunkGen`](Theriapolis.Core/Tactical/TacticalChunkGen.cs) stamps `TacticalSurface` and `TacticalDeco` enum values into each tactical tile based on biome, road polylines, settlement footprints. 2. [`TacticalAtlas`](Theriapolis.Game/Rendering/TacticalAtlas.cs) loads PNGs from `Content/Gfx/tactical/{surface,deco}/` keyed by lowercased enum name. Falls back to procedural color squares for anything missing. 3. [`TacticalRenderer`](Theriapolis.Game/Rendering/TacticalRenderer.cs) draws each tile's surface texture, then its decoration if any, in chunk-by-chunk passes. The art request document for human artists is [`theriapolis-tactical-tile-art-request.md`](theriapolis-tactical-tile-art-request.md) — it's the same content list we're satisfying with Pixellab, just framed for a human painter. Keep both docs in sync if the surface or decoration enums change.