Pokémon HeartGold and SoulSilver
The Nintendo DS remakes of Pokémon Gold and Silver, read through the pret/pokeheartgold decompilation.
Pokémon HeartGold and SoulSilver are the Nintendo DS remakes of Pokémon Gold and Silver. The code read here is pret’s pokeheartgold decompilation, a work in progress that builds the US HeartGold and SoulSilver ROMs from the same code. Each system below is compared with Platinum’s code and with Gold and Silver’s or Crystal’s.
#Time of day
The clock’s hour falls into one of Platinum’s periods, read from the same hour table (5 periods, Platinum's table1)1. Gold and Silver have fewer periods, and their night starts earlier1. Wild Pokémon fold the periods back into morning, day, and night, and the clock-bound evolution methods ask only whether it is night, night from 20:00 to 3:592,2.
#Wild encounters
A step on grass or water rolls twice. The first roll must pass a gate that depends on how the player moves, on very tall grass, on the radio, and on how often the player has turned to face the other way; the second must pass the map’s own rate3. Platinum’s gate is simpler, and for its first attempts after a battle or a map change Platinum adds a grace roll; HeartGold and SoulSilver skip those checks outright3. Gold and Silver have a single roll against a rate that changes with the time of day4,5. Abilities, the flutes, and the Cleanse Tag then adjust the map’s rate6.
The slot odds are Platinum’s, 20 · 20 · 10 · 10 · 10 · 10 · 5 · 5 · 4 · 4 · 1 · 1 %7 over a map’s grass slots7. The time of day picks the species in those slots from a full list for each period. Platinum swaps only a pair of its slots, and Gold and Silver hold a smaller set for each period5.
#Capture
Capture is Platinum’s formula and shake checks, unchanged8. The Apricorn balls, which Crystal also has, change the catch rate itself before the formula runs9. Crystal’s Level Ball skips the HP and status steps, and its disassembly marks the Moon, Love, and Fast Balls as bugged. In the remake every Apricorn ball goes through the HP and status steps, the Moon Ball reads a species list, the Love Ball wants the opposite gender, and the Fast Ball reads base Speed9. The Heavy Ball gains a bug of its own, −20 below 204.8 kg (Crystal −20 below 102.4 kg)10, because its last test reads the catch rate in place of the weight10.
#Critical hits
Critical hits keep Platinum’s stages, abilities, and Lucky Chant11.
Mechanics
| Mechanic | Kind | How |
|---|---|---|
| Time of day | External clock | Platinum's five periods from the DS clock's hour, folded to three for wild Pokémon; night starts at 20:00 |
| Random encounter | By area | a gate set by movement, very tall grass, the radio, and turning in place, then the map's one rate; 12 slots whose species change with the time of day |
| Capture | Four shake checks | Platinum's formula and four shake checks; the Apricorn balls change the catch rate itself |
| Critical hit | From stages | Platinum's stage table, with Super Luck, Lucky Chant, and Sniper |
| Ability | lead-Pokémon abilities that double or halve the encounter rate, and the critical-hit abilities | |
| Held item | a Cleanse Tag or Pure Incense on the lead Pokémon cuts the encounter rate to two thirds |
Every claim about this game, by concept (10)
References
- ^ abcHeartGold and SoulSilver (the US releases, which share this code) split the clock's hour into five periods with a 24-entry table: morning from 4:00 to 9:59, day from 10:00 to 16:59, evening from 17:00 to 19:59, night from 20:00 to 23:59, and late night from 0:00 to 3:59. Platinum's table gives every hour the same period (its names are morning, day, twilight, night, and late night). Gold and Silver have three periods: morning from 4:00, day from 10:00, and night from 18:00.Codepret/pokeheartgold · pret/pokeplatinum · pret/pokegold
How we know
8typedef enum RTC_TimeOfDay { 9 RTC_TIMEOFDAY_MORN = 0, 10 RTC_TIMEOFDAY_DAY, 11 RTC_TIMEOFDAY_EVE, 12 RTC_TIMEOFDAY_NITE, 13 RTC_TIMEOFDAY_LATE,
include/gf_rtc.h lines 8–15 at 6988158 · upstream ↗ 164TIMEOFDAY GF_RTC_GetTimeOfDayByHour(s32 hour) { 165 static const u8 sTimeOfDayByHour[24] = { 166 RTC_TIMEOFDAY_LATE, 167 RTC_TIMEOFDAY_LATE, 168 RTC_TIMEOFDAY_LATE, 169 RTC_TIMEOFDAY_LATE, 170 RTC_TIMEOFDAY_MORN, 171 RTC_TIMEOFDAY_MORN, 172 RTC_TIMEOFDAY_MORN, 173 RTC_TIMEOFDAY_MORN, 174 RTC_TIMEOFDAY_MORN, 175 RTC_TIMEOFDAY_MORN, ⋮
src/gf_rtc.c lines 164–193 at 6988158 · upstream ↗ 153enum TimeOfDay TimeOfDayForHour(int hour) ⋮ 155 static const u8 lookup[24] = { 156 TIMEOFDAY_LATE_NIGHT, 157 TIMEOFDAY_LATE_NIGHT, 158 TIMEOFDAY_LATE_NIGHT, 159 TIMEOFDAY_LATE_NIGHT, 160 TIMEOFDAY_MORNING, 161 TIMEOFDAY_MORNING, 162 TIMEOFDAY_MORNING, 163 TIMEOFDAY_MORNING, 164 TIMEOFDAY_MORNING, 165 TIMEOFDAY_MORNING, ⋮
src/rtc.c lines 153–183 at 49f78f3 · upstream ↗ 37DEF MORN_HOUR EQU 4 ; 4 AM 38DEF DAY_HOUR EQU 10 ; 10 AM 39DEF NITE_HOUR EQU 18 ; 6 PM
constants/misc_constants.asm lines 37–39 at fc702b6 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abHeartGold and SoulSilver fold the five periods into three for wild Pokémon (
GF_RTC_GetTimeOfDayWildParamByHour: morning; day or evening; night or late night), and grass encounters pick their species list the same way.IsNighttimeis true at night or late night, so from 20:00 to 3:59 (computed from the hour table), and the evolution methods tied to the clock read it: friendship of at least 220 by day (EVO_FRIENDSHIP_DAY) or by night (EVO_FRIENDSHIP_NIGHT), and a held item by day or by night (EVO_ITEM_DAY,EVO_ITEM_NIGHT).Codepret/pokeheartgoldHow we know
142BOOL IsNighttime(void) { 143 switch (GF_RTC_GetTimeOfDay()) { 144 case RTC_TIMEOFDAY_NITE: 145 case RTC_TIMEOFDAY_LATE: 146 return TRUE; 147 default: 148 return FALSE;
src/gf_rtc.c lines 142–150 at 6988158 · upstream ↗ 196TimeOfDayWildParam GF_RTC_GetTimeOfDayWildParamByHour(s32 hour) { 197 switch (GF_RTC_GetTimeOfDayByHour(hour)) { 198 case RTC_TIMEOFDAY_MORN: 199 return TIMEOFDAY_WILD_MORN; 200 case RTC_TIMEOFDAY_DAY: 201 case RTC_TIMEOFDAY_EVE: 202 return TIMEOFDAY_WILD_DAY; 203 default: 204 return TIMEOFDAY_WILD_NITE;
src/gf_rtc.c lines 196–205 at 6988158 · upstream ↗ 119static void EncSlotArray_Init_Land(const EncounterData *encData, EncounterSlot *encSlots) { 120 TIMEOFDAY timeOfDay = GF_RTC_GetTimeOfDay(); ⋮ 123 if (timeOfDay == RTC_TIMEOFDAY_MORN) { 124 slotSpecies = encData->landSlots.species_morn; 125 } else if (timeOfDay == RTC_TIMEOFDAY_DAY || timeOfDay == RTC_TIMEOFDAY_EVE) { 126 slotSpecies = encData->landSlots.species_day; 127 } else if (timeOfDay == RTC_TIMEOFDAY_NITE || timeOfDay == RTC_TIMEOFDAY_LATE) { 128 slotSpecies = encData->landSlots.species_nite;
src/field/encounter_check.c lines 119–130 at 6988158 · upstream ↗ 2829 case EVO_FRIENDSHIP_DAY: 2830 if (IsNighttime() == 0 && friendship >= 220) { ⋮ 2835 case EVO_FRIENDSHIP_NIGHT: 2836 if (IsNighttime() == 1 && friendship >= 220) {
src/pokemon.c lines 2829–2839 at 6988158 · upstream ↗ 2902 case EVO_ITEM_DAY: 2903 if (IsNighttime() == 0 && evoTable[i].param == heldItem) { ⋮ 2908 case EVO_ITEM_NIGHT: 2909 if (IsNighttime() == 1 && evoTable[i].param == heldItem) {
src/pokemon.c lines 2902–2909 at 6988158 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abWhen a step ends on grass or water, HeartGold and SoulSilver roll twice, each
LCRandom() % 100. The first roll must fall below a gate that starts at 40, loses 20 unless the player is cycling or surfing or an unnamed avatar check (sub_0205DE98, still assembly) passes, gains 40 in very tall grass or else 30 when cycling, gains 30, 40, or 60 once a counter of reversed facings (reverseTurnFrameSteps) reaches 2, 3, or 4, and gains or loses 25 for the Pokémon March or the Pokémon Lullaby on the radio, capped at 100. The second must fall below the map's own rate for that terrain, capped at 100. A check comes only once a step counter (encounterInhibitSteps), which a wild battle or a warp clears, is past 3. Platinum's gate is 40, plus 30 in very tall grass or when cycling, adjusted on special dates, with no walking penalty, facing counter, or radio. Before its gate, Platinum adds a grace roll: while its count of attempts is below for the rate it is passed (computed), each attempt adds 1 to the count and goes on only ifLCRNG_RandMod(100)is below 5; a comment says this covers the first attempts after each map transition or wild battle. HeartGold and SoulSilver's check has no such roll; they skip the check outright while the step counter is 3 or less.Codepret/pokeheartgold · pret/pokeplatinumrevisedHow we know
565static BOOL FieldSystem_EncounterRateRoll(FieldSystem *fieldSystem, u8 encounterRate, u8 metatileBehavior) { 566 if (encounterRate > 100) { 567 encounterRate = 100; ⋮ 569 int avatarState = PlayerAvatar_GetState(fieldSystem->playerAvatar); 570 u8 modifiedRate = 40; 571 if (avatarState != PLAYER_STATE_CYCLING && avatarState != PLAYER_STATE_SURFING && !sub_0205DE98(fieldSystem->playerAvatar)) { 572 modifiedRate -= 20; ⋮ 574 if (MetatileBehavior_IsVeryTallGrass(metatileBehavior)) { 575 modifiedRate += 40; 576 } else if (avatarState == PLAYER_STATE_CYCLING) { 577 modifiedRate += 30; ⋮ 579 modifiedRate += getStepCountEncounterRateBoost(fieldSystem->reverseTurnFrameSteps); ⋮
src/field/encounter_check.c lines 565–590 at 6988158 · upstream ↗ 595static u8 getStepCountEncounterRateBoost(u16 stepCount) { ⋮ 598 if (stepCount >= 4) { 599 result = 60; 600 } else if (stepCount >= 3) { 601 result = 40; 602 } else if (stepCount >= 2) { 603 result = 30;
src/field/encounter_check.c lines 595–607 at 6988158 · upstream ↗ 623static BOOL FieldSystem_SecondEncounterRoll(FieldSystem *fieldSystem, u16 encounterRate) { ⋮ 626 if (LCRandRange(100) < encounterRate) {
src/field/encounter_check.c lines 623–627 at 6988158 · upstream ↗ 30static inline u16 LCRandRange(const u16 maximum) { ⋮ 35 u16 result = LCRandom() % maximum;
include/math_util.h lines 30–35 at 6988158 · upstream ↗ 1397static void FieldSystem_UpdateTurnFrameCounter(FieldSystem *fieldSystem) { 1398 u8 facingDirection = PlayerAvatar_GetFacingDirection(fieldSystem->playerAvatar); ⋮ 1401 case DIR_NORTH: 1402 if (fieldSystem->lastFacingDirection == DIR_SOUTH) { 1403 isTurnFrame = TRUE; ⋮ 1408 isTurnFrame = TRUE; ⋮ 1413 isTurnFrame = TRUE; ⋮ 1418 isTurnFrame = TRUE; ⋮ 1425 if (isTurnFrame && fieldSystem->reverseTurnFrameSteps < 0xFFFF) { 1426 ++fieldSystem->reverseTurnFrameSteps; ⋮ 1428 fieldSystem->lastFacingDirection = facingDirection;
src/field/encounter_check.c lines 1397–1428 at 6988158 · upstream ↗ 215BOOL FieldSystem_PerformLandOrSurfEncounterCheck(FieldSystem *fieldSystem) { 216 if (fieldSystem->encounterInhibitSteps <= 3) { 217 return FALSE; ⋮ 230 encounterRate = FieldSystem_GetTileEncounterTypeAndRate(fieldSystem, metatileBehavior, &encounterType); 231 if (encounterRate == 0) { 232 return FALSE; ⋮ 234 FieldSystem_UpdateTurnFrameCounter(fieldSystem);
src/field/encounter_check.c lines 215–234 at 6988158 · upstream ↗ 315 if (found) { 316 sub_02050B08(fieldSystem, battleSetup); 317 fieldSystem->encounterInhibitSteps = 0; 318 fieldSystem->reverseTurnFrameSteps = 0;
src/field/encounter_check.c lines 315–318 at 6988158 · upstream ↗ 268 fieldSystem->encounterInhibitSteps = 0; 269 fieldSystem->reverseTurnFrameSteps = 0;
src/field_warp_tasks.c lines 268–269 at 6988158 · upstream ↗ 471static BOOL FieldSystem_CheckWildEncounter(FieldSystem *fieldSystem) { 472 if (fieldSystem->encounterInhibitSteps < 0xFFFF) { 473 fieldSystem->encounterInhibitSteps++;
src/field/field_control.c lines 471–474 at 6988158 · upstream ↗ 1031static int FieldSystem_GetLandEncounterRate(FieldSystem *fieldSystem) { 1032 const EncounterData *encData = MapEvents_GetLoadedEncTable(fieldSystem); 1033 return encData->encounterRate_walking; ⋮ 1037 const EncounterData *encData = MapEvents_GetLoadedEncTable(fieldSystem); 1038 return encData->encounterRate_surfing;
src/field/encounter_check.c lines 1031–1039 at 6988158 · upstream ↗ 763 // flat 60% chance for the encounter attempt to fail, which is decreased by long grass, biking, and on certain dates. 764 u8 flatEncounterRate = 40; ⋮ 766 if (TileBehavior_IsVeryTallGrass(tileBehavior)) { 767 flatEncounterRate += 30; 768 } else if (PlayerAvatar_GetPlayerState(fieldSystem->playerAvatar) == PLAYER_AVATAR_CYCLING) { 769 flatEncounterRate += 30; ⋮ 772 flatEncounterRate = SpecialDates_ModifyEncounterRate(flatEncounterRate, FieldSystem_HasPenalty(fieldSystem)); ⋮ 778 return LCRNG_RandMod(100) < flatEncounterRate && CheckEncounterRateSuccess(fieldSystem, encounterRate);
src/overlay006/wild_encounters.c lines 763–778 at 49f78f3 · upstream ↗ 750static BOOL ShouldGetRandomEncounter(FieldSystem *fieldSystem, const u32 encounterRate, const u8 tileBehavior) ⋮ 752 u32 encRate = encounterRate << 8; ⋮ 754 // lowers effective encounter rate by 95% for the first few steps after each encounter. 755 if (!GracePeriodStepsUsed(fieldSystem, encRate)) { 756 fieldSystem->wildBattleMetadata.encounterAttempts++; ⋮ 758 if (LCRNG_RandMod(100) >= 5) { 759 return FALSE;
src/overlay006/wild_encounters.c lines 750–761 at 49f78f3 · upstream ↗ 797// Suppresses the first few encounter attempts after each map transition or wild battle. 798static BOOL GracePeriodStepsUsed(FieldSystem *fieldSystem, u32 encounterRate) ⋮ 800 encounterRate = (encounterRate / 10) >> 8; ⋮ 802 if (encounterRate > 8) { 803 encounterRate = 8; ⋮ 806 encounterRate = 8 - (encounterRate); // higher encounter rates also lower the grace period. ⋮ 808 return fieldSystem->wildBattleMetadata.encounterAttempts >= encounterRate;
src/overlay006/wild_encounters.c lines 797–808 at 49f78f3 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^On each step, Gold and Silver's
TryWildEncounterreads the map's encounter rate for the current time of day (morning, day, or night), or its water rate, doubles it for the Pokémon March or Ruins of Alph radio, halves it for the Pokémon Lullaby or a Cleanse Tag, and starts a battle whenRandomis below it; Crystal keeps this unchanged.Codepret/pokegold · pret/pokecrystalHow we know
212.EncounterRate: 213 call GetMapEncounterRate 214 call ApplyMusicEffectOnEncounterRate 215 call ApplyCleanseTagEffectOnEncounterRate 216 call Random 217 cp b 218 ret ⋮ 220GetMapEncounterRate: 221 ld hl, wMornEncounterRate 222 call CheckOnWater 223 ld a, wWaterEncounterRate - wMornEncounterRate 224 jr z, .ok ⋮
engine/overworld/wildmons.asm lines 212–251 at fc702b6 · upstream ↗ engine/overworld/wildmons.asm#L176-L251 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-21.
Checked by a second AI model, 2026-10-21.
- ^ abA HeartGold or SoulSilver map's encounter record holds one walking rate and three full species lists for its 12 grass slots, morning, day, and night, sharing one list of levels. Gold and Silver keep a separate grass rate for each time of day and three sets of 7 slots. Platinum keeps one 12-slot list and swaps only slots 2 and 3 for their day or night species outside the morning.Codepret/pokeheartgold · pret/pokegold · pret/pokeplatinum
How we know
23typedef struct LandEncounterData { 24 u8 levels[NUM_ENCOUNTERS_LAND]; // shared between times of day 25 u16 species_morn[NUM_ENCOUNTERS_LAND]; 26 u16 species_day[NUM_ENCOUNTERS_LAND]; 27 u16 species_nite[NUM_ENCOUNTERS_LAND]; 28} LandEncounterData;
include/wild_encounter.h lines 23–28 at 6988158 · upstream ↗ 36typedef struct EncounterData { 37 u8 encounterRate_walking; // 0x00 38 u8 encounterRate_surfing; // 0x01 ⋮ 44 LandEncounterData landSlots; // 0x08
include/wild_encounter.h lines 36–44 at 6988158 · upstream ↗ 133 for (int i = 0; i < NUM_ENCOUNTERS_LAND; ++i) { 134 encSlots[i].species = slotSpecies[i]; 135 encSlots[i].maxLevel = encData->landSlots.levels[i]; 136 encSlots[i].minLevel = encData->landSlots.levels[i];
src/field/encounter_check.c lines 133–136 at 6988158 · upstream ↗ 220GetMapEncounterRate: 221 ld hl, wMornEncounterRate 222 call CheckOnWater 223 ld a, wWaterEncounterRate - wMornEncounterRate 224 jr z, .ok 225 ld a, [wTimeOfDay] 226.ok
engine/overworld/wildmons.asm lines 212–251 at fc702b6 · upstream ↗ 283 ld a, [wTimeOfDay] 284 ld bc, NUM_GRASSMON * 2 285 call AddNTimes
engine/overworld/wildmons.asm lines 283–286 at fc702b6 · upstream ↗ 181// Default encounters are morning. They get replaced by this if it is not morning. 182void WildEncounters_ReplaceTimedEncounters(const WildEncounters *encounterData, int *timedSlot1, int *timedSlot2) ⋮ 184 int timeOfDay = GetTimeOfDay(); ⋮ 186 if (timeOfDay == TIMEOFDAY_DAY || timeOfDay == TIMEOFDAY_TWILIGHT) { 187 *timedSlot1 = encounterData->dayEncounters[0]; ⋮ 189 } else if (timeOfDay == TIMEOFDAY_NIGHT || timeOfDay == TIMEOFDAY_LATE_NIGHT) { 190 *timedSlot1 = encounterData->nightEncounters[0];
src/overlay006/wild_encounters.c lines 181–193 at 49f78f3 · upstream ↗ 334 WildEncounters_ReplaceTimedEncounters(encounterData, &encounterTable[2].species, &encounterTable[3].species);src/overlay006/wild_encounters.c line 334 at 49f78f3 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^Before the two rolls, HeartGold and SoulSilver adjust the map's rate in this order: the lead Pokémon's ability (unless it is an Egg) doubles it for Arena Trap, No Guard, or Illuminate, halves it for White Smoke, Quick Feet, or Stench, or for Snow Cloak in snow, capped at 100; the Black Flute halves it and the White Flute raises it by half; and a Cleanse Tag or Pure Incense held by the lead Pokémon cuts it to 2/3. Gold and Silver instead double the rate for the Pokémon March or the Ruins of Alph radio and halve it for the Pokémon Lullaby or a Cleanse Tag.Codepret/pokeheartgold · pret/pokegold
How we know
244 encounterRate = ApplyAbilityEffectToEncounterRate(FALSE, encounterRate, &encounterGen, LocalFieldData_GetWeatherType(Save_LocalFieldData_Get(fieldSystem->saveData)), leadMon); 245 ApplyFluteEffectToEncounterRate(fieldSystem, &encounterRate); 246 ApplyLeadMonHeldItemEffectToEncounterRate(leadMon, &encounterRate); 247 BOOL ret = FieldSystem_EncounterRateRoll(fieldSystem, encounterRate, metatileBehavior) ? TRUE : FALSE;
src/field/encounter_check.c lines 244–247 at 6988158 · upstream ↗ 1123 if (!encounterGen->isEgg) { ⋮ 1126 ret *= 2; ⋮ 1128 } else if (encounterGen->ability == ABILITY_ARENA_TRAP || encounterGen->ability == ABILITY_NO_GUARD || encounterGen->ability == ABILITY_ILLUMINATE) { 1129 ret *= 2; 1130 } else if (encounterGen->ability == ABILITY_SNOW_CLOAK) { 1131 if (weatherType == WEATHER_SNOW) { 1132 ret /= 2; ⋮ 1134 } else if (encounterGen->ability == ABILITY_WHITE_SMOKE || encounterGen->ability == ABILITY_QUICK_FEET || encounterGen->ability == ABILITY_STENCH) { 1135 ret /= 2; ⋮ 1137 if (ret > 100) { 1138 ret = 100;
src/field/encounter_check.c lines 1120–1140 at 6988158 · upstream ↗ 720 if (heldItem == ITEM_CLEANSE_TAG || heldItem == ITEM_PURE_INCENSE) { 721 *pEncounterRate = *pEncounterRate * 2 / 3; ⋮ 727 if (flute == FLUTE_BLACK) { 728 *pEncounterRate /= 2; 729 } else if (flute == FLUTE_WHITE) { 730 *pEncounterRate += *pEncounterRate / 2;
src/field/encounter_check.c lines 718–731 at 6988158 · upstream ↗ 214 call ApplyMusicEffectOnEncounterRate 215 call ApplyCleanseTagEffectOnEncounterRate ⋮ 234; Pokemon March and Ruins of Alph signal double encounter rate. 235; Pokemon Lullaby halves encounter rate. ⋮ 251; Cleanse Tag halves encounter rate.
engine/overworld/wildmons.asm lines 212–251 at fc702b6 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abHeartGold and SoulSilver have 12 grass slots and 5 surfing slots, picked by one
LCRandom() % 100against bounds inEncounterSlot_WildMonSlotRoll_LandandEncounterSlot_WildMonSlotRoll_Surfing, so they come up 20, 20, 10, 10, 10, 10, 5, 5, 4, 4, 1, and 1 percent and 60, 30, 5, 4, and 1 percent (computed from the bounds). Platinum'sGetGroundEncounterSlotandGetWaterEncounterSlothave the same bounds. Gold and Silver have 7 grass slots and 3 water slots, with cumulative bounds inGrassMonProbTable(30, 60, 80, 90, 95, 99, 100) andWaterMonProbTable(60, 90, 100), so 30, 30, 20, 10, 5, 4, and 1 percent and 60, 30, and 10 percent (computed from the differences).Codepret/pokeheartgold · pret/pokeplatinum · pret/pokegoldrevisedHow we know
632static u8 EncounterSlot_WildMonSlotRoll_Land(void) { 633 u8 rnd = LCRandRange(100); ⋮ 635 if (rnd < 20) { 636 return 0; 637 } else if (rnd >= 20 && rnd < 40) { 638 return 1; ⋮ 640 return 2; ⋮ 642 return 3; ⋮ 644 return 4; ⋮ 651 } else if (rnd >= 90 && rnd < 94) { 652 return 8; 653 } else if (rnd >= 94 && rnd < 98) { ⋮
src/field/encounter_check.c lines 632–676 at 6988158 · upstream ↗ 820static u8 GetGroundEncounterSlot(void) ⋮ 822 u8 roll = LCRNG_RandMod(100); ⋮ 824 if (roll < 20) { 825 return 0; ⋮ 827 return 1; ⋮ 829 return 2; ⋮ 831 return 3; ⋮ 833 return 4; ⋮ 842 } else if (roll >= 94 && roll < 98) { 843 return 9; 844 } else if (roll == 98) { 845 return 10; ⋮
src/overlay006/wild_encounters.c lines 820–866 at 49f78f3 · upstream ↗ 6GrassMonProbTable: 7 table_width 2 8 mon_prob 30, 0 ; 30% chance 9 mon_prob 60, 1 ; 30% chance 10 mon_prob 80, 2 ; 20% chance 11 mon_prob 90, 3 ; 10% chance 12 mon_prob 95, 4 ; 5% chance 13 mon_prob 99, 5 ; 4% chance 14 mon_prob 100, 6 ; 1% chance ⋮ 17WaterMonProbTable: 18 table_width 2 19 mon_prob 60, 0 ; 60% chance ⋮
data/wild/probabilities.asm lines 6–21 at fc702b6 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^HeartGold and SoulSilver's
BattleSystem_CalculateBallShakeskeeps Platinum's capture math: for catch rate , ball multiplier in tenths, max HP and current HP , ×2 for sleep or freeze and ×15/10 for burn, paralysis, poison, or bad poison, a catch at once when is 255 or more, and otherwise four 16-bit rolls each below , with the Master Ball's shakes set to four after the draw. The Ultra, Great, Poké, and Safari Balls keep Platinum's 20, 15, 10, and 15 (an assembly data table in the decompilation), and a Pal Park or tutorial battle always catches.Codepret/pokeheartgold · pret/pokeplatinumHow we know
7145static u32 BattleSystem_CalculateBallShakes(BattleSystem *bsys, BattleContext *ctx) { ⋮ 7150 if (BattleSystem_GetBattleType(bsys) & (BATTLE_TYPE_PAL_PARK | BATTLE_TYPE_TUTORIAL)) { 7151 return BALL_SHAKE_MAX;
src/battle/battle_command.c lines 7145–7152 at 6988158 · upstream ↗ 7290 } else { 7291 // Item IDs from 2-5: Ultra Ball, Great Ball, Pokeball, Safari Ball. 7292 ballMultiplier = sStandardBallCatchRates[ctx->itemTemp - 2]; ⋮ 7295 s32 maxHpTimes3 = ctx->battleMons[ctx->battlerIdTarget].maxHp * 3; 7296 u32 lostHp = maxHpTimes3 - (ctx->battleMons[ctx->battlerIdTarget].hp * 2); ⋮ 7300 u32 baseCatchMultiplier = catchRate * ballMultiplier; 7301 u32 modifiedCatchRate = ((baseCatchMultiplier / 10) * lostHp) / maxHpTimes3; ⋮ 7303 s32 status = ctx->battleMons[ctx->battlerIdTarget].status; 7304 if ((STATUS_SLEEP | STATUS_FREEZE) & status) { 7305 modifiedCatchRate *= 2; ⋮ 7307 if ((STATUS_BURN | STATUS_PARALYSIS | STATUS_BAD_POISON | STATUS_POISON) & status) { 7308 modifiedCatchRate = (modifiedCatchRate * 15) / 10; ⋮
src/battle/battle_command.c lines 7290–7327 at 6988158 · upstream ↗ 721#define BALL_SHAKE_MAX 4include/constants/battle.h line 721 at 6988158 · upstream ↗ 17sStandardBallCatchRates: ; 0x0226C2EC 18 .byte 20, 15, 10, 15
asm/overlay_12_battle_command.s lines 17–18 at 6988158 · upstream ↗ 1112u16 BattleSystem_Random(BattleSystem *battleSystem) { 1113 battleSystem->rand = battleSystem->rand * 1103515245L + 24691; 1114 return battleSystem->rand / 65536L;
src/battle/battle_system.c lines 1112–1114 at 6988158 · upstream ↗ 10978 u32 catchRate = (speciesMod * ballMod / 10) 10979 * (battleCtx->battleMons[battleCtx->defender].maxHP * 3 - battleCtx->battleMons[battleCtx->defender].curHP * 2) 10980 / (battleCtx->battleMons[battleCtx->defender].maxHP * 3); ⋮ 10982 if (battleCtx->battleMons[battleCtx->defender].status & (MON_CONDITION_SLEEP | MON_CONDITION_FREEZE)) { 10983 catchRate *= 2; ⋮ 10986 if (battleCtx->battleMons[battleCtx->defender].status & (MON_CONDITION_POISON | MON_CONDITION_BURN | MON_CONDITION_PARALYSIS | MON_CONDITION_TOXIC)) { 10987 catchRate = catchRate * 15 / 10;
src/battle/battle_script.c lines 10978–10988 at 49f78f3 · upstream ↗ 10991 if (catchRate >= 255) { 10992 shakes = BALL_3_SHAKES_SUCCESS; 10993 } else { 10994 u32 sqrtRate = (0xFF << 16) / catchRate; ⋮ 11003 catchRate = (0xFFFF << 4) / catchRate; ⋮ 11011 if (battleCtx->msgItemTemp == ITEM_MASTER_BALL) { 11012 shakes = BALL_3_SHAKES_SUCCESS;
src/battle/battle_script.c lines 10990–11013 at 49f78f3 · upstream ↗ 10868static const u8 sBasicBallMod[] = { 10869 20, // ITEM_ULTRA_BALL 10870 15, // ITEM_GREAT_BALL 10871 10, // ITEM_POKE_BALL 10872 15, // ITEM_SAFARI_BALL
src/battle/battle_script.c lines 10868–10873 at 49f78f3 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abIn HeartGold and SoulSilver the Apricorn balls change the catch rate itself and leave the multiplier at 10, after which the rate is capped at 255 (a negative one becomes 1) and goes through the usual HP and status steps: the Fast Ball ×4 when the wild species' base Speed is 100 or more, the Level Ball ×2 when the attacking battler's (
battlerIdAttacker) level is above the wild Pokémon's, ×4 when half its level (rounded down) is above it, and ×8 when a quarter of its level (rounded down) is, the Lure Ball ×3 when fishing, the Love Ball ×8 for the same species of the opposite gender, and the Moon Ball ×4 for the 14 species of the Nidoran, Clefairy, Jigglypuff, and Skitty families listed in an assembly data table. Crystal's Level Ball skips the HP and status steps, and its disassembly's comments mark the Moon Ball as never boosting, the Love Ball as boosting the same gender, and the Fast Ball as boosting only three species.Codepret/pokeheartgold · pret/pokecrystalrevisedHow we know
7218 // All the apricorn balls directly alter catchRate instead of setting ballMultiplier. ⋮ 7221 case ITEM_FAST_BALL: { 7222 u32 speed = GetMonBaseStat(ctx->battleMons[ctx->battlerIdTarget].species, BASE_SPEED); 7223 if (speed >= 100) { 7224 catchRate *= 4; ⋮ 7228 case ITEM_LEVEL_BALL: { 7229 u8 attackerLevel = ctx->battleMons[ctx->battlerIdAttacker].level; 7230 u8 defenderLevel = ctx->battleMons[ctx->battlerIdTarget].level; 7231 if (attackerLevel <= defenderLevel) { 7232 } else if (attackerLevel / 2 <= defenderLevel) { 7233 catchRate *= 2; 7234 } else if (attackerLevel / 4 <= defenderLevel) { ⋮
src/battle/battle_command.c lines 7218–7245 at 6988158 · upstream ↗ 7260 case ITEM_LOVE_BALL: { 7261 if (ctx->battleMons[ctx->battlerIdAttacker].species == ctx->battleMons[ctx->battlerIdTarget].species && ctx->battleMons[ctx->battlerIdAttacker].gender != ctx->battleMons[ctx->battlerIdTarget].gender) { 7262 catchRate *= 8; ⋮ 7266 case ITEM_MOON_BALL: { ⋮ 7268 for (i = 0; i < NELEMS(sMoonBallPokemon); i++) { 7269 if (sMoonBallPokemon[i] == ctx->battleMons[ctx->battlerIdTarget].species) { 7270 catchRate *= 4; ⋮ 7285 if (catchRate > 0xFF) { 7286 catchRate = 0xFF; 7287 } else if (catchRate < 0) { 7288 catchRate = 1;
src/battle/battle_command.c lines 7259–7288 at 6988158 · upstream ↗ 7161 ballMultiplier = 10; // All ball multipliers are /10, so this is x1. ⋮ 7165 if (ctx->itemTemp > ITEM_SAFARI_BALL) { // Skip all balls that have no condition.
src/battle/battle_command.c lines 7161–7166 at 6988158 · upstream ↗ 132sMoonBallPokemon: ; 0x0226C3E8 133 .short SPECIES_NIDORAN_F 134 .short SPECIES_NIDORINA 135 .short SPECIES_NIDOQUEEN 136 .short SPECIES_NIDORAN_M 137 .short SPECIES_NIDORINO 138 .short SPECIES_NIDOKING 139 .short SPECIES_CLEFFA 140 .short SPECIES_CLEFAIRY 141 .short SPECIES_CLEFABLE 142 .short SPECIES_IGGLYBUFF 143 .short SPECIES_JIGGLYPUFF ⋮
asm/overlay_12_battle_command.s lines 132–146 at 6988158 · upstream ↗ 272.skip_or_return_from_ball_fn 273 ld a, [wCurItem] 274 cp LEVEL_BALL 275 ld a, b 276 jp z, .skip_hp_calc
engine/items/item_effects.asm lines 272–276 at 97187cd · upstream ↗ 917; BUG: Moon Ball does not boost catch rate (see docs/bugs_and_glitches.md) ⋮ 925 cp MOON_STONE_RED ; BURN_HEAL
engine/items/item_effects.asm lines 917–925 at 97187cd · upstream ↗ 977; BUG: Love Ball boosts catch rate for the wrong gender (see docs/bugs_and_glitches.md) 978 ld a, d 979 pop de 980 cp d 981 pop bc 982 ret nz
engine/items/item_effects.asm lines 977–982 at 97187cd · upstream ↗ 1001FastBallMultiplier: 1002 ld a, [wTempEnemyMonSpecies] 1003 ld c, a 1004 ld hl, FleeMons 1005 ld d, 3 ⋮ 1008; BUG: Fast Ball only boosts catch rate for three Pokémon (see docs/bugs_and_glitches.md)
engine/items/item_effects.asm lines 1001–1008 at 97187cd · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^ abHeartGold and SoulSilver's Heavy Ball adds 40 to the catch rate at 409.6 kg or more, 30 at 307.2 kg, and 20 at 204.8 kg, reading weight in tenths of a kilogram from the Pokédex data. Below 204.8 kg its last branch tests the catch rate against 1024 instead of the weight, and a catch rate is at most 255, so every lighter Pokémon loses 20 (a result below 0 becomes 1); a comment in the decompilation calls this a mistake. Crystal subtracts 20 only below 102.4 kg and adds nothing from there to 204.8 kg.Codepret/pokeheartgold · pret/pokecrystal
How we know
7246 case ITEM_HEAVY_BALL: { 7247 s32 weight = GetMonWeight(ctx->battleMons[ctx->battlerIdTarget].species); 7248 // Weight is in kilograms, moved to the left by 1 decimal point. 7249 if (weight >= 4096) { // 409.6 kg / 903.0 lbs. or more. 7250 catchRate += 40; 7251 } else if (weight >= 3072) { // 307.2 kg / 677.3 lbs. or more. 7252 catchRate += 30; 7253 } else if (weight >= 2048) { // 204.8 kg / 451.5 lbs. or more. 7254 catchRate += 20; 7255 } else if (catchRate < 1024) { // Catch rate is mistakenly checked here instead of weight, causing all Pokemon that do not benefit from the Heavy Ball to be penalized by it. 7256 catchRate -= 20;
src/battle/battle_command.c lines 7246–7260 at 6988158 · upstream ↗ 7286 catchRate = 0xFF; 7287 } else if (catchRate < 0) { 7288 catchRate = 1;
src/battle/battle_command.c lines 7286–7288 at 6988158 · upstream ↗ 779HeavyBallMultiplier: 780; subtract 20 from catch rate if weight < 102.4 kg 781; else add 0 to catch rate if weight < 204.8 kg 782; else add 20 to catch rate if weight < 307.2 kg 783; else add 30 to catch rate if weight < 409.6 kg 784; else add 40 to catch rate
engine/items/item_effects.asm lines 779–784 at 97187cd · upstream ↗ 843 cp HIGH(1024) ; 102.4 kg 844 jr c, .lightmon ⋮ 857 ld a, b ⋮ 859 ld b, a 860 ret nc ⋮ 862 ret ⋮ 864.lightmon 865 ld a, b 866 sub 20 867 ld b, a 868 ret nc 869 ld b, $1 ⋮
engine/items/item_effects.asm lines 843–870 at 97187cd · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^HeartGold and SoulSilver's
TryCriticalHitkeeps Platinum's critical-hit rule: the stage sum (+2 Focus Energy, +1 for aHOLD_EFFECT_CRITRATE_UPitem, +1 Super Luck, +2 for Chansey or Farfetch'd with its own item, plus the move's boosts) capped at 4, a chance of 1 in 16, 8, 4, 3, or 2, blocked by Battle Armor or Shell Armor unless ignored, by Lucky Chant on the target's side, or by bit 15 of the target's move-effect flags (namedMOVE_EFFECT_FLAG_LUCKY_CHANThere), and Sniper turning ×2 into ×3.Codepret/pokeheartgold · pret/pokeplatinumHow we know
5960static const u8 sCritChance[] = { 5961 16, 8, 4, 3, 2 ⋮ 5979 critUp = (((status2 & STATUS2_FOCUS_ENERGY) != 0) * 2) + (item == HOLD_EFFECT_CRITRATE_UP) + critCnt + (ability == ABILITY_SUPER_LUCK) + 2 * ((item == HOLD_EFFECT_CHANSEY_CRITRATE_UP) && (species == SPECIES_CHANSEY)) + 2 * ((item == HOLD_EFFECT_FARFETCHD_CRITRATE_UP) && (species == SPECIES_FARFETCHD)); ⋮ 5981 if (critUp > 4) { 5982 critUp = 4; ⋮ 5985 if ((BattleSystem_Random(battleSystem) % sCritChance[critUp]) == 0) { 5986 if (!CheckBattlerAbilityIfNotIgnored(ctx, battlerIdAttacker, battlerIdTarget, ABILITY_BATTLE_ARMOR) && !CheckBattlerAbilityIfNotIgnored(ctx, battlerIdAttacker, battlerIdTarget, ABILITY_SHELL_ARMOR) && !(sideCondition & SIDE_CONDITION_LUCKY_CHANT) && !(moveEffect & MOVE_EFFECT_FLAG_LUCKY_CHANT)) { 5987 ret = 2; ⋮ 5991 if ((ret == 2) && GetBattlerAbility(ctx, battlerIdAttacker) == ABILITY_SNIPER) { 5992 ret = 3;
src/battle/overlay_12_0224E4FC.c lines 5960–5995 at 6988158 · upstream ↗ 177#define MOVE_EFFECT_FLAG_LUCKY_CHANT (1 << 15)include/constants/battle.h line 177 at 6988158 · upstream ↗ 7097static const u8 sCriticalStageRates[] = { 7098 16, // neutral 7099 8, // +1 7100 4, // +2 7101 3, // +3 7102 2, // +4
src/battle/battle_lib.c lines 7096–7103 at 49f78f3 · upstream ↗ 7123 effectiveCritStage = (((attackerVolStatus & VOLATILE_CONDITION_FOCUS_ENERGY) != FALSE) * 2) 7124 + (itemEffect == HOLD_EFFECT_CRITRATE_UP) 7125 + criticalStage 7126 + (attackerAbility == ABILITY_SUPER_LUCK) ⋮ 7134 if (BattleSystem_RandNext(battleSys) % sCriticalStageRates[effectiveCritStage] == 0 7135 && Battler_IgnorableAbility(battleCtx, attacker, defender, ABILITY_BATTLE_ARMOR) == FALSE 7136 && Battler_IgnorableAbility(battleCtx, attacker, defender, ABILITY_SHELL_ARMOR) == FALSE 7137 && (sideConditions & SIDE_CONDITION_LUCKY_CHANT) == FALSE 7138 && (defenderMoveEffects & MOVE_EFFECT_NO_CRITICAL) == FALSE) { 7139 criticalMul = 2;
src/battle/battle_lib.c lines 7123–7139 at 49f78f3 · upstream ↗ 23#define MOVE_EFFECT_NO_CRITICAL (1 << 15) // checked for, but never setinclude/constants/battle/moves.h line 23 at 49f78f3 · upstream ↗ 7142 if (criticalMul == 2 && Battler_Ability(battleCtx, attacker) == ABILITY_SNIPER) { 7143 criticalMul = 3;
src/battle/battle_lib.c lines 7142–7143 at 49f78f3 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
Sources
Code
- pret contributors. 2026. “pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver.” pret. Commit 6988158. pret/pokeheartgold @ 6988158 · licence, all uses
- pret contributors. 2026. “pokeplatinum: decompilation of Pokémon Platinum.” pret. Commit 49f78f3. pret/pokeplatinum @ 49f78f3 · licence, all uses
- pret contributors. 2026. “pokegold: disassembly of Pokémon Gold and Silver.” pret. Commit fc702b6. pret/pokegold @ fc702b6 · licence, all uses
- pret contributors. 2026. “pokecrystal: disassembly of Pokémon Crystal.” pret. Commit 97187cd. pret/pokecrystal @ 97187cd · licence, all uses