-
Notifications
You must be signed in to change notification settings - Fork 16
Guide Art Pipeline
The asset contract, the two ways a mod's art reaches the screen, and the legal posture that binds both.
The importer decodes all vanilla art to 4-shade grayscale PNGs
(src/import/ImageWriter.lua): white 255, light 170, dark 85, black 0.
Palettes recolor those shades at render time, so art that follows the
contract picks up map/battle palettes for free. Battle pics treat shade 0
(white) as transparency — the matte pass strips it.
- Battle sprites: square, tile-aligned; the species record's
frontSize(1–7, in 8px tiles) drives battle layout. - Overworld sprites (
spritesregistry): 16×16 frames stacked into one sheet; a full walking character is a 16×96 six-frame sheet withwalker = true. See Overworld character sprites. - Tilesets: a sheet plus block definitions — each block a row of 16 tile ids (Reference: Registries).
- Want full color instead? Set
trueColor = trueon the record (pokemon,tilesets,sprites) and the palette pass leaves your image alone.
Original art ships in the mod and is referenced by path:
mod.content.pokemon:patch("MODMON", {
spriteFront = mod.assets:path("assets/front.png"),
})An overrides/ directory in the mod shadows the generated cache by
relative path with no code at all — mods/my_mod/overrides/battle/front/ mew.png replaces Mew's front sprite everywhere it is drawn
(src/render/Assets.lua). Hand-authored overrides beat transform
outputs; the highest-priority mod wins.
Art derived from vanilla must never ship. Instead the manifest declares a recipe that runs once on the player's machine against their own imported cache:
"assets_transforms": "transforms.lua"-- transforms.lua : sandboxed; return function(ctx)
return function(ctx)
local img = ctx.readImage("battle/front/mew.png")
ctx.writeImage(ctx.recolor(img, {
{ 255, 255, 255 }, { 255, 170, 200 }, { 180, 60, 120 }, { 40, 0, 20 },
}), "battle/front/mew.png")
endThe context (src/mods/AssetTransform.lua) is the whole surface:
| Member | Does |
|---|---|
ctx.readImage(rel) |
read assets/generated/<rel> from the cache |
ctx.writeImage(imageData, rel) |
write save/mod-derived/<id>/<rel>
|
ctx.exists(rel) |
probe the cache |
ctx.recolor(imageData, shades) |
remap the 4 gray buckets, lightest first |
ctx.blank(w, h, r, g, b, a) |
new canvas |
ctx.blit(target, source, x, y, ...) |
compose |
ctx.matte(image) |
white-as-transparency matte for battle pics |
No require, no love, no io, no os — the recipe can only read the
cache and write its own derived root. Outputs shadow the cache by
relative path, exactly like overrides/. A stamp (cache marker +
recipe hash) makes re-runs free until the cache or the recipe changes;
a failing recipe disables that mod's derived art and nothing else, with
the error attributed in the manager. assets.transformed fires per mod
on a real run.
-
palettes— four colors per record, raw{ {r,g,b}, ... }quadruple or{ colors = { {r=,g=,b=}, ... } }; exactly 4, validated. -
icons— party icons keyed by species id, drawn by the party menu (src/ui/PartyMenu.lua) and taking precedence over the vanilla dex-indexed default, so a modded or dex-renumbered species keeps the icon it registers. The value is either:- a built-in icon name — one of
BALL,BIRD,BUG,FAIRY,GRASS,HELIX,MON,QUADRUPED,SNAKE,WATER(uppercase, exactly as spelled) — e.g.icons:register("MODMON", "QUADRUPED"); or - a
{ image, frames? }table pointing at your own bundled art:{ image = mod.assets:path("assets/icon.png"), frames = 2 }(two 16×16 frames stacked in a 16×32 sheet for the bounce animation).
A bare string is a name, not a file — a lowercase
"quadruped"or a cache path like"assets/generated/icons/quadruped.png"will not resolve. Use the name form to reuse a vanilla icon, the{ image }form to ship a new one. The pokemon record's owniconfield takes the same value. - a built-in icon name — one of
-
font— a bare id registers a glyph page ({ image, base, glyphsPerRow?, advance?, charmap? }); a"charmap:<name>"id binds one text sequence to a glyph code. Pages above the vanilla$60/$80ranges are free — a kana block registers atbase = 0x100without touching the stock pages.
The walking sprites in the field — the player, rivals, and every NPC — are
the sprites registry (Data.sprites), a system entirely separate
from a Pokémon's battle sprites (spriteFront/spriteBack on the
pokemon record). Ids are the vanilla SPRITE_* names: SPRITE_RED
(player), SPRITE_BLUE (rival), SPRITE_OAK, and the NPC set
(SPRITE_YOUNGSTER, SPRITE_FISHER, SPRITE_BEAUTY, …); the full list is
data/generated/sprites.lua.
The sheet. A record is { image, frames, walker?, trueColor? }. Frames
are 16×16, stacked vertically into one PNG in the fixed order stand down,
stand up, stand left, walk down, walk up, walk left; the right-facing
frames are drawn by horizontally flipping the left ones, so you never
author them (src/render/SpriteRenderer.lua). Three shapes exist in the
vanilla data:
| frames | walker | sheet | use |
|---|---|---|---|
| 6 | true |
16×96 | a full walking character (player, rival, most NPCs) |
| 3 | false |
16×48 | a standing NPC — three facings, no walk cycle |
| 1 | false |
16×16 | a single-frame object (boulder, item, fossil) |
walker = true is what makes the field animate the walk frame as the
sprite steps. Match the frames/walker of the sprite you replace, keep
to the 4-shade grayscale contract (or set trueColor = true), and note
these draw through the overworld's OBP-palette recolor like everything else
in the field.
Two ways to change one. Either through the registry — patch one
field, override the whole record, or register a new id
(Registries):
mod.content.sprites:patch("SPRITE_RED", { -- reskin the player
image = mod.assets:path("assets/red_ow.png"),
})
mod.content.sprites:register("SPRITE_MODNPC", { -- a brand-new NPC sprite
image = mod.assets:path("assets/modnpc.png"), frames = 6, walker = true,
})…or the overrides/ folder with no code at all: a PNG at
overrides/sprites/<name>.png shadows the cached
assets/generated/sprites/<name>.png wherever it is drawn, exactly like
the battle/front/ example above. The <name> is the record's image
basename (SPRITE_RED → red.png):
mods/my_mod/overrides/sprites/red.png
The rule below applies either way: an original sprite you drew ships freely; a recolor derived from the vanilla sprite must go through an asset transform, never shipped pixels.
Distribute the transform, never the pixels. modkit lint (MK301–MK304)
and modkit pack compare candidate assets against cache-derived
signatures and refuse matches — byte-identical and perceptually near-
duplicate. Original art is yours to ship freely; it never hits the lint.
Engine 1.0.0 · mod API 2 · repository · Discord
This project ships no ROM data. All game content is decoded on the player's machine from their own verified Pokemon Red ROM; mods ship recipes and original assets, never extracted content.
Start here
Concepts
Tutorials
- The ladder
- 01 Sprite and Text Tweak
- 02 Balance Patch
- 03 New Species
- 04 New Item and Ball
- 05 New Move
- 06 NPC and Dialogue
- 07 New Map
- 08 Quest
- 09 Custom Music
- 10 New Mechanic
- 11 Custom UI Screen
- 12 Mini Total Conversion
Reference
Guides
- Art Pipeline
- Audio Authoring
- Translations
- Total Conversions
- Link Compatibility
- Publishing a Mod
- Docs Style Guide
Playing