Emergency Exit on hazards activation + fix end of turn activation (#8075)

This commit is contained in:
PhallenTree 2025-10-30 16:01:13 +00:00 committed by GitHub
parent 44dc720260
commit 9d06a4fb55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 179 additions and 32 deletions

View File

@ -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.

View File

@ -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[];

View File

@ -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;

View File

@ -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)

View File

@ -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;
}

View File

@ -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

View File

@ -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 {

View File

@ -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!");
}
}