02 — The UI Challenge
Seven screens cut from a codebase that never expected them.
The Problem
The gear system worked. Gear dropped, stats applied, relics boosted. There was no way to see any of it. No inventory. No equip screen. No merchant. No way to touch what I’d built.
FireRed has one UI paradigm: blue gradient backgrounds, yellow header text, bordered windows. Every menu looks the same — Bag, PC, Pokédex, party screen. Consistent. Charming. And the only pattern the game’s code knows how to draw.
I needed 7 new screens:
- Gear UI — equip gear from the party menu
- Relic UI — manage trainer relics
- Gear Bag — browse all owned gear
- Gear Merchant — buy/sell with rotating stock
- Item Chest — overflow storage for gear and relics
- Altar of Recrafting — reroll affixes and quality with shards
- Scrapper — break gear into shard currency
Each one would be a full-screen takeover with its own state machine, window layout, input handling, and palette management.
GBA Window System
The GBA’s window system runs on a few core pieces:
- BG layers (0-3): tiled background planes. Text windows live on BG0.
- Windows: rectangular regions defined by
(x, y, width, height)in 8x8 tile units. Each window has abaseBlock— its starting offset in VRAM tile memory. - Palettes: 16 background palettes of 16 colors each. Text color comes from 3-index arrays:
{background, foreground, shadow}. - Tile budget: ~1024 tiles of VRAM shared across ALL windows on a BG layer.
Every window must have a unique, non-overlapping baseBlock. Get it wrong and windows overwrite each other’s tile data. Garbled graphics is the result.
The Overlay Approach
The Gear UI was first. I chose a task-overlay — the gear equip UI runs on top of the party menu instead of replacing the entire screen. That saved reimplementing Pokémon selection. It introduced a new problem: fillValue.
gMultiuseListMenuTemplate.fillValue = 1; // 1 = white bg
// fillValue = 0 would be transparent, showing party menu through the overlay
That one field — fillValue — made the gear list invisible on the first attempt. The list was there, responding to input, but drawing with transparent pixels. The party screen showed through as if nothing existed above it. The hardware doesn’t ask questions.
Full-Screen State Machines
The other 6 UIs followed a pattern that Claude and I codified through trial:
static void Task_MyScreen(u8 taskId)
{
switch (data[0]) // state counter
{
case 0: // Setup BG, callbacks
SetVBlankCallback(NULL);
SetGpuReg(REG_OFFSET_DISPCNT, 0);
data[0]++;
break;
case 1: // Reset memory
ResetSpriteData();
FreeAllSpritePalettes();
FreeAllWindowBuffers();
data[0]++;
break;
case 2: // Init windows, palettes
InitBgsFromTemplates(sBgTemplates);
InitWindows(sWindowTemplates);
FillPalette(0, 0, PLTT_SIZE); // CRITICAL: clear stale palette
data[0]++;
break;
case 3: // Load assets, draw
LoadPalette(gGearUIPalette, BG_PLTT_ID(15), PLTT_SIZE_4BPP);
// Draw content...
BeginNormalPaletteFade(PALETTES_ALL, 0, 16, 0, RGB_BLACK);
data[0]++;
break;
case 4: // Wait for fade, handle input
if (!gPaletteFade.active)
// Process A, B, DPAD...
}
}
Every step exists because skipping it caused a specific bug:
- Missing
FillPalette→ stale overworld palette bleeds through (red/orange flash) - Missing
FreeAllWindowBuffers→ heap exhaustion after 3-4 NPC visits - Missing
SetVBlankCallback(NULL)→ DMA conflicts during setup
The Merchant Economy
The Gear Merchant was the most involved screen. It needed rotating stock — re-rolled per town, persisted in SaveBlock2 — buy/sell modes with confirmation dialogs, dual currency display, and a full item info panel showing affixes, quality, and stat bonuses.
I designed a dual-currency economy:
- Gear Shards (common) — from scrapping gear, used at the Altar to reroll affixes
- Radiant Shards (rare) — from scrapping rare+ gear, used to reroll quality
Two progression loops: players scrap common drops for Gear Shards to gamble on affixes, and save their best rare drops for Radiant Shards to sharpen quality rolls.
The Altar of Recrafting
The Altar modifies gear in-place in the SaveBlock. Rerolling affixes regenerates the prefix/suffix bits of the u32 encoding while preserving rarity, base type, and quality. Rerolling quality regenerates the quality nibble while preserving everything else.
Direct mutation of save data during gameplay — something the base game almost never does.
Every Town, Every NPC
Python scripts automated NPC placement across all 10 Kanto towns. Each town got:
- A Gear Merchant (GENTLEMAN sprite)
- An Item Chest (ITEM_BALL sprite, later a custom chest)
- An Altar of Recrafting (custom crystal sprite)
- A Scrapper (end-game towns only: Fuchsia, Saffron, Cinnabar)
The specials system connects map scripts to C code:
// In scripts.inc:
special SpecialOpenGearMerchant
// In field_specials.c:
void SpecialOpenGearMerchant(void) {
SetMainCallback2(CB2_InitGearMerchant);
}
Seven working screens by the end of this phase. Blue backgrounds, garbled text, inconsistent layouts — not pretty yet. They worked.
Then the grey screen happened.
By the Numbers
| Metric | Value |
|---|---|
| Commits | ~8 |
| Copilot requests | ~17 |
| Tool executions | ~800 |
| Sub-agents | 0 |
Next: 03 — The Grey Screen