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.
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 中文.
Here's how it looks on a phone, on turn 1 of Shadow Dragon's first chapter:
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.
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.
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.
In the game, the camera zooms in on the same art and the game writes the place name on top:
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.
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.
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.