05 — Making It Beautiful

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


The Starting Point

The crashes were fixed. The strings were encoded. The UIs worked and looked terrible — bright blue backgrounds, garbled color schemes, misaligned text, visible borders eating screen space. A debug menu, not an RPG loot screen.

The goal was always Diablo — dark, atmospheric, with rarity colors that register at a glance. The GBA has 15-bit color (32,768 possible colors), 16-color palettes, and 240x160 resolution. Work with what you have.

Phase 1: Cutting the Borders

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

Claude switched to full-bleed windows: x=0, width=30 — the full screen width in tiles. Spacious immediately. But black bars appeared between adjacent windows — 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 touch every menu in the game — Claude created 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. The single source of truth for the visual identity.

Phase 3: Rarity Colors

Colors carry information in loot games. You should read a drop’s rarity without checking its name:

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 calls it. No hardcoded rarity colors anywhere.

Phase 4: The BG Texture

Flat dark backgrounds felt empty. Diablo’s item tooltips have that subtle worn texture — aged and rich.

Claude created a dark grunge tileset: 580 unique 8x8 tiles forming a 240x160 repeating pattern. Dark grays and navy blues with subtle noise and variation. It 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

Windows fill with PIXEL_FILL(0) — palette index 0, which is transparent. The dark grunge shows through every window. Rich, textured look with no per-screen art.

The Palette Loading Dance

Getting all of this right required a precise initialization sequence:

  1. FillPalette(0, 0, PLTT_SIZE) — clear ALL 256 palette entries to black (purges 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. An alarming flash of red/orange before the correct palette settles.

Miss step 4 and the hardware backdrop color is wrong. 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         Y42   |  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 building the same screen 5 different ways and finding each one slightly off. A single pass through all 5 full-screen UIs brought them into line.

The Result

From debug menu to dark RPG interface. The GBA’s constraints helped — with only 16 colors, every shade earns its place. The limited palette forced intention rather than the gradient soup you’d get with more colors.

The closest references: Diablo III’s item tooltips and Path of Exile’s dark item panels, pressed into 240x160 pixels and 15-bit color. Not bad for 16 colors.

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