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 turned up three bugs on the title screen, two in the lore intro, and a start menu that looked like someone had taken a maul 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 found 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. Looked first-rate in the gear bag, the codex, the merchant screens. On the overworld start menu it looked first-rate too. Right up until you walked into Viridian City and the building roofs went black.

Sixteen Slots, Zero Free

The GBA has sixteen background palette slots. That’s the whole count. On the overworld, every 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 was subtle. LoadPalette() writes to gPlttBufferUnfaded. The hardware reads from gPlttBufferFaded. As long as no palette fade syncs the two buffers, the roofs hold. But UpdatePaletteFade eventually propagates the change, and when it does, every tile referencing palette 11 switches from tileset colors to ember palette colors. Roofs go black. Fences go black. Half the town goes dark.

Save and restore on palette 11 was tried — save before the ember frame loads, restore when the menu closes. It worked mostly. The problem is the corruption is visible while the menu is open, and some buildings sit behind the stats window. No surgical fix exists. Load palette 11 for the ember frame on the overworld and you corrupt the overworld. That’s the whole story.

The Redesign

The fix was accepting the constraint. The ember frame stays in full-screen UIs where all sixteen palette slots are under control. On the overworld, the start menu uses standard frames — 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 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 passes. 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 one line. A dozen of them:

The filter row got cut. The loot filter was a power-user feature for a game that doesn’t yet have power users.

Documenting the Scars

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

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