From 9a09f1e3b9921a99a478d9dcc3ff83f11d444816 Mon Sep 17 00:00:00 2001 From: DizzyEggg Date: Thu, 6 Jul 2023 13:41:16 +0200 Subject: [PATCH 1/4] Fix Roullout Fury Cutter AI power calc --- include/battle_util.h | 4 +++- src/battle_ai_util.c | 20 +++++++++++++++++--- src/battle_util.c | 30 ++++++++++++++++++++++-------- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/include/battle_util.h b/include/battle_util.h index b5ba5249e..2a966e0ac 100644 --- a/include/battle_util.h +++ b/include/battle_util.h @@ -165,8 +165,10 @@ bool32 IsBattlerGrounded(u8 battlerId); bool32 IsBattlerAlive(u8 battlerId); u8 GetBattleMonMoveSlot(struct BattlePokemon *battleMon, u16 move); u32 GetBattlerWeight(u8 battlerId); +u32 CalcRolloutBasePower(u32 battlerAtk, u32 basePower, u32 rolloutTimer); +u32 CalcFuryCutterBasePower(u32 basePower, u32 furyCutterCounter); s32 CalculateMoveDamage(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, s32 fixedBasePower, bool32 isCrit, bool32 randomFactor, bool32 updateFlags); -s32 CalculateMoveDamageAndEffectiveness(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, u16 *typeEffectivenessModifier); +s32 CalculateMoveDamageAndEffectiveness(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, s32 fixedBasePower, u16 *typeEffectivenessModifier); u16 CalcTypeEffectivenessMultiplier(u16 move, u8 moveType, u8 battlerAtk, u8 battlerDef, bool32 recordAbilities); u16 CalcPartyMonTypeEffectivenessMultiplier(u16 move, u16 speciesDef, u16 abilityDef); u16 GetTypeModifier(u8 atkType, u8 defType); diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 7730604c8..9fdf08123 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -785,7 +785,7 @@ static bool32 AI_GetIfCrit(u32 move, u8 battlerAtk, u8 battlerDef) s32 AI_CalcDamage(u16 move, u8 battlerAtk, u8 battlerDef, u8 *typeEffectiveness, bool32 considerZPower) { - s32 dmg, moveType, critDmg, normalDmg; + s32 dmg, moveType, critDmg, normalDmg, fixedBasePower, n; s8 critChance; u16 effectivenessMultiplier; @@ -814,8 +814,22 @@ s32 AI_CalcDamage(u16 move, u8 battlerAtk, u8 battlerDef, u8 *typeEffectiveness, { ProteanTryChangeType(battlerAtk, AI_DATA->abilities[battlerAtk], move, moveType); critChance = GetInverseCritChance(battlerAtk, battlerDef, move); - normalDmg = CalculateMoveDamageAndEffectiveness(move, battlerAtk, battlerDef, moveType, &effectivenessMultiplier); - critDmg = CalculateMoveDamage(move, battlerAtk, battlerDef, moveType, 0, TRUE, FALSE, FALSE); + // Certain moves like Rollout calculate damage based on values which change during the move execution, but before calling dmg calc. + switch (gBattleMoves[move].effect) + { + case EFFECT_ROLLOUT: + n = gDisableStructs[battlerAtk].rolloutTimer - 1; + fixedBasePower = CalcRolloutBasePower(battlerAtk, gBattleMoves[move].power, n < 0 ? 5 : n); + break; + case EFFECT_FURY_CUTTER: + fixedBasePower = CalcFuryCutterBasePower(gBattleMoves[move].power, min(gDisableStructs[battlerAtk].furyCutterCounter + 1, 5)); + break; + default: + fixedBasePower = 0; + break; + } + normalDmg = CalculateMoveDamageAndEffectiveness(move, battlerAtk, battlerDef, moveType, fixedBasePower, &effectivenessMultiplier); + critDmg = CalculateMoveDamage(move, battlerAtk, battlerDef, moveType, fixedBasePower, TRUE, FALSE, FALSE); if (critChance == -1) dmg = normalDmg; diff --git a/src/battle_util.c b/src/battle_util.c index e608b0e51..3d56cc2b7 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -8447,6 +8447,24 @@ const struct TypePower gNaturalGiftTable[] = [ITEM_TO_BERRY(ITEM_MARANGA_BERRY)] = {TYPE_DARK, 100}, }; +u32 CalcRolloutBasePower(u32 battlerAtk, u32 basePower, u32 rolloutTimer) +{ + u32 i; + for (i = 1; i < (5 - rolloutTimer); i++) + basePower *= 2; + if (gBattleMons[battlerAtk].status2 & STATUS2_DEFENSE_CURL) + basePower *= 2; + return basePower; +} + +u32 CalcFuryCutterBasePower(u32 basePower, u32 furyCutterCounter) +{ + u32 i; + for (i = 1; i < furyCutterCounter; i++) + basePower *= 2; + return basePower; +} + static u16 CalcMoveBasePower(u16 move, u8 battlerAtk, u8 battlerDef) { u32 i; @@ -8483,14 +8501,10 @@ static u16 CalcMoveBasePower(u16 move, u8 battlerAtk, u8 battlerDef) basePower = 10 * (MAX_FRIENDSHIP - gBattleMons[battlerAtk].friendship) / 25; break; case EFFECT_FURY_CUTTER: - for (i = 1; i < gDisableStructs[battlerAtk].furyCutterCounter; i++) - basePower *= 2; + basePower = CalcFuryCutterBasePower(basePower, gDisableStructs[battlerAtk].furyCutterCounter); break; case EFFECT_ROLLOUT: - for (i = 1; i < (5 - gDisableStructs[battlerAtk].rolloutTimer); i++) - basePower *= 2; - if (gBattleMons[battlerAtk].status2 & STATUS2_DEFENSE_CURL) - basePower *= 2; + basePower = CalcRolloutBasePower(battlerAtk, basePower, gDisableStructs[battlerAtk].rolloutTimer); break; case EFFECT_MAGNITUDE: basePower = gBattleStruct->magnitudeBasePower; @@ -9694,10 +9708,10 @@ s32 CalculateMoveDamage(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, s32 } // for AI - get move damage and effectiveness with one function call -s32 CalculateMoveDamageAndEffectiveness(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, u16 *typeEffectivenessModifier) +s32 CalculateMoveDamageAndEffectiveness(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, s32 fixedBasePower, u16 *typeEffectivenessModifier) { *typeEffectivenessModifier = CalcTypeEffectivenessMultiplier(move, moveType, battlerAtk, battlerDef, FALSE); - return DoMoveDamageCalc(move, battlerAtk, battlerDef, moveType, 0, FALSE, FALSE, FALSE, *typeEffectivenessModifier); + return DoMoveDamageCalc(move, battlerAtk, battlerDef, moveType, fixedBasePower, FALSE, FALSE, FALSE, *typeEffectivenessModifier); } static void MulByTypeEffectiveness(u16 *modifier, u16 move, u8 moveType, u8 battlerDef, u8 defType, u8 battlerAtk, bool32 recordAbilities) From 2a249654e7ea32bc6ab1285d3e44eb8d534e25b7 Mon Sep 17 00:00:00 2001 From: DizzyEggg Date: Sat, 15 Jul 2023 14:00:55 +0200 Subject: [PATCH 2/4] fix mirror move/metronome powder/multi hit moves & tests --- include/battle.h | 1 + include/battle_util.h | 1 + src/battle_message.c | 2 +- src/battle_script_commands.c | 33 +++++++------- src/battle_util.c | 12 +++-- test/ability_magic_bounce.c | 1 - test/move_effect_metronome.c | 69 +++++++++++++++++++++++++++++ test/move_effect_mirror_move.c | 81 ++++++++++++++++++++++++++++++++++ 8 files changed, 180 insertions(+), 20 deletions(-) create mode 100644 test/move_effect_metronome.c create mode 100644 test/move_effect_mirror_move.c diff --git a/include/battle.h b/include/battle.h index 9cf4f100a..0219ebd72 100644 --- a/include/battle.h +++ b/include/battle.h @@ -594,6 +594,7 @@ struct BattleStruct u8 wishPerishSongBattlerId; bool8 overworldWeatherDone; bool8 terrainDone; + u8 isAtkCancelerForCalledMove; // Certain cases in atk canceler should only be checked once, when the original move is called, however others need to be checked the twice. u8 atkCancellerTracker; struct BattleTvMovePoints tvMovePoints; struct BattleTv tv; diff --git a/include/battle_util.h b/include/battle_util.h index b5ba5249e..684aa2b51 100644 --- a/include/battle_util.h +++ b/include/battle_util.h @@ -136,6 +136,7 @@ u8 DoBattlerEndTurnEffects(void); bool8 HandleWishPerishSongOnTurnEnd(void); bool8 HandleFaintedMonActions(void); void TryClearRageAndFuryCutter(void); +void SetAtkCancellerForCalledMove(void); u8 AtkCanceller_UnableToUseMove(void); u8 AtkCanceller_UnableToUseMove2(void); bool8 HasNoMonsToSwitch(u8 battlerId, u8 r1, u8 r2); diff --git a/src/battle_message.c b/src/battle_message.c index 3edb02f60..d2ca9dfd7 100644 --- a/src/battle_message.c +++ b/src/battle_message.c @@ -350,7 +350,7 @@ static const u8 sText_DontLeaveBirch[] = _("PROF. BIRCH: Don't leave me like thi static const u8 sText_ButNothingHappened[] = _("But nothing happened!"); static const u8 sText_ButItFailed[] = _("But it failed!"); static const u8 sText_ItHurtConfusion[] = _("It hurt itself in its\nconfusion!"); -static const u8 sText_MirrorMoveFailed[] = _("The MIRROR MOVE failed!"); +static const u8 sText_MirrorMoveFailed[] = _("The Mirror Move failed!"); static const u8 sText_StartedToRain[] = _("It started to rain!"); static const u8 sText_DownpourStarted[] = _("A downpour started!"); // corresponds to DownpourText in pokegold and pokecrystal and is used by Rain Dance in GSC static const u8 sText_RainContinues[] = _("Rain continues to fall."); diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index d4975fd7b..b77f92964 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -1614,7 +1614,8 @@ static void Cmd_attackcanceler(void) PressurePPLose(gBattlerAttacker, gBattlerTarget, MOVE_MAGIC_COAT); gProtectStructs[gBattlerTarget].usesBouncedMove = TRUE; gBattleCommunication[MULTISTRING_CHOOSER] = 0; - gBattleStruct->atkCancellerTracker = CANCELLER_POWDER_MOVE; // Edge case for bouncing a powder move against a grass type pokemon. + // Edge case for bouncing a powder move against a grass type pokemon. + SetAtkCancellerForCalledMove(); if (BlocksPrankster(gCurrentMove, gBattlerTarget, gBattlerAttacker, TRUE)) { // Opponent used a prankster'd magic coat -> reflected status move should fail against a dark-type attacker @@ -1634,7 +1635,8 @@ static void Cmd_attackcanceler(void) { gProtectStructs[gBattlerTarget].usesBouncedMove = TRUE; gBattleCommunication[MULTISTRING_CHOOSER] = 1; - gBattleStruct->atkCancellerTracker = CANCELLER_POWDER_MOVE; // Edge case for bouncing a powder move against a grass type pokemon. + // Edge case for bouncing a powder move against a grass type pokemon. + SetAtkCancellerForCalledMove(); BattleScriptPushCursor(); gBattlescriptCurrInstr = BattleScript_MagicCoatBounce; gBattlerAbility = gBattlerTarget; @@ -6342,6 +6344,7 @@ static void Cmd_moveend(void) gBattleStruct->zmove.toBeUsed[gBattlerAttacker] = MOVE_NONE; gBattleStruct->zmove.effect = EFFECT_HIT; gBattleStruct->hitSwitchTargetFailed = FALSE; + gBattleStruct->isAtkCancelerForCalledMove = FALSE; gBattleScripting.moveendState++; break; case MOVEEND_COUNT: @@ -11408,12 +11411,20 @@ static void Cmd_tryhealhalfhealth(void) gBattlescriptCurrInstr = cmd->nextInstr; } +static void SetMoveForMirrorMove(u32 move) +{ + gHitMarker &= ~HITMARKER_ATTACKSTRING_PRINTED; + gCurrentMove = move; + SetAtkCancellerForCalledMove(); + gBattlerTarget = GetMoveTarget(gCurrentMove, NO_TARGET_OVERRIDE); + gBattlescriptCurrInstr = gBattleScriptsForMoveEffects[gBattleMoves[gCurrentMove].effect]; +} + static void Cmd_trymirrormove(void) { CMD_ARGS(); - s32 validMovesCount; - s32 i; + s32 i, validMovesCount; u16 move; u16 validMoves[MAX_BATTLERS_COUNT] = {0}; @@ -11422,7 +11433,6 @@ static void Cmd_trymirrormove(void) if (i != gBattlerAttacker) { move = gBattleStruct->lastTakenMoveFrom[gBattlerAttacker][i]; - if (move != MOVE_NONE && move != MOVE_UNAVAILABLE) { validMoves[validMovesCount] = move; @@ -11432,21 +11442,13 @@ static void Cmd_trymirrormove(void) } move = gBattleStruct->lastTakenMove[gBattlerAttacker]; - if (move != MOVE_NONE && move != MOVE_UNAVAILABLE) { - gHitMarker &= ~HITMARKER_ATTACKSTRING_PRINTED; - gCurrentMove = move; - gBattlerTarget = GetMoveTarget(gCurrentMove, NO_TARGET_OVERRIDE); - gBattlescriptCurrInstr = gBattleScriptsForMoveEffects[gBattleMoves[gCurrentMove].effect]; + SetMoveForMirrorMove(move); } else if (validMovesCount != 0) { - gHitMarker &= ~HITMARKER_ATTACKSTRING_PRINTED; - i = Random() % validMovesCount; - gCurrentMove = validMoves[i]; - gBattlerTarget = GetMoveTarget(gCurrentMove, NO_TARGET_OVERRIDE); - gBattlescriptCurrInstr = gBattleScriptsForMoveEffects[gBattleMoves[gCurrentMove].effect]; + SetMoveForMirrorMove(validMoves[Random() % validMovesCount]); } else // no valid moves found { @@ -13003,6 +13005,7 @@ static void Cmd_metronome(void) if (!(sForbiddenMoves[gCurrentMove] & FORBIDDEN_METRONOME)) { gHitMarker &= ~HITMARKER_ATTACKSTRING_PRINTED; + SetAtkCancellerForCalledMove(); gBattlescriptCurrInstr = gBattleScriptsForMoveEffects[gBattleMoves[gCurrentMove].effect]; gBattlerTarget = GetMoveTarget(gCurrentMove, NO_TARGET_OVERRIDE); return; diff --git a/src/battle_util.c b/src/battle_util.c index e608b0e51..f13e89bff 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -3383,6 +3383,12 @@ void TryClearRageAndFuryCutter(void) } } +void SetAtkCancellerForCalledMove(void) +{ + gBattleStruct->atkCancellerTracker = CANCELLER_HEAL_BLOCKED; + gBattleStruct->isAtkCancelerForCalledMove = TRUE; +} + u8 AtkCanceller_UnableToUseMove(void) { u8 effect = 0; @@ -3565,7 +3571,7 @@ u8 AtkCanceller_UnableToUseMove(void) gBattleStruct->atkCancellerTracker++; break; case CANCELLER_CONFUSED: // confusion - if (gBattleMons[gBattlerAttacker].status2 & STATUS2_CONFUSION) + if (!gBattleStruct->isAtkCancelerForCalledMove && gBattleMons[gBattlerAttacker].status2 & STATUS2_CONFUSION) { if (!(gStatuses4[gBattlerAttacker] & STATUS4_INFINITE_CONFUSION)) gBattleMons[gBattlerAttacker].status2 -= STATUS2_CONFUSION_TURN(1); @@ -3601,7 +3607,7 @@ u8 AtkCanceller_UnableToUseMove(void) gBattleStruct->atkCancellerTracker++; break; case CANCELLER_PARALYSED: // paralysis - if ((gBattleMons[gBattlerAttacker].status1 & STATUS1_PARALYSIS) && !RandomPercentage(RNG_PARALYSIS, 75)) + if (!gBattleStruct->isAtkCancelerForCalledMove && (gBattleMons[gBattlerAttacker].status1 & STATUS1_PARALYSIS) && !RandomPercentage(RNG_PARALYSIS, 75)) { gProtectStructs[gBattlerAttacker].prlzImmobility = TRUE; // This is removed in FRLG and Emerald for some reason @@ -3613,7 +3619,7 @@ u8 AtkCanceller_UnableToUseMove(void) gBattleStruct->atkCancellerTracker++; break; case CANCELLER_IN_LOVE: // infatuation - if (gBattleMons[gBattlerAttacker].status2 & STATUS2_INFATUATION) + if (!gBattleStruct->isAtkCancelerForCalledMove && gBattleMons[gBattlerAttacker].status2 & STATUS2_INFATUATION) { gBattleScripting.battler = CountTrailingZeroBits((gBattleMons[gBattlerAttacker].status2 & STATUS2_INFATUATION) >> 0x10); if (!RandomPercentage(RNG_INFATUATION, 50)) diff --git a/test/ability_magic_bounce.c b/test/ability_magic_bounce.c index eaf925975..ab51369ef 100644 --- a/test/ability_magic_bounce.c +++ b/test/ability_magic_bounce.c @@ -56,7 +56,6 @@ SINGLE_BATTLE_TEST("Magic Bounce cannot bounce back powder moves against Grass T } } - DOUBLE_BATTLE_TEST("Magic Bounce bounces back moves hitting both foes at two foes") { GIVEN { diff --git a/test/move_effect_metronome.c b/test/move_effect_metronome.c new file mode 100644 index 000000000..11a906f94 --- /dev/null +++ b/test/move_effect_metronome.c @@ -0,0 +1,69 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_METRONOME].effect == EFFECT_METRONOME); +} + +// To do: Turn the seeds to work with WITH_RNG for Metronome. +#define RNG_METRONOME_SCRATCH 0x118 +#define RNG_METRONOME_PSN_POWDER 0x119 +#define RNG_METRONOME_ROCK_BLAST 0x1F5 + +SINGLE_BATTLE_TEST("Metronome picks a random move") +{ + GIVEN { + RNGSeed(RNG_METRONOME_SCRATCH); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_METRONOME); } + } SCENE { + MESSAGE("Wobbuffet used Metronome!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_METRONOME, player); + MESSAGE("Wobbuffet used Scratch!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, player); + HP_BAR(opponent); + } +} + +SINGLE_BATTLE_TEST("Metronome's called powder move fails against Grass Types") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_POISON_POWDER].flags & FLAG_POWDER); + ASSUME(gSpeciesInfo[SPECIES_TANGELA].types[0] == TYPE_GRASS); + ASSUME(gBattleMoves[MOVE_POISON_POWDER].effect == EFFECT_POISON); + RNGSeed(RNG_METRONOME_PSN_POWDER); + PLAYER(SPECIES_WOBBUFFET) {Speed(5);} + OPPONENT(SPECIES_TANGELA) {Speed(2);} + } WHEN { + TURN { MOVE(player, MOVE_METRONOME); } + } SCENE { + MESSAGE("Wobbuffet used Metronome!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_METRONOME, player); + MESSAGE("Wobbuffet used PoisonPowder!"); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_POISON_POWDER, player); + MESSAGE("It doesn't affect Foe Tangela…"); + NOT STATUS_ICON(opponent, poison: TRUE); + } +} + +SINGLE_BATTLE_TEST("Metronome's called multi-hit move hits multiple times") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_ROCK_BLAST].effect == EFFECT_MULTI_HIT); + RNGSeed(RNG_METRONOME_ROCK_BLAST); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_METRONOME); } + } SCENE { + MESSAGE("Wobbuffet used Metronome!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_METRONOME, player); + MESSAGE("Wobbuffet used Rock Blast!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_ROCK_BLAST, player); + HP_BAR(opponent); + MESSAGE("Hit 4 time(s)!"); + } +} diff --git a/test/move_effect_mirror_move.c b/test/move_effect_mirror_move.c new file mode 100644 index 000000000..2b8b9dbe9 --- /dev/null +++ b/test/move_effect_mirror_move.c @@ -0,0 +1,81 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_MIRROR_MOVE].effect == EFFECT_MIRROR_MOVE); +} + +SINGLE_BATTLE_TEST("Mirror Move copies the last used move by the target") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) {Speed(2);} + OPPONENT(SPECIES_WOBBUFFET) {Speed(5);} + } WHEN { + TURN { MOVE(opponent, MOVE_TACKLE); MOVE(player, MOVE_MIRROR_MOVE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); + HP_BAR(player); + MESSAGE("Wobbuffet used Mirror Move!"); + MESSAGE("Wobbuffet used Tackle!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player); + HP_BAR(opponent); + } +} + +SINGLE_BATTLE_TEST("Mirror Move fails if no move was used before") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) {Speed(5);} + OPPONENT(SPECIES_WOBBUFFET) {Speed(2);} + } WHEN { + TURN { MOVE(opponent, MOVE_TACKLE); MOVE(player, MOVE_MIRROR_MOVE); } + } SCENE { + MESSAGE("Wobbuffet used Mirror Move!"); + MESSAGE("The Mirror Move failed!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); + HP_BAR(player); + } +} + +SINGLE_BATTLE_TEST("Mirror Move's called powder move fails against Grass Types") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_STUN_SPORE].flags & FLAG_POWDER); + ASSUME(gSpeciesInfo[SPECIES_ODDISH].types[0] == TYPE_GRASS); + ASSUME(gBattleMoves[MOVE_STUN_SPORE].effect == EFFECT_PARALYZE); + PLAYER(SPECIES_ODDISH) {Speed(5);} + OPPONENT(SPECIES_WOBBUFFET) {Speed(2);} + } WHEN { + TURN { MOVE(player, MOVE_STUN_SPORE); MOVE(opponent, MOVE_MIRROR_MOVE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_STUN_SPORE, player); + STATUS_ICON(opponent, paralysis: TRUE); + MESSAGE("Foe Wobbuffet used Mirror Move!"); + MESSAGE("Foe Wobbuffet used Stun Spore!"); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_STUN_SPORE, opponent); + MESSAGE("It doesn't affect Oddish…"); + NOT STATUS_ICON(player, paralysis: TRUE); + } +} + +// It hits first 2 times, then 5 times with the default rng seed. +SINGLE_BATTLE_TEST("Mirror Move's called multi-hit move hits multiple times") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_BULLET_SEED].effect == EFFECT_MULTI_HIT); + PLAYER(SPECIES_WOBBUFFET) {Speed(5);} + OPPONENT(SPECIES_WOBBUFFET) {Speed(2);} + } WHEN { + TURN { MOVE(player, MOVE_BULLET_SEED); MOVE(opponent, MOVE_MIRROR_MOVE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_BULLET_SEED, player); + HP_BAR(opponent); + MESSAGE("Hit 2 time(s)!"); + MESSAGE("Foe Wobbuffet used Mirror Move!"); + MESSAGE("Foe Wobbuffet used Bullet Seed!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_BULLET_SEED, opponent); + HP_BAR(player); + MESSAGE("Hit 5 time(s)!"); + } +} From 381aa585870a6a5471f087b8cd2b84c7d8011133 Mon Sep 17 00:00:00 2001 From: DizzyEggg Date: Sun, 16 Jul 2023 07:38:00 +0200 Subject: [PATCH 3/4] Fix Bad Dreams ability pop-up + tests (#3131) * Fix all bad dreams issues and add tests Co-authored-by: ShaeTsuPog --- data/battle_scripts_1.s | 29 +++++---- test/ability_bad_dreams.c | 130 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 test/ability_bad_dreams.c diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 6161648ce..28a4a33cd 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -8957,16 +8957,7 @@ BattleScript_PsychicSurgeActivates:: call BattleScript_ActivateTerrainEffects end3 -BattleScript_HurtTarget_NoString: - orword gHitMarker, HITMARKER_IGNORE_SUBSTITUTE | HITMARKER_PASSIVE_DAMAGE - healthbarupdate BS_TARGET - datahpupdate BS_TARGET - tryfaintmon BS_TARGET - return - BattleScript_BadDreamsActivates:: - call BattleScript_AbilityPopUp - setbyte sFIXED_ABILITY_POPUP, TRUE setbyte gBattlerTarget, 0 BattleScript_BadDreamsLoop: jumpiftargetally BattleScript_BadDreamsIncrement @@ -8975,16 +8966,32 @@ BattleScript_BadDreamsLoop: jumpifstatus BS_TARGET, STATUS1_SLEEP, BattleScript_BadDreams_Dmg goto BattleScript_BadDreamsIncrement BattleScript_BadDreams_Dmg: + jumpifbyteequal sFIXED_ABILITY_POPUP, sZero, BattleScript_BadDreams_ShowPopUp +BattleScript_BadDreams_DmgAfterPopUp: printstring STRINGID_BADDREAMSDMG waitmessage B_WAIT_TIME_LONG dmg_1_8_targethp - call BattleScript_HurtTarget_NoString + orword gHitMarker, HITMARKER_IGNORE_SUBSTITUTE | HITMARKER_PASSIVE_DAMAGE + healthbarupdate BS_TARGET + datahpupdate BS_TARGET + jumpifhasnohp BS_TARGET, BattleScript_BadDreams_HidePopUp BattleScript_BadDreamsIncrement: addbyte gBattlerTarget, 1 jumpifbytenotequal gBattlerTarget, gBattlersCount, BattleScript_BadDreamsLoop -BattleScript_BadDreamsEnd: + jumpifbyteequal sFIXED_ABILITY_POPUP, sZero, BattleScript_BadDreamsEnd destroyabilitypopup + pause 15 +BattleScript_BadDreamsEnd: end3 +BattleScript_BadDreams_ShowPopUp: + copybyte gBattlerAbility, gBattlerAttacker + call BattleScript_AbilityPopUp + setbyte sFIXED_ABILITY_POPUP, TRUE + goto BattleScript_BadDreams_DmgAfterPopUp +BattleScript_BadDreams_HidePopUp: + destroyabilitypopup + tryfaintmon BS_TARGET + goto BattleScript_BadDreamsIncrement BattleScript_TookAttack:: attackstring diff --git a/test/ability_bad_dreams.c b/test/ability_bad_dreams.c new file mode 100644 index 000000000..9bf800534 --- /dev/null +++ b/test/ability_bad_dreams.c @@ -0,0 +1,130 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(P_GEN_4_POKEMON == TRUE); // Because only Darkrai can have this ability. +} + +// Also checks that non-sleeping enemy is not affected. +SINGLE_BATTLE_TEST("Bad Dreams causes the sleeping enemy Pokemon to lose 1/8 of hp") +{ + u32 status; + PARAMETRIZE { status = STATUS1_NONE; } + PARAMETRIZE { status = STATUS1_SLEEP; } + GIVEN { + PLAYER(SPECIES_DARKRAI); + OPPONENT(SPECIES_WOBBUFFET) {Status1(status);} + } WHEN { + TURN {;} + } SCENE { + if (status == STATUS1_SLEEP) { + ABILITY_POPUP(player, ABILITY_BAD_DREAMS); + MESSAGE("Foe Wobbuffet is tormented!"); + HP_BAR(opponent); + } + else { + NONE_OF { + ABILITY_POPUP(player, ABILITY_BAD_DREAMS); + MESSAGE("Foe Wobbuffet is tormented!"); + HP_BAR(opponent); + }; + } + } THEN { + if (status == STATUS1_SLEEP) { + EXPECT_EQ(opponent->hp, opponent->maxHP - opponent->maxHP / 8); + } + else { + EXPECT_EQ(opponent->hp, opponent->maxHP); + } + } +} + +DOUBLE_BATTLE_TEST("Bad Dreams does not activate if only the partner Pokemon is sleeping") +{ + GIVEN { + PLAYER(SPECIES_DARKRAI); + PLAYER(SPECIES_WOBBUFFET) {Status1(STATUS1_SLEEP);} + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN {;} + } SCENE { + NONE_OF { + ABILITY_POPUP(playerLeft, ABILITY_BAD_DREAMS); + MESSAGE("Wobbuffet is tormented!"); + HP_BAR(playerRight); + }; + } THEN { + EXPECT_EQ(opponentLeft->hp, opponentLeft->maxHP); + EXPECT_EQ(opponentRight->hp, opponentRight->maxHP); + EXPECT_EQ(playerRight->hp, playerRight->maxHP); + } +} + +DOUBLE_BATTLE_TEST("Bad Dreams activates for both sleeping pokemon on the player side") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) {Status1(STATUS1_SLEEP);} + PLAYER(SPECIES_WOBBUFFET) {Status1(STATUS1_SLEEP);} + OPPONENT(SPECIES_DARKRAI); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN {;} + } SCENE { + ABILITY_POPUP(opponentLeft, ABILITY_BAD_DREAMS); + MESSAGE("Wobbuffet is tormented!"); + HP_BAR(playerLeft); + MESSAGE("Wobbuffet is tormented!"); + HP_BAR(playerRight); + } THEN { + EXPECT_EQ(opponentLeft->hp, opponentLeft->maxHP); + EXPECT_EQ(opponentRight->hp, opponentRight->maxHP); + EXPECT_EQ(playerLeft->hp, playerLeft->maxHP - playerLeft->maxHP / 8); + EXPECT_EQ(playerRight->hp, playerRight->maxHP - playerRight->maxHP / 8); + } +} + +DOUBLE_BATTLE_TEST("Bad Dreams faints both sleeping Pokemon on player side") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) {Status1(STATUS1_SLEEP); HP(1);} + PLAYER(SPECIES_WOBBUFFET) {Status1(STATUS1_SLEEP); HP(1);} + PLAYER(SPECIES_WOBBUFFET) {Status1(STATUS1_SLEEP);} + PLAYER(SPECIES_WOBBUFFET) {Status1(STATUS1_SLEEP);} + OPPONENT(SPECIES_DARKRAI); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN {SEND_OUT(playerLeft, 2); SEND_OUT(playerRight, 3);} + } SCENE { + ABILITY_POPUP(opponentLeft, ABILITY_BAD_DREAMS); + MESSAGE("Wobbuffet is tormented!"); + HP_BAR(playerLeft); + MESSAGE("Wobbuffet fainted!"); + MESSAGE("Wobbuffet is tormented!"); + HP_BAR(playerRight); + MESSAGE("Wobbuffet fainted!"); + } +} + +DOUBLE_BATTLE_TEST("Bad Dreams faints both sleeping Pokemon on opponent side") +{ + GIVEN { + PLAYER(SPECIES_DARKRAI); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) {Status1(STATUS1_SLEEP); HP(1);} + OPPONENT(SPECIES_WOBBUFFET) {Status1(STATUS1_SLEEP); HP(1);} + OPPONENT(SPECIES_WOBBUFFET) {Status1(STATUS1_SLEEP);} + OPPONENT(SPECIES_WOBBUFFET) {Status1(STATUS1_SLEEP);} + } WHEN { + TURN {SEND_OUT(opponentLeft, 2); SEND_OUT(opponentRight, 3);} + } SCENE { + ABILITY_POPUP(playerLeft, ABILITY_BAD_DREAMS); + MESSAGE("Foe Wobbuffet is tormented!"); + HP_BAR(opponentLeft); + MESSAGE("Foe Wobbuffet fainted!"); + MESSAGE("Foe Wobbuffet is tormented!"); + HP_BAR(opponentRight); + MESSAGE("Foe Wobbuffet fainted!"); + } +} From 3c5c68ac582ae668e65c5626746dfe5a3e2c76b9 Mon Sep 17 00:00:00 2001 From: DizzyEggg Date: Sun, 16 Jul 2023 08:24:59 +0200 Subject: [PATCH 4/4] Fix how AI categorizes Weak moves and give priority to always hits moves when needed (#3109) * ai weak move fix and always hits move prioritarizing --- include/battle_ai_util.h | 1 + src/battle_ai_main.c | 21 +++++++++++++++++++++ src/battle_ai_util.c | 8 ++++++-- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index 67ff6968e..284022092 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -85,6 +85,7 @@ bool32 ShouldLowerEvasion(u8 battlerAtk, u8 battlerDef, u16 defAbility); bool32 IsAffectedByPowder(u8 battler, u16 ability, u16 holdEffect); bool32 MovesWithSplitUnusable(u32 attacker, u32 target, u32 split); s32 AI_CalcDamage(u16 move, u8 battlerAtk, u8 battlerDef, u8 *effectiveness, bool32 considerZPower); +u32 GetNoOfHitsToKO(u32 dmg, s32 hp); u8 GetMoveDamageResult(u16 move); u32 GetCurrDamageHpPercent(u8 battlerAtk, u8 battlerDef); u16 AI_GetTypeEffectiveness(u16 move, u8 battlerAtk, u8 battlerDef); diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 97ae1f35c..c40d8a649 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -3102,6 +3102,19 @@ static bool32 IsPinchBerryItemEffect(u16 holdEffect) return FALSE; } +static u32 GetAIMostDamagingMoveId(u8 battlerAtk, u8 battlerDef) +{ + u32 i, id = 0; + u32 mostDmg = 0; + + for (i = 0; i < MAX_MON_MOVES; i++) + { + if (AI_DATA->simulatedDmg[battlerAtk][battlerDef][i] > mostDmg) + id = i, mostDmg = AI_DATA->simulatedDmg[battlerAtk][battlerDef][i]; + } + return id; +} + // AI_FLAG_CHECK_VIABILITY - a weird mix of increasing and decreasing scores static s16 AI_CheckViability(u8 battlerAtk, u8 battlerDef, u16 move, s16 score) { @@ -3122,6 +3135,14 @@ static s16 AI_CheckViability(u8 battlerAtk, u8 battlerDef, u16 move, s16 score) // check always hits if (!IS_MOVE_STATUS(move) && gBattleMoves[move].accuracy == 0) { + // If 2 moves can KO the target in the same number of turns, but one of them always hits and there is a risk the other move could miss, prioritize the always hits move. + if (gBattleMons[battlerDef].statStages[STAT_EVASION] > 6 || gBattleMons[battlerAtk].statStages[STAT_ACC] < 6) + { + u32 mostDmgMoveId = GetAIMostDamagingMoveId(battlerAtk, battlerDef); + u32 *dmgs = AI_DATA->simulatedDmg[battlerAtk][battlerDef]; + if (GetNoOfHitsToKO(dmgs[mostDmgMoveId], gBattleMons[battlerDef].hp) == GetNoOfHitsToKO(dmgs[AI_THINKING_STRUCT->movesetIndex], gBattleMons[battlerDef].hp)) + score++; + } if (gBattleMons[battlerDef].statStages[STAT_EVASION] >= 10 || gBattleMons[battlerAtk].statStages[STAT_ACC] <= 2) score++; if (AI_RandLessThan(100) && (gBattleMons[battlerDef].statStages[STAT_EVASION] >= 8 || gBattleMons[battlerAtk].statStages[STAT_ACC] <= 4)) diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 9fdf08123..d7e32a452 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -953,6 +953,11 @@ static u32 WhichMoveBetter(u32 move1, u32 move2) return 2; } +u32 GetNoOfHitsToKO(u32 dmg, s32 hp) +{ + return hp / (dmg + 1) + 1; +} + u8 GetMoveDamageResult(u16 move) { s32 i, checkedMove, bestId, currId, hp; @@ -1018,9 +1023,8 @@ u8 GetMoveDamageResult(u16 move) currId = AI_THINKING_STRUCT->movesetIndex; if (currId == bestId) AI_THINKING_STRUCT->funcResult = MOVE_POWER_BEST; - // Compare percentage difference. else if ((moveDmgs[currId] >= hp || moveDmgs[bestId] < hp) // If current move can faint as well, or if neither can - && (moveDmgs[bestId] * 100 / hp) - (moveDmgs[currId] * 100 / hp) <= 30 + && GetNoOfHitsToKO(moveDmgs[currId], hp) - GetNoOfHitsToKO(moveDmgs[bestId], hp) <= 2 // Consider a move weak if it needs to be used at least 2 times more to faint the target, compared to the best move. && WhichMoveBetter(gBattleMons[sBattler_AI].moves[bestId], gBattleMons[sBattler_AI].moves[currId]) != 0) AI_THINKING_STRUCT->funcResult = MOVE_POWER_GOOD; else