LANTERNHOLD — Art Bible
Status: iteration 1 first draft.
- No reference image exists yet (
docs/reference/inspiration.pngis missing). Direction below comes from the written brief. Once the image arrives, re-check §2 and §4 against it.- Canonical palette:
docs/art/palette.hex.- Mockups so far:
art/mockups/m01_palette_swatch.png.
The look in one line: warm light against cool dark. An indigo night, amber windows and firelight, cyan wisps and crystal, and violet only where something is wrong. Most of the richness comes from systems (lighting, palette, particles, weather, parallax), not from drawings. The asset set is small. The renderer makes it rich.
1. Resolution, tiles, sprites, camera
1.1 Screen and tiles
- Internal resolution: 480 × 270 px, 16:9. Every element (world, UI, text) renders at this one pixel grid. Never mix pixel scales.
- Output: integer scale only (×2 = 960×540, ×3 = 1440×810, ×4 = 1920×1080, ×8 = 4K), with black letterbox for non-integer windows. Nearest-neighbor filtering only.
- Tile: 8 × 8 px. The view is 60 × 33.75 tiles, so the whole surface of the island (60–76 tiles wide) is roughly one screen wide, like a diorama.
1.2 Sprite sizes
Sizes are frame size in px. Anchor is bottom-center unless noted.
| Thing | Frame | Notes |
|---|---|---|
| Player | 12 × 16 | body about 7 px wide; collision 6 × 14 |
| Wick (cat) | 10 × 8 | |
| Clay / Bone / Glass / Root golem | 8×10 / 8×12 / 8×11 / 10×12 | |
| Wisp | 3 × 3 core | procedural trail |
| Gleaner / Arranger / Whisperer / Dimmer | 12×7 / 8×20 / 14×12 / 11×7 | |
| Frog / moth | 6×4 / 3×2 | |
| Lantern Tree | 48 × 64 | |
| The Heart | 64 × 64 | |
| Keeper skeleton | 16 × 12 | |
| Machines | tile size × 8 | e.g. Alembic 16 × 24, Crucible 16 × 16 (GDD §6.2.3) |
| Item icon | 12 × 12 | inside 16 × 16 UI slots |
| Essence glyph | 7 × 7 | 9 × 9 with tier ring |
1.3 Camera
- Follows the player with a dead-zone of 64 × 40 px and lerp 0.15 per frame. The position is snapped to whole internal pixels, so there's no sub-pixel shimmer.
- Look-down: hold Down to shift the view 80 px. In shafts, the camera leads 40 px in the direction of travel.
- Map view: 1 px per tile for the whole island (128 × 256), with any lens overlay.
- No zoom. Pixel scale is sacred.
2. Palette
64 colors in 8 ramps × 8 shades (dark to light, index = ramp·8 + shade). Each ramp is hue-shifted: dark ends lean indigo and light ends lean warm or cyan by role. Mockup m01 confirmed every ramp is monotonic in OKLab lightness.
| Ramp | Role | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|---|
| R0 NIGHT | void, sky, shadow, ink | #06050d |
#0b0a1a |
#11122a |
#181b3d |
#212752 |
#2c3567 |
#3a467d |
#4b5a94 |
| R1 SKY | moon, clouds, rain, moonlit rims, distant islands | #2a3350 |
#3b4769 |
#4f5e84 |
#6779a0 |
#8396bb |
#a4b6d4 |
#c8d6ea |
#eef3fa |
| R2 EMBER | firelight, windows, brass, warm UI | #1f0e0c |
#3b1712 |
#632614 |
#8f3b17 |
#bb5a1d |
#e0832b |
#f5b14a |
#ffe29a |
| R3 GLIM | wisps, crystal, cool magic, glass | #071c24 |
#0b3138 |
#0f4d50 |
#156f6b |
#20968a |
#3fbfa9 |
#82e3cd |
#d2fbef |
| R4 FLUX | corruption, roots, the Heart, warnings | #160a1c |
#2a0f30 |
#461549 |
#661e62 |
#8c2c7a |
#b44593 |
#d76db0 |
#f3a8d4 |
| R5 EARTH | soil, wood, bone, parchment | #160f12 |
#261a1a |
#3a2720 |
#533826 |
#6f4e33 |
#8f6a48 |
#b8956f |
#e0cba3 |
| R6 STONE | rock, iron, pipes | #0f1018 |
#1a1c27 |
#272a39 |
#363b4e |
#4a5065 |
#61687e |
#7f8699 |
#a7adbb |
| R7 LIFE | foliage, moss, crops | #0a1412 |
#10221c |
#173326 |
#20462f |
#2c5c38 |
#3e7542 |
#5e9150 |
#8fb46a |
2.1 Palette rules
- No color outside the palette ever reaches the screen. This includes lighting, UI, particles and warp effects (§3). There is no pure black and no pure white.
- Violet means wrong. R4 shades ≥ 3 are reserved for flux, corruption, roots, the Heart, and error and warning states. Nothing cozy uses R4.
- Danger is violet, never red. The palette has no true red, and R2 shades 1–3 are rust-brown.
- Warm vs cool is a gameplay signal. R2 highlights mean warm light (safety from entities). R3 highlights mean cool light (flux suppression). Never use R2 or R3 shades ≥ 5 on non-emissive decoration.
- Value hierarchy:
- Interactive foreground tiles use shades 2–5.
- Background walls use the same ramp at −2 shades (minimum 0).
- Distant parallax uses only R0 and R1 shades 0–3.
- Emissives use shades 6–7.
- Gameplay-critical objects (machines, lamps, chests) must keep a shade difference of ≥ 2 from the tile behind them.
- Per-sprite budget: at most 12 colors and at most 4 ramps. Tiles use at most 6 colors from at most 2 ramps.
2.2 Essence signature colors
Pipes, glyphs and jars show essences in these colors. The glyph shape is always the primary identifier; color is secondary, for colorblind safety.
| Essences | Color |
|---|---|
| Ember | R2·5 |
| Dew | R1·4 |
| Loam | R5·5 |
| Breath | R1·6 |
| Glim | R3·5 |
| Hush | R0·6 |
| Taint | R4·5 |
| Compounds | a fixed palette index per compound: T1 from R1/R5/R6/R7 mid shades, T2 from shades 4–6 of R2/R3/R5/R7, T3/T4 from R4·6, R3·7, R2·7, R1·7 |
Identity colors are set in data/essences.json and don't change per seed.
2.3 Depth shifts (by zone)
Each stratum is a lighting zone with its own ambient floor, darkness target ramp and allowed terrain ramps. Terrain gets fewer hues with depth, so lights and flux pop harder the deeper you go.
| Zone | Ambient floor | Darkness falls toward | Terrain ramps (+ light ramps) |
|---|---|---|---|
| 0 Surface | R1·3 × 0.10 | R0 | R0, R1, R5, R6, R7 (+R2, R3) |
| 1 Fossil | R5·3 × 0.04 | R5·0 / R0·0 (earthy dark) | R5, R6 (+R0, R2, R3) |
| 2 Crystal | R3·3 × 0.06 | R3·0 / R0·0 (teal dark) | R6, R3, R1 (+R0, R2) |
| 3 Roots | R4·3 × 0.05 | R4·0 (plum dark) | R4, R5 (+R0, R3) |
| 4 Heart | R4·2 × 0.03 | R0·0 | R0, R4, R3 |
| 5 Tide (any cell under the tide line) | R0·4 × 0.02 | R0·0 | as underlying zone; fog overlay |
| 6 Dawn (Mend endless) | R2·7 × 0.60 | R1·2 | all |
2.4 Warp shifts
All warp shifts are post-LUT index remaps, so they stay palette-compliant.
| Warp | Effect |
|---|---|
| W ≥ 20 | 8 px dithered R4·1 vignette at the screen edges (25% coverage); one peripheral shape per 40 s |
| W ≥ 40 | Shadow breath: every 20–40 s, R0·0–2 remap to R4·0–2 inside a random 96 × 64 region for 6 frames |
| W ≥ 60 | Echo entities; one UI glyph swaps for one frame every 30 s |
| W ≥ 80 | A second moon (R4·6 limb, R4·7 core) that has no pond reflection; stars align into an eye shape for 2 s every 5 min |
Warp effects never appear inside the hearth sanctuary.
3. Lighting model (central system)
Light is mood and gameplay at once, so a single model drives both.
3.1 Gameplay light (from the sim)
Per tile, a warm and a cool level (0–15), computed by the sim (GDD §6.4).
3.2 Render light field
- Upload the tile light grid as an RG8 texture (warm, cool). Sample it bilinearly at tile centers, which gives a smooth falloff per pixel.
- Convert to 8 render levels:
l = level/15 · 7. - Quantize with a 4 × 4 Bayer ordered dither anchored to world pixel coordinates (not
screen coordinates), so the dither doesn't crawl when the camera moves:
L = clamp(floor(l + (bayer/16 − 0.47)), 0, 7). - Flame flicker: the warm level of flame sources is modulated ±1 (on the 0–15 scale) by stepped 1D noise at 8 Hz. Wisp light breathes ±1 at 0.5 Hz.
3.3 The LUT (palette-locked lighting)
Every on-screen pixel is: albedo palette index + zone + warm level W (0–7) + cool level K (0–7) → output palette index, through a precomputed lookup table.
- LUT texture: 64 albedo × (7 zones × 8 W × 8 K), so 64 × 448 in R8UI.
- Built offline by
lanternforge lut:
for zone z, albedo i, W in 0..7, K in 0..7:
A = srgb_to_linear(P[i])
E = amb[z] + (W/7)^1.4 · 1.8 · lin(R2·6) + (K/7)^1.4 · 1.5 · lin(R3·6)
c = clamp(A ⊙ E, 0, 1)
j = argmin_j ΔOKLab(c, P[j]) + penalty(i, j, W, K)
penalty = 0 if ramp(j) ∈ {ramp(i), darkRamp[z]}
= 0 if W > K and ramp(j) = R2, or K > W and ramp(j) = R3
= 0.04 otherwise
post-pass: enforce monotonic lightness in W and in K
(if L(lut[W][K]) < L(lut[W−1][K]), copy the previous value)
- Emissive pixels (a per-pixel flag from sprite metadata) skip the LUT and output their albedo. Window panes, flames, wisps, charged crystal cores, pilot lights and root-blooms are emissive.
- Result: a lantern-lit stone wall turns into amber-tinged stone shades, a wisp-lit wall into teal-tinged ones, and a dark wall into indigo silhouette. AI-generated and procedural assets go through the same LUT, which is what makes them sit together (Milestone 1, acceptance 4).
3.4 Darkness rendering
- Cells with no light and no ambient resolve to the zone's darkness ramp shades 0–1. Darkness has shape: explored solid tiles next to explored air keep a 1 px R0·2 "memory edge", so caves read as remembered outlines.
- Unexplored cells are flat R0·0.
- Real entities take 2 levels from the light field in their own cells (the sim does this), so they cut shadows into lamp glow. Hallucinations don't (GDD §6.6).
3.5 Atmosphere and halos
- Parallax layers sample the light field at 35% strength at their world position. Air around a lit window glows amber against the sky with no bloom shader.
- Rain particles sample at 100%, so rain turns amber as it falls past a window and teal past a wisp. This is a signature cozy image, and it costs nothing.
- Fog and mist sample at 60%.
3.6 Implementation (WebGL2)
- Index pass: draw all world sprites and tiles into an R8UI color target holding the palette index, plus an R8 flags target (emissive, entity, UI, unlit). Use integer positions only.
- Resolve pass (full-screen): read the index, flags, light texture (bilinear), zone texture (tile resolution, nearest), Bayer texture, LUT texture and palette texture. Output RGB.
- Post-index remap: warp effects, the tide fog, lens overlays (overlays are drawn as indices with the "unlit" flag).
- UI pass: the same index buffer with the unlit flag, so UI is palette-exact.
This pipeline is the M1 renderer. Mockup M02 (planned) will prototype the LUT in Python before the build.
4. Environment
4.1 Sky (code)
- A vertical gradient across R0·5 (top) → R0·2 (horizon band) with dithered transitions. The sky darkens with depth: below the surface line it becomes cave darkness.
- Stars: 180 per screen-width of sky in 3 sizes (1 px R1·5, 1 px R1·7, plus-shapes R1·6/7). They twinkle by palette cycling between shades 5 and 7 with a random 2–6 s period.
- Moon: a 40 px disc, R1·5–7, with procedural craters (3–5 discs of R1·5). Phase is drawn with a terminator ellipse. The halo is a dithered R0·6/R0·7 ring 10 px wide, which matches m01. The moon's reflection in the pond is a column of R1·6 dashes.
4.2 Clouds (code)
Three layers built from two octaves of value noise, thresholded into 3 bands:
- Far (R1·0–1, parallax 0.1): wide, slow, 2 px/s.
- Mid (R1·1–2 with R1·3 moonlit tops, parallax 0.3): 5 px/s.
- Near (rare, R1·2–3, parallax 1.15, partly transparent with dither): wisps of cloud that cross in front of the island.
Wind sets direction and speed (×(1 + strength)). Rain fronts darken the layers by one shade and cover the moon.
Poisoned clouds (gameplay signal, GDD §15.3): when the flux reservoir R is high, cloud bands swap their R1 shades for the same shades of R4 (one band per 2,000 motes of R). A violet sky means the next rain will carry vented flux back down.
4.3 Rain (code)
- Streaks 1 × 3 px (light) or 1 × 4 px (heavy), slanted 1 px per 4 px of fall at wind ≥ 2.
- Two depth layers: the far layer is R1·2 and isn't lit; the near layer is R1·3 and lit by the light field (§3.5).
- A 2-frame splash on solid tops. Roofs drip in a column at the eaves.
- Budget: 400 drops at intensity 3.
4.4 Parallax stack
| Layer | Speed (×camera) | Lit |
|---|---|---|
| Sky gradient | 0 | — |
| Stars | 0.02 | — |
| Moon | 0.03 | — |
| Far clouds | 0.10 | — |
| Distant islands | 0.15 | — |
| Mid clouds | 0.30 | 35% |
| Island (tiles, sprites) | 1.0 | 100% |
| Near rain | 1.0 | 100% |
| Near clouds | 1.15 | 35% |
Distant islands (code) are procedural silhouettes 20–60 px wide in R0·3–4 with a dangling root fringe and 1–2 window dots of R2·6. One window goes dark every 3 Nights (GDD §13.2).
4.5 The void below
- A gradient from R0·2 to R0·0 below the island.
- The underside fringe has dangling roots (R4·1) swaying ±1 px.
- Occasionally (1 per 5 min) a "deep shape" drifts past: a large, soft R0·0 silhouette against R0·1, parallax 0.2, gone in 20 s. Pure atmosphere.
- The Visitor (code): a 320 × 96 noise silhouette crossing the void at parallax 0.4, which hides stars and clouds behind it.
4.6 Tilt rendering
The island stays grid-aligned. Every procedural background layer (sky gradient, stars, clouds, distant islands, horizon) is evaluated in rotated coordinates at −θ around the screen center. Because these layers are procedural, rotation stays crisp with no rotated textures and no jaggies. Hanging lamps and chains swing to stay vertical in world space.
4.7 Strata looks
| Stratum | Terrain | Light character | Signature detail |
|---|---|---|---|
| Surface | Grass R7·2–4 with R1·4 moonlit top edge and 1–2 px tufts overhanging the edge; soil R5·1–3; clay R5·3–4 with R6·3 flecks; house planks R5·2–4; shingle roof R6·2–3; window panes R2·7 emissive with R2·5 mullions | moonlit, warm pools at windows | the lit window seen from the island edge |
| Fossil | Shale R6·1–3 with horizontal 1 px banding of R5·2; limestone R5·4–6, pale enough to bounce lantern light so walls glow warm; bone inclusions R5·6–7 with R5·4 shadows; bone moss R3·4 dots | pitch dark between lamps; earthy darkness | a skeleton seen first as a pale shape at the edge of the lantern |
| Crystal | Stone R6·2–5; crystal facets R3·4–6 with R1·7 glints and emissive R3·7 cores; glass lichen R3·5 specks; tainted crystal R4·4–6 | self-lit teal ambience | the cave that is bright before you bring any lamps |
| Roots | Root-wood R4·1–3 with fibrous 1 px R5·3 striations; a slow pulse of a +1 shade wave travelling along connected root tiles every 6 s (palette cycling); root-blooms R1·6 / R3·6 emissive | plum dark | a pipe bent into a curve |
| Heart | Heartstone R0·1–3 with R4·4 veins | — | the Heart (64 × 64, hand-finished): the left half is R3 crystal facets, the right half R4 root knots, with a central emissive R3·7 / R4·7 seam; the inverse light is a ring of −3 levels that breathes on a 12 s cycle |
| Tide | dithered R0·0–2 fog sliding horizontally at 3 px/s; lamp halos look clipped | — | — |
5. Characters, creatures and entities
5.1 Silhouette rules
- Every character must read as a black silhouette at 1×.
- Living things are rounded (cozy).
- Golems are blocky and slightly wrong: limbs too short, a head that doesn't sit centered.
- Void entities are shapes of absence: bodies of R0·0, edged with a 1 px R4·2 or R4·3 rim, with exactly two 1 px R1·6 eye glints at most. Never teeth, blood, flesh or wounds.
5.2 Animation frame counts
Frame rates are in fps; animation is stepped.
| Character | Animations | Method |
|---|---|---|
| Player (keeper, hooded coat, scarf) | idle 2 @1, walk 6 @10, jump 1, fall 1, climb 4 @8, use/dig 3 @12, sleep 1. A held item is drawn at a per-frame hand anchor, and the held lantern is a real light source. | AI base + hand frames (D01) |
| Wick | idle 2 @1, walk 4 @8, sleep 2 @0.5, hiss 2 @6 (arched back, R1·7 eye glints), pet 2 @4 | hand (D02) |
| Golems | walk 4 @6, work 2 @6, head-turn 2 (they turn to watch you), wait-facing-dark 1. Eyes are R3·6 in light and R4·5 in darkness. | AI body + code puppet legs (stubs drawn by code, which gives deliberately stiff motion) |
| Wisps | a 3 × 3 core (R3·7 center, R3·5 cross), a trail of 4 fading R3·5/R3·4 pixels, sine bob ±2 px at 0.7 Hz, a 1 px R3·6 sparkle every 1.5 s | code |
| Gleaner | skitter 4 @12, carry 4 @12 (the stolen item icon rides on its back, dimmed one shade) | AI body + code legs |
| Arranger | idle 1 plus a code "fold" (vertical squash in 1 px steps), walk 2 @3, work 3 @4 | AI base |
| Whisperer | 1 frame plus 3–6 procedural tendrils (1 px lines, R4·2) that curl toward the player | AI base + code |
| Dimmer | wing flap 2 @8. Wings are R0·0 with an R4·3 rim and a pale R1·3 "eye" spot on each wing. | code |
| Echoes (hallucinations) | identical to the real sprites | sim flag |
| Frogs | idle 2, hop 2 | hand (D07) |
| Moths | 2 px bodies, 1 px wings flickering | code |
| Keepers' remains | 6 poses, 16 × 12 (Ilse: 24 × 12, curled) | AI + hand polish of Ilse (D06) |
6. Machines and pipes: the visual language of automation
6.1 Materials (fixed ramp per material)
- Brass: R2·3–5
- Iron: R6·2–5
- Glass: R3·2–4 with an R1·6 highlight pixel
- Wood: R5·2–5
- Bone: R5·5–7
- Root: R4·1–3
6.2 Silhouette by function
| Function | Shape cue |
|---|---|
| Distill (Alembic) | tall, with a bulb |
| Combine (Crucible) | squat, a bowl |
| Store | box or jar |
| Move (Bucket Lift, Chute) | vertical rails |
| Purify (Scrubber) | vents and fins |
| Protect (Ward) | a monolith with a glyph |
| Bind (Pylon) | a cage of bars |
Each function owns one silhouette family, so a player can identify a machine from its shape alone at 1×.
6.3 Pilot light
Every machine has a 2 × 2 pilot light at its top-right, which is emissive:
| State | Display |
|---|---|
| Working | steady R2·6 |
| Starved | R2·6 blinking at 1 Hz |
| Output blocked / full | steady R3·6 |
| Flux-strained / error | R4·6 blinking at 2 Hz |
| Off | R0·2 |
6.4 Working motion
Code overlays add 2–4 frame motion only while a machine is working: bubbles rising in glass (2 px, R3·6), a flame (3 frames), and a gear (5 × 5, 4 rotation frames drawn by code). A machine that isn't moving isn't working. That's the single most important readability rule.
6.5 Pipes (code, 8 × 8 autotile, 16 connection masks)
- A 4 px-wide iron body: R6·3 casing, R6·5 top highlight, R6·1 underside.
- A 2 px window down the center shows the fluid in its signature color, alternating shades s and s−1 in 2 px dashes. The dashes move along the flow at a speed proportional to throughput.
- An empty pipe shows an R0·1 window.
- The locked fluid type is always shown by a 1 px rivet at each joint in the type color.
- A blocked junction shows a blinking R4·6 cross.
6.6 Containers and golem routes
- Jars and cisterns: a fill level shown as colored rows inside the glass (8 steps).
- Golem routes: shown with the chalk tool held, as a dotted R1·6 line with numbered waypoint chalk marks (3 × 5 digits). Arranger spirals use R1·5 chalk too: a 5 × 5 spiral with a single R4·5 pixel at the center.
6.7 Lamps
- Each lamp body is a sprite. Its flame or light is a separate emissive overlay.
- A starved or guttered lamp shows a cold R6·2 wick.
- A straining lamp flickers its light-field level and its flame overlay stutters.
7. Flux, mutation and warp visuals: eerie, never gory
7.1 Air flux (no lens)
Particles in R4, never solid color:
| Band | Look |
|---|---|
| Trace (10–49) | 1 dim drifting mote per 128 px² (R4·1, 1 px). A lamp-held workshop at capacity sits here (S01: 3–14 motes), so a working factory visibly breathes without looking poisoned. |
| Tinged | 1 sparkle per 64 px² (R4·5, 1 px, lives 0.5 s) |
| Fouled | drifting haze: 1 px R4·2/R4·3 dither dots at 1/16 coverage, sinking 1 px/s (flux is heavy) |
| Thick | 1/4 coverage plus slow curls |
| Rift | 2–4 px tear lines of R4·6 bordered by R0·0, which open and close over 2 s |
7.2 Soil and water flux
- Soil flux > 150: procedural violet veins (R4·3 one-pixel crack paths) over the tile. At > 280, the veins pulse.
- Water flux: the water surface shade shifts from R0·4/R3·3 toward R4·3/R4·2 as dissolved flux rises. The pond's moon reflection fades.
7.3 Crystal taint
Charge < 0.5 swaps the R3 shades for R4 shades of the same index, and the emissive core dims to R4·5.
7.4 Mutation (palette and shape, never flesh)
- Turned: 30–50% of R7 pixels swap to R4 shades of the same index, and 1–3 extra "eye" pixels (R3·7 with an R0·0 pupil pixel beside them) are stamped onto bark, wheat heads or frog backs. Shapes stretch 1–2 px taller.
- Hollowed: interior pixels become R0·0, keeping a 1 px rim of the original ramp, as if the organism were a hole shaped like itself. Hollowed organisms give off a faint R4·2 shimmer.
7.5 Hallucinations
Identical sprites, but they never darken light, never appear in reflections, and never leave a flux wake. The visual rules guarantee the tells (GDD §6.6). There are no "glitch" shaders and no chromatic aberration. The pixel grid always stays intact.
7.6 Collapse and cracks
- Cracks: 3 overlay stages of R0·0 one-pixel crack lines.
- Dust: 1 px R5·5 particles falling.
- Collapse: a 1 px screen shake for 0.3 s.
8. UI style, fonts and icons
- Panels: parchment (R5·6 fill, R5·7 highlight row) with ink text (R0·1), a 1 px brass frame (R2·4, with R2·3 on the shadow side) and a 1 px R0·0 drop shadow. 9-slice with 4 px corners, drawn by code.
- Selection: a 1 px R3·5 outline that pulses to R3·6.
- Warnings: R4·5 with a glyph. Never red.
8.1 Fonts (no CC0, no purchase)
Fonts are defined as bitmap data in data/fonts/*.json (glyph rows as strings), and the
build agent authors them.
- Lantern 5×7: proportional, 3–5 px advance, 7 px cap height, 2 px descenders, 10 px line
height. All UI text. Printable ASCII plus
° · — … ✓ ✗ ←→↑↓. - Wick 3×5: digits, uppercase and
+−%/:. Stack counts and machine numbers. - Keeper Hand: Lantern 5×7 with a 1 px shear every 3 rows (generated by code) and ink in R5·1. Used for journals and the Keeper's Log. Warped keepers' text uses R4·4 ink with a ±1 px vertical jitter per glyph.
8.2 Icons
- Item icons: 12 × 12, one ramp-consistent sprite each, with outlines by the lanternforge rule.
- Essence glyphs: 7 × 7 one-pixel stroke glyphs, drawn by code from stroke specs in
data/glyphs.json. Each primal has a simple form (Ember is a flame tick, Dew a droplet, Loam a square, Breath three lines, Glim a star, Hush a crescent). Compounds combine strokes. Tier rings: 9 × 9 circle segments, one per tier.
8.3 HUD placement (480 × 270)
| Element | Position and size |
|---|---|
| Hotbar | bottom center, 10 × 16 px slots with 2 px gaps |
| Depth ruler | right edge, 12 × 160 px |
| Warp sigil | top left, 16 × 16 |
| Watch and moon | top right, 24 × 16 |
| Keeper's Log | bottom left, 3 lines × 10 px |
Lenses use their own overlay inks: flux in R4, light in R2 and R3, structure in R6.
9. Animation and effects guidelines
- Stepped animation only: 8–12 fps for characters and 4–8 fps for machines. No sub-pixel sprite motion; positions are whole internal pixels.
- Never rotate or scale sprites. Pre-draw frames, or use procedural geometry (§4.6).
- Palette cycling (shifting indices within a ramp) is the main ambient-animation tool: water shimmer, star twinkle, crystal sparkle, root pulse, Heart breath, pipe flow.
- Particle budget: 2,000 live particles. Every particle is 1–2 px, palette-indexed and lit through the LUT unless it's emissive.
- Light is an animation. Flame flicker, wisp breathing and lamp strain are changes in the light field, not sprite changes.
- No flashes faster than 3 Hz, nothing full-screen, and screen shake of at most 1 px. New threats fade in over at least 3 s (sim rule) and are never instant.
- Effects list:
- lamp ignition (light grows over 1 s)
- wisp catch (a 6-pixel sparkle)
- successful Crucible test (a ring of R3·6 pixels expanding 8 px)
- failed test (a violet puff of 30 haze particles)
- gleaner theft (the item icon fades as it's carried)
- arrangement (chalk spiral draws in over 1 s)
- crack, collapse, tide rise (the fog edge advances), rain splash
- the Visitor's passage
10. Production plan (no purchased or commissioned assets)
10.1 Methods
- (a) Code: runtime-generated visuals, effects, UI.
- (b) Procedural: tiles and plants generated from rules, deterministic by seed. The tile generator lives in TS (shared with runtime variation) and can export an atlas PNG for review and compliance checks.
- (c) AI + lanternforge: text-to-image generation followed by the automated cleanup in §10.4.
- (d) Hand edit by the owner: at most 20 assets; 18 are allocated in §10.3.
- CC0: none planned. The fonts are custom. Any future CC0 asset needs a DECISIONS.md entry.
10.2 Full asset list
Each row belongs to exactly one milestone (i4 split the old multi-milestone rows; see
sims/S10_scope.results.md). "×N" counts distinct assets. Tiles count as one type each,
because their variants and autotile edges come from rules.
| ID | Asset | Size / frames | Method | MS |
|---|---|---|---|---|
| A01 | Sky gradient + dithered bands | screen | a | M1 |
| A02 | Stars (3 kinds, twinkle) | 1–3 px | a | M1 |
| A03 | Moon, phases, halo, reflection | 40 px | a | M1 |
| A04 | Clouds ×3 layers | noise | a | M1 |
| A05 | Distant islands + windows | 20–60 px | a | M1 |
| A06 | Rain streaks + splashes | 1×3/1×4, 2 fr | a | M1 |
| A08 | Void gradient + deep shapes | screen | a | M1 |
| A10 | Light field + LUT resolve | — | a | M1 |
| A11 | Atmospheric halos (parallax lighting) | — | a | M1 |
| A12 | Flames (candle, lantern, hearth) | 3 fr each | a | M1 |
| A13 | Wisps + trails | 3×3 | a | M1 |
| A14 | Flux haze: trace, sparkles, drift, tears | particles | a | M1 |
| A15 | Soil flux veins overlay | 8×8 masks | a | M1 |
| A18 | Water surface, shimmer, reflections | tile | a | M1 |
| A20 | Pilot lights | 2×2 | a | M1 |
| A21 | Machine overlays (bubbles, flames, gears) | 2–4 fr | a | M1 |
| A27 | UI panels, frames, buttons (9-slice) | — | a | M1 |
| A28 | Fonts ×3 (Lantern 5×7, Wick 3×5, Keeper Hand) | data | a | M1 |
| A29a | Essence glyphs, M1 set ×10 (6 primals, Growth, Ash, Salt, Glass) + tier rings | 7×7 | a | M1 |
| A30a | Lens overlays ×2 (flux, light) | — | a | M1 |
| A31a | Watch and moon icon | 24×16 | a | M1 |
| A32 | Moths, motes | 1–3 px | a | M1 |
| A37a | Plants: silverleaf (3 stages), nightshade ×2 | 8×8 | b (L-system) | M1 |
| B01 | Grass/topsoil + tufts | 8×8 | b | M1 |
| B02 | Soil | 8×8 | b | M1 |
| B03 | Clay | 8×8 | b | M1 |
| B07a | Bedrock (M1 floor of the slice; becomes Stone in M3) | 8×8 | b | M1 |
| B11 | Planks | 8×8 | b | M1 |
| B13 | Glass | 8×8 | b | M1 |
| B14a | Background walls ×2 (soil, plank) | 8×8 | b (−2 shade rule) | M1 |
| B15 | Ladder, platform ×2 | 8×8 | b | M1 |
| B17 | Roof shingles | 8×8 | b | M1 |
| C01 | Player base pose | 12×16 | c → D01 | M1 |
| C02 | Wick base | 10×8 | c → D02 | M1 |
| C04 | Gleaner body | 12×7 | c | M1 |
| C07 | Lantern Tree | 48×64 | c → D08 | M1 |
| C08 | Alembic (the M1 pipeline test) | 16×24 | c (pure pipeline, no hand edit before the test) | M1 |
| C09a | Research Desk (with crucible bowl), Jar, Chest ×3 | various | c | M1 |
| C10a | Hearth, bed, table, door, workbench ×5 | various | c (Hearth → D05) | M1 |
| C11a | Lamps: candle, lantern, wisp lantern ×3 | 8×8 | c | M1 |
| C14a | Item icons, M1 set ×16 | 12×12 | c (one batch sheet) | M1 |
| A16 | Crack overlays | 3 stages | a | M2 |
| A17 | Dust / rubble particles | 1 px | a | M2 |
| A19 | Pipes autotile + flow | 16 masks | a | M2 |
| A23 | Whisperer tendrils | lines | a | M2 |
| A25 | Chalk marks, spirals, routes | 3×5, 5×5 | a | M2 |
| A29b | Essence glyphs, remaining ×21 | 7×7 | a | M2 |
| A30b | Lens overlay: dark paths, structure ×2 | — | a | M2 |
| A31b | Warp sigil (5 stages) | 16×16 | a | M2 |
| A34 | Map view | 1 px/tile | a | M2 |
| A36 | Mutation transforms (swaps, eye stamps, hollow fill) | — | a | M2 |
| A37b | Crops: moonwheat, emberpepper ×2 | 8×8, 3 stages | b (L-system) | M2 |
| A38 | Broken-machine variants (keeper sites) | from C-machines | a (crack/missing-pixel/rust remap) | M2 |
| B04 | Shale (banded) | 8×8 | b | M2 |
| B05 | Limestone | 8×8 | b | M2 |
| B06 | Bone inclusion stamps | 8×8 | b | M2 |
| B12 | Stone brick | 8×8 | b | M2 |
| B14b | Background wall: stone | 8×8 | b | M2 |
| B15b | Rope, beam, scaffold ×3 | 8×8 | b | M2 |
| B16 | Rubble | 8×8 | b | M2 |
| B18a | Moss and fern stamps | 8×8 | b | M2 |
| C03a | Golems: clay, bone ×2 | 8×10, 8×12 | c | M2 |
| C05 | Arranger | 8×20 | c | M2 |
| C06 | Whisperer body | 14×12 | c | M2 |
| C09b | Prism add-on, shelf ×2 | various | c | M2 |
| C12a | Machines ×13 (Crucible, Cistern, Golem Workshop, Spoil Chute, Vent, Scrubber, Ward Stone, Separator, Pond Intake, Rain Barrel, Valve, Chute, Bucket Lift head) | per GDD | c | M2 |
| C13 | Keeper remains ×5 poses | 16×12 | c | M2 |
| C14b | Item icons ×20 | 12×12 | c | M2 |
| C15 | Journal doodles ×12 | 32×32, 2 colours (R5·1 on R5·6) | c | M2 |
| A07 | Mist / tide fog | dither field | a | M3 |
| A24 | Dimmer | 11×7, 2 fr | a | M3 |
| A26 | Warp effects (vignette, remaps, second moon, peripheral shapes) | — | a | M3 |
| A30c | Lens overlay: buoyancy | — | a | M3 |
| A31c | Depth ruler | 12×160 | a | M3 |
| A35 | Tilt-rotated background evaluation | — | a | M3 |
| B07 | Stone | 8×8 | b | M3 |
| B08 | Crystal (+ tainted via remap) | 8×8 | b | M3 |
| B18b | Glass lichen stamps | 8×8 | b | M3 |
| C03b | Golem: glass | 8×11 | c | M3 |
| C11b | Lamps: ember lamp, crystal lamp ×2 | 8×8 | c | M3 |
| C12b | Machines ×4 (Auger, Crystal Tap, Assembler, Lift Bladder) | per GDD | c | M3 |
| C14c | Item icons ×16 | 12×12 | c | M3 |
| A22 | Root tendrils / growth | L-system | a | M4 |
| A33 | Heart pulse + inverse light | — | a | M4 |
| B09 | Root-wood (+ pulse) | 8×8 | b | M4 |
| B10 | Heartstone | 8×8 | b | M4 |
| B14c | Background wall: root | 8×8 | b | M4 |
| C12c | Machines ×3 (Crystal Nursery, Lodestone Keel, Root Stake) | per GDD | c | M4 |
| C14d | Item icons ×8 | 12×12 | c | M4 |
| C16 | The Heart (base) | 64×64 | c → D03 | M4 |
| C17 | Fetter Pylon, Dawn Lamp ×2 (hero) | 16×24, 8×8 | c → D18 | M4 |
Cut from 1.0 (i4, GDD §19): A09 The Visitor (gameplay cut; the A08 "deep shapes" keep the atmosphere), the Root golem body, the Lure, the Root Graft, and the Glowfruit Lamp.
10.3 Hand-edit budget (≤ 20; 18 allocated)
| # | Asset |
|---|---|
| D01 | Player animation sheet |
| D02 | Wick sheet |
| D03 | The Heart |
| D04 | Title wordmark |
| D05 | Hearth |
| D06 | Ilse's remains |
| D07 | Frogs |
| D08 | Lantern Tree |
| D09 | Gleaner |
| D10 | Arranger |
| D11 | House window and door details |
| D12 | Keeper remains set |
| D13–D16 | Four item icons, reserved for fixes |
| D17 | Golem head (shared by all golems) |
| D18 | Fetter Pylon and Dawn Lamp |
10.4 The cleanup tool: lanternforge (the build agent must create it)
Python 3.11+, Pillow, NumPy. Deterministic, with pytest coverage for every step.
CLI
lanternforge clean <in.png> --spec <spec.json> --out <out.png> # AI image -> game sprite
lanternforge check <dir> # CI gate, nonzero on failure
lanternforge lut --zones data/zones.json --out assets/lut.png # §3.3
lanternforge preview <sprite.png> --tiles <atlas.png> --light hearth|wisp|dark --out <png>
lanternforge sheet <dir> --out <contact.png> # review sheets, blind tests
Spec (per asset)
{ "id": "alembic", "size": [16, 24], "frames": 1, "anchor": "bottom-center",
"ramps": ["EMBER", "STONE", "GLIM", "EARTH"], "max_colors": 12,
"max_shades_per_ramp": 4, "outline": "selective", "background": "magenta",
"emissive": { "auto": true, "force_rects": [], "exclude_rects": [] } }
clean pipeline (in order)
- Key out the background. Remove pixels within ΔOKLab 0.12 of
#ff00ffand despill magenta-tinted edge pixels toward their neighbors. If the input has real alpha, use it. - Crop to the alpha bounding box.
- Detect the grid. If the image is pseudo-pixel-art, estimate its block size
bfrom 2 to 32 by maximizing the autocorrelation of horizontal and vertical gradient energy at periodb, then take the median color of each block. Otherwise, resize to fit the target (minus a 1 px outline margin) with an area-average in linear light. - Place in the frame by the anchor rule.
- Binarize alpha at 0.5. Fill 1 px holes surrounded by ≥ 6 opaque pixels.
- Quantize. Deterministic k-means in OKLab (k = max_colors, k-means++ with seed 0), then
map each centroid to the nearest palette color among the allowed ramps. Ramp
coherence: inside each ramp, the clusters' luminance order must map to increasing shades
(resolve collisions by shifting shades), and clusters that map to the same index merge.
If a ramp uses more than
max_shades_per_rampshades, merge its closest adjacent shades. - Clean up.
- Orphans: a non-emissive pixel that differs from all 8 neighbors takes the mode of its neighbors.
- Outline jaggies: remove doubled L-corners on 1 px outlines.
- Thin-feature warning: flag any part narrower than 2 px whose total area is under 6 px.
- Outline (selective). Every transparent pixel that is 4-adjacent to the opaque sprite
becomes outline. Its color is shade
max(0, s−3)of the adjacent interior pixel's ramp, ormax(0, s−2)when that interior pixel faces top-left (the light side). The interior is never black-lined. The same rule applies to tile edges (B-assets: exposed top edges get a +1 highlight, exposed sides and bottoms get −2 or −3), so sprites and tiles share one edge language. - Emissive mask. Automatically mark interior (non-outline) pixels at R2·6–7, R3·6–7,
R1·7, plus anything in
force_rects, minusexclude_rects. - Export an indexed PNG (PLTE = the 64 colors plus index 64 transparent via tRNS) and
<id>.meta.jsoncontaining size, frames, anchor, the emissive mask (RLE), ramps used and color count. - Report a before/after/preview strip at ×4 plus warnings: colors merged, contrast against the stratum's tiles below 2 shades, thin features.
check (CI) — every PNG under /assets must:
- use only palette indices;
- have binary alpha;
- match its declared size and frames;
- stay within its color and ramp budget;
- follow the outline rule (every opaque pixel that touches transparency is outline-class);
- have a meta file.
Prompt template (works with any text-to-image model):
Pixel art game sprite of {subject}, side view, {W}×{H} pixel sprite shown at 16× with hard square pixels, limited palette of {ramp words: e.g. "warm brass, slate iron, teal glass"}, flat even lighting from the top left, no cast shadow, no glow, no background scenery, solid flat magenta #FF00FF background, centered, full object visible, cozy dark-fantasy alchemy, clean readable silhouette. Negative: anti-aliasing, gradients, blur, glow, bloom, text, watermark, frame, 3D render, photo.
Generate 4–8 candidates, pick the one with the best silhouette, run clean, then preview
it under the 3 lights.
The albedo rule: AI sprites are unlit albedo. The engine applies the night look through the LUT. Never prompt for "night", "moonlit" or "glowing". Emissive parts come from the mask.
10.5 Why this style survives a small asset set
- Strict palette plus LUT lighting: every asset, whatever its source, ends up as the same 64 colors under the same light, so cohesion is mechanical rather than artistic.
- 8 px tiles at 480 × 270: small sprites are cheap to generate and forgiving to clean up.
- Richness from systems: about 40 code-driven effects (sky, moon, rain, halos, wisps, haze, water, palette cycling) make up most of the screen at any moment. On a typical surface frame, about 60% of pixels come from code or procedural layers. [UNVERIFIED: M04 cross-section mockup must measure this.]
11. Planned mockups
| ID | Mockup | Status |
|---|---|---|
| m01 | Palette swatch + vignette | done. Ramps are monotonic; warm/cool/violet separate clearly on indigo; rain and halo dithering read well at 4×. |
| m02 | LUT lighting test | the same wall/tile strip under hearth, wisp, both, and darkness, for every zone |
| m03 | Procedural tiles | B01–B03, B11 with autotile edges |
| m04 | Surface cross-section scene | 480 × 270, composited with code layers plus procedural tiles; mood histogram per GDD M1 acceptance 2 |
| m05 | lanternforge prototype | run on a synthetic "AI-like" input and compare with tiles under the LUT |
| m06 | Flux haze and warp remaps | — |