Refactors ruin ability checks into a field effect (#7711)

This commit is contained in:
Alex 2025-09-14 15:41:10 +02:00 committed by GitHub
parent abc0e10558
commit 78a05c97ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 167 additions and 13 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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