11 — The Pixel Wars

18 commits, 4 crashes, 1 tile that kept destroying Charmander’s name


The Morning After

After the overnight sprint merged — 27 commits across 8 branches, all building clean — I ran a focused play session. Within an hour I had a list.

Merchants were standing on water. The EMBER menu text was clipped. The scrapper layout was broken. The gear confirm screen had a mysterious black band across the bottom third. And clicking on a gear item crashed the game.

Day 4 was supposed to be polish. It became a pixel-by-pixel war with the GBA’s rendering pipeline.

The Crash That Started Everything

The first thing I hit was a hard crash: opening the gear detail view on the party screen. The culprit was GetGearName — it was writing past the end of a buffer when a name got too long. A 10-character buffer for a name like “Flame Amulet of the Storm” doesn’t end well. Buffer overflow, corrupted stack, white screen.

Fix: wider buffer, truncation with ellipsis. But this was just the opening act.

NPC Placement and the Map Viewer

Every town had merchants, chests, altars, and scrappers now — but several were standing on blocked tiles, water, or overlapping warps. I couldn’t tell by looking at the JSON coordinates, and loading each town in an emulator was painfully slow.

So I built a tool: map_viewer.py. It parses the raw blockdata and metatile attributes, cross-references NPC positions from map.json, and produces ASCII walkability maps. Green dots for walkable tiles, blue for water, red for blocked, letters for NPCs.

python3 tools/map_viewer.py --all-towns --check-only

One command, every town validated. It found 12 bad placements across 8 towns. All fixed in a single commit.

The Tile Overflow

The gear confirm screen — the one that shows “Equip Fire Amulet to CHARMANDER?” — had a black artifact covering its bottom third. This one was subtle: the window’s baseBlock value determines where its tile data lives in VRAM. With the full-height redesign (expanding the window from 6 to 16 tiles tall), its tiles now exceeded 1024 — the maximum tile index for BG text layers on the GBA.

The fix was recalculating every window’s baseBlock to pack them tighter. The broader lesson: on the GBA, window layout isn’t just visual design — it’s memory allocation.

The Dark Side

With the mechanics working, I turned to aesthetics. The default Pokémon UI — white backgrounds, blue borders, bright and cheerful — clashed with the Diablo-inspired gear system. I wanted something darker.

Act 1: The Background

First pass was simple: replace the menu background texture. I designed a dark grunge image, exported it as an indexed 4bpp tileset, and loaded it onto BG1. Every full-screen menu (merchant, bag, altar, forge, codex) now renders its text windows as transparent overlays on this dark texture.

The transparent window trick is elegant: PIXEL_FILL(0) fills with GBA hardware-transparent pixels, so the BG1 texture shows through everywhere text isn’t drawn. Dark background, bright text, zero extra memory.

Act 2: The Ember Frame

But the gear overlay on the party screen doesn’t get its own background layer — it’s a popup over the existing party menu. It needed a custom window frame.

I designed a 24×24 pixel frame: purple gradient border, inner shadow, dark grey fill. Nine tiles in a 3×3 grid. On paper, straightforward. In practice, it started a war that consumed most of the afternoon.

Round 1: Palette collision. Loaded the frame palette into slot 12. That’s where the text window palette lives. Every standard text window in the game turned purple. Moved to slot 11 — safe, but now I needed to merge text colors and border colors into a single 16-entry palette.

Round 2: The tile that ate Charmander. I placed the frame tiles at VRAM offset 0x70. Looked fine on initial load. Then I noticed Charmander’s name was garbled — random pixels where “CHARMANDER Lv5” should be. The party menu uses a complex tile allocation: slots 0x63 through 0x1B6 are reserved for the six party slot windows. My frame tiles at 0x70 were overwriting the lead Pokémon’s name tiles.

I mapped every tile allocation in the party menu — 62 tiles of BG graphics, 9 tiles for the user frame, 9 for the standard frame, 70 tiles per party slot, message bars, action windows. Found a 17-tile gap between 0x3E and 0x4E. Moved the frame there. Charmander survived.

Round 3: The transparent shadow. The frame has rounded corners that should show the party menu through them, plus an inner shadow between the border and fill area. Both used palette index 14 in the source art. GBA BG tiles treat index 0 as hardware transparent. I needed corner pixels transparent but shadow pixels opaque — same source index, different behavior.

Blanket replacement broke the shadow. The fix was a flood-fill algorithm: seed from each tile’s outer edge, walk through connected index-14 pixels, convert only those to index 0. Inner shadow pixels — not reachable from any edge — stay opaque.

Round 4: The white cursor. The engine’s built-in cursor system (Menu_RedrawCursor) erases the old cursor position by filling with pixel value 1 — white. On a white background, invisible. On my dark frame, a bright white rectangle. Replaced the entire cursor system with manual tracking: erase with PIXEL_FILL(15) (dark fill), redraw with AddTextPrinterParameterized4.

Four rounds. Four different GBA subsystems fighting back. But the result is worth it — the gear overlay now has its own visual identity, distinct from both the vanilla menus and the full-screen dark theme.

Consistency Pass

With the frame working, I audited every menu screen for consistency:

Then I documented everything. The STYLEGUIDE grew a section on the ember frame system, hint bar conventions, and text formatting rules. AI_CONTEXT got a complete VRAM tile allocation map for the party menu — the kind of reference that would’ve saved me hours during the tile collision debugging.

What I Learned

The GBA doesn’t have layers of abstraction — it has layers of hardware. Palettes are 16-color slots in a flat array. Tiles are offsets in a shared VRAM bank. Windows are rectangles that claim ranges of that tile space. Touch one thing and another thing three abstraction levels away might break. There are no error messages. There are only wrong pixels.

Tool-building pays for itself immediately. The map viewer took one commit to build and saved hours of manual NPC placement checking. Every new tool I build for this project has had ROI within the same day.

Dark UIs on light engines require custom everything. The GBA’s UI library assumes white backgrounds everywhere. Cursors erase with white. Frames load with white fill. Palettes expect text-on-white contrast. Going dark means replacing each of these assumptions one by one, and the replacements can’t use the convenient helper functions because those encode the assumption.

By the Numbers

Metric Value
Commits 18
Copilot requests ~50
Tool executions ~849
Sub-agents 13

18 commits. A map viewer tool, a custom window frame system, 12 NPC relocations, 4 tile/palette bugs, and every menu screen aligned to a single visual standard. The game doesn’t just work now — it looks like it belongs to itself.

Back to README