From 9d06a4fb55f8f4bee4b1bdd9abd437dc783aca32 Mon Sep 17 00:00:00 2001 From: PhallenTree <168426989+PhallenTree@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:01:13 +0000 Subject: [PATCH] Emergency Exit on hazards activation + fix end of turn activation (#8075) --- include/battle.h | 4 +- include/battle_script_commands.h | 1 + src/battle_end_turn.c | 28 +++---- src/battle_main.c | 2 + src/battle_script_commands.c | 24 ++++-- src/battle_util.c | 7 +- test/battle/ability/emergency_exit.c | 35 +++++++++ test/battle/hazards.c | 110 +++++++++++++++++++++++++++ 8 files changed, 179 insertions(+), 32 deletions(-) diff --git a/include/battle.h b/include/battle.h index af6e72774a..96698e4e9a 100644 --- a/include/battle.h +++ b/include/battle.h @@ -583,8 +583,9 @@ struct BattlerState u32 stompingTantrumTimer:2; u32 canPickupItem:1; u32 ateBoost:1; + u32 wasAboveHalfHp:1; // For Berserk, Emergency Exit, Wimp Out and Anger Shell. u32 commanderSpecies:11; - u32 padding:5; + u32 padding:4; // End of Word }; @@ -714,7 +715,6 @@ struct BattleStruct u8 stolenStats[NUM_BATTLE_STATS]; // hp byte is used for which stats to raise, other inform about by how many stages u8 lastMoveTarget[MAX_BATTLERS_COUNT]; // The last target on which each mon used a move, for the sake of Instruct enum Ability tracedAbility[MAX_BATTLERS_COUNT]; - u16 hpBefore[MAX_BATTLERS_COUNT]; // Hp of battlers before using a move. For Berserk and Anger Shell. struct Illusion illusion[MAX_BATTLERS_COUNT]; u8 soulheartBattlerId; u8 friskedBattler; // Frisk needs to identify 2 battlers in double battles. diff --git a/include/battle_script_commands.h b/include/battle_script_commands.h index ff4a4d56c7..4b6ffce98b 100644 --- a/include/battle_script_commands.h +++ b/include/battle_script_commands.h @@ -75,6 +75,7 @@ bool32 IsMoveAffectedByParentalBond(u32 move, u32 battler); void SaveBattlerTarget(u32 battler); void SaveBattlerAttacker(u32 battler); bool32 CanBurnHitThaw(u16 move); +bool32 EmergencyExitCanBeTriggered(u32 battler); extern void (*const gBattleScriptingCommandsTable[])(void); extern const struct StatFractions gAccuracyStageRatios[]; diff --git a/src/battle_end_turn.c b/src/battle_end_turn.c index 2aafa6624d..ac1563f1fb 100644 --- a/src/battle_end_turn.c +++ b/src/battle_end_turn.c @@ -170,7 +170,7 @@ static bool32 HandleEndTurnVarious(u32 battler) if (gBattleMons[i].volatiles.laserFocus && gDisableStructs[i].laserFocusTimer == gBattleTurnCounter) gBattleMons[i].volatiles.laserFocus = FALSE; - gBattleStruct->hpBefore[i] = gBattleMons[i].hp; + gBattleStruct->battlerState[i].wasAboveHalfHp = gBattleMons[i].hp > gBattleMons[i].maxHP / 2; } if (gBattleStruct->incrementEchoedVoice) @@ -289,27 +289,17 @@ static bool32 HandleEndTurnEmergencyExit(u32 battler) gBattleStruct->turnEffectsBattlerId++; - if (ability == ABILITY_EMERGENCY_EXIT || ability == ABILITY_WIMP_OUT) + if (EmergencyExitCanBeTriggered(battler)) { - u32 cutoff = gBattleMons[battler].maxHP / 2; - bool32 HadMoreThanHalfHpNowDoesnt = gBattleStruct->hpBefore[battler] > cutoff && gBattleMons[battler].hp <= cutoff; + gBattlerAbility = battler; + gLastUsedAbility = ability; - if (HadMoreThanHalfHpNowDoesnt - && IsBattlerAlive(battler) - && (CanBattlerSwitch(battler) || !(gBattleTypeFlags & BATTLE_TYPE_TRAINER)) - && !(gBattleTypeFlags & BATTLE_TYPE_ARENA) - && gBattleMons[battler].volatiles.semiInvulnerable != STATE_SKY_DROP) // Not currently held by Sky Drop - { - gBattlerAbility = battler; - gLastUsedAbility = ability; + if (gBattleTypeFlags & BATTLE_TYPE_TRAINER) + BattleScriptExecute(BattleScript_EmergencyExitEnd2); + else + BattleScriptExecute(BattleScript_EmergencyExitWildEnd2); - if (gBattleTypeFlags & BATTLE_TYPE_TRAINER) - BattleScriptExecute(BattleScript_EmergencyExitEnd2); - else - BattleScriptExecute(BattleScript_EmergencyExitWildEnd2); - - effect = TRUE; - } + effect = TRUE; } return effect; diff --git a/src/battle_main.c b/src/battle_main.c index a57155753d..3e2631a0c1 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -3237,6 +3237,7 @@ void SwitchInClearSetData(u32 battler, struct Volatiles *volatilesCopy) gBattleStruct->battlerState[battler].stompingTantrumTimer = 0; gBattleStruct->palaceFlags &= ~(1u << battler); gBattleStruct->battlerState[battler].canPickupItem = FALSE; + gBattleStruct->battlerState[battler].wasAboveHalfHp = gBattleMons[battler].hp > gBattleMons[battler].maxHP / 2; gBattleStruct->hazardsCounter = 0; gDisableStructs[battler].hazardsDone = FALSE; gSpecialStatuses[battler].switchInItemDone = FALSE; @@ -5144,6 +5145,7 @@ static void TurnValuesCleanUp(bool8 var0) gBattleMons[i].volatiles.recharge = FALSE; } gBattleStruct->battlerState[i].canPickupItem = FALSE; + gBattleStruct->battlerState[i].wasAboveHalfHp = FALSE; } if (gDisableStructs[i].substituteHP == 0) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 3ec480f9a2..9a7088dca9 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -1093,8 +1093,7 @@ bool32 EmergencyExitCanBeTriggered(u32 battler) if (ability != ABILITY_EMERGENCY_EXIT && ability != ABILITY_WIMP_OUT) return FALSE; - if (IsBattlerTurnDamaged(battler) - && IsBattlerAlive(battler) + if (IsBattlerAlive(battler) && HadMoreThanHalfHpNowDoesnt(battler) && (CanBattlerSwitch(battler) || !(gBattleTypeFlags & BATTLE_TYPE_TRAINER)) && !(gBattleTypeFlags & BATTLE_TYPE_ARENA) @@ -2412,6 +2411,8 @@ static void Cmd_datahpupdate(void) MoveDamageDataHpUpdate(battler, cmd->battler, cmd->nextInstr); break; } + if (gBattleMons[battler].hp > gBattleMons[battler].maxHP / 2) + gBattleStruct->battlerState[battler].wasAboveHalfHp = TRUE; } @@ -6659,13 +6660,13 @@ static void Cmd_moveend(void) case MOVEEND_EMERGENCY_EXIT: // Special case, because moves hitting multiple opponents stop after switching out { // Because sorting the battlers by speed takes lots of cycles, - // we check if EE can be activated and cound how many. + // we check if EE can be activated and count how many. u32 numEmergencyExitBattlers = 0; u32 emergencyExitBattlers = 0; for (i = 0; i < gBattlersCount; i++) { - if (EmergencyExitCanBeTriggered(i)) + if (IsBattlerTurnDamaged(i) && EmergencyExitCanBeTriggered(i)) { emergencyExitBattlers |= 1u << i; numEmergencyExitBattlers++; @@ -7106,7 +7107,7 @@ static void Cmd_getswitchedmondata(void) if (TESTING && gBattlerPartyIndexes[battler] == gBattleStruct->monToSwitchIntoId[battler] - && gBattleStruct->hpBefore[battler] != 0) // battler is alive + && IsBattlerAlive(battler)) Test_ExitWithResult(TEST_RESULT_ERROR, 0, ":L:%s:%d: battler is trying to switch to themself", __FILE__, __LINE__); gBattlerPartyIndexes[battler] = gBattleStruct->monToSwitchIntoId[battler]; @@ -7826,6 +7827,16 @@ static bool32 DoSwitchInEffectsForBattler(u32 battler) gBattleStruct->battlerState[battler].storedLunarDance = FALSE; } } + else if (EmergencyExitCanBeTriggered(battler)) + { + gBattleScripting.battler = gBattlerAbility = battler; + gSpecialStatuses[battler].switchInItemDone = FALSE; + gBattleStruct->battlerState[battler].forcedSwitch = FALSE; + if (gBattleTypeFlags & BATTLE_TYPE_TRAINER) + BattleScriptCall(BattleScript_EmergencyExit); + else + BattleScriptCall(BattleScript_EmergencyExitWild); + } else if (!gDisableStructs[battler].hazardsDone) { TryHazardsOnSwitchIn(battler, side, gBattleStruct->hazardsQueue[side][gBattleStruct->hazardsCounter]); @@ -7845,7 +7856,6 @@ static bool32 DoSwitchInEffectsForBattler(u32 battler) gBattleScripting.battler = battler; gBattleCommunication[MULTISTRING_CHOOSER] = B_MSG_Z_HP_TRAP; BattleScriptCall(BattleScript_HealReplacementZMove); - return TRUE; } else { @@ -7916,8 +7926,8 @@ static bool32 DoSwitchInEffectsForBattler(u32 battler) } gSpecialStatuses[battler].switchInItemDone = FALSE; - gDisableStructs[battler].hazardsDone = FALSE; gBattleStruct->battlerState[battler].forcedSwitch = FALSE; + gBattleStruct->battlerState[battler].wasAboveHalfHp = FALSE; return FALSE; } diff --git a/src/battle_util.c b/src/battle_util.c index 648f6a9e76..abb87e3c4e 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -589,7 +589,7 @@ void HandleAction_UseMove(void) BattleArena_AddMindPoints(gBattlerAttacker); for (i = 0; i < MAX_BATTLERS_COUNT; i++) - gBattleStruct->hpBefore[i] = gBattleMons[i].hp; + gBattleStruct->battlerState[i].wasAboveHalfHp = gBattleMons[i].hp > gBattleMons[i].maxHP / 2; gCurrentActionFuncId = B_ACTION_EXEC_SCRIPT; } @@ -3322,10 +3322,9 @@ static inline uq4_12_t GetSupremeOverlordModifier(u32 battler) bool32 HadMoreThanHalfHpNowDoesnt(u32 battler) { - u32 cutoff = gBattleMons[battler].maxHP / 2; // Had more than half of hp before, now has less - return gBattleStruct->hpBefore[battler] > cutoff - && gBattleMons[battler].hp <= cutoff; + return gBattleStruct->battlerState[battler].wasAboveHalfHp + && gBattleMons[battler].hp <= gBattleMons[battler].maxHP / 2; } #define ANIM_STAT_HP 0 diff --git a/test/battle/ability/emergency_exit.c b/test/battle/ability/emergency_exit.c index 1dcd0be21b..3592b91b01 100644 --- a/test/battle/ability/emergency_exit.c +++ b/test/battle/ability/emergency_exit.c @@ -81,6 +81,23 @@ SINGLE_BATTLE_TEST("Emergency Exit activates when taking residual damage and fal } } +SINGLE_BATTLE_TEST("Emergency Exit activates when healing from under 50% max-hp and taking residual damage to under 50% max-hp - Burn") +{ + // Might fail if users set healing higher than burn damage + GIVEN { + ASSUME(GetMoveEffect(MOVE_AQUA_RING) == EFFECT_AQUA_RING); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_GOLISOPOD) { Ability(ABILITY_EMERGENCY_EXIT); MaxHP(263); HP(130); Status1(STATUS1_BURN); }; + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_AQUA_RING); SEND_OUT(opponent, 1); } + } SCENE { + HP_BAR(opponent); + HP_BAR(opponent); + ABILITY_POPUP(opponent, ABILITY_EMERGENCY_EXIT); + } +} + SINGLE_BATTLE_TEST("Emergency Exit activates when taking residual damage and falling under 50% max-hp - Weather") { GIVEN { @@ -95,6 +112,24 @@ SINGLE_BATTLE_TEST("Emergency Exit activates when taking residual damage and fal } } +SINGLE_BATTLE_TEST("Emergency Exit activates when healing from under 50% max-hp and taking residual damage to under 50% max-hp - Sticky Barb") +{ + // Might fail if users set healing higher than sticky barb damage + GIVEN { + ASSUME(GetMoveEffect(MOVE_AQUA_RING) == EFFECT_AQUA_RING); + ASSUME(GetItemHoldEffect(ITEM_STICKY_BARB) == HOLD_EFFECT_STICKY_BARB); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_GOLISOPOD) { Ability(ABILITY_EMERGENCY_EXIT); MaxHP(263); HP(130); Item(ITEM_STICKY_BARB); }; + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_AQUA_RING); SEND_OUT(opponent, 1); } + } SCENE { + HP_BAR(opponent); + HP_BAR(opponent); + ABILITY_POPUP(opponent, ABILITY_EMERGENCY_EXIT); + } +} + SINGLE_BATTLE_TEST("Emergency Exit activates when taking residual damage and falling under 50% max-hp - Salt Cure") { GIVEN { diff --git a/test/battle/hazards.c b/test/battle/hazards.c index 17ecb41f5c..ea0aef70e5 100644 --- a/test/battle/hazards.c +++ b/test/battle/hazards.c @@ -61,3 +61,113 @@ SINGLE_BATTLE_TEST("Hazards are applied correctly after a battler faints") MESSAGE("Pointed stones dug into Wynaut!"); } } + +SINGLE_BATTLE_TEST("Toxic Spikes can be removed after fainting to other hazards") +{ + KNOWN_FAILING; // tryfaintmon changes something that doesn't allow other switch-in effects on the battler + + GIVEN { + PLAYER(SPECIES_WYNAUT); + PLAYER(SPECIES_GRIMER) { HP(1); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_STEALTH_ROCK); } + TURN { MOVE(opponent, MOVE_TOXIC_SPIKES); } + TURN { MOVE(opponent, MOVE_STICKY_WEB); } + TURN { MOVE(opponent, MOVE_SPIKES); } + TURN { MOVE(opponent, MOVE_STEALTH_ROCK); SWITCH(player, 1); SEND_OUT(player, 0); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_STEALTH_ROCK, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_STICKY_WEB, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPIKES, opponent); + MESSAGE("Pointed stones dug into Grimer!"); + MESSAGE("Grimer fainted!"); + MESSAGE("The poison spikes disappeared from the ground around your team!"); + NONE_OF { + MESSAGE("Grimer was caught in a sticky web!"); + MESSAGE("Grimer was hurt by the spikes!"); + } + } THEN { + EXPECT_EQ(gBattleStruct->hazardsQueue[0][0], HAZARDS_STEALTH_ROCK); + EXPECT_EQ(gBattleStruct->hazardsQueue[0][1], HAZARDS_STICKY_WEB); + EXPECT_EQ(gBattleStruct->hazardsQueue[0][2], HAZARDS_SPIKES); + EXPECT_EQ(gBattleStruct->hazardsQueue[0][3], HAZARDS_NONE); + EXPECT_EQ(gBattleStruct->hazardsQueue[0][4], HAZARDS_NONE); + EXPECT_EQ(gBattleStruct->hazardsQueue[0][5], HAZARDS_NONE); + } +} + +SINGLE_BATTLE_TEST("Hazards can trigger Emergency Exit and other hazards don't activate") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_GOLISOPOD) { HP(105); MaxHP(200); Ability(ABILITY_EMERGENCY_EXIT); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_STEALTH_ROCK); } + TURN { MOVE(opponent, MOVE_TOXIC_SPIKES); } + TURN { MOVE(opponent, MOVE_STICKY_WEB); } + TURN { MOVE(opponent, MOVE_SPIKES); } + TURN { MOVE(opponent, MOVE_STEALTH_ROCK); SWITCH(player, 1); SEND_OUT(player, 0); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_STEALTH_ROCK, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_STICKY_WEB, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPIKES, opponent); + MESSAGE("Pointed stones dug into Golisopod!"); + ABILITY_POPUP(player, ABILITY_EMERGENCY_EXIT); + NONE_OF { + MESSAGE("Golisopod was poisoned!"); + MESSAGE("Golisopod was caught in a sticky web!"); + MESSAGE("Golisopod was hurt by the spikes!"); + } + MESSAGE("Pointed stones dug into Wobbuffet!"); + MESSAGE("Wobbuffet was poisoned!"); + MESSAGE("Wobbuffet was caught in a sticky web!"); + MESSAGE("Wobbuffet was hurt by the spikes!"); + NOT MESSAGE("Pointed stones dug into Wobbuffet!"); // Because the previous switch in effects instruction is still kept + } +} + +DOUBLE_BATTLE_TEST("Hazards can trigger Emergency Exit and hazards still activate for other battlers") +{ + GIVEN { + ASSUME(GetMoveEffect(MOVE_FINAL_GAMBIT) == EFFECT_FINAL_GAMBIT); + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + PLAYER(SPECIES_GOLISOPOD) { HP(105); MaxHP(200); Ability(ABILITY_EMERGENCY_EXIT); } + PLAYER(SPECIES_WYNAUT); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(opponentLeft, MOVE_STEALTH_ROCK); MOVE(opponentRight, MOVE_TOXIC_SPIKES); } + TURN { MOVE(opponentLeft, MOVE_STICKY_WEB); MOVE(opponentRight, MOVE_SPIKES); } + TURN { MOVE(playerLeft, MOVE_FINAL_GAMBIT, target: opponentRight); + MOVE(playerRight, MOVE_FINAL_GAMBIT, target: opponentRight); + SEND_OUT(playerLeft, 2); + SEND_OUT(playerRight, 3); + SEND_OUT(playerLeft, 4); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_STEALTH_ROCK, opponentLeft); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, opponentRight); + ANIMATION(ANIM_TYPE_MOVE, MOVE_STICKY_WEB, opponentLeft); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPIKES, opponentRight); + MESSAGE("Pointed stones dug into Golisopod!"); + ABILITY_POPUP(playerLeft, ABILITY_EMERGENCY_EXIT); + NONE_OF { + MESSAGE("Golisopod was poisoned!"); + MESSAGE("Golisopod was caught in a sticky web!"); + MESSAGE("Golisopod was hurt by the spikes!"); + } + MESSAGE("Pointed stones dug into Wobbuffet!"); + MESSAGE("Wobbuffet was poisoned!"); + MESSAGE("Wobbuffet was caught in a sticky web!"); + MESSAGE("Wobbuffet was hurt by the spikes!"); + MESSAGE("Pointed stones dug into Wynaut!"); + MESSAGE("Wynaut was poisoned!"); + MESSAGE("Wynaut was caught in a sticky web!"); + MESSAGE("Wynaut was hurt by the spikes!"); + } +}