Home Gfx Sprite Art Generation Spec

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:

  1. Write PNGs under cells/…
  2. Create or update <sheet_id>.yaml
  3. Add an entry to manifest.yaml if new sheet
  4. Set item/NPC tile_id: <sheet_id> and optional sprite_modes in content YAML
  5. 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): availableidle, harvestingharvest, cooldowndepleted 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: modes closed, open
  • building.door_closed / building.door_open: separate sheets or modes
  • Animated terrain (water): idle with 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 cooldowndepleted).

Visual intent:

  • idle: full node, inviting
  • harvest: impact motion, chips flying, tool strike — 2–3 frames
  • depleted: 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 silhouette
  • combat: strike pose, still front-facing
  • telegraph: 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:

  1. Sheet id (e.g. resource.copper_ore)
  2. Kind (resource, npc, …)
  3. cell_size (default 64)
  4. Modes and frame count per mode
  5. Presentation mapping — which item/NPC sprite_modes keys will point at which modes
  6. Subject description and color notes

Output bundle:

  1. All cell PNGs (square, RGBA, correct size)
  2. Complete <sheet_id>.yaml
  3. Snippet for manifest.yaml entry (if new)
  4. Suggested tile_id + sprite_modes for 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_modes or 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_sprite loads each cell PNG as its own texture
  • Picks mode by name (from presentation resolve) or first mode
  • Animates: frame = floor(time * effective_fps) % cell_count where effective_fps = mode.fps ?? sheet.fps
  • Draws with FilterMode::Nearest and scales to map cell cell_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