From c3dec7d03083f06c2b109d162aeb8fec1fd02e5a Mon Sep 17 00:00:00 2001 From: surskitty Date: Sun, 7 Sep 2025 12:00:02 -0400 Subject: [PATCH] AI can use Z-status moves (#7666) --- include/config/ai.h | 4 ++ src/battle_ai_main.c | 8 ++- src/battle_ai_util.c | 91 ++++++++++++++++++++++++--- test/battle/ai/gimmick_z_move.c | 91 +++++++++++++++++++++++++++ test/battle/move_effect/last_resort.c | 40 ++++++++++++ 5 files changed, 225 insertions(+), 9 deletions(-) diff --git a/include/config/ai.h b/include/config/ai.h index b6eb7bfe21..877b3da427 100644 --- a/include/config/ai.h +++ b/include/config/ai.h @@ -110,4 +110,8 @@ #define POWER_SPLIT_ALLY_PERCENTAGE 150 #define POWER_SPLIT_ENEMY_PERCENTAGE 50 +// HP thresholds to use a status z-move. +#define Z_EFFECT_FOLLOW_ME_THRESHOLD 30 +#define Z_EFFECT_RESTORE_HP_THRESHOLD 60 + #endif // GUARD_CONFIG_AI_H diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index f77d142407..a28ef05aa9 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -1985,7 +1985,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) ADJUST_SCORE(-10); if (aiData->abilities[battlerAtk] == ABILITY_CONTRARY) ADJUST_SCORE(-10); - else if (aiData->hpPercents[battlerAtk] <= 60) + else if (aiData->hpPercents[battlerAtk] <= 60 && (GetActiveGimmick(battlerAtk) != GIMMICK_Z_MOVE || GetMoveZEffect(move) == Z_EFFECT_NONE)) ADJUST_SCORE(-10); break; case EFFECT_FUTURE_SIGHT: @@ -2760,6 +2760,9 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_HOLD_HANDS: case EFFECT_CELEBRATE: case EFFECT_HAPPY_HOUR: + case EFFECT_LAST_RESORT: + if (gBattleStruct->gimmick.usableGimmick[battlerAtk] == GIMMICK_Z_MOVE && ShouldUseZMove(battlerAtk, battlerDef, move)) + break; ADJUST_SCORE(-10); break; case EFFECT_INSTRUCT: @@ -4443,7 +4446,8 @@ static u32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move) case EFFECT_HOLD_HANDS: case EFFECT_CELEBRATE: case EFFECT_HAPPY_HOUR: - //todo - check z splash, z celebrate, z happy hour (lol) + if (gBattleStruct->gimmick.usableGimmick[battlerAtk] == GIMMICK_Z_MOVE && ShouldUseZMove(battlerAtk, battlerDef, move)) + ADJUST_SCORE(BEST_EFFECT); break; case EFFECT_TELEPORT: // Either remove or add better logic if (!(gBattleTypeFlags & BATTLE_TYPE_TRAINER) || !IsOnPlayerSide(battlerAtk)) diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 08bcf26fad..0b440ec39f 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -4847,15 +4847,97 @@ bool32 AI_MoveMakesContact(u32 ability, enum ItemHoldEffect holdEffect, u32 move bool32 ShouldUseZMove(u32 battlerAtk, u32 battlerDef, u32 chosenMove) { // simple logic. just upgrades chosen move to z move if possible, unless regular move would kill opponent - if ((IsDoubleBattle()) && battlerDef == BATTLE_PARTNER(battlerAtk)) + if ((IsDoubleBattle()) && battlerDef == BATTLE_PARTNER(battlerAtk) && !(GetBattlerMoveTargetType(battlerAtk, chosenMove) & MOVE_TARGET_ALLY)) return FALSE; // don't use z move on partner if (HasTrainerUsedGimmick(battlerAtk, GIMMICK_Z_MOVE)) return FALSE; // can't use z move twice if (IsViableZMove(battlerAtk, chosenMove)) { - uq4_12_t effectiveness; u32 zMove = GetUsableZMove(battlerAtk, chosenMove); + + if (IsBattleMoveStatus(chosenMove)) + { + u8 zEffect = GetMoveZEffect(chosenMove); + enum StatChange statChange = 0; + + if (zEffect == Z_EFFECT_CURSE) + { + if (IS_BATTLER_OF_TYPE(battlerAtk, TYPE_GHOST)) + zEffect = Z_EFFECT_RECOVER_HP; + else + zEffect = Z_EFFECT_ATK_UP_1; + } + + switch (zEffect) + { + case Z_EFFECT_NONE: + return FALSE; + case Z_EFFECT_RESET_STATS: + if (CountNegativeStatStages(battlerAtk) > 1) + return TRUE; + break; + case Z_EFFECT_ALL_STATS_UP_1: + if (AreBattlersStatsMaxed(battlerAtk)) + return FALSE; + return IncreaseStatUpScore(battlerAtk, battlerDef, STAT_CHANGE_ATK) > 0 + || IncreaseStatUpScore(battlerAtk, battlerDef, STAT_CHANGE_SPATK) > 0; + break; + case Z_EFFECT_BOOST_CRITS: + return TRUE; + case Z_EFFECT_FOLLOW_ME: + return HasPartnerIgnoreFlags(battlerAtk) && (GetHealthPercentage(battlerAtk) <= Z_EFFECT_FOLLOW_ME_THRESHOLD || GetBestNoOfHitsToKO(battlerDef, battlerAtk, AI_DEFENDING) == 1); + break; + case Z_EFFECT_RECOVER_HP: + return gAiLogicData->hpPercents[battlerAtk] <= Z_EFFECT_RESTORE_HP_THRESHOLD; + case Z_EFFECT_RESTORE_REPLACEMENT_HP: + break; + case Z_EFFECT_ACC_UP_1: + case Z_EFFECT_ACC_UP_2: + case Z_EFFECT_ACC_UP_3: + statChange = STAT_CHANGE_ACC; + break; + case Z_EFFECT_EVSN_UP_1: + case Z_EFFECT_EVSN_UP_2: + case Z_EFFECT_EVSN_UP_3: + statChange = STAT_CHANGE_EVASION; + break; + case Z_EFFECT_ATK_UP_1: + case Z_EFFECT_DEF_UP_1: + case Z_EFFECT_SPD_UP_1: + case Z_EFFECT_SPATK_UP_1: + case Z_EFFECT_SPDEF_UP_1: + statChange = STAT_CHANGE_ATK + zEffect - Z_EFFECT_ATK_UP_1; + break; + case Z_EFFECT_ATK_UP_2: + case Z_EFFECT_DEF_UP_2: + case Z_EFFECT_SPD_UP_2: + case Z_EFFECT_SPATK_UP_2: + case Z_EFFECT_SPDEF_UP_2: + statChange = STAT_CHANGE_ATK_2 + zEffect - Z_EFFECT_ATK_UP_2; + break; + case Z_EFFECT_ATK_UP_3: + case Z_EFFECT_DEF_UP_3: + case Z_EFFECT_SPD_UP_3: + case Z_EFFECT_SPATK_UP_3: + case Z_EFFECT_SPDEF_UP_3: + statChange = STAT_CHANGE_ATK_2 + zEffect - Z_EFFECT_ATK_UP_3; + break; + default: + return FALSE; + } + + if (statChange != 0 && IncreaseStatUpScore(battlerAtk, battlerDef, statChange) > 0) + return TRUE; + } + else if (GetMoveEffect(zMove) == EFFECT_EXTREME_EVOBOOST) + { + return (!AreBattlersStatsMaxed(battlerAtk) && (IncreaseStatUpScore(battlerAtk, battlerDef, STAT_CHANGE_ATK_2) || IncreaseStatUpScore(battlerAtk, battlerDef, STAT_CHANGE_SPATK_2))); + } + else if (!IsBattleMoveStatus(chosenMove) && IsBattleMoveStatus(zMove)) + return FALSE; + + uq4_12_t effectiveness; struct SimulatedDamage dmg; if (gBattleMons[battlerDef].ability == ABILITY_DISGUISE @@ -4867,11 +4949,6 @@ bool32 ShouldUseZMove(u32 battlerAtk, u32 battlerDef, u32 chosenMove) && gBattleMons[battlerDef].species == SPECIES_EISCUE_ICE && IsBattleMovePhysical(chosenMove)) return FALSE; // Don't waste a Z-Move busting Ice Face - if (IsBattleMoveStatus(chosenMove) && !IsBattleMoveStatus(zMove)) - return FALSE; - else if (!IsBattleMoveStatus(chosenMove) && IsBattleMoveStatus(zMove)) - return FALSE; - dmg = AI_CalcDamageSaveBattlers(chosenMove, battlerAtk, battlerDef, &effectiveness, NO_GIMMICK, NO_GIMMICK); if (!IsBattleMoveStatus(chosenMove) && dmg.minimum >= gBattleMons[battlerDef].hp) diff --git a/test/battle/ai/gimmick_z_move.c b/test/battle/ai/gimmick_z_move.c index bf6b3cc3ec..0b74507b22 100644 --- a/test/battle/ai/gimmick_z_move.c +++ b/test/battle/ai/gimmick_z_move.c @@ -32,3 +32,94 @@ AI_SINGLE_BATTLE_TEST("AI does not use damaging Z-moves if the player would fain TURN { EXPECT_MOVE(opponent, MOVE_QUICK_ATTACK, gimmick: GIMMICK_NONE); } } } + +AI_SINGLE_BATTLE_TEST("AI uses Z-Moves -- Z-Splash") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT ); + ASSUME(GetMoveType(MOVE_QUICK_ATTACK) == TYPE_NORMAL); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_NORMALIUM_Z); Moves(MOVE_POUND, MOVE_SPLASH); } + } WHEN { + TURN { EXPECT_MOVE(opponent, MOVE_SPLASH, gimmick: GIMMICK_Z_MOVE); + SCORE_GT_VAL(opponent, MOVE_SPLASH, AI_SCORE_DEFAULT); } + TURN { EXPECT_MOVE(opponent, MOVE_POUND, gimmick: GIMMICK_NONE); + SCORE_EQ_VAL(opponent, MOVE_SPLASH, 90); } + } +} + +AI_SINGLE_BATTLE_TEST("AI uses Z-Moves -- Z-Happy Hour") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT ); + ASSUME(GetMoveType(MOVE_QUICK_ATTACK) == TYPE_NORMAL); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_NORMALIUM_Z); Moves(MOVE_POUND, MOVE_HAPPY_HOUR); } + } WHEN { + TURN { EXPECT_MOVE(opponent, MOVE_HAPPY_HOUR, gimmick: GIMMICK_Z_MOVE); + SCORE_GT_VAL(opponent, MOVE_HAPPY_HOUR, AI_SCORE_DEFAULT); } + TURN { EXPECT_MOVE(opponent, MOVE_POUND, gimmick: GIMMICK_NONE); + SCORE_EQ_VAL(opponent, MOVE_HAPPY_HOUR, 90); } + } +} + +// Last Resort itself is missing logic! +AI_SINGLE_BATTLE_TEST("AI uses Z-Moves -- Extreme Evoboost") +{ + KNOWN_FAILING; + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT ); + ASSUME(GetMoveType(MOVE_QUICK_ATTACK) == TYPE_NORMAL); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_EEVEE) { Item(ITEM_EEVIUM_Z); Moves(MOVE_POUND, MOVE_LAST_RESORT); } + } WHEN { + TURN { EXPECT_MOVE(opponent, MOVE_LAST_RESORT, gimmick: GIMMICK_Z_MOVE); } + TURN { EXPECT_MOVE(opponent, MOVE_POUND, gimmick: GIMMICK_NONE); + SCORE_EQ_VAL(opponent, MOVE_LAST_RESORT, 80); } +// Uncomment when Last Resort works correctly. +// TURN { EXPECT_MOVE(opponent, MOVE_LAST_RESORT, gimmick: GIMMICK_NONE); } + } +} + +AI_SINGLE_BATTLE_TEST("AI uses Z-Moves -- 10,000,000 Volt Thunderbolt") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT ); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_PIKACHU_PARTNER) { Item(ITEM_PIKASHUNIUM_Z); Moves(MOVE_THUNDERBOLT); } + } WHEN { + TURN { EXPECT_MOVE(opponent, MOVE_THUNDERBOLT, gimmick: GIMMICK_Z_MOVE); } + } +} + +AI_SINGLE_BATTLE_TEST("AI uses Z-Moves -- Z-Destiny Bond is not used in singles") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT ); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_GHOSTIUM_Z); Moves(MOVE_DESTINY_BOND); } + } WHEN { + TURN { EXPECT_MOVE(opponent, MOVE_DESTINY_BOND, gimmick: GIMMICK_NONE); } + } +} + +AI_DOUBLE_BATTLE_TEST("AI uses Z-Moves -- Z-Destiny Bond is used when about to die") +{ + u32 currentHP; + PARAMETRIZE { currentHP = 1; } + PARAMETRIZE { currentHP = 500; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT ); + PLAYER(SPECIES_WOBBUFFET) { Moves(MOVE_CELEBRATE, MOVE_POUND); } + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { HP(currentHP); Item(ITEM_GHOSTIUM_Z); Moves(MOVE_DESTINY_BOND); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + if (currentHP == 1) + TURN { EXPECT_MOVE(opponentLeft, MOVE_DESTINY_BOND, gimmick: GIMMICK_Z_MOVE); } + else + TURN { EXPECT_MOVE(opponentLeft, MOVE_DESTINY_BOND, gimmick: GIMMICK_NONE); } + } +} + diff --git a/test/battle/move_effect/last_resort.c b/test/battle/move_effect/last_resort.c index c0b2380f27..602f4ad2a3 100644 --- a/test/battle/move_effect/last_resort.c +++ b/test/battle/move_effect/last_resort.c @@ -114,3 +114,43 @@ SINGLE_BATTLE_TEST("Last Resort works with Sleep Talk") HP_BAR(opponent); } } + +AI_SINGLE_BATTLE_TEST("AI uses Last Resort - 2 moves") +{ + KNOWN_FAILING; + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_LAST_RESORT, MOVE_SCRATCH); } + } WHEN { + TURN { NOT_EXPECT_MOVE(opponent, MOVE_LAST_RESORT); } + TURN { EXPECT_MOVE(opponent, MOVE_LAST_RESORT); } + } +} + +AI_SINGLE_BATTLE_TEST("AI uses Last Resort - 3 moves") +{ + KNOWN_FAILING; + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_LAST_RESORT, MOVE_QUICK_ATTACK, MOVE_SCRATCH); } + } WHEN { + TURN { NOT_EXPECT_MOVE(opponent, MOVE_LAST_RESORT); } + TURN { NOT_EXPECT_MOVE(opponent, MOVE_LAST_RESORT); } + TURN { EXPECT_MOVE(opponent, MOVE_LAST_RESORT); } + } +} + +AI_SINGLE_BATTLE_TEST("AI uses Last Resort - 4 moves") +{ + KNOWN_FAILING; + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_LAST_RESORT, MOVE_QUICK_ATTACK, MOVE_SCRATCH, MOVE_GUST); } + } WHEN { + TURN { NOT_EXPECT_MOVE(opponent, MOVE_LAST_RESORT); } + TURN { NOT_EXPECT_MOVE(opponent, MOVE_LAST_RESORT); } + TURN { NOT_EXPECT_MOVE(opponent, MOVE_LAST_RESORT); } + TURN { EXPECT_MOVE(opponent, MOVE_LAST_RESORT); } + } +} +