17 — The Palette Wars

Sixteen palette slots, sixteen problems, and the overnight sprint that broke the start menu.


The Morning After

The overnight forge shipped seven features. The morning after found three bugs on the title screen, two in the lore intro, and a start menu that looked like someone had taken a sledgehammer to it. ASCII borders where custom frames should be. Wrong colors. Text bleeding into the help bar. The standard post-sprint cleanup — except this time the cleanup revealed something deeper.

The EMBER submenu had always used the ember frame — our custom dark Diablo-style window with purple gradient borders and near-black fill. It looked gorgeous in the gear bag, the codex, the merchant screens. On the overworld start menu, it looked gorgeous too. Right up until you walked into Viridian City and the building roofs turned black.

Sixteen Slots, Zero Free

The GBA has sixteen background palette slots. That’s it. On the overworld, every single one is spoken for: twelve for the map tileset (buildings, trees, water, paths), one for the help bar, one for the standard window frame, one for dialog text. The ember frame loads its custom palette into slot 11. Map tilesets also use slot 11 — for things like building roofs.

The corruption mechanism was subtle. LoadPalette() writes to gPlttBufferUnfaded. The hardware reads from gPlttBufferFaded. As long as no palette fade syncs the two buffers, the roofs look fine. But UpdatePaletteFade eventually propagates the change, and when it does, every tile on screen that references palette 11 switches from the tileset colors to the ember palette colors. Roofs go black. Fences go black. Half the town goes dark.

We tried saving and restoring palette 11 — saving before the ember frame loads, restoring when the menu closes. It worked… mostly. The problem is that the corruption is visible while the menu is open, and some buildings are visible behind the stats window. There is no surgical fix. If you use palette 11 for the ember frame on the overworld, you corrupt the overworld.

The Redesign

The fix was accepting the constraint. The ember frame stays in full-screen UIs where we control all sixteen palette slots. On the overworld, the start menu uses standard frames — the same white-background, dark-text style as the main menu. Palette 15, already loaded, no corruption possible.

This meant reworking the entire EMBER menu structure that had been built the day before. The submenu went from a dark ember-framed window to a clean standard popup. The stats display — shards, relics, streak, quests, gear score — moved from inline text in the submenu to a dedicated info box in the upper-left corner, always visible when the start menu is open.

Death by a Thousand Pixels

Getting the standard frame right took iteration. The text was white (should be dark). The cursor arrow was white (should be dark). The cursor erase was PIXEL_FILL(15) — blue in palette 15 — leaving a blue stripe down the arrow column. Each fix was one line. But there were a dozen of them:

The filter row got cut entirely. The loot filter was a power-user feature for a game that doesn’t yet have power users. When you’re generating that much loot, the design has bigger problems than the notification frequency.

Documenting the Scars

The last step was the most important one: making sure this never happens again. Four documents got updated:

The lesson is always the same with the GBA: the hardware has exactly enough resources for what the original game needed, and not one byte more. Every custom feature fights for space with the vanilla engine. The art of ROM hacking is finding the seams.

By the Numbers

Metric Value
Commits 22
Bug fixes 15
Docs updated 4
Palette slots wasted trying to make ember frame work on overworld 1
Copilot requests 83
Tool executions ~3,200
Sub-agents 11

The roofs stay on the buildings now. That’s the bar.

Next: Back to README