Gfx Sprite Art Generation — LLM / Image Model Specification
Purpose
This document tells an image-generation model exactly how to produce Flatland3 world sprites: individual cell PNGs, naming, visual style, animation modes, and the YAML sheet definition the game loads.
You are not drawing a single combined atlas texture. Flatland3 stores one PNG per animation frame and references them from a sheet YAML. The runtime loads each file separately and animates by cycling cells within a mode.
Hard technical requirements (non-negotiable)
| Rule | Requirement |
|---|---|
| Format | PNG only |
| Alpha | RGBA with real transparency — alpha channel, not a fake background |
| Background | Fully transparent outside the subject. No white, gray, or checkerboard backdrop baked into pixels |
| Cell dimensions | Square: width == height == cell_size |
cell_size |
Must be exactly 32, 64, or 128 (schema enum). Default for new art: 64 |
| Filter | Nearest-neighbor at runtime — design for crisp pixel art, not soft photo blur |
| Orientation | Top-down (bird's-eye). Subject readable when map cell is small on screen |
| Text | No letters, numbers, UI, watermarks, or signatures in the image |
| Color | sRGB. Avoid pure #000000 outlines on large areas (reads as holes with transparency) |
Transparency — yes, required
The gfx client draws sprites over terrain and buildings. Opaque rectangular backgrounds look wrong immediately. Every cell PNG must be cut out with a clean alpha matte.
Reject outputs that:
- Use a solid color “keyed” background instead of alpha
- Leave halos or white fringes around edges
- Bake a shadow as an opaque gray square under the subject (a small soft drop shadow inside the cell is OK if still mostly transparent)
Non-square images — source art must be square; display may stretch
Source PNG cells: Must be square. All 48 shipped cells today are 64×64. The loader does not reject non-square files, but draw_sprite scales the full texture to the destination rectangle with dest_size, so a 64×96 source stretched into a square map cell will look distorted.
On-screen map cells: cell_w and cell_h can differ when the map pane aspect ratio is not 1:1, so sprites may stretch slightly in the window. Design centered subjects with a little margin so mild stretch is tolerable. Do not rely on non-square source art to fix layout.
Player hero atlas (assets/gfx/player/hero.png) is a separate system: one PNG atlas, 32×32 frames in a 4×2 grid (N/E/S/W × idle/walk). This spec covers world sprites under assets/gfx/sprites/.
Visual style (Flatland3 look)
Canonical style guide (prompts + master palette):
| Asset | Role |
|---|---|
assets/gfx/sprites/style-guide.yaml |
Machine-readable; stamped into every flatland-sprite-gen Gemini/OpenRouter request |
docs/gfx-sprite-art-style.html |
Human-readable interactive guide |
tools/flatland-sprite-gen/data/style-guide.yaml |
Embedded CLI fallback (keep in sync with assets copy) |
| Attribute | Target |
|---|---|
| Genre | Retro top-down MMORPG, Ultima Online–adjacent soft fantasy |
| Camera (sprites / buildings / props) | 3/4 top-down tilt |
| Facing (characters / wildlife) | Front-facing toward camera (face toward bottom of cell / south). One view reused for all travel directions — Ultima Online style; no N/E/S/W art sets or side profiles |
| Camera (terrain) | Straight top-down — seamless tileable ground only |
| Pixel grid | Author as 32px-grid chunky pixels even inside 64/128 cells |
| Palette | Shared master palette in style-guide.yaml (4–6 colors per sprite + outline) |
| Outline | Dark warm-brown #231814 1px silhouette (sprites/props); none on terrain tile borders |
| Shading | Flat cel: exactly 3 tones per color (base / shadow / highlight); light from upper-left |
| Subject fill | Characters/props: max dimension ≈ cell_size - 10px. Terrain fills edge-to-edge |
| Ground contact | Soft flat ground shadow under sprites; props/characters sit at bottom-center |
| Don't | Gradients, anti-aliasing, painterly noise, per-sprite off-palette colors, terrain vignettes/shadows |
Inspect the composed Gemini block:
flatland-sprite-gen style-guide --kind npc --cell-size 64
flatland-sprite-gen style-guide --kind terrain --cell-size 64
Repository layout
assets/gfx/sprites/
manifest.yaml # index of all sheets
<sheet_id>.yaml # e.g. resource.iron_ore.yaml
cells/
static/ # single-frame terrain/building tiles
resource/ # resource node animations
npc/ # NPC / wildlife animations
container/ # chests, crates
After creating art:
- Write PNGs under
cells/… - Create or update
<sheet_id>.yaml - Add an entry to
manifest.yamlif new sheet - Set item/NPC
tile_id: <sheet_id>and optionalsprite_modesin content YAML - Run content-admin Validate or
GET /api/gfx/sprites/validate
Sheet YAML schema
id: resource.iron_ore # must match filename stem: resource.iron_ore.yaml
kind: resource # terrain | resource | container | npc | item | building | other
cell_size: 64 # 32 | 64 | 128
fps: 5.0 # default animation speed (modes may override)
modes:
- id: idle # mode id (animation row)
cells:
- path: cells/resource/iron_ore_idle_0.png
- path: cells/resource/iron_ore_idle_1.png
- id: harvest
fps: 8.0 # optional per-mode override
cells:
- path: cells/resource/iron_ore_h0.png
| Field | Meaning |
|---|---|
id |
Sheet id — referenced by content tile_id |
kind |
Category for admin filtering |
cell_size |
Intended pixel size of every cell PNG in this sheet |
fps |
Default frames per second when cycling cells; modes may override with modes[].fps |
modes[].id |
Mode name used by sprite_modes mapping (see below) |
modes[].fps |
Optional per-mode fps; falls back to sheet fps |
modes[].cells[].path |
Path relative to assets/gfx/sprites/ |
First mode in the list = default when no mode is resolved.
Sheet id naming convention
| Kind | Pattern | Example |
|---|---|---|
| Terrain | terrain.<kind> |
terrain.grass, terrain.shallow_water |
| Building | building.<part> |
building.wall_h, building.door_closed |
| Resource | resource.<item_template_id> |
resource.iron_ore |
| Container | container.<template_id> |
container.wood_chest |
| NPC | npc.<npc_id> |
npc.goblin, npc.ada |
| Item (ground loot) | item.<template_id> or reuse resource/container id |
— |
Use lowercase a-z, digits, _, ., - only.
Game states vs sprite modes (critical)
Modes live on the sprite sheet.
State → mode mapping lives on item/NPC YAML as sprite_modes.
The sim emits presentation keys; content maps them to mode ids on the sheet.
Harvestable resources
Sim states (ResourceNodeState):
| Presentation key | Meaning | Typical mode ids on sheet |
|---|---|---|
available |
Node ready to harvest | idle |
harvesting |
Player actively harvesting | harvest |
cooldown |
Depleted / respawning | depleted, cooldown, or back to idle |
Example item YAML:
tile_id: resource.iron_ore
sprite_modes:
available: idle
harvesting: harvest
cooldown: depleted
Alias fallback (when sprite_modes omitted): available→idle, harvesting→harvest, cooldown→depleted or cooldown.
When generating art for a resource, ask which states need distinct visuals and produce one mode per distinct look, with 1–4 frames per mode for motion.
NPCs / wildlife
Presentation key = behavior FSM state id, except when attack windup is active → key telegraph overrides.
Example: goblin_raider behavior states: idle, alert, chase, combat, plus telegraph for windup.
tile_id: npc.goblin
sprite_modes:
telegraph: telegraph
# other states default to mode id matching state name if present on sheet
Shipped npc.goblin modes: idle, alert, chase, combat, telegraph.
When generating NPC art, list all behavior states from the NPC's behavior_ref YAML and produce one mode per state that should look different.
Static kinds (terrain, building, container)
Usually one mode idle with one cell. Exceptions:
container.wood_chest: modesclosed,openbuilding.door_closed/building.door_open: separate sheets or modes- Animated terrain (water):
idlewith 2–4 ripple frames
File naming convention
cells/<category>/<sheet_short>_<mode>_<frame>.png
Examples:
cells/resource/iron_ore_idle_0.png
cells/resource/iron_ore_idle_1.png
cells/resource/iron_ore_h0.png # harvest frame 0 (short name OK)
cells/npc/goblin_combat_0.png
cells/static/terrain_grass.png # single frame, no index
cells/static/building_wall_v.png
Rules:
- Lowercase, underscores
- Frame index
0,1,2, … within a mode - All frames in a mode must be same pixel dimensions
Animation guidance
| Kind | Typical fps | Frames per mode |
|---|---|---|
| Terrain water | 4–6 | 3–4 subtle loop |
| Resource idle | 4–6 | 2–4 gentle bob/shimmer |
| Resource harvest | 8–12 | 2–4 shake / spark / chop |
| Resource depleted | 2–4 | 1–2 dim / rubble |
| NPC idle | 5–8 | 2–4 breathe / sway |
| NPC chase/combat | 8–12 | 2–3 aggressive motion |
| NPC telegraph | 6–10 | 2 bright windup frames |
| Building/container | 1 | 1 (static) |
Loops must tile seamlessly in time (frame N → frame 0).
Per-kind generation workflows
A. Terrain tile (terrain.*)
Deliver: 1 mode idle, 1–4 cells, cell_size: 64, seamlessly tileable when repeated on the map (edges connect left↔right, top↔bottom).
Critical: paint a distributed ground texture filling the entire cell — not a single focal drip, splash, puddle, or rock. terrain.grass is the reference; avoid shallow_water-style centered drips that look wrong when tiled.
Prompt skeleton:
Top-down pixel art game tile, exactly 64x64 pixels, PNG with alpha transparency.
Subject: seamless [grass patch / shallow water field / rocky ground / dirt] micro-texture.
View: straight down; texture fills cell edge-to-edge; toroidal tiling (edges match neighbors).
Style: soft fantasy, muted [colors], readable at small zoom.
No text, no border, no drop shadow box, no centered hero object.
[For frame N of M]: subtle animation offset for water/shimmer — each frame stays tileable.
B. Resource node (resource.*)
Deliver: modes for idle, harvest, and optionally depleted (or map cooldown → depleted).
Visual intent:
idle: full node, invitingharvest: impact motion, chips flying, tool strike — 2–3 framesdepleted: dimmer, cracked, partial stump/empty vein
Prompt skeleton (per frame):
Top-down pixel art sprite, exactly 64x64 pixels, PNG RGBA transparent background.
Subject: [iron ore vein / oak log stack / …] for a fantasy sandbox game.
State: [idle gentle pulse | harvest impact frame 2 of 3 | depleted empty vein].
Center subject; ground contact at bottom of cell; soft fantasy palette.
1px outline optional; light from north-west.
No text, no UI, no opaque background.
C. NPC (npc.*)
Deliver: one mode per behavior state that needs a unique pose; include telegraph if combat-enabled.
Visual intent:
- Top-down / 3/4 creature, front-facing toward the camera (face toward bottom of cell) — Ultima-style single facing reused for all movement directions
- Do not author east/west side profiles or per-direction art for ordinary travel
alert: raised posture (same facing)chase: lean / foot bob on the same front-facing silhouettecombat: strike pose, still front-facingtelegraph: bright windup, obvious danger, same facing
Prompt skeleton:
Top-down pixel art character sprite, exactly 64x64 pixels, PNG with transparency.
Subject: [goblin raider / deer / …], state: [idle | alert | chase | combat windup].
Single character centered, feet near bottom center of cell.
Soft fantasy, readable silhouette, no text, transparent background.
Frame [N] of [M] for [state] loop.
D. Building piece (building.*)
Deliver: usually 1 cell, idle mode. Walls/doors/floors are architectural top-down (plan view).
Top-down pixel art building tile, 64x64, PNG transparent alpha.
Subject: [vertical wooden wall segment | closed door | floor plank].
Aligned to cell edges where appropriate; transparent outside structure.
No text; soft fantasy interior/exterior set.
E. Container (container.*)
Deliver: closed and open modes (1 frame each or short open animation).
Reference sheets (copy structure, not pixels)
| Sheet id | Modes | Notes |
|---|---|---|
resource.iron_ore |
idle×4, harvest×3, depleted×2 | Full state coverage |
resource.oak_log |
idle×3, harvest×3 | |
npc.goblin |
idle, alert, chase, combat, telegraph | Combat NPC |
npc.ada |
idle×4 | Friendly NPC bob |
container.wood_chest |
closed, open | |
terrain.shallow_water |
idle×4 | Ripple loop |
terrain.grass |
idle×1 | Static |
Image model session checklist
When asked to create or refresh a sheet, gather:
- Sheet id (e.g.
resource.copper_ore) - Kind (
resource,npc, …) - cell_size (default 64)
- Modes and frame count per mode
- Presentation mapping — which item/NPC
sprite_modeskeys will point at which modes - Subject description and color notes
Output bundle:
- All cell PNGs (square, RGBA, correct size)
- Complete
<sheet_id>.yaml - Snippet for
manifest.yamlentry (if new) - Suggested
tile_id+sprite_modesfor the content item/NPC YAML
Quality gate before handoff:
Anti-patterns (do not)
- Combined sprite sheet JPEG/PNG atlases (engine expects separate files per cell)
- Isometric or side-view character art
- Non-square “tall” props in a single cell (use square canvas; compose subject inside)
- Photorealistic renders that muddy at nearest-neighbor scale
- Gradients with heavy banding at 64×64
- Different sizes per frame within one mode
- Mode ids on the sheet that don't match any planned
sprite_modesor behavior state - Embedding text labels (“IRON”, “HP”, etc.)
Optional: batch prompt for Gemini / image LLMs
Use this as a system or instruction block when calling an image model:
You generate Flatland3 world sprite cells.
OUTPUT: Individual PNG files, one per frame. Each file must be square (64×64 default),
RGBA with true transparent background, top-down pixel art, soft fantasy style.
Do NOT output a combined atlas. Do NOT use opaque backgrounds.
For sheet "{sheet_id}" ({kind}), create modes:
{modes_list_with_frame_counts}
File names: cells/{category}/{name}_{mode}_{frame}.png
Also output the YAML sheet definition for assets/gfx/sprites/{sheet_id}.yaml
Presentation mapping for content authors:
{sprite_modes_table}
Every frame in a mode must share the same dimensions and visual scale.
Subject grounded at bottom-center of cell. No text in images.
Engine behavior summary (for model context)
SpriteCatalog::draw_spriteloads each cell PNG as its own texture- Picks mode by name (from
presentationresolve) or first mode - Animates:
frame = floor(time * effective_fps) % cell_countwhereeffective_fps = mode.fps ?? sheet.fps - Draws with
FilterMode::Nearestand scales to map cellcell_w × cell_h - Missing texture → skip cell; missing sheet → glyph fallback in TUI / tinted rect in gfx
Cross-references
- Short prompt template:
docs/gfx-tile-prompts.md - JSON Schema:
schemas/gfx-sprites.schema.json - State resolution:
crates/presentation/src/resolve.rs - Admin editor:
tools/content-admin/web/src/plugins/sprites.ts - Runtime loader:
crates/gfx/src/sprites.rs