Files

296 lines
13 KiB
Markdown
Raw Permalink Normal View History

# 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 `<name>.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[<your-project-path>].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 13 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 5090 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 3080%
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-<surface> --sheet /tmp/<surface>-sheet.png
```
Output is a per-tile table + an optional 4×-upscaled labeled contact sheet.
Each label shows:
- **`brd:N`** — number of edges (04) 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, 3070% 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 <download-dir> --sheet <out-sheet.png>`.
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 `<surface>_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": "<see §3.3 — emphasize opacity + edge-to-edge>",
"style_images": '[{"base64": "<grass_0.png 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": "<see request doc>",
"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.