05 — Making It Beautiful

Dark theme, transparent windows, and a custom palette — Diablo aesthetics on a GBA


The Starting Point

After fixing crashes and string encoding, the UIs worked. They also looked terrible. Bright blue backgrounds, garbled color schemes, misaligned text, visible borders eating screen space. It looked like a debug menu, not an RPG loot screen.

The vision was always Diablo — dark, atmospheric, with rarity colors that pop. But GBA has 15-bit color (32,768 possible colors), 16-color palettes, and 240×160 resolution. How do you make that feel like a modern ARPG?

Phase 1: Kill the Borders

FireRed’s standard windows have a 1-tile (8px) border on each side — that “frame” look from the vanilla menus. For my screens, that was 16 pixels of wasted space in each dimension. On a 240-pixel-wide screen, that’s significant.

Claude switched to full-bleed windows: x=0, width=30 (the full screen width in tiles). This immediately made everything feel more spacious. But it revealed a new problem — black bars appeared between adjacent windows because the GBA’s backdrop color (palette index 0 of BG palette 0) was black.

Fix: set gPlttBufferUnfaded[0] to match the window fill color after loading palettes. The backdrop becomes invisible.

Phase 2: The Custom Palette

FireRed ships with 5 standard palettes (stdpal_0 through stdpal_4). None of them work for a dark loot UI:

Rather than modifying the standard palettes (which would affect every menu in the game), I had Claude create gGearUIPalette — a custom 16-color palette loaded at BG palette slot 15:

const u16 gGearUIPalette[16] = {
    RGB(3, 4, 7),     // 0: bg dark navy #182038
    RGB(31, 30, 28),  // 1: off-white (Common)
    RGB(12, 28, 28),  // 2: cyan (Uncommon)
    RGB(8, 16, 31),   // 3: bright blue (Rare)
    RGB(22, 10, 28),  // 4: purple (Epic)
    RGB(30, 22, 4),   // 5: amber gold (Legendary)
    RGB(30, 22, 4),   // 6: gold (titles/currency)
    RGB(20, 20, 22),  // 7: light gray (secondary text)
    RGB(8, 9, 12),    // 8: shadow
    // ... etc
};

One palette. 16 colors. Shared across all 8 custom UIs. This became the single source of truth for the entire visual identity.

Phase 3: Rarity Colors

Colors carry information in loot games. You should be able to tell an item’s rarity at a glance:

Rarity Color Hex GBA RGB
Common Off-white #F8F0E0 RGB(31,30,28)
Uncommon Cyan #60E0E0 RGB(12,28,28)
Rare Blue #4080F8 RGB(8,16,31)
Epic Purple #B050E0 RGB(22,10,28)
Legendary Gold #F0B020 RGB(30,22,4)

GetRarityTextColor() returns the correct 3-index color array (background, foreground, shadow) for any rarity. Every UI uses it. No hardcoded rarity colors anywhere.

Phase 4: The BG Texture

Flat dark backgrounds felt empty. I wanted the subtle visual texture of Diablo’s item tooltips — that aged, slightly worn look.

Claude created a dark grunge tileset: 580 unique 8×8 tiles forming a 240×160 repeating pattern. Dark grays and navy blues with subtle noise and variation. This lives on BG1, behind the text on BG0:

BG0: Text windows (charBase 0, priority 0) — in front
BG1: Grunge texture (charBase 2, priority 1) — behind

The key trick: windows are filled with PIXEL_FILL(0) — palette index 0, which is transparent. The dark grunge texture shows through the windows, giving every screen a rich, textured look without any per-screen art.

The Palette Loading Dance

Getting all of this to work required a precise initialization sequence:

  1. FillPalette(0, 0, PLTT_SIZE) — clear ALL 256 palette entries to black (kills stale overworld data)
  2. LoadPalette(gGearUIPalette, BG_PLTT_ID(15), ...) — load the custom palette at slot 15
  3. LoadPalette(sMenuBgPalette, BG_PLTT_ID(14), ...) — load texture palette at slot 14
  4. gPlttBufferUnfaded[0] = gPlttBufferUnfaded[15*16+15] — set backdrop to match the BG

Miss step 1 and you get the “red screen” bug — stale stdpal_2 data (which has RED at index 4) bleeds through during the fade-in, causing an alarming flash of red/orange before the correct palette stabilizes.

Miss step 4 and the hardware backdrop color is wrong, creating visible seams between windows.

Phase 5: The 3-Zone Layout

After applying the dark theme to all screens independently, inconsistencies crept in. Claude and I standardized a 3-zone layout:

┌──────────────────────────────┐
│ ★ SCREEN TITLE          ¥42 │  Zone 1: Gold title + currency
├──────────────────────────────┤
│                              │
│   Content area               │  Zone 2: Lists, info, details
│   (varies per screen)        │
│                              │
├──────────────────────────────┤
│ (A) Select   (B) Back        │  Zone 3: Icon-based controls
└──────────────────────────────┘

This standardization happened after implementing the same screen 5 different ways and realizing each one looked slightly different. A single pass through all 5 full-screen UIs brought them into alignment.

The Result

From “debug menu that happens to have gear data” to “dark RPG interface that feels like it belongs in a loot game.” The GBA’s constraints actually helped — with only 16 colors, every shade matters. The limited palette forced intentional color choices rather than the gradient soup you’d get with more colors.

The closest reference points: Diablo III’s item tooltips and Path of Exile’s dark item panels, adapted to 240×160 pixels and 15-bit color.

By the Numbers

Metric Value
Active ~16 hours
Commits 59
Lines of C added ~2,300
New source files 4 (codex, drop notify, gear bag, menu BG)
Fix/crash commits 27
Major crises 3 (grey screen crash, string encoding, palette bleed)
Copilot requests 83
Tool executions ~3,200
Sub-agents 11

Next: 06 — The World Comes Alive