From 78a05c97acab8a79fc742c5dce06cf9e621551c4 Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Sun, 14 Sep 2025 15:41:10 +0200 Subject: [PATCH] Refactors ruin ability checks into a field effect (#7711) --- data/battle_scripts_1.s | 2 +- include/battle_util.h | 1 + include/constants/battle.h | 8 ++- src/battle_ai_util.c | 28 ++++++++++- src/battle_script_commands.c | 8 ++- src/battle_util.c | 58 +++++++++++++++++++--- src/trade.c | 2 +- test/battle/ability/vessel_of_ruin.c | 73 ++++++++++++++++++++++++++++ 8 files changed, 167 insertions(+), 13 deletions(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index af5f7aa49d..1a7e91193f 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -2042,7 +2042,7 @@ BattleScript_EffectEntrainment:: tryentrainment BattleScript_ButItFailed attackanimation waitanimation - setlastusedability + switchinabilities BS_TARGET printstring STRINGID_PKMNACQUIREDABILITY waitmessage B_WAIT_TIME_LONG goto BattleScript_MoveEnd diff --git a/include/battle_util.h b/include/battle_util.h index 745ad9a7f8..a1eea3b770 100644 --- a/include/battle_util.h +++ b/include/battle_util.h @@ -413,5 +413,6 @@ bool32 IsSemiInvulnerable(u32 battler, enum SemiInvulnerableExclusion excludeCom bool32 BreaksThroughSemiInvulnerablity(u32 battler, u32 move); u32 GetNaturePowerMove(u32 battler); u32 GetNaturePowerMove(u32 battler); +void RemoveAbilityFlags(u32 battler); #endif // GUARD_BATTLE_UTIL_H diff --git a/include/constants/battle.h b/include/constants/battle.h index 503caf406e..47fff193a6 100644 --- a/include/constants/battle.h +++ b/include/constants/battle.h @@ -195,7 +195,11 @@ enum VolatileFlags F(VOLATILE_HEAL_BLOCK, healBlock, (u32, 1), V_BATON_PASSABLE) \ F(VOLATILE_AQUA_RING, aquaRing, (u32, 1), V_BATON_PASSABLE) \ F(VOLATILE_LASER_FOCUS, laserFocus, (u32, 1)) \ - F(VOLATILE_POWER_TRICK, powerTrick, (u32, 1), V_BATON_PASSABLE) + F(VOLATILE_POWER_TRICK, powerTrick, (u32, 1), V_BATON_PASSABLE) \ + F(VOLATILE_VESSEL_OF_RUIN, vesselOfRuin, (u32, 1)) \ + F(VOLATILE_SWORD_OF_RUIN, swordOfRuin, (u32, 1)) \ + F(VOLATILE_TABLETS_OF_RUIN, tabletsOfRuin, (u32, 1)) \ + F(VOLATILE_BEADS_OF_RUIN, beadsOfRuin, (u32, 1)) /* Use within a macro to get the maximum allowed value for a volatile. Requires _typeMaxValue as input. */ @@ -363,7 +367,7 @@ enum BattleWeather #define B_WEATHER_LOW_LIGHT (B_WEATHER_FOG | B_WEATHER_ICY_ANY | B_WEATHER_RAIN | B_WEATHER_SANDSTORM) #define B_WEATHER_PRIMAL_ANY (B_WEATHER_RAIN_PRIMAL | B_WEATHER_SUN_PRIMAL | B_WEATHER_STRONG_WINDS) -// Explicit numbers until frostbite because those shouldn't be shifted +// Explicit numbers until frostbite because those shouldn't be shifted enum __attribute__((packed)) MoveEffect { MOVE_EFFECT_NONE = 0, diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index c350c21c8f..eac7e728b1 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -2160,7 +2160,7 @@ u32 IncreaseStatDownScore(u32 battlerAtk, u32 battlerDef, u32 stat) case STAT_SPEED: { u32 predictedMoveSpeedCheck = GetIncomingMoveSpeedCheck(battlerAtk, battlerDef, gAiLogicData); - if (AI_IsSlower(battlerAtk, battlerDef, MOVE_NONE, predictedMoveSpeedCheck, DONT_CONSIDER_PRIORITY) + if (AI_IsSlower(battlerAtk, battlerDef, MOVE_NONE, predictedMoveSpeedCheck, DONT_CONSIDER_PRIORITY) || AI_IsSlower(BATTLE_PARTNER(battlerAtk), battlerDef, MOVE_NONE, predictedMoveSpeedCheck, DONT_CONSIDER_PRIORITY)) tempScore += DECENT_EFFECT; break; @@ -4251,6 +4251,28 @@ void FreeRestoreBattleMons(struct BattlePokemon *savedBattleMons) Free(savedBattleMons); } +// Set potential field effect from ability for switch in +static void SetBattlerFieldStatusForSwitchin(u32 battler) +{ + switch (gAiLogicData->abilities[battler]) + { + case ABILITY_VESSEL_OF_RUIN: + gBattleMons[battler].volatiles.vesselOfRuin = TRUE; + break; + case ABILITY_SWORD_OF_RUIN: + gBattleMons[battler].volatiles.swordOfRuin = TRUE; + break; + case ABILITY_TABLETS_OF_RUIN: + gBattleMons[battler].volatiles.tabletsOfRuin = TRUE; + break; + case ABILITY_BEADS_OF_RUIN: + gBattleMons[battler].volatiles.beadsOfRuin = TRUE; + break; + default: + break; + } +} + // party logic s32 AI_CalcPartyMonDamage(u32 move, u32 battlerAtk, u32 battlerDef, struct BattlePokemon switchinCandidate, enum DamageCalcContext calcContext) { @@ -4263,6 +4285,7 @@ s32 AI_CalcPartyMonDamage(u32 move, u32 battlerAtk, u32 battlerDef, struct Battl gBattleMons[battlerAtk] = switchinCandidate; gAiThinkingStruct->saved[battlerDef].saved = TRUE; SetBattlerAiData(battlerAtk, gAiLogicData); // set known opposing battler data + SetBattlerFieldStatusForSwitchin(battlerAtk); gAiThinkingStruct->saved[battlerDef].saved = FALSE; } else if (calcContext == AI_DEFENDING) @@ -4270,6 +4293,7 @@ s32 AI_CalcPartyMonDamage(u32 move, u32 battlerAtk, u32 battlerDef, struct Battl gBattleMons[battlerDef] = switchinCandidate; gAiThinkingStruct->saved[battlerAtk].saved = TRUE; SetBattlerAiData(battlerDef, gAiLogicData); // set known opposing battler data + SetBattlerFieldStatusForSwitchin(battlerDef); gAiThinkingStruct->saved[battlerAtk].saved = FALSE; } @@ -4947,7 +4971,9 @@ bool32 ShouldUseZMove(u32 battlerAtk, u32 battlerDef, u32 chosenMove) return ShouldRaiseAnyStat(battlerAtk, battlerDef); } else if (!IsBattleMoveStatus(chosenMove) && IsBattleMoveStatus(zMove)) + { return FALSE; + } uq4_12_t effectiveness; struct SimulatedDamage dmg; diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 616b64dce6..d39642dd21 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -6150,7 +6150,7 @@ static void Cmd_moveend(void) if (!IsOnPlayerSide(gBattlerAttacker)) UpdateStallMons(); if ((gBattleStruct->moveResultFlags[gBattlerTarget] & (MOVE_RESULT_FAILED | MOVE_RESULT_DOESNT_AFFECT_FOE)) - || (gBattleMons[gBattlerAttacker].volatiles.flinched) + || gBattleMons[gBattlerAttacker].volatiles.flinched || gBattleStruct->pledgeMove == TRUE // Is the battler that uses the first Pledge move in the combo || gProtectStructs[gBattlerAttacker].nonVolatileStatusImmobility) gBattleStruct->battlerState[gBattlerAttacker].stompingTantrumTimer = 2; @@ -12681,6 +12681,7 @@ static void Cmd_trycopyability(void) } else { + RemoveAbilityFlags(battler); gBattleScripting.abilityPopupOverwrite = gBattleMons[battler].ability; gBattleMons[battler].ability = gDisableStructs[battler].overwrittenAbility = defAbility; gLastUsedAbility = defAbility; @@ -12856,6 +12857,8 @@ static void Cmd_tryswapabilities(void) if (!IsBattlerAlly(gBattlerAttacker, gBattlerTarget)) gBattleScripting.abilityPopupOverwrite = gBattleMons[gBattlerAttacker].ability; gLastUsedAbility = gBattleMons[gBattlerTarget].ability; + RemoveAbilityFlags(gBattlerTarget); + RemoveAbilityFlags(gBattlerAttacker); gBattleMons[gBattlerTarget].ability = gDisableStructs[gBattlerTarget].overwrittenAbility = gBattleMons[gBattlerAttacker].ability; gBattleMons[gBattlerAttacker].ability = gDisableStructs[gBattlerAttacker].overwrittenAbility = gLastUsedAbility; @@ -14266,6 +14269,7 @@ static void Cmd_tryworryseed(void) if (gBattleMons[gBattlerTarget].ability == ABILITY_NEUTRALIZING_GAS) gSpecialStatuses[gBattlerTarget].neutralizingGasRemoved = TRUE; + RemoveAbilityFlags(gBattlerTarget); gBattleScripting.abilityPopupOverwrite = gBattleMons[gBattlerTarget].ability; gBattleMons[gBattlerTarget].ability = gDisableStructs[gBattlerTarget].overwrittenAbility = ABILITY_INSOMNIA; gBattlescriptCurrInstr = cmd->nextInstr; @@ -17063,6 +17067,7 @@ void BS_SetSimpleBeam(void) if (gBattleMons[gBattlerTarget].ability == ABILITY_NEUTRALIZING_GAS) gSpecialStatuses[gBattlerTarget].neutralizingGasRemoved = TRUE; + RemoveAbilityFlags(gBattlerTarget); gBattleScripting.abilityPopupOverwrite = gBattleMons[gBattlerTarget].ability; gBattleMons[gBattlerTarget].ability = gDisableStructs[gBattlerTarget].overwrittenAbility = ABILITY_SIMPLE; gBattlescriptCurrInstr = cmd->nextInstr; @@ -17092,6 +17097,7 @@ void BS_TryEntrainment(void) } else { + RemoveAbilityFlags(gBattlerTarget); gBattleMons[gBattlerTarget].ability = gDisableStructs[gBattlerTarget].overwrittenAbility = gBattleMons[gBattlerAttacker].ability; gBattlescriptCurrInstr = cmd->nextInstr; } diff --git a/src/battle_util.c b/src/battle_util.c index 6a1eba7136..a75c8397dd 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -4173,6 +4173,7 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 case ABILITY_VESSEL_OF_RUIN: if (!gSpecialStatuses[battler].switchInAbilityDone) { + gBattleMons[battler].volatiles.vesselOfRuin = TRUE; PREPARE_STAT_BUFFER(gBattleTextBuff1, STAT_SPATK); gSpecialStatuses[battler].switchInAbilityDone = TRUE; BattleScriptPushCursorAndCallback(BattleScript_RuinAbilityActivates); @@ -4182,6 +4183,7 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 case ABILITY_SWORD_OF_RUIN: if (!gSpecialStatuses[battler].switchInAbilityDone) { + gBattleMons[battler].volatiles.swordOfRuin = TRUE; PREPARE_STAT_BUFFER(gBattleTextBuff1, STAT_DEF); gSpecialStatuses[battler].switchInAbilityDone = TRUE; BattleScriptPushCursorAndCallback(BattleScript_RuinAbilityActivates); @@ -4191,6 +4193,7 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 case ABILITY_TABLETS_OF_RUIN: if (!gSpecialStatuses[battler].switchInAbilityDone) { + gBattleMons[battler].volatiles.tabletsOfRuin = TRUE; PREPARE_STAT_BUFFER(gBattleTextBuff1, STAT_ATK); gSpecialStatuses[battler].switchInAbilityDone = TRUE; BattleScriptPushCursorAndCallback(BattleScript_RuinAbilityActivates); @@ -4200,6 +4203,7 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 case ABILITY_BEADS_OF_RUIN: if (!gSpecialStatuses[battler].switchInAbilityDone) { + gBattleMons[battler].volatiles.beadsOfRuin = TRUE; PREPARE_STAT_BUFFER(gBattleTextBuff1, STAT_SPDEF); gSpecialStatuses[battler].switchInAbilityDone = TRUE; BattleScriptPushCursorAndCallback(BattleScript_RuinAbilityActivates); @@ -4694,7 +4698,8 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 RecordItemEffectBattle(gBattlerAttacker, HOLD_EFFECT_ABILITY_SHIELD); break; } - + + RemoveAbilityFlags(gBattlerAttacker); gLastUsedAbility = gBattleMons[gBattlerAttacker].ability; gBattleMons[gBattlerAttacker].ability = gDisableStructs[gBattlerAttacker].overwrittenAbility = gBattleMons[gBattlerTarget].ability; BattleScriptCall(BattleScript_MummyActivates); @@ -4720,6 +4725,7 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 break; } + RemoveAbilityFlags(gBattlerAttacker); gLastUsedAbility = gBattleMons[gBattlerAttacker].ability; gBattleMons[gBattlerAttacker].ability = gDisableStructs[gBattlerAttacker].overwrittenAbility = gBattleMons[gBattlerTarget].ability; gBattleMons[gBattlerTarget].ability = gDisableStructs[gBattlerTarget].overwrittenAbility = gLastUsedAbility; @@ -8696,6 +8702,20 @@ static inline u32 CalcMoveBasePowerAfterModifiers(struct DamageContext *ctx) return uq4_12_multiply_by_int_half_down(modifier, basePower); } +static bool32 IsRuinStatusActive(u32 fieldEffect) +{ + if (IsNeutralizingGasOnField()) // Neutralizing Gas still blocks Ruin field statuses + return FALSE; + + for (u32 battler = 0; battler < gBattlersCount; battler++) + { + if (GetBattlerVolatile(battler, fieldEffect)) + return TRUE; + } + + return FALSE; +} + static inline u32 CalcAttackStat(struct DamageContext *ctx) { u8 atkStage; @@ -8927,11 +8947,11 @@ static inline u32 CalcAttackStat(struct DamageContext *ctx) } } - // field abilities - if (IsAbilityOnField(ABILITY_VESSEL_OF_RUIN) && ctx->abilityAtk != ABILITY_VESSEL_OF_RUIN && IsBattleMoveSpecial(move)) + // Ruin field effects + if (IsBattleMoveSpecial(move) && !gBattleMons[ctx->battlerAtk].volatiles.vesselOfRuin && IsRuinStatusActive(VOLATILE_VESSEL_OF_RUIN)) modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(0.75)); - if (IsAbilityOnField(ABILITY_TABLETS_OF_RUIN) && ctx->abilityAtk != ABILITY_TABLETS_OF_RUIN && IsBattleMovePhysical(move)) + if (IsBattleMovePhysical(move) && !gBattleMons[ctx->battlerAtk].volatiles.tabletsOfRuin && IsRuinStatusActive(VOLATILE_TABLETS_OF_RUIN)) modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(0.75)); // attacker's hold effect @@ -9095,11 +9115,11 @@ static inline u32 CalcDefenseStat(struct DamageContext *ctx) } } - // field abilities - if (IsAbilityOnField(ABILITY_SWORD_OF_RUIN) && ctx->abilityDef != ABILITY_SWORD_OF_RUIN && usesDefStat) + // Ruin field effects + if (usesDefStat && !gBattleMons[ctx->battlerDef].volatiles.swordOfRuin && IsRuinStatusActive(VOLATILE_SWORD_OF_RUIN)) modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(0.75)); - if (IsAbilityOnField(ABILITY_BEADS_OF_RUIN) && ctx->abilityDef != ABILITY_BEADS_OF_RUIN && !usesDefStat) + if (!usesDefStat && !gBattleMons[ctx->battlerDef].volatiles.beadsOfRuin && IsRuinStatusActive(VOLATILE_BEADS_OF_RUIN)) modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(0.75)); // target's hold effects @@ -12141,3 +12161,27 @@ static u32 GetMeFirstMove(void) return move; } + +void RemoveAbilityFlags(u32 battler) +{ + switch (GetBattlerAbility(battler)) + { + case ABILITY_FLASH_FIRE: + gDisableStructs[battler].flashFireBoosted = FALSE; + break; + case ABILITY_VESSEL_OF_RUIN: + gBattleMons[battler].volatiles.vesselOfRuin = FALSE; + break; + case ABILITY_TABLETS_OF_RUIN: + gBattleMons[battler].volatiles.tabletsOfRuin = FALSE; + break; + case ABILITY_SWORD_OF_RUIN: + gBattleMons[battler].volatiles.swordOfRuin = FALSE; + break; + case ABILITY_BEADS_OF_RUIN: + gBattleMons[battler].volatiles.beadsOfRuin = FALSE; + break; + default: + break; + } +} diff --git a/src/trade.c b/src/trade.c index 5fd04e1f82..9a76c9aec5 100644 --- a/src/trade.c +++ b/src/trade.c @@ -138,7 +138,7 @@ enum { #define NUM_CHOOSE_PKMN_SPRITES (1 + GFXTAG_CHOOSE_PKMN_EMPTY_3 - GFXTAG_CHOOSE_PKMN_L) // Values for signaling to/from the link partner -enum { +enum SignalStatus { STATUS_NONE, STATUS_READY, STATUS_CANCEL, diff --git a/test/battle/ability/vessel_of_ruin.c b/test/battle/ability/vessel_of_ruin.c index 30c7520c36..1a075db8cf 100644 --- a/test/battle/ability/vessel_of_ruin.c +++ b/test/battle/ability/vessel_of_ruin.c @@ -73,3 +73,76 @@ SINGLE_BATTLE_TEST("Vessel of Ruin's message displays correctly after all battle MESSAGE("The opposing Ting-Lu's Vessel of Ruin weakened the Sp. Atk of all surrounding Pokémon!"); } } + +DOUBLE_BATTLE_TEST("Vessel of Ruin does not reduce Sp. Atk if Neutralizing Gas is on the field") +{ + s16 damage[2]; + + GIVEN { + PLAYER(SPECIES_WYNAUT); + PLAYER(SPECIES_TING_LU) { Ability(ABILITY_VESSEL_OF_RUIN); } + OPPONENT(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WEEZING) { Ability(ABILITY_NEUTRALIZING_GAS); } + } WHEN { + TURN { + MOVE(playerLeft, MOVE_WATER_GUN, target: opponentLeft); + } + TURN { + SWITCH(opponentRight, 2); + MOVE(playerLeft, MOVE_WATER_GUN, target: opponentLeft); + } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_WATER_GUN, playerLeft); + HP_BAR(opponentLeft, captureDamage: &damage[0]); + ANIMATION(ANIM_TYPE_MOVE, MOVE_WATER_GUN, playerLeft); + HP_BAR(opponentLeft, captureDamage: &damage[1]); + } THEN { + EXPECT_MUL_EQ(damage[0], Q_4_12(1.33), damage[1]); + } +} + +SINGLE_BATTLE_TEST("Vessel of Ruin is still active if removed by Mold Breaker + Entrainment") +{ + s16 damage[2]; + + GIVEN { + PLAYER(SPECIES_TING_LU) { Ability(ABILITY_VESSEL_OF_RUIN); } + OPPONENT(SPECIES_PINSIR) { Ability(ABILITY_MOLD_BREAKER); } + } WHEN { + TURN { MOVE(opponent, MOVE_WATER_GUN); } + TURN { MOVE(opponent, MOVE_ENTRAINMENT); } + TURN { MOVE(opponent, MOVE_WATER_GUN); } + } SCENE { + ABILITY_POPUP(player, ABILITY_VESSEL_OF_RUIN); + MESSAGE("Ting-Lu's Vessel of Ruin weakened the Sp. Atk of all surrounding Pokémon!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_WATER_GUN, opponent); + HP_BAR(player, captureDamage: &damage[0]); + ANIMATION(ANIM_TYPE_MOVE, MOVE_ENTRAINMENT, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_WATER_GUN, opponent); + HP_BAR(player, captureDamage: &damage[1]); + } THEN { + EXPECT_EQ(damage[0], damage[1]); + } +} + +DOUBLE_BATTLE_TEST("Vessel of Ruin is active if removed by Mold Breaker Entrainment and Sword of Ruin is active after obtaining it") +{ + GIVEN { + PLAYER(SPECIES_TING_LU) { Ability(ABILITY_VESSEL_OF_RUIN); } + PLAYER(SPECIES_CHIEN_PAO) { Ability(ABILITY_SWORD_OF_RUIN); } + OPPONENT(SPECIES_PINSIR) { Ability(ABILITY_MOLD_BREAKER); } + OPPONENT(SPECIES_WOBBUFFET) { Ability(ABILITY_TELEPATHY); } + } WHEN { + TURN { + MOVE(opponentLeft, MOVE_ENTRAINMENT, target: playerLeft); + MOVE(opponentRight, MOVE_SKILL_SWAP, target: playerLeft); + MOVE(playerRight, MOVE_SKILL_SWAP, target: playerLeft); + } + } THEN { + bool32 isVesselOfRuinActive = gBattleMons[B_POSITION_PLAYER_LEFT].volatiles.vesselOfRuin; + bool32 isSwordOfRuinActive = gBattleMons[B_POSITION_PLAYER_LEFT].volatiles.swordOfRuin; + EXPECT_EQ(isVesselOfRuinActive, TRUE); + EXPECT_EQ(isSwordOfRuinActive, TRUE); + } +}