Art book
The Art Book
How every pixel is made.
Every pixel in Væ Victis is one of 32 colours, and every one of them has a known source: a recipe in code, or a 3D figure rendered and quantised by code. This is the tour, with pictures made only by that pipeline. There is no photograph, no painting and no generated image in it.
1. The palette
The palette is fixed in src/core.js (PAL_LIST): 32 entries in nine families, a game of blacks, stone, bone, blood, rust, verdigris and one cold blue, with a single violet kept for spells. A picture is a grid of numbers 1 to 32 (0 is clear). The screen is 320 by 240 and nothing in the game draws a colour that is not on this list; the debug overlay counts what reaches the screen.
#050407
#0d0b12
#1a1520
#2a2630
#48434f
#77707d
#a8a0a6
#8c7d68
#c4b392
#eee4cc
#240408
#520a11
#8e1318
#c8231f
#ff4f3a
#2b1810
#553221
#875634
#c39535
#f3d37a
#0b1c1d
#15332f
#275651
#468a7f
#8cc6b3
#0a1030
#1b2d6c
#3658bd
#86a8ff
#6a5a63
#b9a6a6
#8a4fc0
2. Characters: a 3D model, eight directions, thirty-two colours
A figure starts as a rigged 3D model: the hero and the people are Clint Bellanger's Flare models (CC-BY 3.0), the props are KayKit packs (CC0). They are composed on one rig, recoloured and costumed at render time (tools/render/jobs-*.mjs: the Tower's soldiers, the gutter's watch, the orphanage's sisters, the Wheel's circus).
tools/render/render.mjs poses the model bone by bone (tools/render/hero-moves.mjs), lights it, renders it from 8 directions at 56 pixels in a headless browser, and quantises every pixel to the 32-colour palette by material. The output is src/sprites-<name>.js: one string of palette indices per frame. The sheet below is that data drawn back out, nothing else.

Data:
src/sprites-hero.js (tools/render/render.mjs, jobs in tools/render/hero-moves.mjs). 20 of the 32 colours are used.
Data:
src/sprites-tw_earl.js (tools/render/jobs-tower.mjs). 19 of the 32 colours are used.
Data:
src/sprites-or_priest.js (tools/render/jobs-orphanage.mjs). 15 of the 32 colours are used.
Data:
src/sprites-hero_bat.js (tools/render/hero-hooks.js). 20 of the 32 colours are used.3. Paintings: a recipe, a field, a dither
A picture that is not a figure (a texture, a portrait, a still, a stained-glass window, the world map) is a RECIPE: a short script in tools/art/paint/ that sets a pixel field (a material, a ramp of palette keys and a value per pixel) with seeded noise, signed-distance shapes and a Bayer dither, then resolves it to palette indices. tools/art/paint.mjs runs it inside the game and writes the pictures as run-length strings.
A recipe is deterministic: run it twice and you get the same picture. That is the provenance. No generated image is a source, a reference or an input of any kind (v83, 2026-10-05).

Recipe:
tools/art/paint/tex-haven.js
Recipe:
tools/art/paint/portrait-lord.js and portrait-render.js
Recipe:
tools/art/paint/still-haven.js
Recipe:
tools/art/paint/still-tower.js
Recipe:
tools/art/paint/still-orphanage.js
Recipe:
tools/art/paint/still-hub.js4. The dither, close up
Zoom into any picture and the dither shows: the game never blends two colours, it places them in a Bayer pattern, so a ramp of four steps reads as a smooth shade at the game's size and as a lattice here.

5. A recipe, line by line
This is the recipe of the haven's floor, the first texture above (tools/art/paint/tex-haven.js). A material, a value and a seed per pixel, nothing else: no image is read.
const F = T.cobbles(96, 96, { n: 3, m: 3, seed: 81, joint: 1, lo: .32, hi: .78, dome: .3, jit: .75 });
F.each((x, y, p) => { // the blood: dried black-red in the joints, rust spreading out from them in a ragged tide-line
const c = pvor(x + .5, y + .5, 96, 96, 3, 3, 81, .75), n = pfbm(x, y, 96, 96, 6, 83, 4);
const reach = c.edge - n * 10;
const pool = pfbm(x, y, 96, 96, 3, 84, 4);
if (p.m === 'void') return { m: pool > .5 ? 'gore' : 'void', v: .25 + n * .15 };
if (pool > .54 && reach < 4) return { m: 'gore', v: .2 + n * .25 };
if (pool > .46 && reach < 8) return { m: 'rust', v: clamp(p.v * .6, 0, 1) };
return null; });
const r = K.rng('bfcrack');
for (let i = 0; i < 5; i++) { let x = r() * 96, y = r() * 96, a = r() * K.TAU; for (let k = 0; k < 18 + r() * 14; k++) { x += Math.cos(a); y += Math.sin(a); a += (r() - .5) * .8; F.set(wrap(x | 0, 96), wrap(y | 0, 96), 'gore', .12); } }
return F.toPix();
} };
// the aisles' wet stone: dark irregular stones, black water standing between them, green slime and a glint where it pools
6. Where the pieces come from
| Source | What it makes | Licence |
|---|---|---|
| Clint Bellanger, Flare hero and heroine | The hero and every person, recoloured and costumed at render time | CC-BY 3.0 |
| Kay Lousberg, KayKit | The props of the graveyard, the dungeon and the furniture | CC0 1.0 |
| Mathers and Crowley, Goetia (1904) | The seals | Public domain |
| Serifeus Studio, tools/art/paint/ | Textures, portraits, stills, cards, windows, the map, the title lettering | Ours: code and recipes |
Full credits: CREDITS.md in the repository, and the game's credits screen.
The making of the game
Where the game came from, what was decided on the way, and the rule that turned out to matter most.