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>
13 KiB
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.
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.
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 withoutstyle_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:
"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:
{ "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:
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; 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/:
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
- Read this whole document first.
- Verify Pixellab MCP is loaded (
/mcpin Claude Code shows it). - 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.txtif it survived. - Call
create_tiles_prowith: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}
- Note the returned
tile_id. Schedule aScheduleWakeupfor ~480 s. - On wakeup,
get_tiles_pro(tile_id). If still processing, reschedule another 240 s. Once complete, download all 16 PNGs from the B2 URLs. - Run
tile-analyze --dir <download-dir> --sheet <out-sheet.png>. - 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).
- After user confirms, save the picks as
<surface>_0/1/2.pnginContent/Gfx/tactical/surface/. - Rebuild Desktop (
dotnet build Theriapolis.Desktop) so the runtimebin/Debug/net8.0/Gfx/copy refreshes.
6. Known issues / deferred work
rock_0.pnghas a -87 shadow on the right edge. Detected in a spot-check but not yet rerolled. Use the §3.2 flat-shading params.rock_1androck_2haven't been spot-checked at all — verify before rerolling so we know if all three need replacing.mud_0andcobble_0are 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
# 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:
TacticalChunkGenstampsTacticalSurfaceandTacticalDecoenum values into each tactical tile based on biome, road polylines, settlement footprints.TacticalAtlasloads PNGs fromContent/Gfx/tactical/{surface,deco}/keyed by lowercased enum name. Falls back to procedural color squares for anything missing.TacticalRendererdraws 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
— 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.