← All posts
Oct 9, 2026

Fire Emblem Collection: Painted Remasters of the First Two Fire Emblems, in a Phone Browser

Ten days, 208 commits: a tactics RPG framework that plays Shadow Dragon (25 chapters) and Gaiden (54 chapters) on a phone, with AI-painted art held to a standard that's enforced in code and a $20 budget per title.

Ben Cao · 8 min read · 0 comments

For the last ten days I've been building Fire Emblem Collection, a mobile-first tactics RPG for the browser. It remakes the first two Fire Emblem games as "painted HD 2D" remasters: open the link on a phone and you can play all of Shadow Dragon and the Blade of Light (1990, 25 chapters) and Gaiden (1992, 54 chapters across five acts). It speaks English, 日本語 and 中文.

Painted cover for Shadow Dragon: a young blue-haired lord in a white-and-blue cape stands before mounted knights, a burning castle and a dragon in the storm clouds behind them

Here's how it looks on a phone, on turn 1 of Shadow Dragon's first chapter:

Shadow Dragon chapter 1 on a phone: painted knights, cavaliers and a hooded fighter on a grass map split by rivers, with three village houses along the shore. The top bar reads "Turn 1 · Player, Gold 0, Seize the castle", and Auto Play and End Turn buttons sit at the bottom

Some numbers from Sep 29 to Oct 8: 208 commits, 42 merged pull requests, about 30k lines of TypeScript, 115 test files and 37 architecture decision records. Most of it was written with Claude Code. I've come to treat the ADRs as the real product: they're what keeps a codebase moving this fast coherent.

A framework first, games second

I didn't build "a Fire Emblem game". I built a framework, and each game is a content pack of plain data. A third title called Embers of Vell is an original one-chapter demo that proves the framework doesn't depend on either series.

A deterministic, headless engine

src/engine/ is pure TypeScript with no React and no display text. Every state change goes through one function, applyAction(), and the random number generator's state lives inside the battle state. That one decision paid off over and over:

  • A save is just the JSON. Suspend and resume, per-series save slots and cloud sync all store the same object.
  • Auto play came almost for free. The enemy AI already plans moves. Pointing it at the player's side and having it greedily pick the best move across the whole army became the Auto Play button in the screenshot above.
  • Every chapter is tested. A validator checks each pack, and an AI smoke test plays every chapter of every pack in CI.

Series differences are data, not forks

The engine started with Game Boy Advance-era rules. Shadow Dragon has no weapon triangle, different hit and damage formulas, terrain that only adds avoid, a movement-cost row per class, weapon levels and enemies that hunt the lord. Rather than branching the engine, a pack declares rules: the triangle on or off, the gba or fe1 formula set, terrain overrides, movement rows and AI quirks. Old saves with no rules field just play on the defaults.

Gaiden pushed this much further, because it plays very differently:

  • Innate class weapons. A villager or brigand with empty hands still fights with their class's own weapon.
  • Magic costs HP. Each cast costs the caster HP, and a caster too weak to pay can't cast, so a spell never kills its caster.
  • Spells learned by level. Each character has their own spell table, and healing is a spell too.
  • Mila's shrines for class change, and lion springs that raise a stat or bring a fallen ally back to life.
  • Two armies. Alm and Celica march separately, and each chapter deploys only its army's roster.

All of it is opt-in data, so Shadow Dragon plays exactly as before.

Both games also have the campaign features you'd expect: villages, talk recruitment, thieves, doors and chests, shops, promotion items, difficulty modes, and scripted events (map tiles that change on a set turn, enemies that leave the map, and reinforcements that stop once the cantor summoning them falls).

How the art works

The art bar was "a lavish, hand-painted storybook", and I didn't commission anything. Every image comes from an AI image model through a pipeline in the repo.

Painted portrait of Marth, a blue-haired lord in a tiara and blue cape

One house style. Every prompt starts from the same preamble: "a living storybook oil painting… saturated jewel tones held together by one warm, unified palette… strong backlight and warm rim light." A pack only adds its setting. A test rejects any prompt that names another game, studio or artist.

A pipeline, not a chat window. npm run art -- plan | generate | pixelize | sync runs the whole thing. plan prints every prompt and the estimated cost without calling anything. generate calls Gemini 3 Pro Image through Vercel AI Gateway. The finish step keys out the chroma-green background, trims, scales each kind to its target size (portrait 512×768, story scene 720×1280, map unit 192×192…) and writes WebP. Switching from PNG cut the shared library from 34.6 MB to 5.1 MB.

Keeping characters on-model. Each image is passed as a reference to the next: portrait → map avatar → fight stance → strike pose. For walk cycles, frames from separate calls drift in outfit and proportions, which flickers once they animate. So each clip is generated as one image holding a 2×2 grid of frames. The pipeline splits it, scales every frame by the same factor onto a shared baseline, and rejects images where a pose crosses a cell line. At runtime, walk frames advance with the distance travelled, so feet don't slide.

