Skip to content

Guide Art Pipeline

bryanthaboi edited this page Jul 19, 2026 · 1 revision

Art pipeline

The asset contract, the two ways a mod's art reaches the screen, and the legal posture that binds both.

The pixel contract

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 (sprites registry): image + frames, 16×16 per frame; walker = true for 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 = true on the record (pokemon, tilesets, sprites) and the palette pass leaves your image alone.

Path 1 — your own art, by path

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.

Path 2 — derived art, by transform

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")
end

The 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, icons, fonts

  • 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/$80 ranges are free — a kana block registers at base = 0x100 without touching the stock pages.

The rule

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.

Clone this wiki locally