01 — The Idea & Foundation
Building a Diablo-style loot system inside Pokémon FireRed
I grew up on Pokémon Blue — my first game, played it obsessively, collected the cards. Decades later — after shipping software in more languages than I can count — I picked up a small handheld for emulation and fell back in love with it. Some quality-of-life FireRed ROM hacks rekindled the itch. But not just to play it. To change it. Claude handles the GBA-specific C; I handle the design, the lore, and the decisions.
The Pitch
What if Pokémon had gear? Not held items — real gear. Randomized loot with prefixes, suffixes, rarity tiers, and quality rolls. Diablo 3 meets Pokémon FireRed.
The idea was simple: take a standard Pokémon playthrough and add one major twist. Every battle has a chance to drop gear for your Pokémon. An Amulet with +ATK. Armor with +DEF. Rare drops with exotic affixes. Your Pokémon aren’t just getting stronger through levels — they’re getting kitted out.
And for the trainer? A single relic slot. Equip a Mentor’s Tome for +50% XP. A Lucky Charm for better shiny odds. A Hunter’s Mark for improved drop rates. The kind of passive bonuses that change how you play.
Why a Decompilation?
There are two ways to hack a GBA ROM:
- Binary ROM hacking: Hex-edit the compiled ROM directly. Use tools like HxD, AdvanceMap, PGE. Poke bytes at specific offsets.
- Decompilation hacking: Start from pret/pokefirered — a full C decompilation of the game. Write real C code. Compile a new ROM.
Binary hacking is the traditional approach. Most ROM hacks use it. But for adding entirely new systems — not just moving trainers or changing Pokémon stats, but creating new game mechanics — decompilation is dramatically more powerful. You get:
- Real C code with types, structs, and functions
- A build system that produces a byte-identical ROM from source
- The ability to
#includeyour new systems into existing game logic - Compiler errors instead of mysterious crashes
The tradeoff: the codebase is enormous (~500K lines of C), the toolchain is arcane (DevKitARM cross-compiler, custom preprocessor for string encoding), and the GBA’s constraints are brutal.
Designing the Gear System
The core data structure had to be tiny. Every Pokémon stores its gear in a u16 field (2 bytes), and the actual gear data lives in a bag stored in the save file. The gear encoding packs everything into a single u32:
Bits 0-1: Slot type (Accessory / Armor)
Bits 2-3: Rarity (Common / Uncommon / Rare / Epic)
Bits 4-7: Base item (8 types: Amulet, Bracelet, Ring, Crown, Vest, Mail, Cloak, Shield)
Bits 8-11: Prefix 1 (10 options: Lucky, Stalwart, Fierce, etc.)
Bits 12-15: Prefix 2
Bits 16-19: Suffix 1 (8 options: of Focus, of Power, of the Moon, etc.)
Bits 20-21: Suffix 2
Bits 22-25: Quality (85%-130% effectiveness multiplier)
Bit 26: Is Legendary
Bits 27-30: Legendary ID
One u32. 4 bytes. That’s the entire identity of a piece of gear.
The GearEncode macro assembles it:
#define GearEncode(slot, rarity, base, p1, p2, s1, s2, quality) \
((slot) | ((rarity) << 2) | ((base) << 4) | ...)
The Drop System
After every battle, TryDropGear() runs. It rolls against the base drop rate (modified by the Hunter’s Mark relic if equipped), picks a rarity tier, randomizes affixes, and generates a quality roll. The gear appears in a post-battle notification screen.
Drop rates scale with rarity:
- Common: ~30% base chance
- Uncommon: ~15%
- Rare: ~5%
- Epic: ~1%
Higher rarity means more affix slots filled, and affixes have stronger effect ranges at higher rarities.
The Relic System
Relics are simpler than gear — one slot on the trainer, passive effects that apply globally:
| Relic | Effect |
|---|---|
| Lucky Charm | Increased shiny encounter rate |
| Mentor’s Tome | Bonus XP from battles |
| Gold Coin | Bonus prize money |
| Hunter’s Mark | Improved gear drop rate |
| Seeker’s Eye | Better item find chance |
| Breeder’s Loop | Faster egg hatching |
| Sprinter’s Boots | Walk/run speed boost |
Each relic has 4 rarity tiers with scaling effect strength (stored as per-mille values for integer math on the GBA — no floating point).
The Save File Challenge
GBA save data is fixed-size. FireRed’s SaveBlock1 and SaveBlock2 have specific byte layouts. You can’t just append data — you have to carve space from existing unused filler bytes.
I carved:
gearBag[50](200 bytes) from SaveBlock1’sunused_348CrelicBag[20](80 bytes) from SaveBlock1equippedRelic,gearChest[100],relicChest[30],merchantStock[10],gearShards,radiantShardsfrom SaveBlock2’sfiller_B20
Total SaveBlock2 must stay exactly 0xF24 bytes on GBA. I verified this obsessively.
First Code
The initial commit added ~420 lines of core gear logic (gear.h + gear.c), hooked drops into the battle system, added stat bonuses to CalculateMonStats(), and created a “GEAR” option in the party menu. The relic system followed immediately — 6 types with hooks scattered across the codebase: XP in the battle controller, money in battle scripts, shiny in wild encounter generation, egg hatch in daycare, item find in field control.
By the end of this phase, the data layer was solid. What I didn’t have was any way to see it.
That’s what the UI phase would fix — and break — and fix again.
By the Numbers
| Metric | Value |
|---|---|
| Active | ~4 hours |
| Commits | 2 |
| Lines of C added | ~6,300 |
| New source files | 12 |
| Systems built | gear, relic, legendary gear, shards, merchant, chest, altar, scrapper, drop notify |
| UI screens | 7 |
| Save file carved | ~500 bytes from filler |
| Copilot requests | 17 |
| Tool executions | ~1,270 |
| Sub-agents | 8 |
Next: 02 — The UI Challenge