From fadf4721571683420734e64b73c6eaefde079636 Mon Sep 17 00:00:00 2001 From: PhallenTree <168426989+PhallenTree@users.noreply.github.com> Date: Thu, 1 May 2025 09:16:15 +0100 Subject: [PATCH] Adds RNG tags and tests for Moody and Starf Berry (#6718) --- include/battle_util.h | 2 + include/constants/generational_changes.h | 1 + include/generational_changes.h | 1 + include/random.h | 3 + src/battle_util.c | 73 ++++++++-------- test/battle/ability/moody.c | 76 +++++++++++++++++ test/battle/hold_effect/random_stat_up.c | 101 ++++++++++++++++++++++- 7 files changed, 220 insertions(+), 37 deletions(-) create mode 100644 test/battle/ability/moody.c diff --git a/include/battle_util.h b/include/battle_util.h index e4a54990c0..492c188278 100644 --- a/include/battle_util.h +++ b/include/battle_util.h @@ -311,6 +311,8 @@ bool32 IsMoveEffectRemoveSpeciesType(u32 move, u32 moveEffect, u32 argument); bool32 MoveHasChargeTurnAdditionalEffect(u32 move); bool32 CanTargetPartner(u32 battlerAtk, u32 battlerDef); bool32 TargetFullyImmuneToCurrMove(u32 battlerAtk, u32 battlerDef); +bool32 MoodyCantRaiseStat(u32 stat); +bool32 MoodyCantLowerStat(u32 stat); bool32 CanBeSlept(u32 battler, u32 ability, enum SleepClauseBlock isBlockedBySleepClause); bool32 CanBePoisoned(u32 battlerAtk, u32 battlerDef, u32 defAbility); diff --git a/include/constants/generational_changes.h b/include/constants/generational_changes.h index e9653d26d5..5e93679643 100644 --- a/include/constants/generational_changes.h +++ b/include/constants/generational_changes.h @@ -12,6 +12,7 @@ enum GenConfigTag GEN_CONFIG_GALE_WINGS, GEN_CONFIG_HEAL_BELL_SOUNDPROOF, GEN_CONFIG_TELEPORT_BEHAVIOR, + GEN_CONFIG_MOODY_STATS, GEN_CONFIG_COUNT }; diff --git a/include/generational_changes.h b/include/generational_changes.h index 1aad93f915..4f555b9368 100644 --- a/include/generational_changes.h +++ b/include/generational_changes.h @@ -15,6 +15,7 @@ static const u8 sGenerationalChanges[GEN_CONFIG_COUNT] = [GEN_CONFIG_GALE_WINGS] = B_GALE_WINGS, [GEN_CONFIG_HEAL_BELL_SOUNDPROOF] = B_HEAL_BELL_SOUNDPROOF, [GEN_CONFIG_TELEPORT_BEHAVIOR] = B_TELEPORT_BEHAVIOR, + [GEN_CONFIG_MOODY_STATS] = B_MOODY_ACC_EVASION, }; #if TESTING diff --git a/include/random.h b/include/random.h index c8b4a039b5..34b03db450 100644 --- a/include/random.h +++ b/include/random.h @@ -146,11 +146,14 @@ enum RandomTag RNG_INFATUATION, RNG_LOADED_DICE, RNG_METRONOME, + RNG_MOODY_INCREASE, + RNG_MOODY_DECREASE, RNG_PARALYSIS, RNG_PICKUP, RNG_POISON_POINT, RNG_POISON_TOUCH, RNG_RAMPAGE_TURNS, + RNG_RANDOM_STAT_UP, RNG_SECONDARY_EFFECT, RNG_SECONDARY_EFFECT_2, RNG_SECONDARY_EFFECT_3, diff --git a/src/battle_util.c b/src/battle_util.c index adc6ac182d..72d203609e 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -1604,6 +1604,18 @@ u32 GetBattlerAffectionHearts(u32 battler) return GetMonAffectionHearts(&party[gBattlerPartyIndexes[battler]]); } +// gBattlerAttacker is the battler that's trying to raise their stats and due to limitations of RandomUniformExcept, cannot be an argument +bool32 MoodyCantRaiseStat(u32 stat) +{ + return CompareStat(gBattlerAttacker, stat, MAX_STAT_STAGE, CMP_EQUAL); +} + +// gBattlerAttacker is the battler that's trying to lower their stats and due to limitations of RandomUniformExcept, cannot be an argument +bool32 MoodyCantLowerStat(u32 stat) +{ + return stat == GET_STAT_BUFF_ID(gBattleScripting.statChanger) || CompareStat(gBattlerAttacker, stat, MIN_STAT_STAGE, CMP_EQUAL); +} + void TryToRevertMimicryAndFlags(void) { u32 i; @@ -5469,7 +5481,7 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 if (gDisableStructs[battler].isFirstTurn != 2) { u32 validToRaise = 0, validToLower = 0; - u32 statsNum = B_MOODY_ACC_EVASION >= GEN_8 ? NUM_STATS : NUM_BATTLE_STATS; + u32 statsNum = GetGenConfig(GEN_CONFIG_MOODY_STATS) >= GEN_8 ? NUM_STATS : NUM_BATTLE_STATS; for (i = STAT_ATK; i < statsNum; i++) { @@ -5479,29 +5491,21 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 validToRaise |= 1u << i; } - if (validToLower != 0 || validToRaise != 0) // Can lower one stat, or can raise one stat + gBattleScripting.statChanger = gBattleScripting.savedStatChanger = 0; // for raising and lowering stat respectively + if (validToRaise) // Find stat to raise { - gBattleScripting.statChanger = gBattleScripting.savedStatChanger = 0; // for raising and lowering stat respectively - if (validToRaise != 0) // Find stat to raise - { - do - { - i = (Random() % statsNum) + STAT_ATK; - } while (!(validToRaise & (1u << i))); - SET_STATCHANGER(i, 2, FALSE); - validToLower &= ~(1u << i); // Can't lower the same stat as raising. - } - if (validToLower != 0) // Find stat to lower - { - do - { - i = (Random() % statsNum) + STAT_ATK; - } while (!(validToLower & (1u << i))); - SET_STATCHANGER2(gBattleScripting.savedStatChanger, i, 1, TRUE); - } - BattleScriptPushCursorAndCallback(BattleScript_MoodyActivates); - effect++; + i = RandomUniformExcept(RNG_MOODY_INCREASE, STAT_ATK, statsNum - 1, MoodyCantRaiseStat); + SET_STATCHANGER(i, 2, FALSE); + validToLower &= ~(1u << i); // Can't lower the same stat as raising. } + if (validToLower) // Find stat to lower + { + // MoodyCantLowerStat already checks that both stats are different + i = RandomUniformExcept(RNG_MOODY_DECREASE, STAT_ATK, statsNum - 1, MoodyCantLowerStat); + SET_STATCHANGER2(gBattleScripting.savedStatChanger, i, 1, TRUE); + } + BattleScriptPushCursorAndCallback(BattleScript_MoodyActivates); + effect++; } break; case ABILITY_TRUANT: @@ -6976,23 +6980,24 @@ static enum ItemEffect StatRaiseBerry(u32 battler, u32 itemId, u32 statId, enum static enum ItemEffect RandomStatRaiseBerry(u32 battler, u32 itemId, enum ItemCaseId caseID) { - s32 i; + s32 stat; u16 stringId; - for (i = 0; i < NUM_STATS - 1; i++) + for (stat = STAT_ATK; stat < NUM_STATS; stat++) { - if (CompareStat(battler, STAT_ATK + i, MAX_STAT_STAGE, CMP_LESS_THAN)) + if (CompareStat(battler, stat, MAX_STAT_STAGE, CMP_LESS_THAN)) break; } - if (i != NUM_STATS - 1 && HasEnoughHpToEatBerry(battler, GetBattlerItemHoldEffectParam(battler, itemId), itemId)) + if (stat != NUM_STATS && HasEnoughHpToEatBerry(battler, GetBattlerItemHoldEffectParam(battler, itemId), itemId)) { u16 battlerAbility = GetBattlerAbility(battler); - do - { - i = Random() % (NUM_STATS - 1); - } while (!CompareStat(battler, STAT_ATK + i, MAX_STAT_STAGE, CMP_LESS_THAN)); + u32 savedAttacker = gBattlerAttacker; + // MoodyCantRaiseStat requires that the battler is set to gBattlerAttacker + gBattlerAttacker = battler; + stat = RandomUniformExcept(RNG_RANDOM_STAT_UP, STAT_ATK, NUM_STATS - 1, MoodyCantRaiseStat); + gBattlerAttacker = savedAttacker; - PREPARE_STAT_BUFFER(gBattleTextBuff1, i + 1); + PREPARE_STAT_BUFFER(gBattleTextBuff1, stat); stringId = (battlerAbility == ABILITY_CONTRARY) ? STRINGID_STATFELL : STRINGID_STATROSE; gBattleTextBuff2[0] = B_BUFF_PLACEHOLDER_BEGIN; gBattleTextBuff2[1] = B_BUFF_STRING; @@ -7004,11 +7009,11 @@ static enum ItemEffect RandomStatRaiseBerry(u32 battler, u32 itemId, enum ItemCa gBattleTextBuff2[7] = EOS; gEffectBattler = battler; if (battlerAbility == ABILITY_RIPEN) - SET_STATCHANGER(i + 1, 4, FALSE); + SET_STATCHANGER(stat, 4, FALSE); else - SET_STATCHANGER(i + 1, 2, FALSE); + SET_STATCHANGER(stat, 2, FALSE); - gBattleScripting.animArg1 = STAT_ANIM_PLUS2 + i + 1; + gBattleScripting.animArg1 = STAT_ANIM_PLUS2 + stat; gBattleScripting.animArg2 = 0; if (caseID == ITEMEFFECT_ON_SWITCH_IN_FIRST_TURN || caseID == ITEMEFFECT_NORMAL) { diff --git a/test/battle/ability/moody.c b/test/battle/ability/moody.c new file mode 100644 index 0000000000..888e088f21 --- /dev/null +++ b/test/battle/ability/moody.c @@ -0,0 +1,76 @@ +#include "global.h" +#include "test/battle.h" + +SINGLE_BATTLE_TEST("Moody randomly raises the user's Attack, Defense, Sp. Atk, Sp. Def, or Speed by two stages") +{ + u32 config, statsNum; + + PARAMETRIZE { config = GEN_8; statsNum = NUM_STATS; } + PARAMETRIZE { config = GEN_7; statsNum = NUM_BATTLE_STATS; } + + // HP is not included + PASSES_RANDOMLY(1, statsNum - 1, RNG_MOODY_INCREASE); + GIVEN { + WITH_CONFIG(GEN_CONFIG_MOODY_STATS, config); + PLAYER(SPECIES_OCTILLERY) { Ability(ABILITY_MOODY); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { } + } SCENE { + ABILITY_POPUP(player, ABILITY_MOODY); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); + MESSAGE("Octillery's Attack sharply rose!"); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); + } THEN { + EXPECT_EQ(player->statStages[STAT_ATK], DEFAULT_STAT_STAGE + 2); + } +} + +SINGLE_BATTLE_TEST("Moody randomly lowers the user's Attack, Defense, Sp. Atk, Sp. Def, or Speed by one stage") +{ + u32 config, statsNum; + + PARAMETRIZE { config = GEN_8; statsNum = NUM_STATS; } + PARAMETRIZE { config = GEN_7; statsNum = NUM_BATTLE_STATS; } + + // One stat becomes unavailable due to it already increasing + PASSES_RANDOMLY(1, statsNum - 2, RNG_MOODY_DECREASE); + GIVEN { + WITH_CONFIG(GEN_CONFIG_MOODY_STATS, config); + PLAYER(SPECIES_OCTILLERY) { Ability(ABILITY_MOODY); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { } + } SCENE { + ABILITY_POPUP(player, ABILITY_MOODY); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); + MESSAGE("Octillery's Attack fell!"); + } THEN { + EXPECT_EQ(player->statStages[STAT_ATK], DEFAULT_STAT_STAGE - 1); + } +} + +SINGLE_BATTLE_TEST("Moody randomly raises the holder's Attack, Defense, Sp. Atk, Sp. Def, or Speed by two stages and lowers a different stat by one stage") +{ + u32 config, statsNum; + + PARAMETRIZE { config = GEN_8; statsNum = NUM_STATS; } + PARAMETRIZE { config = GEN_7; statsNum = NUM_BATTLE_STATS; } + + PASSES_RANDOMLY(statsNum - 1, statsNum - 1, RNG_MOODY_DECREASE); + GIVEN { + WITH_CONFIG(GEN_CONFIG_MOODY_STATS, config); + PLAYER(SPECIES_OCTILLERY) { Ability(ABILITY_MOODY); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { } + } SCENE { + ABILITY_POPUP(player, ABILITY_MOODY); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); + } THEN { + for (int stat = STAT_ATK; stat < statsNum; stat++) + EXPECT_NE(player->statStages[stat], DEFAULT_STAT_STAGE + 1); // Both raised and lowered + } +} diff --git a/test/battle/hold_effect/random_stat_up.c b/test/battle/hold_effect/random_stat_up.c index a0b8d8f50f..ff6b03e035 100644 --- a/test/battle/hold_effect/random_stat_up.c +++ b/test/battle/hold_effect/random_stat_up.c @@ -6,6 +6,101 @@ ASSUMPTIONS ASSUME(gItemsInfo[ITEM_STARF_BERRY].holdEffect == HOLD_EFFECT_RANDOM_STAT_UP); } -TO_DO_BATTLE_TEST("Starf Berry randomly raises the holder's Attack, Defense, Sp. Atk, Sp. Def, or Speed by two stages when the holder's HP drop to 1/4 or below") -TO_DO_BATTLE_TEST("Starf Berry randomly raises the holder's Attack, Defense, Sp. Atk, Sp. Def, or Speed by two stages when the holder's HP drop to 1/2 or below if it has Gluttony") -TO_DO_BATTLE_TEST("Starf Berry randomly raises the holder's Attack, Defense, Sp. Atk, Sp. Def, or Speed by four stages when the holder's HP drop to 1/4 or below if it has Riped") +SINGLE_BATTLE_TEST("Starf Berry randomly raises the holder's Attack, Defense, Sp. Atk, Sp. Def, or Speed by two stages") +{ + PASSES_RANDOMLY(1, 5, RNG_RANDOM_STAT_UP); + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_STARF_BERRY); HP(100); MaxHP(400); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { } + } SCENE { + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); + MESSAGE("Using Starf Berry, the Attack of Wobbuffet sharply rose!"); + } THEN { + EXPECT_EQ(player->statStages[STAT_ATK], DEFAULT_STAT_STAGE + 2); + } +} + +SINGLE_BATTLE_TEST("Starf Berry randomly raises the holder's Attack, Defense, Sp. Atk, Sp. Def, or Speed by two stages when the holder's HP drop to 1/4 or below") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_STARF_BERRY); HP(101); MaxHP(400); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_SCRATCH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponent); + HP_BAR(player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); + } THEN { + int boostedStats = 0; + EXPECT_EQ(player->item, ITEM_NONE); + EXPECT_LE(player->hp * 4, player->maxHP); + for (int stat = STAT_ATK; stat < NUM_STATS; stat++) + { + if (player->statStages[stat] == DEFAULT_STAT_STAGE + 2) + boostedStats++; + else + EXPECT_EQ(player->statStages[stat], DEFAULT_STAT_STAGE); + } + EXPECT_EQ(boostedStats, 1); + } +} + +SINGLE_BATTLE_TEST("Starf Berry randomly raises the holder's Attack, Defense, Sp. Atk, Sp. Def, or Speed by two stages when the holder's HP drop to 1/2 or below if it has Gluttony") +{ + GIVEN { + PLAYER(SPECIES_MUNCHLAX) { Item(ITEM_STARF_BERRY); HP(201); MaxHP(400); Ability(ABILITY_GLUTTONY); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_SCRATCH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponent); + HP_BAR(player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); + } THEN { + int boostedStats = 0; + EXPECT_EQ(player->item, ITEM_NONE); + EXPECT_LE(player->hp * 2, player->maxHP); + for (int stat = STAT_ATK; stat < NUM_STATS; stat++) + { + if (player->statStages[stat] == DEFAULT_STAT_STAGE + 2) + boostedStats++; + else + EXPECT_EQ(player->statStages[stat], DEFAULT_STAT_STAGE); + } + EXPECT_EQ(boostedStats, 1); + } +} + +SINGLE_BATTLE_TEST("Starf Berry randomly raises the holder's Attack, Defense, Sp. Atk, Sp. Def, or Speed by four stages when the holder's HP drop to 1/4 or below if it has Ripen") +{ + GIVEN { + PLAYER(SPECIES_FLAPPLE) { Item(ITEM_STARF_BERRY); HP(101); MaxHP(400); Ability(ABILITY_RIPEN); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_SCRATCH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponent); + HP_BAR(player); + ABILITY_POPUP(player, ABILITY_RIPEN); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); + } THEN { + int boostedStats = 0; + EXPECT_EQ(player->item, ITEM_NONE); + EXPECT_LE(player->hp * 4, player->maxHP); + for (int stat = STAT_ATK; stat < NUM_STATS; stat++) + { + if (player->statStages[stat] == DEFAULT_STAT_STAGE + 4) + boostedStats++; + else + EXPECT_EQ(player->statStages[stat], DEFAULT_STAT_STAGE); + } + EXPECT_EQ(boostedStats, 1); + } +}