A four-frame walk cycle strip of an armoured knight walking towards the camera, generated as one 2×2 grid and split into frames

Shared-library battle art of a generic armoured knight in a strike pose

Two layers to fit the budget. Each title gets $20 for images and audio. A unique map and battle set for every character in Shadow Dragon would be about 800 images, which doesn't fit. So only close-up art is unique to a title: the cover, portraits, story scenes and the lords' sprites. Map, fight and walk art for 23 generic classes lives in a shared library that every title reuses, so it's paid for once.

The standard is code. artGaps() lists exactly what a title is missing: a portrait for every character, both backdrops for every chapter, art for every class and terrain that appears. Art below its target resolution counts as missing. Production only lists titles with zero gaps. On Sep 30, Shadow Dragon reached the title screen with no cover and 16×16 placeholder sprites. That's when this rule was written, and it's why a title can't ship half-painted.

Audio uses the same pipeline. Music comes from ElevenLabs as an orchestral composition built from whole bars, with a reprise of the opening so a loop point exists. Tested TypeScript then finds the loop, ramps the gain so the end matches the start, bakes in an equal-power crossfade and rejects "stuck note" drones. Every sound effect also has a tiny procedural (ZzFX) recipe, so the game makes sound even before any file loads.

The game UI

The look is a dark lacquer panel with a gold hairline and filigree corners, plus gold-leaf buttons. I started with pixel fonts, but next to painted art they looked cheap, and at 10 px "25" read as "2S". Headlines are now Cinzel and the UI is Barlow Semi Condensed, whose even-width digits keep stat columns lined up. Everything is tap-only and designed for a phone first.

A few screens I'm happy with:

World map beats

Each chapter opens on a painted parchment map. The lord's banner walks a dashed route while a narrator sets the scene and the camera pans to each place mentioned. The art has no lettering: the game draws place names, so they read correctly in all three languages. Archanea has 20 places, and Gaiden's Valentia has 50.

Painted parchment map of Archanea with castles, rivers, forests and the volcanic land of Dolhr, with no lettering on the art

In the game, the camera zooms in on the same art and the game writes the place name on top:

The opening of Chapter 1, Marth's Quest: the camera has zoomed in on the parchment map to Dolhr, a black castle on lava-veined mountains, with the name "Dolhr" drawn over it by the game. The narrator box reads "Archanea. A century after the hero Anri slew the Shadow Dragon, Medeus has risen again in Dolhr."

Battle preparation

Party, Shop and Items tabs before each chapter. Drag an item onto another to swap, or onto a portrait to hand it over. Tapping works too.

The Preparation screen for Chapter 1, Marth's Quest, on the Items tab: a row of painted portraits for Marth, Cain, Abel, Draug, Gordin, Jagen, Caeda and the Convoy, with Marth's inventory (a Rapier, 28 uses, equipped) beside an empty convoy, and a gold Sortie · 7 button at the bottom

And the rest

  • A full-screen trade scene. Trades are a draft with Undo, and nothing changes until you press Done. The first version committed every drop at once, so one stray drop wasted a turn.
  • A fight HUD with the attacker on the left, name and weapon plates, and a stat table. All fight art is painted facing right, and the stage mirrors it by side.
  • Small touches: a danger-area overlay showing tiles enemies can reach, Restart Turn (which re-rolls level-ups), a level-up card with stats that count up and drifting embers, a turntable-style music box, and a character explorer with growth rates and animations.

Painted cover for Gaiden: the faces of a serene goddess and a horned, bearded god loom in sunset clouds above a white castle and a green valley of villages and ruins

Other features I enjoyed building

  • Cloud saves that ask instead of overwrite. Sign in with Clerk and saves sync through Convex. Each series slot carries a write stamp. If two devices both changed the same series, the game shows both copies and asks which to keep, rather than letting the newer one silently win.
  • Audio diagnostics in production. Sound bugs only happen on someone else's device. The game keeps a short playback log, and await __fecAudioReport() in the console measures the music and master buses. It found a buzz in Chrome at the loop point (fixed by ending the loop one frame early) and audio that stayed silent after the tab was suspended.
  • Safari fallbacks. Safari's canvas has no filter, so terrain blending is blurred in script instead.

What I took away

  • Make the engine deterministic on day one. Saves, retries, cloud sync, auto play and smoke tests all fell out of it.
  • Put taste into code. A house-style prompt, an artGaps() check and a title gate did more for quality than any single good generation.
  • Budgets shape architecture. The $20 cap is the reason the shared class library exists, and that library is why a new series looks complete on day one.

Play it at fire-emblem.bencao.it. It works best on a phone.

No comments yet. Be the first.

Optional. Leave it blank to post as Anonymous.