Væ Victis

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.

The making of the game

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.

1 k
#050407
2 n0
#0d0b12
3 d1
#1a1520
4 gr0
#2a2630
5 gr1
#48434f
6 gr2
#77707d
7 gr3
#a8a0a6
8 bn0
#8c7d68
9 bn1
#c4b392
10 w
#eee4cc
11 r0
#240408
12 r1
#520a11
13 r2
#8e1318
14 r3
#c8231f
15 r4
#ff4f3a
16 br0
#2b1810
17 br1
#553221
18 br2
#875634
19 gd
#c39535
20 y
#f3d37a
21 v0
#0b1c1d
22 v1
#15332f
23 v2
#275651
24 v3
#468a7f
25 v4
#8cc6b3
26 b0
#0a1030
27 b1
#1b2d6c
28 b2
#3658bd
29 b3
#86a8ff
30 sk0
#6a5a63
31 sk1
#b9a6a6
32 p1
#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.

Wick walking
Wick walking. The Flare hero model on the studio's rig, rendered from 8 directions (rows: S, SE, E, NE, N, NW, W, SW) and 8 frames of the walk, each pixel quantised to the palette. 8 frames of the walk clip, 54 by 50 pixels each (shown at 3x).
Data: src/sprites-hero.js (tools/render/render.mjs, jobs in tools/render/hero-moves.mjs). 20 of the 32 colours are used.
The Earl of the Tower walking
The Earl of the Tower walking. The same model recoloured and costumed at render time (a cape, plate, a greatsword), by the Tower's job. 8 frames of the walk clip, 64 by 58 pixels each (shown at 3x).
Data: src/sprites-tw_earl.js (tools/render/jobs-tower.mjs). 19 of the 32 colours are used.
Father Fulcran walking
Father Fulcran walking. The same model again with a cassock, a beard and a staff, by the orphanage's job. 8 frames of the walk clip, 24 by 49 pixels each (shown at 3x).
Data: src/sprites-or_priest.js (tools/render/jobs-orphanage.mjs). 15 of the 32 colours are used.
Wick as the bat
Wick as the bat. The bat form: a second sheet of the same hero, posed in code. 8 frames of the bat clip, 74 by 66 pixels each (shown at 3x).
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).

The haven's floor (a texture, tiled 2 by 2)
The haven's floor (a texture, tiled 2 by 2) (192x192, 10 colours). Slabs of stone, blood dried into the joints and spreading from them: periodic noise and Voronoi cells, resolved through a ramp with a Bayer dither.
Recipe: tools/art/paint/tex-haven.js
Wick's portrait in the HUD
Wick's portrait in the HUD (54x116, 19 colours). Rendered from his own 3D model on the rig under one candle and a moon rim, crushed to the palette by material, the face and blood drawn in code.
Recipe: tools/art/paint/portrait-lord.js and portrait-render.js
The empty throne (a still)
The empty throne (a still) (240x240, 27 colours). Composed in the engine: the haven's own class draws itself from a fixed camera, moonlight on.
Recipe: tools/art/paint/still-haven.js
The Tower at evening (a still)
The Tower at evening (a still) (240x240, 26 colours). A stage built from the game's own textures and rendered figures, props and lights in code.
Recipe: tools/art/paint/still-tower.js
A stained-glass window
A stained-glass window (30x54, 13 colours). Glass drawn as a field of cells with a lead came, each pane a ramp of the palette.
Recipe: tools/art/paint/still-orphanage.js
The world map
The world map (320x240, 9 colours). The city on the hill: a landscape field, a river, the roads and the houses' marks.
Recipe: tools/art/paint/still-hub.js

4. 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.

a dithered corner at 8x
A 60 by 40 pixel corner of the Tower still (still_tower_evening), shown at 8x.

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

SourceWhat it makesLicence
Clint Bellanger, Flare hero and heroineThe hero and every person, recoloured and costumed at render timeCC-BY 3.0
Kay Lousberg, KayKitThe props of the graveyard, the dungeon and the furnitureCC0 1.0
Mathers and Crowley, Goetia (1904)The sealsPublic domain
Serifeus Studio, tools/art/paint/Textures, portraits, stills, cards, windows, the map, the title letteringOurs: 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.

Read the making ofPlay now, free in your browser

Every picture on this page is a live frame of the game, the game's own carved title, or a figure drawn by the game's own pipeline. No painted or generated art.