data/claims/pokeheartgold.yamlPokémon HeartGold and SoulSilver (pret/pokeheartgold)
HeartGold 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. pokeheartgold/time-of-day
About: Pokémon HeartGold and SoulSilver Time of day
Value
{
"morning": [
4,
10
],
"day": [
10,
17
],
"evening": [
17,
20
],
"night": [
20,
24
],
"lateNight": [
0,
4
],
"goldSilver": {
"morning": [
4,
10
],
"day": [
10,
18
],
"night": [
18,
4
]
}
}include/gf_rtc.h#L8-L15typedef enum RTC_TimeOfDay {
RTC_TIMEOFDAY_MORN = 0,
RTC_TIMEOFDAY_DAY,
RTC_TIMEOFDAY_EVE,
RTC_TIMEOFDAY_NITE,
RTC_TIMEOFDAY_LATE,pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/gf_rtc.c#L164-L193TIMEOFDAY GF_RTC_GetTimeOfDayByHour(s32 hour) {
static const u8 sTimeOfDayByHour[24] = {
RTC_TIMEOFDAY_LATE,
…
RTC_TIMEOFDAY_MORN,
…
RTC_TIMEOFDAY_DAY,
…
RTC_TIMEOFDAY_EVE,
…
RTC_TIMEOFDAY_NITE,
…
…pokeplatinum: decompilation of Pokémon Platinum · src/rtc.c#L153-L183enum TimeOfDay TimeOfDayForHour(int hour)
{
static const u8 lookup[24] = {
TIMEOFDAY_LATE_NIGHT,
…
TIMEOFDAY_MORNING,
…
TIMEOFDAY_DAY,
…
TIMEOFDAY_TWILIGHT,
…
TIMEOFDAY_NIGHT,
…pokegold: disassembly of Pokémon Gold and Silver · constants/misc_constants.asm#L37-L39DEF MORN_HOUR EQU 4 ; 4 AM
DEF DAY_HOUR EQU 10 ; 10 AM
DEF NITE_HOUR EQU 18 ; 6 PMChecked by claude, 2026-10-22 — counted the 24 entries of both tables (HGSS 4 late, 6 morning, 7 day, 3 evening, 4 night; Platinum the same counts in the same order), read the HGSS enum, and read Gold's three hour constantsCited by: Pokémon HeartGold and SoulSilver, Time of day
HeartGold 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. IsNighttime is 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). pokeheartgold/time-of-day-readers
About: Pokémon HeartGold and SoulSilver Time of day
Value
{
"wild": {
"morning": [
"morning"
],
"day": [
"day",
"evening"
],
"night": [
"night",
"lateNight"
]
},
"isNighttime": [
20,
4
],
"friendship": 220
}src/gf_rtc.c#L142-L150BOOL IsNighttime(void) {
switch (GF_RTC_GetTimeOfDay()) {
case RTC_TIMEOFDAY_NITE:
case RTC_TIMEOFDAY_LATE:
return TRUE;
default:
return FALSE;pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/gf_rtc.c#L196-L205TimeOfDayWildParam GF_RTC_GetTimeOfDayWildParamByHour(s32 hour) {
switch (GF_RTC_GetTimeOfDayByHour(hour)) {
case RTC_TIMEOFDAY_MORN:
return TIMEOFDAY_WILD_MORN;
case RTC_TIMEOFDAY_DAY:
case RTC_TIMEOFDAY_EVE:
return TIMEOFDAY_WILD_DAY;
default:
return TIMEOFDAY_WILD_NITE;pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/field/encounter_check.c#L119-L130static void EncSlotArray_Init_Land(const EncounterData *encData, EncounterSlot *encSlots) {
TIMEOFDAY timeOfDay = GF_RTC_GetTimeOfDay();
…
if (timeOfDay == RTC_TIMEOFDAY_MORN) {
slotSpecies = encData->landSlots.species_morn;
} else if (timeOfDay == RTC_TIMEOFDAY_DAY || timeOfDay == RTC_TIMEOFDAY_EVE) {
slotSpecies = encData->landSlots.species_day;
} else if (timeOfDay == RTC_TIMEOFDAY_NITE || timeOfDay == RTC_TIMEOFDAY_LATE) {
slotSpecies = encData->landSlots.species_nite;pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/pokemon.c#L2829-L2839case EVO_FRIENDSHIP_DAY:
if (IsNighttime() == 0 && friendship >= 220) {
…
case EVO_FRIENDSHIP_NIGHT:
if (IsNighttime() == 1 && friendship >= 220) {pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/pokemon.c#L2902-L2909case EVO_ITEM_DAY:
if (IsNighttime() == 0 && evoTable[i].param == heldItem) {
…
case EVO_ITEM_NIGHT:
if (IsNighttime() == 1 && evoTable[i].param == heldItem) {Checked by claude, 2026-10-22 — read IsNighttime, the wild-parameter fold, the land slot pick, and the four clock cases in the level-up evolution switch; the hours come from the table cited in time-of-dayCited by: Evolution, Pokémon HeartGold and SoulSilver, Time of day
When 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 if LCRNG_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. pokeheartgold/encounter-rate
About: Pokémon HeartGold and SoulSilver Random encounter
Value
{
"gate": {
"base": 40,
"onFoot": -20,
"veryTallGrass": 40,
"cycling": 30,
"reversals": {
"2": 30,
"3": 40,
"4": 60
},
"march": 25,
"lullaby": -25,
"cap": 100
},
"rate": "map rate in 100, cap 100",
"skipWhileCounterAtMost": 3
}src/field/encounter_check.c#L565-L590static BOOL FieldSystem_EncounterRateRoll(FieldSystem *fieldSystem, u8 encounterRate, u8 metatileBehavior) {
if (encounterRate > 100) {
encounterRate = 100;
}
int avatarState = PlayerAvatar_GetState(fieldSystem->playerAvatar);
u8 modifiedRate = 40;
if (avatarState != PLAYER_STATE_CYCLING && avatarState != PLAYER_STATE_SURFING && !sub_0205DE98(fieldSystem->playerAvatar)) {
modifiedRate -= 20;
}
if (MetatileBehavior_IsVeryTallGrass(metatileBehavior)) {
modifiedRate += 40;
} else if (avatarState == PLAYER_STATE_CYCLING) {
…pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/field/encounter_check.c#L595-L607static u8 getStepCountEncounterRateBoost(u16 stepCount) {
…
if (stepCount >= 4) {
result = 60;
} else if (stepCount >= 3) {
result = 40;
} else if (stepCount >= 2) {
result = 30;pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/field/encounter_check.c#L623-L627static BOOL FieldSystem_SecondEncounterRoll(FieldSystem *fieldSystem, u16 encounterRate) {
…
if (LCRandRange(100) < encounterRate) {pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · include/math_util.h#L30-L35static inline u16 LCRandRange(const u16 maximum) {
…
u16 result = LCRandom() % maximum;pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/field/encounter_check.c#L1397-L1428static void FieldSystem_UpdateTurnFrameCounter(FieldSystem *fieldSystem) {
u8 facingDirection = PlayerAvatar_GetFacingDirection(fieldSystem->playerAvatar);
…
case DIR_NORTH:
if (fieldSystem->lastFacingDirection == DIR_SOUTH) {
isTurnFrame = TRUE;
…
if (isTurnFrame && fieldSystem->reverseTurnFrameSteps < 0xFFFF) {
++fieldSystem->reverseTurnFrameSteps;
}
fieldSystem->lastFacingDirection = facingDirection;pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/field/encounter_check.c#L215-L234BOOL FieldSystem_PerformLandOrSurfEncounterCheck(FieldSystem *fieldSystem) {
if (fieldSystem->encounterInhibitSteps <= 3) {
return FALSE;
…
encounterRate = FieldSystem_GetTileEncounterTypeAndRate(fieldSystem, metatileBehavior, &encounterType);
if (encounterRate == 0) {
return FALSE;
}
FieldSystem_UpdateTurnFrameCounter(fieldSystem);pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/field/encounter_check.c#L315-L318if (found) {
sub_02050B08(fieldSystem, battleSetup);
fieldSystem->encounterInhibitSteps = 0;
fieldSystem->reverseTurnFrameSteps = 0;pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/field_warp_tasks.c#L268-L269fieldSystem->encounterInhibitSteps = 0;
fieldSystem->reverseTurnFrameSteps = 0;pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/field/field_control.c#L471-L474static BOOL FieldSystem_CheckWildEncounter(FieldSystem *fieldSystem) {
if (fieldSystem->encounterInhibitSteps < 0xFFFF) {
fieldSystem->encounterInhibitSteps++;pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/field/encounter_check.c#L1031-L1039static int FieldSystem_GetLandEncounterRate(FieldSystem *fieldSystem) {
const EncounterData *encData = MapEvents_GetLoadedEncTable(fieldSystem);
return encData->encounterRate_walking;
…
return encData->encounterRate_surfing;pokeplatinum: decompilation of Pokémon Platinum · src/overlay006/wild_encounters.c#L763-L778// flat 60% chance for the encounter attempt to fail, which is decreased by long grass, biking, and on certain dates.
u8 flatEncounterRate = 40;
…
if (TileBehavior_IsVeryTallGrass(tileBehavior)) {
flatEncounterRate += 30;
} else if (PlayerAvatar_GetPlayerState(fieldSystem->playerAvatar) == PLAYER_AVATAR_CYCLING) {
flatEncounterRate += 30;
}
…
flatEncounterRate = SpecialDates_ModifyEncounterRate(flatEncounterRate, FieldSystem_HasPenalty(fieldSystem));
…
return LCRNG_RandMod(100) < flatEncounterRate && CheckEncounterRateSuccess(fieldSystem, encounterRate);pokeplatinum: decompilation of Pokémon Platinum · src/overlay006/wild_encounters.c#L750-L761static BOOL ShouldGetRandomEncounter(FieldSystem *fieldSystem, const u32 encounterRate, const u8 tileBehavior)
{
u32 encRate = encounterRate << 8;
// lowers effective encounter rate by 95% for the first few steps after each encounter.
if (!GracePeriodStepsUsed(fieldSystem, encRate)) {
fieldSystem->wildBattleMetadata.encounterAttempts++;
if (LCRNG_RandMod(100) >= 5) {
return FALSE;pokeplatinum: decompilation of Pokémon Platinum · src/overlay006/wild_encounters.c#L797-L808// Suppresses the first few encounter attempts after each map transition or wild battle.
static BOOL GracePeriodStepsUsed(FieldSystem *fieldSystem, u32 encounterRate)
{
encounterRate = (encounterRate / 10) >> 8;
if (encounterRate > 8) {
encounterRate = 8;
}
encounterRate = 8 - (encounterRate); // higher encounter rates also lower the grace period.
return fieldSystem->wildBattleMetadata.encounterAttempts >= encounterRate;Checked by claude, 2026-10-22 — read the gate, the reversal boost, the second roll, LCRandRange, the facing counter and where it and the step counter are cleared and incremented, the per-terrain rate getters, and Platinum's gate and grace roll at its pin; $$\lfloor \lfloor 256r/10 \rfloor / 256 \rfloor = \lfloor r/10 \rfloor$$. Searched FieldSystem_PerformLandOrSurfEncounterCheck and FieldSystem_EncounterRateRoll for a matching roll and found none. sub_0205DE98 is assembly (asm/unk_0205CB48.s) and was not interpreted.The step counter goes up by 1 in FieldSystem_CheckWildEncounter before the check runs, so after it is cleared the next three checks are skipped. A header comment on encounterInhibitSteps reads "don't generate an encounter in the first 4 steps on a new map".
Cited by: Pokémon HeartGold and SoulSilver
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. pokeheartgold/encounter-rate-modifiers
About: Pokémon HeartGold and SoulSilver Random encounter
Value
{
"abilityDouble": [
"arena-trap",
"no-guard",
"illuminate"
],
"abilityHalve": [
"white-smoke",
"quick-feet",
"stench",
"snow-cloak (snow)"
],
"blackFlute": 0.5,
"whiteFlute": 1.5,
"cleanseTag": "×2/3"
}src/field/encounter_check.c#L244-L247encounterRate = ApplyAbilityEffectToEncounterRate(FALSE, encounterRate, &encounterGen, LocalFieldData_GetWeatherType(Save_LocalFieldData_Get(fieldSystem->saveData)), leadMon);
ApplyFluteEffectToEncounterRate(fieldSystem, &encounterRate);
ApplyLeadMonHeldItemEffectToEncounterRate(leadMon, &encounterRate);
BOOL ret = FieldSystem_EncounterRateRoll(fieldSystem, encounterRate, metatileBehavior) ? TRUE : FALSE;pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/field/encounter_check.c#L1120-L1140if (!encounterGen->isEgg) {
…
} else if (encounterGen->ability == ABILITY_ARENA_TRAP || encounterGen->ability == ABILITY_NO_GUARD || encounterGen->ability == ABILITY_ILLUMINATE) {
ret *= 2;
} else if (encounterGen->ability == ABILITY_SNOW_CLOAK) {
if (weatherType == WEATHER_SNOW) {
ret /= 2;
…
} else if (encounterGen->ability == ABILITY_WHITE_SMOKE || encounterGen->ability == ABILITY_QUICK_FEET || encounterGen->ability == ABILITY_STENCH) {
ret /= 2;
}
if (ret > 100) {
…pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/field/encounter_check.c#L718-L731if (heldItem == ITEM_CLEANSE_TAG || heldItem == ITEM_PURE_INCENSE) {
*pEncounterRate = *pEncounterRate * 2 / 3;
…
if (flute == FLUTE_BLACK) {
*pEncounterRate /= 2;
} else if (flute == FLUTE_WHITE) {
*pEncounterRate += *pEncounterRate / 2;pokegold: disassembly of Pokémon Gold and Silver · engine/overworld/wildmons.asm#L212-L251call ApplyMusicEffectOnEncounterRate
call ApplyCleanseTagEffectOnEncounterRate
…
; Pokemon March and Ruins of Alph signal double encounter rate.
; Pokemon Lullaby halves encounter rate.
…
; Cleanse Tag halves encounter rate.Checked by claude, 2026-10-22 — read the call order in FieldSystem_PerformLandOrSurfEncounterCheck, the ability, flute, and held-item adjusters, and the Gold block already cited by pokegold/encounter-rateGold's radio and Cleanse Tag effects are read from the disassembly's comments over the routines, as in pokegold/encounter-rate.
Cited by: Pokémon HeartGold and SoulSilver
HeartGold and SoulSilver have 12 grass slots and 5 surfing slots, picked by one LCRandom() % 100 against bounds in EncounterSlot_WildMonSlotRoll_Land and EncounterSlot_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's GetGroundEncounterSlot and GetWaterEncounterSlot have the same bounds. Gold and Silver have 7 grass slots and 3 water slots, with cumulative bounds in GrassMonProbTable (30, 60, 80, 90, 95, 99, 100) and WaterMonProbTable (60, 90, 100), so 30, 30, 20, 10, 5, 4, and 1 percent and 60, 30, and 10 percent (computed from the differences). pokeheartgold/encounter-slots
About: Pokémon HeartGold and SoulSilver Random encounter
Value
{
"grass": [
20,
20,
10,
10,
10,
10,
5,
5,
4,
4,
1,
1
],
"surf": [
60,
30,
5,
4,
1
],
"of": 100,
"goldSilver": {
"grass": [
30,
30,
20,
10,
5,
4,
1
],
"water": [
60,
30,
10
]
}
}src/field/encounter_check.c#L632-L676static u8 EncounterSlot_WildMonSlotRoll_Land(void) {
u8 rnd = LCRandRange(100);
if (rnd < 20) {
return 0;
} else if (rnd >= 20 && rnd < 40) {
return 1;
…
} else if (rnd >= 90 && rnd < 94) {
return 8;
} else if (rnd >= 94 && rnd < 98) {
return 9;
} else if (rnd == 98) {
…pokeplatinum: decompilation of Pokémon Platinum · src/overlay006/wild_encounters.c#L820-L866static u8 GetGroundEncounterSlot(void)
{
u8 roll = LCRNG_RandMod(100);
if (roll < 20) {
return 0;
…
} else if (roll >= 94 && roll < 98) {
return 9;
} else if (roll == 98) {
return 10;
}
return 11;
…pokegold: disassembly of Pokémon Gold and Silver · data/wild/probabilities.asm#L6-L21GrassMonProbTable:
table_width 2
mon_prob 30, 0 ; 30% chance
mon_prob 60, 1 ; 30% chance
mon_prob 80, 2 ; 20% chance
mon_prob 90, 3 ; 10% chance
mon_prob 95, 4 ; 5% chance
mon_prob 99, 5 ; 4% chance
mon_prob 100, 6 ; 1% chance
…
WaterMonProbTable:
table_width 2
…Checked by claude, 2026-10-22 — read every branch of both HGSS slot rolls and both Platinum slot rolls at their pins and compared the bounds one by one; read Gold's two tables and took the differences of their cumulative mon_prob boundsCited by: Pokémon HeartGold and SoulSilver
A 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. pokeheartgold/encounter-time-lists
About: Pokémon HeartGold and SoulSilver Random encounter
Value
{
"hgss": {
"rates": 1,
"lists": 3,
"slots": 12
},
"goldSilver": {
"rates": 3,
"lists": 3,
"slots": 7
},
"platinum": {
"rates": 1,
"timedSlots": [
2,
3
]
}
}include/wild_encounter.h#L23-L28typedef struct LandEncounterData {
u8 levels[NUM_ENCOUNTERS_LAND]; // shared between times of day
u16 species_morn[NUM_ENCOUNTERS_LAND];
u16 species_day[NUM_ENCOUNTERS_LAND];
u16 species_nite[NUM_ENCOUNTERS_LAND];
} LandEncounterData;pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · include/wild_encounter.h#L36-L44typedef struct EncounterData {
u8 encounterRate_walking; // 0x00
u8 encounterRate_surfing; // 0x01
…
LandEncounterData landSlots; // 0x08pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/field/encounter_check.c#L133-L136for (int i = 0; i < NUM_ENCOUNTERS_LAND; ++i) {
encSlots[i].species = slotSpecies[i];
encSlots[i].maxLevel = encData->landSlots.levels[i];
encSlots[i].minLevel = encData->landSlots.levels[i];pokegold: disassembly of Pokémon Gold and Silver · engine/overworld/wildmons.asm#L212-L251GetMapEncounterRate:
ld hl, wMornEncounterRate
call CheckOnWater
ld a, wWaterEncounterRate - wMornEncounterRate
jr z, .ok
ld a, [wTimeOfDay]pokegold: disassembly of Pokémon Gold and Silver · engine/overworld/wildmons.asm#L283-L286ld a, [wTimeOfDay]
ld bc, NUM_GRASSMON * 2
call AddNTimespokeplatinum: decompilation of Pokémon Platinum · src/overlay006/wild_encounters.c#L181-L193// Default encounters are morning. They get replaced by this if it is not morning.
void WildEncounters_ReplaceTimedEncounters(const WildEncounters *encounterData, int *timedSlot1, int *timedSlot2)
{
int timeOfDay = GetTimeOfDay();
if (timeOfDay == TIMEOFDAY_DAY || timeOfDay == TIMEOFDAY_TWILIGHT) {
*timedSlot1 = encounterData->dayEncounters[0];
…
} else if (timeOfDay == TIMEOFDAY_NIGHT || timeOfDay == TIMEOFDAY_LATE_NIGHT) {
*timedSlot1 = encounterData->nightEncounters[0];pokeplatinum: decompilation of Pokémon Platinum · src/overlay006/wild_encounters.c#L334WildEncounters_ReplaceTimedEncounters(encounterData, &encounterTable[2].species, &encounterTable[3].species);Checked by claude, 2026-10-22 — read the HGSS record structs and the land slot fill, Gold's rate lookup and slot offset by wTimeOfDay (the block cited by pokegold/encounter-rate and pokegold/encounter-slots), and Platinum's timed swap and its call on slots 2 and 3Swarms and the Hoenn and Sinnoh Sound radio shows then overwrite some of the 12 HGSS slots (EncSlots_Update_LandSwarm, EncSlotArray_Update_HoennSinnohSounds); not covered here.
Cited by: Pokémon HeartGold and SoulSilver
HeartGold and SoulSilver's BattleSystem_CalculateBallShakes keeps 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. pokeheartgold/capture-same-math
About: Pokémon HeartGold and SoulSilver Capture
Value
{
"kind": "shake-checks",
"catchAtOrAbove": 255,
"numerator": 1048560,
"inner": 16711680,
"rolls": 4,
"status": {
"sleepOrFreeze": 2,
"poisonBurnParalysis": 1.5
},
"ultra": 20,
"great": 15,
"poke": 10,
"safari": 15
}src/battle/battle_command.c#L7145-L7152static u32 BattleSystem_CalculateBallShakes(BattleSystem *bsys, BattleContext *ctx) {
…
if (BattleSystem_GetBattleType(bsys) & (BATTLE_TYPE_PAL_PARK | BATTLE_TYPE_TUTORIAL)) {
return BALL_SHAKE_MAX;pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/battle/battle_command.c#L7290-L7327} else {
// Item IDs from 2-5: Ultra Ball, Great Ball, Pokeball, Safari Ball.
ballMultiplier = sStandardBallCatchRates[ctx->itemTemp - 2];
}
s32 maxHpTimes3 = ctx->battleMons[ctx->battlerIdTarget].maxHp * 3;
u32 lostHp = maxHpTimes3 - (ctx->battleMons[ctx->battlerIdTarget].hp * 2);
…
u32 baseCatchMultiplier = catchRate * ballMultiplier;
u32 modifiedCatchRate = ((baseCatchMultiplier / 10) * lostHp) / maxHpTimes3;
s32 status = ctx->battleMons[ctx->battlerIdTarget].status;
if ((STATUS_SLEEP | STATUS_FREEZE) & status) {
modifiedCatchRate *= 2;
…pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · include/constants/battle.h#L721#define BALL_SHAKE_MAX 4pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · asm/overlay_12_battle_command.s#L17-L18sStandardBallCatchRates: ; 0x0226C2EC
.byte 20, 15, 10, 15pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/battle/battle_system.c#L1112-L1114u16 BattleSystem_Random(BattleSystem *battleSystem) {
battleSystem->rand = battleSystem->rand * 1103515245L + 24691;
return battleSystem->rand / 65536L;pokeplatinum: decompilation of Pokémon Platinum · src/battle/battle_script.c#L10978-L10988u32 catchRate = (speciesMod * ballMod / 10)
* (battleCtx->battleMons[battleCtx->defender].maxHP * 3 - battleCtx->battleMons[battleCtx->defender].curHP * 2)
/ (battleCtx->battleMons[battleCtx->defender].maxHP * 3);
…
if (battleCtx->battleMons[battleCtx->defender].status & (MON_CONDITION_SLEEP | MON_CONDITION_FREEZE)) {
catchRate *= 2;
…
if (battleCtx->battleMons[battleCtx->defender].status & (MON_CONDITION_POISON | MON_CONDITION_BURN | MON_CONDITION_PARALYSIS | MON_CONDITION_TOXIC)) {
catchRate = catchRate * 15 / 10;pokeplatinum: decompilation of Pokémon Platinum · src/battle/battle_script.c#L10990-L11013if (catchRate >= 255) {
shakes = BALL_3_SHAKES_SUCCESS;
} else {
u32 sqrtRate = (0xFF << 16) / catchRate;
…
catchRate = (0xFFFF << 4) / catchRate;
…
if (battleCtx->msgItemTemp == ITEM_MASTER_BALL) {
shakes = BALL_3_SHAKES_SUCCESS;pokeplatinum: decompilation of Pokémon Platinum · src/battle/battle_script.c#L10868-L10873static const u8 sBasicBallMod[] = {
20, // ITEM_ULTRA_BALL
15, // ITEM_GREAT_BALL
10, // ITEM_POKE_BALL
15, // ITEM_SAFARI_BALLChecked by claude, 2026-10-22 — read BattleSystem_CalculateBallShakes from its battle-type test to the shake loop and the Master Ball override, the shake constant, the ball table in the assembly data, and the battle RNG; compared each step with Platinum's BattleScript_CalcCatchShakes as cited by pokeplatinum/capture-same-math (0xFF0000 = 16711680, 0xFFFF0 = 1048560)Platinum folds the ball multiplier in before dividing by 10 in one expression; HGSS stores the product first. Both compute in unsigned integers.
Cited by: Capture, Pokémon HeartGold and SoulSilver
In 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. pokeheartgold/apricorn-balls
About: Pokémon HeartGold and SoulSilver Capture
Value
{
"fast": {
"times": 4,
"baseSpeedAtLeast": 100
},
"level": [
2,
4,
8
],
"lure": 3,
"love": 8,
"moon": {
"times": 4,
"species": 14
},
"cap": 255,
"floor": 1
}src/battle/battle_command.c#L7218-L7245// All the apricorn balls directly alter catchRate instead of setting ballMultiplier.
…
case ITEM_FAST_BALL: {
u32 speed = GetMonBaseStat(ctx->battleMons[ctx->battlerIdTarget].species, BASE_SPEED);
if (speed >= 100) {
catchRate *= 4;
…
case ITEM_LEVEL_BALL: {
u8 attackerLevel = ctx->battleMons[ctx->battlerIdAttacker].level;
u8 defenderLevel = ctx->battleMons[ctx->battlerIdTarget].level;
if (attackerLevel <= defenderLevel) {
} else if (attackerLevel / 2 <= defenderLevel) {
…pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/battle/battle_command.c#L7259-L7288case ITEM_LOVE_BALL: {
if (ctx->battleMons[ctx->battlerIdAttacker].species == ctx->battleMons[ctx->battlerIdTarget].species && ctx->battleMons[ctx->battlerIdAttacker].gender != ctx->battleMons[ctx->battlerIdTarget].gender) {
catchRate *= 8;
…
case ITEM_MOON_BALL: {
…
for (i = 0; i < NELEMS(sMoonBallPokemon); i++) {
if (sMoonBallPokemon[i] == ctx->battleMons[ctx->battlerIdTarget].species) {
catchRate *= 4;
…
if (catchRate > 0xFF) {
catchRate = 0xFF;
…pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/battle/battle_command.c#L7161-L7166ballMultiplier = 10; // All ball multipliers are /10, so this is x1.
…
if (ctx->itemTemp > ITEM_SAFARI_BALL) { // Skip all balls that have no condition.pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · asm/overlay_12_battle_command.s#L132-L146sMoonBallPokemon: ; 0x0226C3E8
.short SPECIES_NIDORAN_F
.short SPECIES_NIDORINA
.short SPECIES_NIDOQUEEN
.short SPECIES_NIDORAN_M
.short SPECIES_NIDORINO
.short SPECIES_NIDOKING
.short SPECIES_CLEFFA
.short SPECIES_CLEFAIRY
.short SPECIES_CLEFABLE
.short SPECIES_IGGLYBUFF
.short SPECIES_JIGGLYPUFF
…pokecrystal: disassembly of Pokémon Crystal · engine/items/item_effects.asm#L272-L276.skip_or_return_from_ball_fn
ld a, [wCurItem]
cp LEVEL_BALL
ld a, b
jp z, .skip_hp_calcpokecrystal: disassembly of Pokémon Crystal · engine/items/item_effects.asm#L917-L925; BUG: Moon Ball does not boost catch rate (see docs/bugs_and_glitches.md)
…
cp MOON_STONE_RED ; BURN_HEALpokecrystal: disassembly of Pokémon Crystal · engine/items/item_effects.asm#L977-L982; BUG: Love Ball boosts catch rate for the wrong gender (see docs/bugs_and_glitches.md)
ld a, d
pop de
cp d
pop bc
ret nzpokecrystal: disassembly of Pokémon Crystal · engine/items/item_effects.asm#L1001-L1008FastBallMultiplier:
ld a, [wTempEnemyMonSpecies]
ld c, a
ld hl, FleeMons
ld d, 3
…
; BUG: Fast Ball only boosts catch rate for three Pokémon (see docs/bugs_and_glitches.md)Checked by claude, 2026-10-22 — read the five Apricorn cases, the cap and floor after the switch, the multiplier's default, and the Moon Ball table in the HGSS assembly data; in Crystal, the Level Ball skip and the three BUG comments with the lines under them (the Love Ball returns unless the genders match; the Moon Ball compares the evolution item with MOON_STONE_RED, which a comment equates with BURN_HEAL; the Fast Ball loops three times over FleeMons)The atlas reads battlerIdAttacker as the player's Pokémon on the turn a ball is thrown. The cap and floor run for every ball past the Safari Ball, not only the Apricorn balls.
Cited by: Capture, Pokémon HeartGold and SoulSilver
HeartGold 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. pokeheartgold/heavy-ball
About: Pokémon HeartGold and SoulSilver Capture
Value
{
"addByWeightKg": {
"409.6": 40,
"307.2": 30,
"204.8": 20
},
"below204_8": -20,
"crystal": {
"below102_4": -20,
"below204_8": 0,
"below307_2": 20,
"below409_6": 30,
"above": 40
}
}src/battle/battle_command.c#L7246-L7260case ITEM_HEAVY_BALL: {
s32 weight = GetMonWeight(ctx->battleMons[ctx->battlerIdTarget].species);
// Weight is in kilograms, moved to the left by 1 decimal point.
if (weight >= 4096) { // 409.6 kg / 903.0 lbs. or more.
catchRate += 40;
} else if (weight >= 3072) { // 307.2 kg / 677.3 lbs. or more.
catchRate += 30;
} else if (weight >= 2048) { // 204.8 kg / 451.5 lbs. or more.
catchRate += 20;
} 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.
catchRate -= 20;pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · src/battle/battle_command.c#L7286-L7288catchRate = 0xFF;
} else if (catchRate < 0) {
catchRate = 1;pokecrystal: disassembly of Pokémon Crystal · engine/items/item_effects.asm#L779-L784HeavyBallMultiplier:
; subtract 20 from catch rate if weight < 102.4 kg
; else add 0 to catch rate if weight < 204.8 kg
; else add 20 to catch rate if weight < 307.2 kg
; else add 30 to catch rate if weight < 409.6 kg
; else add 40 to catch ratepokecrystal: disassembly of Pokémon Crystal · engine/items/item_effects.asm#L843-L870cp HIGH(1024) ; 102.4 kg
jr c, .lightmon
…
.lightmon
ld a, b
sub 20
ld b, a
ret nc
ld b, $1Checked by claude, 2026-10-22 — read the HGSS Heavy Ball case and the floor after the switch; the catch rate there is the species' base catch rate (a byte) or the Safari-scaled one, so it is under 1024. In Crystal, read the comparison against HIGH(1024) and the light branch, and the header comment for the heavier bandsA catch rate below 20 minus 20 is negative and becomes 1; a catch rate of exactly 20 becomes 0, which the floor leaves at 0. Crystal's heavier bands are read from its comment, and the lookup table under it (.WeightsTable) agrees.
Cited by: Capture, Pokémon HeartGold and SoulSilver
HeartGold and SoulSilver's TryCriticalHit keeps Platinum's critical-hit rule: the stage sum (+2 Focus Energy, +1 for a HOLD_EFFECT_CRITRATE_UP item, +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 (named MOVE_EFFECT_FLAG_LUCKY_CHANT here), and Sniper turning ×2 into ×3. pokeheartgold/critical-chance
About: Pokémon HeartGold and SoulSilver Critical hit
Value
{
"byStage": [
16,
8,
4,
3,
2
],
"cap": 4,
"sniper": 3
}src/battle/overlay_12_0224E4FC.c#L5960-L5995static const u8 sCritChance[] = {
16, 8, 4, 3, 2
};
…
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));
if (critUp > 4) {
critUp = 4;
}
if ((BattleSystem_Random(battleSystem) % sCritChance[critUp]) == 0) {
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)) {
ret = 2;
}
…pokeheartgold: decompilation of Pokémon HeartGold and SoulSilver · include/constants/battle.h#L177#define MOVE_EFFECT_FLAG_LUCKY_CHANT (1 << 15)pokeplatinum: decompilation of Pokémon Platinum · src/battle/battle_lib.c#L7096-L7103static const u8 sCriticalStageRates[] = {
16, // neutral
8, // +1
4, // +2
3, // +3
2, // +4pokeplatinum: decompilation of Pokémon Platinum · src/battle/battle_lib.c#L7123-L7139effectiveCritStage = (((attackerVolStatus & VOLATILE_CONDITION_FOCUS_ENERGY) != FALSE) * 2)
+ (itemEffect == HOLD_EFFECT_CRITRATE_UP)
+ criticalStage
+ (attackerAbility == ABILITY_SUPER_LUCK)
…
if (BattleSystem_RandNext(battleSys) % sCriticalStageRates[effectiveCritStage] == 0
&& Battler_IgnorableAbility(battleCtx, attacker, defender, ABILITY_BATTLE_ARMOR) == FALSE
&& Battler_IgnorableAbility(battleCtx, attacker, defender, ABILITY_SHELL_ARMOR) == FALSE
&& (sideConditions & SIDE_CONDITION_LUCKY_CHANT) == FALSE
&& (defenderMoveEffects & MOVE_EFFECT_NO_CRITICAL) == FALSE) {
criticalMul = 2;pokeplatinum: decompilation of Pokémon Platinum · include/constants/battle/moves.h#L23#define MOVE_EFFECT_NO_CRITICAL (1 << 15) // checked for, but never setpokeplatinum: decompilation of Pokémon Platinum · src/battle/battle_lib.c#L7142-L7143if (criticalMul == 2 && Battler_Ability(battleCtx, attacker) == ABILITY_SNIPER) {
criticalMul = 3;Checked by claude, 2026-10-22 — read TryCriticalHit whole, the flag constant, and Platinum's table, sum, and tests as cited by pokeplatinum/critical-chance; both draw the roll before the blocking testsPlatinum's header comment says its bit-15 flag is never set. The atlas has not checked whether HGSS sets MOVE_EFFECT_FLAG_LUCKY_CHANT, since some of its battle code is still assembly.
Cited by: Pokémon HeartGold and SoulSilver