b451f83174
Captures the pre-Godot-port state of the codebase. This is the rollback anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md). All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
296 lines
13 KiB
Markdown
296 lines
13 KiB
Markdown
# 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 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-<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 (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 <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.
|