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:

  1. Gear UI — equip gear from the party menu
  2. Relic UI — manage trainer relics
  3. Gear Bag — browse all owned gear
  4. Gear Merchant — buy/sell with rotating stock
  5. Item Chest — overflow storage for gear and relics
  6. Altar of Recrafting — reroll affixes and quality with shards
  7. 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:

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:

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:

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:

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