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:
stdpal_0/1/2: Light themes (white/cream backgrounds)stdpal_3: Light blue/tealstdpal_4: Dark blue-gray#405868— closest to what I wanted
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:
FillPalette(0, 0, PLTT_SIZE)— clear ALL 256 palette entries to black (purges stale overworld data)LoadPalette(gGearUIPalette, BG_PLTT_ID(15), ...)— load the custom palette at slot 15LoadPalette(sMenuBgPalette, BG_PLTT_ID(14), ...)— load texture palette at slot 14gPlttBufferUnfaded[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
+------------------------------+
- Zone 1 (rows 0-1): Gold
sColorGoldtitle, right-aligned currency - Zone 2 (rows 2-17): Screen-specific content
- Zone 3 (rows 18-19): Button glyphs (
{A_BUTTON},{B_BUTTON}) + action labels
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 |