diff --git a/include/battle.h b/include/battle.h index 5a69fc8d5018..b537be776eb2 100644 --- a/include/battle.h +++ b/include/battle.h @@ -278,7 +278,8 @@ struct FieldTimer struct WishFutureKnock { u8 futureSightCounter[MAX_BATTLERS_COUNT]; - u8 futureSightAttacker[MAX_BATTLERS_COUNT]; + u8 futureSightBattlerIndex[MAX_BATTLERS_COUNT]; + u8 futureSightPartyIndex[MAX_BATTLERS_COUNT]; u16 futureSightMove[MAX_BATTLERS_COUNT]; u8 wishCounter[MAX_BATTLERS_COUNT]; u8 wishPartyId[MAX_BATTLERS_COUNT]; diff --git a/src/battle_main.c b/src/battle_main.c index 732bd97035d5..ab97617c55ea 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -4207,11 +4207,11 @@ void BattleTurnPassed(void) if (DoBattlerEndTurnEffects()) return; } + if (HandleWishPerishSongOnTurnEnd()) + return; if (HandleFaintedMonActions()) return; gBattleStruct->faintedActionsState = 0; - if (HandleWishPerishSongOnTurnEnd()) - return; TurnValuesCleanUp(FALSE); gHitMarker &= ~HITMARKER_NO_ATTACKSTRING; diff --git a/src/battle_message.c b/src/battle_message.c index 3ad1dc409b86..70c6e8d48dfb 100644 --- a/src/battle_message.c +++ b/src/battle_message.c @@ -1571,7 +1571,7 @@ const u16 gMentalHerbCureStringIds[] = [B_MSG_MENTALHERBCURE_DISABLE] = STRINGID_PKMNMOVEDISABLEDNOMORE, }; -const u16 gStartingStatusStringIds[B_MSG_STARTING_STATUS_COUNT] = +const u16 gStartingStatusStringIds[B_MSG_STARTING_STATUS_COUNT] = { [B_MSG_TERRAIN_SET_MISTY] = STRINGID_TERRAINBECOMESMISTY, [B_MSG_TERRAIN_SET_ELECTRIC] = STRINGID_TERRAINBECOMESELECTRIC, diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index bbee19244eb0..5713b7c6fa26 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -13705,7 +13705,8 @@ static void Cmd_trysetfutureattack(void) { gSideStatuses[GetBattlerSide(gBattlerTarget)] |= SIDE_STATUS_FUTUREATTACK; gWishFutureKnock.futureSightMove[gBattlerTarget] = gCurrentMove; - gWishFutureKnock.futureSightAttacker[gBattlerTarget] = gBattlerAttacker; + gWishFutureKnock.futureSightBattlerIndex[gBattlerTarget] = gBattlerAttacker; + gWishFutureKnock.futureSightPartyIndex[gBattlerTarget] = gBattlerPartyIndexes[gBattlerAttacker]; gWishFutureKnock.futureSightCounter[gBattlerTarget] = 3; if (gCurrentMove == MOVE_DOOM_DESIRE) diff --git a/src/battle_util.c b/src/battle_util.c index ec849753fffb..cc18e61edbea 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -2964,17 +2964,15 @@ bool32 HandleWishPerishSongOnTurnEnd(void) while (gBattleStruct->wishPerishSongBattlerId < gBattlersCount) { battler = gBattleStruct->wishPerishSongBattlerId; - if (gAbsentBattlerFlags & gBitTable[battler]) - { - gBattleStruct->wishPerishSongBattlerId++; - continue; - } gBattleStruct->wishPerishSongBattlerId++; + if (gWishFutureKnock.futureSightCounter[battler] != 0 && --gWishFutureKnock.futureSightCounter[battler] == 0 - && gBattleMons[battler].hp != 0) + && !(gAbsentBattlerFlags & gBitTable[battler])) { + struct Pokemon *party; + if (gWishFutureKnock.futureSightMove[battler] == MOVE_FUTURE_SIGHT) gBattleCommunication[MULTISTRING_CHOOSER] = B_MSG_FUTURE_SIGHT; else @@ -2983,10 +2981,14 @@ bool32 HandleWishPerishSongOnTurnEnd(void) PREPARE_MOVE_BUFFER(gBattleTextBuff1, gWishFutureKnock.futureSightMove[battler]); gBattlerTarget = battler; - gBattlerAttacker = gWishFutureKnock.futureSightAttacker[battler]; + gBattlerAttacker = gWishFutureKnock.futureSightBattlerIndex[battler]; gSpecialStatuses[gBattlerTarget].shellBellDmg = IGNORE_SHELL_BELL; gCurrentMove = gWishFutureKnock.futureSightMove[battler]; - SetTypeBeforeUsingMove(gCurrentMove, battler); + + party = GetSideParty(GetBattlerSide(gBattlerAttacker)); + if (&party[gWishFutureKnock.futureSightPartyIndex[gBattlerTarget]] == &party[gBattlerPartyIndexes[gBattlerAttacker]]) + SetTypeBeforeUsingMove(gCurrentMove, gBattlerAttacker); + BattleScriptExecute(BattleScript_MonTookFutureAttack); if (gWishFutureKnock.futureSightCounter[battler] == 0 @@ -2994,6 +2996,7 @@ bool32 HandleWishPerishSongOnTurnEnd(void) { gSideStatuses[GetBattlerSide(gBattlerTarget)] &= ~SIDE_STATUS_FUTUREATTACK; } + return TRUE; } } @@ -9833,6 +9836,66 @@ static inline s32 DoMoveDamageCalc(u32 move, u32 battlerAtk, u32 battlerDef, u32 updateFlags, typeEffectivenessModifier, weather, holdEffectAtk, holdEffectDef, abilityAtk, abilityDef); } +static inline s32 DoFutureSightAttackDamageCalcVars(u32 move, u32 battlerAtk, u32 battlerDef, u32 moveType, + bool32 isCrit, bool32 randomFactor, bool32 updateFlags, uq4_12_t typeEffectivenessModifier, u32 weather, + u32 holdEffectDef, u32 abilityDef) +{ + s32 dmg; + u32 userFinalAttack; + u32 targetFinalDefense; + + struct Pokemon *party = GetSideParty(GetBattlerSide(battlerAtk)); + struct Pokemon *partyMon = &party[gWishFutureKnock.futureSightPartyIndex[battlerDef]]; + u32 partyMonLevel = GetMonData(partyMon, MON_DATA_LEVEL, NULL); + u32 partyMonSpecies = GetMonData(partyMon, MON_DATA_SPECIES, NULL); + gBattleMovePower = gMovesInfo[move].power; + + if (IS_MOVE_PHYSICAL(move)) + userFinalAttack = GetMonData(partyMon, MON_DATA_ATK, NULL); + else + userFinalAttack = GetMonData(partyMon, MON_DATA_SPATK, NULL); + + targetFinalDefense = CalcDefenseStat(move, battlerAtk, battlerDef, moveType, isCrit, updateFlags, ABILITY_NONE, abilityDef, holdEffectDef, weather); + dmg = CalculateBaseDamage(gBattleMovePower, userFinalAttack, partyMonLevel, targetFinalDefense); + + DAMAGE_APPLY_MODIFIER(GetCriticalModifier(isCrit)); + + if (randomFactor) + { + dmg *= 100 - RandomUniform(RNG_DAMAGE_MODIFIER, 0, 15); + dmg /= 100; + } + + // Same type attack bonus + if (gSpeciesInfo[partyMonSpecies].types[0] == moveType || gSpeciesInfo[partyMonSpecies].types[1] == moveType) + DAMAGE_APPLY_MODIFIER(UQ_4_12(1.5)); + else + DAMAGE_APPLY_MODIFIER(UQ_4_12(1.0)); + DAMAGE_APPLY_MODIFIER(typeEffectivenessModifier); + + if (dmg == 0) + dmg = 1; + + gSpecialStatuses[battlerAtk].preventLifeOrbDamage = TRUE; + + return dmg; +} + +static inline s32 DoFutureSightAttackDamageCalc(u32 move, u32 battlerAtk, u32 battlerDef, u32 moveType, + bool32 isCrit, bool32 randomFactor, bool32 updateFlags, uq4_12_t typeEffectivenessModifier, u32 weather) +{ + u32 holdEffectDef, abilityDef; + + if (typeEffectivenessModifier == UQ_4_12(0.0)) + return 0; + + holdEffectDef = GetBattlerHoldEffect(battlerDef, TRUE); + abilityDef = GetBattlerAbility(battlerDef); + + return DoFutureSightAttackDamageCalcVars(move, battlerAtk, battlerDef, moveType, isCrit, randomFactor, + updateFlags, typeEffectivenessModifier, weather, holdEffectDef, abilityDef); +} + #undef DAMAGE_APPLY_MODIFIER static u32 GetWeather(void) @@ -9845,9 +9908,21 @@ static u32 GetWeather(void) s32 CalculateMoveDamage(u32 move, u32 battlerAtk, u32 battlerDef, u32 moveType, s32 fixedBasePower, bool32 isCrit, bool32 randomFactor, bool32 updateFlags) { - return DoMoveDamageCalc(move, battlerAtk, battlerDef, moveType, fixedBasePower, isCrit, randomFactor, + struct Pokemon *party = GetSideParty(GetBattlerSide(gBattlerAttacker)); + + if (gMovesInfo[move].effect == EFFECT_FUTURE_SIGHT + && (&party[gWishFutureKnock.futureSightPartyIndex[battlerDef]] != &party[gBattlerPartyIndexes[battlerAtk]]) ) + { + return DoFutureSightAttackDamageCalc(move, battlerAtk, battlerDef, moveType, isCrit, randomFactor, + updateFlags, CalcTypeEffectivenessMultiplier(move, moveType, battlerAtk, battlerDef, GetBattlerAbility(battlerDef), updateFlags), + GetWeather()); + } + else + { + return DoMoveDamageCalc(move, battlerAtk, battlerDef, moveType, fixedBasePower, isCrit, randomFactor, updateFlags, CalcTypeEffectivenessMultiplier(move, moveType, battlerAtk, battlerDef, GetBattlerAbility(battlerDef), updateFlags), GetWeather()); + } } // for AI so that typeEffectivenessModifier, weather, abilities and holdEffects are calculated only once diff --git a/test/battle/move_effect/future_sight.c b/test/battle/move_effect/future_sight.c new file mode 100644 index 000000000000..6a163a736d8f --- /dev/null +++ b/test/battle/move_effect/future_sight.c @@ -0,0 +1,134 @@ +#include "global.h" +#include "test/battle.h" + +ASSUMPTIONS +{ + ASSUME(gMovesInfo[MOVE_SEED_FLARE].power == gMovesInfo[MOVE_FUTURE_SIGHT].power); + ASSUME(gMovesInfo[MOVE_SEED_FLARE].category == gMovesInfo[MOVE_FUTURE_SIGHT].category); +} + +SINGLE_BATTLE_TEST("Future Sight uses Sp. Atk stat of the original user without modifiers") +{ + u32 item; + s16 seedFlareDmg; + s16 futureSightDmg; + + PARAMETRIZE { item = ITEM_TWISTED_SPOON; } + PARAMETRIZE { item = ITEM_PSYCHIC_GEM; } + + GIVEN { + PLAYER(SPECIES_PIKACHU) { Item(item); } + PLAYER(SPECIES_RAICHU) { Item(item); } + OPPONENT(SPECIES_REGICE); + } WHEN { + TURN { MOVE(player, MOVE_SEED_FLARE, WITH_RNG(RNG_SECONDARY_EFFECT, FALSE)); } + TURN { MOVE(player, MOVE_FUTURE_SIGHT); } + TURN { SWITCH(player, 1); } + TURN { } + TURN { } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SEED_FLARE, player); + HP_BAR(opponent, captureDamage: &seedFlareDmg); + ANIMATION(ANIM_TYPE_MOVE, MOVE_FUTURE_SIGHT, player); + MESSAGE("Foe Regice took the Future Sight attack!"); + HP_BAR(opponent, captureDamage: &futureSightDmg); + } THEN { + EXPECT_EQ(seedFlareDmg, futureSightDmg); + } +} + +SINGLE_BATTLE_TEST("Future Sight is not boosted by Life Orb is original user if not on the field") +{ + s16 seedFlareDmg; + s16 futureSightDmg; + + GIVEN { + PLAYER(SPECIES_PIKACHU); + PLAYER(SPECIES_RAICHU) { Item(ITEM_LIFE_ORB); } + OPPONENT(SPECIES_REGICE); + } WHEN { + TURN { MOVE(player, MOVE_SEED_FLARE, WITH_RNG(RNG_SECONDARY_EFFECT, FALSE)); } + TURN { MOVE(player, MOVE_FUTURE_SIGHT); } + TURN { SWITCH(player, 1); } + TURN { } + TURN { } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SEED_FLARE, player); + HP_BAR(opponent, captureDamage: &seedFlareDmg); + ANIMATION(ANIM_TYPE_MOVE, MOVE_FUTURE_SIGHT, player); + MESSAGE("Foe Regice took the Future Sight attack!"); + HP_BAR(opponent, captureDamage: &futureSightDmg); + NOT MESSAGE("Raichu was hurt by its Life Orb!"); + } THEN { + EXPECT_EQ(seedFlareDmg, futureSightDmg); + } +} + +SINGLE_BATTLE_TEST("Future Sight receives STAB from party mon") +{ + s16 seedFlareDmg; + s16 futureSightDmg; + + GIVEN { + PLAYER(SPECIES_RALTS); + PLAYER(SPECIES_RAICHU); + OPPONENT(SPECIES_REGICE); + } WHEN { + TURN { MOVE(player, MOVE_SEED_FLARE, WITH_RNG(RNG_SECONDARY_EFFECT, FALSE)); } + TURN { MOVE(player, MOVE_FUTURE_SIGHT); } + TURN { SWITCH(player, 1); } + TURN { } + TURN { } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SEED_FLARE, player); + HP_BAR(opponent, captureDamage: &seedFlareDmg); + ANIMATION(ANIM_TYPE_MOVE, MOVE_FUTURE_SIGHT, player); + HP_BAR(opponent, captureDamage: &futureSightDmg); + } THEN { + EXPECT_MUL_EQ(seedFlareDmg, Q_4_12(1.5), futureSightDmg); + } +} + +SINGLE_BATTLE_TEST("Future Sight is affected by type effectiveness") +{ + GIVEN { + PLAYER(SPECIES_PIKACHU); + PLAYER(SPECIES_RAICHU); + OPPONENT(SPECIES_HOUNDOOM); + } WHEN { + TURN { MOVE(player, MOVE_SEED_FLARE, WITH_RNG(RNG_SECONDARY_EFFECT, FALSE)); } + TURN { MOVE(player, MOVE_FUTURE_SIGHT); } + TURN { SWITCH(player, 1); } + TURN { } + TURN { } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SEED_FLARE, player); + HP_BAR(opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_FUTURE_SIGHT, player); + MESSAGE("Foe Houndoom took the Future Sight attack!"); + MESSAGE("It doesn't affect Foe Houndoom…"); + NOT HP_BAR(opponent); + } +} + +SINGLE_BATTLE_TEST("Future Sight will miss timing if target faints before it is about to get hit") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(player, MOVE_FUTURE_SIGHT); } + TURN { MOVE(player, MOVE_CELEBRATE); } + TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_MEMENTO); SEND_OUT(opponent, 1); } + TURN { } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_FUTURE_SIGHT, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_MEMENTO, opponent); + MESSAGE("Foe Wobbuffet fainted!"); + MESSAGE("2 sent out Wynaut!"); + NOT MESSAGE("Foe Wynaut took the Future Sight attack!"); + } +}