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:

  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 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:

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:

The Merchant Economy

The Gear Merchant was the most complex screen. It needed:

I designed a dual-currency economy:

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:

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