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:
AddTextPrinterParameterized3with explicit white color →AddTextPrinterParameterizedwith default dark colorsPIXEL_FILL(15)cursor erase →PIXEL_FILL(1)(white, matching the window fill)- Stats window text cramped at 10px spacing → 12px with taller window
- RELIC label crammed on one line with the name → two-line layout at the bottom
- Labels mixed case → ALL CAPS for stat labels (SHARDS, RADIANT, STREAK, QUESTS, GEAR, RELIC)
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:
- CLAUDE.md gained constraint #21: all 16 BG palette slots are occupied on the overworld, never load a custom palette
- STYLEGUIDE.md got a full overworld popup checklist and updated the frame reference table
- AI_CONTEXT.md got the BG palette map, updated baseBlock ranges, and a new troubleshooting entry
- GAME_DESIGN.md was updated to reflect the split stats-box/submenu architecture
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