From 2231b2fdcaef0f20131b90ee7f52a7fab78973f5 Mon Sep 17 00:00:00 2001 From: spindrift64 <102487911+spindrift64@users.noreply.github.com> Date: Tue, 30 Sep 2025 19:07:23 +0200 Subject: [PATCH] Immunity abilities trigger on turn 0 (leads) (#7814) --- data/battle_scripts_1.s | 7 ++ include/battle_scripts.h | 1 + include/battle_util.h | 2 + src/battle_main.c | 2 + src/battle_script_commands.c | 10 +- src/battle_util.c | 198 ++++++++++++++++++--------------- test/battle/ability/immunity.c | 16 +++ test/battle/sleep_clause.c | 1 + 8 files changed, 146 insertions(+), 91 deletions(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 4248807b01..9d75ac304e 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -8285,6 +8285,13 @@ BattleScript_AbilityCuredStatus:: updatestatusicon BS_SCRIPTING return +BattleScript_AbilityCuredStatusEnd3:: + call BattleScript_AbilityPopUp + printstring STRINGID_PKMNSXCUREDITSYPROBLEM + waitmessage B_WAIT_TIME_LONG + updatestatusicon BS_SCRIPTING + end3 + BattleScript_BattlerShookOffTaunt:: call BattleScript_AbilityPopUp printstring STRINGID_PKMNSHOOKOFFTHETAUNT diff --git a/include/battle_scripts.h b/include/battle_scripts.h index d9bb476af7..16a07e971a 100644 --- a/include/battle_scripts.h +++ b/include/battle_scripts.h @@ -202,6 +202,7 @@ extern const u8 BattleScript_AbilityStatusEffect[]; extern const u8 BattleScript_SynchronizeActivates[]; extern const u8 BattleScript_NoItemSteal[]; extern const u8 BattleScript_AbilityCuredStatus[]; +extern const u8 BattleScript_AbilityCuredStatusEnd3[]; extern const u8 BattleScript_IgnoresWhileAsleep[]; extern const u8 BattleScript_IgnoresAndUsesRandomMove[]; extern const u8 BattleScript_MoveUsedLoafingAround[]; diff --git a/include/battle_util.h b/include/battle_util.h index fd0fbbac46..f22fd8b0ac 100644 --- a/include/battle_util.h +++ b/include/battle_util.h @@ -55,6 +55,7 @@ enum { ABILITYEFFECT_SWITCH_IN_WEATHER, ABILITYEFFECT_OPPORTUNIST, ABILITYEFFECT_SWITCH_IN_STATUSES, + ABILITYEFFECT_ON_SWITCHIN_IMMUNITIES, }; // For the first argument of ItemBattleEffects, to deteremine which block of item effects to try @@ -307,6 +308,7 @@ struct Pokemon *GetIllusionMonPtr(u32 battler); void ClearIllusionMon(u32 battler); u32 GetIllusionMonPartyId(struct Pokemon *party, struct Pokemon *mon, struct Pokemon *partnerMon, u32 battler); bool32 SetIllusionMon(struct Pokemon *mon, u32 battler); +u32 TryImmunityAbilityHealStatus(u32 battler, u32 caseID); bool32 ShouldGetStatBadgeBoost(u16 flagId, u32 battler); enum DamageCategory GetBattleMoveCategory(u32 move); void SetDynamicMoveCategory(u32 battlerAtk, u32 battlerDef, u32 move); diff --git a/src/battle_main.c b/src/battle_main.c index 967520f9fb..e787366de8 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -3870,6 +3870,8 @@ static void TryDoEventsBeforeFirstTurn(void) return; if (TryClearIllusion(i, ABILITYEFFECT_ON_SWITCHIN)) return; + if (AbilityBattleEffects(ABILITYEFFECT_ON_SWITCHIN_IMMUNITIES, i, 0, 0, 0) != 0) + return; } gBattleStruct->switchInBattlerCounter = 0; gBattleStruct->eventsBeforeFirstTurnState++; diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 6c4042b3d2..85184e803a 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -6121,9 +6121,12 @@ static void Cmd_moveend(void) gBattleScripting.moveendState++; break; case MOVEEND_STATUS_IMMUNITY_ABILITIES: // status immunities - if (AbilityBattleEffects(ABILITYEFFECT_IMMUNITY, 0, 0, 0, 0)) - effect = TRUE; // it loops through all battlers, so we increment after its done with all battlers - else + for (u16 battler = 0; battler < gBattlersCount; battler++) + { + if (AbilityBattleEffects(ABILITYEFFECT_IMMUNITY, battler, 0, 0, 0)) + effect = TRUE; + } + if(!effect) gBattleScripting.moveendState++; break; case MOVEEND_SYNCHRONIZE_ATTACKER: // attacker synchronize @@ -17250,6 +17253,7 @@ void BS_SwitchinAbilities(void) AbilityBattleEffects(ABILITYEFFECT_NEUTRALIZINGGAS, battler, 0, 0, 0); AbilityBattleEffects(ABILITYEFFECT_ON_SWITCHIN, battler, 0, 0, 0); AbilityBattleEffects(ABILITYEFFECT_OPPORTUNIST, battler, 0, 0, 0); + AbilityBattleEffects(ABILITYEFFECT_IMMUNITY, battler, 0, 0, 0); if (gBattleWeather & B_WEATHER_ANY && HasWeatherEffect()) AbilityBattleEffects(ABILITYEFFECT_ON_WEATHER, battler, 0, 0, 0); diff --git a/src/battle_util.c b/src/battle_util.c index 77bed472d1..ea904ee88c 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -5092,94 +5092,12 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 } break; case ABILITYEFFECT_IMMUNITY: - for (battler = 0; battler < gBattlersCount; battler++) - { - switch (GetBattlerAbilityIgnoreMoldBreaker(battler)) - { - case ABILITY_IMMUNITY: - case ABILITY_PASTEL_VEIL: - if (gBattleMons[battler].status1 & (STATUS1_POISON | STATUS1_TOXIC_POISON | STATUS1_TOXIC_COUNTER)) - { - StringCopy(gBattleTextBuff1, gStatusConditionString_PoisonJpn); - effect = 1; - } - break; - case ABILITY_OWN_TEMPO: - if (gBattleMons[battler].volatiles.confusionTurns > 0) - { - StringCopy(gBattleTextBuff1, gStatusConditionString_ConfusionJpn); - effect = 2; - } - break; - case ABILITY_LIMBER: - if (gBattleMons[battler].status1 & STATUS1_PARALYSIS) - { - StringCopy(gBattleTextBuff1, gStatusConditionString_ParalysisJpn); - effect = 1; - } - break; - case ABILITY_INSOMNIA: - case ABILITY_VITAL_SPIRIT: - if (gBattleMons[battler].status1 & STATUS1_SLEEP) - { - TryDeactivateSleepClause(GetBattlerSide(battler), gBattlerPartyIndexes[battler]); - gBattleMons[battler].volatiles.nightmare = FALSE; - StringCopy(gBattleTextBuff1, gStatusConditionString_SleepJpn); - effect = 1; - } - break; - case ABILITY_WATER_VEIL: - case ABILITY_WATER_BUBBLE: - case ABILITY_THERMAL_EXCHANGE: - if (gBattleMons[battler].status1 & STATUS1_BURN) - { - StringCopy(gBattleTextBuff1, gStatusConditionString_BurnJpn); - effect = 1; - } - break; - case ABILITY_MAGMA_ARMOR: - if (gBattleMons[battler].status1 & (STATUS1_FREEZE | STATUS1_FROSTBITE)) - { - StringCopy(gBattleTextBuff1, gStatusConditionString_IceJpn); - effect = 1; - } - break; - case ABILITY_OBLIVIOUS: - if (gBattleMons[battler].volatiles.infatuation) - effect = 3; - else if (gDisableStructs[battler].tauntTimer != 0) - effect = 4; - break; - } - - if (effect != 0) - { - switch (effect) - { - case 1: // status cleared - gBattleMons[battler].status1 = 0; - BattleScriptCall(BattleScript_AbilityCuredStatus); - break; - case 2: // get rid of confusion - RemoveConfusionStatus(battler); - BattleScriptCall(BattleScript_AbilityCuredStatus); - break; - case 3: // get rid of infatuation - gBattleMons[battler].volatiles.infatuation = 0; - BattleScriptCall(BattleScript_BattlerGotOverItsInfatuation); - break; - case 4: // get rid of taunt - gDisableStructs[battler].tauntTimer = 0; - BattleScriptCall(BattleScript_BattlerShookOffTaunt); - break; - } - - gBattleScripting.battler = gBattlerAbility = battler; - BtlController_EmitSetMonData(battler, B_COMM_TO_CONTROLLER, REQUEST_STATUS_BATTLE, 0, 4, &gBattleMons[battler].status1); - MarkBattlerForControllerExec(battler); - return effect; - } - } + effect = TryImmunityAbilityHealStatus(battler, caseID); + if (effect) + return effect; + break; + case ABILITYEFFECT_ON_SWITCHIN_IMMUNITIES: + effect = TryImmunityAbilityHealStatus(battler, caseID); break; case ABILITYEFFECT_SYNCHRONIZE: if (gLastUsedAbility == ABILITY_SYNCHRONIZE && gBattleStruct->synchronizeMoveEffect != MOVE_EFFECT_NONE) @@ -10404,6 +10322,110 @@ bool32 SetIllusionMon(struct Pokemon *mon, u32 battler) return FALSE; } +u32 TryImmunityAbilityHealStatus(u32 battler, u32 caseID) +{ + u32 effect = 0; + switch (GetBattlerAbilityIgnoreMoldBreaker(battler)) + { + case ABILITY_IMMUNITY: + case ABILITY_PASTEL_VEIL: + if (gBattleMons[battler].status1 & (STATUS1_POISON | STATUS1_TOXIC_POISON | STATUS1_TOXIC_COUNTER)) + { + StringCopy(gBattleTextBuff1, gStatusConditionString_PoisonJpn); + effect = 1; + } + break; + case ABILITY_OWN_TEMPO: + if (gBattleMons[battler].volatiles.confusionTurns > 0) + { + StringCopy(gBattleTextBuff1, gStatusConditionString_ConfusionJpn); + effect = 2; + } + break; + case ABILITY_LIMBER: + if (gBattleMons[battler].status1 & STATUS1_PARALYSIS) + { + StringCopy(gBattleTextBuff1, gStatusConditionString_ParalysisJpn); + effect = 1; + } + break; + case ABILITY_INSOMNIA: + case ABILITY_VITAL_SPIRIT: + if (gBattleMons[battler].status1 & STATUS1_SLEEP) + { + TryDeactivateSleepClause(GetBattlerSide(battler), gBattlerPartyIndexes[battler]); + gBattleMons[battler].volatiles.nightmare = FALSE; + StringCopy(gBattleTextBuff1, gStatusConditionString_SleepJpn); + effect = 1; + } + break; + case ABILITY_WATER_VEIL: + case ABILITY_WATER_BUBBLE: + case ABILITY_THERMAL_EXCHANGE: + if (gBattleMons[battler].status1 & STATUS1_BURN) + { + StringCopy(gBattleTextBuff1, gStatusConditionString_BurnJpn); + effect = 1; + } + break; + case ABILITY_MAGMA_ARMOR: + if (gBattleMons[battler].status1 & (STATUS1_FREEZE | STATUS1_FROSTBITE)) + { + StringCopy(gBattleTextBuff1, gStatusConditionString_IceJpn); + effect = 1; + } + break; + case ABILITY_OBLIVIOUS: + if (gBattleMons[battler].volatiles.infatuation) + effect = 3; + else if (gDisableStructs[battler].tauntTimer != 0) + effect = 4; + break; + } + + if (effect != 0) + { + switch (effect) + { + case 1: // status cleared + gBattleMons[battler].status1 = 0; + if(caseID == ABILITYEFFECT_ON_SWITCHIN_IMMUNITIES) + BattleScriptExecute(BattleScript_AbilityCuredStatusEnd3); + else + BattleScriptCall(BattleScript_AbilityCuredStatus); + break; + case 2: // get rid of confusion + RemoveConfusionStatus(battler); + if(caseID == ABILITYEFFECT_ON_SWITCHIN_IMMUNITIES) + BattleScriptExecute(BattleScript_AbilityCuredStatusEnd3); + else + BattleScriptCall(BattleScript_AbilityCuredStatus); + break; + case 3: // get rid of infatuation + gBattleMons[battler].volatiles.infatuation = 0; + if(caseID == ABILITYEFFECT_ON_SWITCHIN_IMMUNITIES) + BattleScriptExecute(BattleScript_AbilityCuredStatusEnd3); + else + BattleScriptCall(BattleScript_AbilityCuredStatus); + break; + case 4: // get rid of taunt + gDisableStructs[battler].tauntTimer = 0; + if(caseID == ABILITYEFFECT_ON_SWITCHIN_IMMUNITIES) + BattleScriptExecute(BattleScript_AbilityCuredStatusEnd3); + else + BattleScriptCall(BattleScript_AbilityCuredStatus); + break; + } + + gBattleScripting.battler = gBattlerAbility = battler; + BtlController_EmitSetMonData(battler, B_COMM_TO_CONTROLLER, REQUEST_STATUS_BATTLE, 0, 4, &gBattleMons[battler].status1); + MarkBattlerForControllerExec(battler); + return effect; + } + + return 0; +} + bool32 ShouldGetStatBadgeBoost(u16 badgeFlag, u32 battler) { if (B_BADGE_BOOST == GEN_3 && badgeFlag != 0) diff --git a/test/battle/ability/immunity.c b/test/battle/ability/immunity.c index 88ff45d2a4..e18aef667c 100644 --- a/test/battle/ability/immunity.c +++ b/test/battle/ability/immunity.c @@ -63,3 +63,19 @@ SINGLE_BATTLE_TEST("Immunity doesn't prevent Pokémon from being poisoned by Tox NOT HP_BAR(player); } } + +SINGLE_BATTLE_TEST("Immunity cures existing poison on turn 0") +{ + GIVEN { + PLAYER(SPECIES_ZANGOOSE) { + Ability(ABILITY_IMMUNITY); + Status1(STATUS1_POISON); + } + OPPONENT(SPECIES_WOBBUFFET); + } SCENE { + ABILITY_POPUP(player, ABILITY_IMMUNITY); + TURN { MOVE(player, MOVE_SPLASH); } + } THEN { + EXPECT_EQ(player->status1, STATUS1_NONE); + } +} diff --git a/test/battle/sleep_clause.c b/test/battle/sleep_clause.c index 73f419289c..3b8999f668 100644 --- a/test/battle/sleep_clause.c +++ b/test/battle/sleep_clause.c @@ -1075,6 +1075,7 @@ SINGLE_BATTLE_TEST("Sleep Clause: Sleep clause is deactivated when a sleeping mo u32 ability; PARAMETRIZE { ability = ABILITY_VITAL_SPIRIT; } PARAMETRIZE { ability = ABILITY_INSOMNIA; } + GIVEN { FLAG_SET(B_FLAG_SLEEP_CLAUSE); ASSUME(GetMoveEffect(MOVE_SPORE) == EFFECT_NON_VOLATILE_STATUS);