02 — The UI Challenge
Building 7 custom screens on a system that really doesn’t want you to
The Problem
The gear system worked. Gear dropped, stats applied, relics boosted. But there was no way to see any of it. No inventory screen. No equip interface. No merchant. No way to interact with the systems I’d designed.
FireRed has exactly one UI paradigm: blue gradient backgrounds, yellow header text, bordered windows. Every menu looks the same — Bag, PC, Pokédex, party screen. It’s consistent and charming. It is also the only pattern the game’s code knows how to draw.
I needed 7 new UI 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 down 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 Crash Course
The GBA’s window system is built around a few core concepts:
- BG layers (0-3): tiled background planes. Text windows live on BG0.
- Windows: rectangular regions defined by
(x, y, width, height)in 8×8 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 this wrong and windows overwrite each other’s tile data, producing garbled graphics.
The Overlay Approach
The Gear UI was the first screen. I chose a task-overlay approach — instead of replacing the entire screen, the gear equip UI runs on top of the party menu. This saved Claude from reimplementing Pokémon selection but introduced a subtle problem: fillValue.
gMultiuseListMenuTemplate.fillValue = 1; // 1 = white bg
// fillValue = 0 would be transparent, showing party menu through the overlay
That single field — fillValue — caused the gear list to be invisible on the first attempt. The list was there, responding to inputs, but rendering with transparent pixels. The party screen showed through as if the gear UI didn’t exist.
Full-Screen State Machines
The other 6 UIs followed a consistent pattern that Claude and I eventually codified:
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...
}
}
This pattern emerged through painful iteration. 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 complex screen. It needed:
- Rotating stock (re-rolls per town, persisted in SaveBlock2)
- Buy/sell modes with confirmation dialogs
- Currency display (dual currencies: Gear Shards + Radiant Shards)
- Full item info panel showing affixes, quality, 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
This creates two progression loops: players scrap common drops for Gear Shards to gamble on affixes, and save their best rare drops for Radiant Shards to perfect quality rolls.
The Altar of Recrafting
The Altar was conceptually simple but technically interesting — it modifies gear in-place in the SaveBlock. When you reroll affixes, it regenerates the prefix/suffix bits of the u32 encoding while preserving rarity, base type, and quality. When you reroll quality, it regenerates the quality nibble while preserving everything else.
This is direct mutation of save data during gameplay — something the base game almost never does (items are consumed, not modified).
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 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);
}
By the end of this phase, I had 7 working UI screens. They weren’t pretty yet — blue backgrounds, garbled text, inconsistent layouts. But they worked.
Sort of. 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