-
Notifications
You must be signed in to change notification settings - Fork 2
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):image+frames, 16×16 per frame;walker = truefor two-frame walkers. - 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 (a string icon name or{ image, frames? }). -
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.
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
Playing