16-bit Pixel RPG — our demo

One example among many. Don't reuse its story, arc, shots, props or timings.

Demo: The Last Save Point (54.5 s) · pixel-rpg.mp4 · source in demo/

A 30–60 second film: a lone hero at a save crystal remembers his lost companion through three eras of colour depth, then opens the great door.

Story & structure

A world drawn at 320×180 native pixels, scaled ×6 with nearest-neighbour to 1080p. Every color comes from a limited, indexed palette. Gradients are ordered dither, not blends. Characters are sprites around 22×34 px that animate in a handful of frames. Story is told the way a game tells it: through dialog boxes with portraits, menus, status bars, battle messages and save files.

It is not 2.5D Pixel Art (no depth of field, no bloom, no real-time light), not CRT nostalgia (no scanlines, no grain), and not a parody. It takes the game's systems seriously as a language for feeling.

Never use existing game names, characters, UI frames, fonts, logos, melodies or sound effects. Learn the grammar, not the assets.

Pick stories where the game's own systems carry the emotion. The demo used these native powers, the strongest (the palette collapse and flood) at the emotional peak.

Native power Story use
Palette = feeling Because the image is indexed, you can swap the whole palette in one frame. Memory, grief, hope can literally be color depth. In the demo: older memories have fewer colors (16-bit → 8-bit → 4-color). The present after a loss is faded. The collapse of a memory is the palette dropping one level per beat. Hope is the full palette flooding back through an ordered-dither wave.
Protected colors Exempt a few palette indices from every palette effect. One red object (a scarf) that never fades becomes the thread through the whole film, and a perfect match cut between eras.
Game UI as narration Save files = a list of memories (choosing NEW FILE = refusing to overwrite them). A name greyed out in the party list = death, without a word. "No inn will ever heal it" = grief in the medium's own vocabulary.
Text boxes & blips A dialog box is the subtitle. A character with no voice actor, only text blips, reads as someone who now exists only in text.
Era shifts The same characters redrawn in older hardware: 4 shades at half resolution, a 13-color 8-bit set, full 16-bit. Time travel for free.
Genre transitions Mosaic in and out for gentle memories. The encounter swirl to crash into danger. White flash on the climax.

Adapting any topic: find the game system hidden in it. A product launch is a new item obtained (fanfare, item box, stat comparison). A farewell is a save file that is never overwritten. A career is a level-up screen. A city is an overworld map with save points.

Arc for 45–60 s: loneliness (quiet, faded, long scroll) → warmth (a memory) → loss (the system breaks) → resolve (the system restored in full color) → an end card that is also a game prompt (CONTINUE ▶).

Shots

Beat Camera
Opening Long side-scroll through a colonnade, hero small (1/6 of the frame height), four parallax layers, one colored object ahead
Menus Locked-off frame, UI on the left third, hero and prop visible on the right
Warm memory Wide, static or slow pan; characters small against sky or fire
Battle Classic side view: enemy left, party right facing left, UI window below, message window on top
Loss Hold the frame. No cut, no move while the palette collapses
The monument Integer-pixel tilt up a three-screen-tall door to a rose window, hold, tilt back down. The one memorable camera move
Climax Back view at the threshold, light spilling on the floor, color flooding outward from the hero

Score structure

Palette & props

Titles, subtitles & end card

Pitfalls

Build notes

styles/pixel-rpg/demo/
  px.js        indexed framebuffer, palettes & LUTs (full/nes/gb4/faded/c16/c8/c4/c2), dither, mosaic
  font.js      variable-width pixel font      ui.js        windows, dialog, menus, file list, status, save
  sprites.js   rig + ASCII heads, scarf tails portraits.js 32×32 portraits + expressions
  props.js     crystal, halo, pedestal, door  memsprites.js 4-color kid sprites
  scenes.js    corridor (parallax), door tilt, LIGHT/DARK index lighting, torches
  memscenes.js village (160×90), campfire (8-bit), battle (16-bit), thumbnails, title, end card
  main.js      director: shots, transitions, palette timing, EV (sound events), SUBS
  timeline.json single source of timing (music, picture, mix)   lines.json voice script
  music/score.py original chiptune from timeline.json            mix.py foley + voice + ducking
  sheet.js     model sheet (?sheet=1)          tools/subs.mjs srt export      build.sh one-shot rebuild
  1. Write timeline.json first: sections, BPM, downbeats, hits, silences, the collapse steps. Music, picture and mix all read it.
  2. ?sheet=1 → model sheet; ?frame=corridor|door&lut=faded → environment self-check frames before laying out shots.
  3. core/tts/tts.py → asr_check.py (all OK), then compose music/score.py against the timeline (a parallel sub-agent works well).
  4. node core/render/still.mjs demo --range 0.3:54.3:1.5 → sheet.py overview; check the key moments frame by frame.
  5. events.mjs → mix.py → tools/subs.mjs → video.mjs --workers 3 (1308 frames in about 20 s) → mux.sh … 24 0.
  6. sh demo/build.sh reproduces everything from scratch.