From 82ab4fe98dbcae035a7f0fcd3aa1e6b3d4b9100a Mon Sep 17 00:00:00 2001 From: surskitty Date: Wed, 24 Sep 2025 13:55:29 -0400 Subject: [PATCH] Z Status move handling: Conversion, Detect, Nature Power, Transform (#7721) --- include/config/ai.h | 3 +- src/battle_ai_main.c | 8 +- src/battle_ai_util.c | 50 ++++++++++- test/battle/ai/gimmick_z_move.c | 141 +++++++++++++++++++++++++------- 4 files changed, 166 insertions(+), 36 deletions(-) diff --git a/include/config/ai.h b/include/config/ai.h index b834829edf..5a7ce40783 100644 --- a/include/config/ai.h +++ b/include/config/ai.h @@ -112,6 +112,7 @@ // HP thresholds to use a status z-move. #define Z_EFFECT_FOLLOW_ME_THRESHOLD 30 -#define Z_EFFECT_RESTORE_HP_THRESHOLD 60 +#define Z_EFFECT_RESTORE_HP_LOWER_THRESHOLD ENABLE_RECOVERY_THRESHOLD // threshold used for moves you could conceivably use more than once +#define Z_EFFECT_RESTORE_HP_HIGHER_THRESHOLD 90 // these moves are one-time use or drop your HP #endif // GUARD_CONFIG_AI_H diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 8399f7f977..1ff97b39bc 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -1986,7 +1986,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 && (GetActiveGimmick(battlerAtk) != GIMMICK_Z_MOVE || GetMoveZEffect(move) == Z_EFFECT_NONE)) + else if (aiData->hpPercents[battlerAtk] <= 60 && !IsConsideringZMove(battlerAtk, battlerDef, move)) ADJUST_SCORE(-10); break; case EFFECT_FUTURE_SIGHT: @@ -4322,7 +4322,13 @@ static s32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move, stru break; case EFFECT_CONVERSION: if (!IS_BATTLER_OF_TYPE(battlerAtk, GetMoveType(gBattleMons[battlerAtk].moves[0]))) + { ADJUST_SCORE(WEAK_EFFECT); + if (aiData->abilities[battlerAtk] == ABILITY_ADAPTABILITY) + ADJUST_SCORE(WEAK_EFFECT); + if (IsConsideringZMove(battlerAtk, battlerDef, move)) + ADJUST_SCORE(BEST_EFFECT); + } break; case EFFECT_SWALLOW: if (gDisableStructs[battlerAtk].stockpileCounter == 0) diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 38e8d341a5..2c8e9fd0ce 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -4920,12 +4920,16 @@ bool32 AI_MoveMakesContact(u32 ability, enum ItemHoldEffect holdEffect, u32 move bool32 IsConsideringZMove(u32 battlerAtk, u32 battlerDef, u32 move) { + if (GetMovePower(move) == 0 && GetMoveZEffect(move) == Z_EFFECT_NONE) + return FALSE; + return gBattleStruct->gimmick.usableGimmick[battlerAtk] == GIMMICK_Z_MOVE && ShouldUseZMove(battlerAtk, battlerDef, move); } //TODO - this could use some more sophisticated logic 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) && !(GetBattlerMoveTargetType(battlerAtk, chosenMove) & MOVE_TARGET_ALLY)) return FALSE; // don't use z move on partner @@ -4934,6 +4938,39 @@ bool32 ShouldUseZMove(u32 battlerAtk, u32 battlerDef, u32 chosenMove) if (IsViableZMove(battlerAtk, chosenMove)) { + enum BattleMoveEffects baseEffect = GetMoveEffect(chosenMove); + bool32 isEager = FALSE; // more likely to use a z move than typical + + u32 predictedMoveSpeedCheck = GetIncomingMoveSpeedCheck(battlerAtk, battlerDef, gAiLogicData); + bool32 isSlower = AI_IsSlower(battlerAtk, battlerDef, chosenMove, predictedMoveSpeedCheck, CONSIDER_PRIORITY); + + switch (baseEffect) + { + case EFFECT_BELLY_DRUM: + case EFFECT_FILLET_AWAY: + if (isSlower) + return TRUE; + isEager = TRUE; + break; + case EFFECT_PROTECT: + if (HasDamagingMoveOfType(battlerAtk, GetMoveType(gMovesInfo[chosenMove].type))) + return FALSE; + else + isEager = TRUE; + break; + case EFFECT_TELEPORT: + isEager = TRUE; + break; + case EFFECT_TRANSFORM: + if (IsBattlerTrapped(battlerDef, battlerAtk) && !HasDamagingMoveOfType(battlerDef, GetMoveType(gMovesInfo[chosenMove].type))) + return TRUE; + if (isSlower) + isEager = TRUE; + break; + default: + break; + } + u32 zMove = GetUsableZMove(battlerAtk, chosenMove); if (IsBattleMoveStatus(chosenMove)) @@ -4952,7 +4989,9 @@ bool32 ShouldUseZMove(u32 battlerAtk, u32 battlerDef, u32 chosenMove) switch (zEffect) { case Z_EFFECT_NONE: - return FALSE; + if (GetMovePower(chosenMove) == 0) + return FALSE; + break; case Z_EFFECT_RESET_STATS: if (CountNegativeStatStages(battlerAtk) > 1) return TRUE; @@ -4965,7 +5004,11 @@ bool32 ShouldUseZMove(u32 battlerAtk, u32 battlerDef, u32 chosenMove) 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; + if (GetBestNoOfHitsToKO(battlerDef, battlerAtk, AI_DEFENDING) == 1 && GetHealthPercentage(battlerAtk) > Z_EFFECT_RESTORE_HP_HIGHER_THRESHOLD) + return TRUE; + if (isEager) + return GetHealthPercentage(battlerAtk) <= Z_EFFECT_RESTORE_HP_HIGHER_THRESHOLD; + return GetHealthPercentage(battlerAtk) <= Z_EFFECT_RESTORE_HP_LOWER_THRESHOLD; case Z_EFFECT_RESTORE_REPLACEMENT_HP: break; case Z_EFFECT_ACC_UP_1: @@ -5003,8 +5046,9 @@ bool32 ShouldUseZMove(u32 battlerAtk, u32 battlerDef, u32 chosenMove) return FALSE; } - if (statChange != 0 && IncreaseStatUpScore(battlerAtk, battlerDef, statChange) > 0) + if (statChange != 0 && (isEager || IncreaseStatUpScore(battlerAtk, battlerDef, statChange) > 0)) return TRUE; + } else if (GetMoveEffect(zMove) == EFFECT_EXTREME_EVOBOOST) { diff --git a/test/battle/ai/gimmick_z_move.c b/test/battle/ai/gimmick_z_move.c index 1332abe6c0..6a2ed89101 100644 --- a/test/battle/ai/gimmick_z_move.c +++ b/test/battle/ai/gimmick_z_move.c @@ -32,37 +32,6 @@ 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); } - } -} - AI_SINGLE_BATTLE_TEST("AI uses Z-Moves -- Extreme Evoboost") { GIVEN { @@ -88,6 +57,25 @@ AI_SINGLE_BATTLE_TEST("AI uses Z-Moves -- 10,000,000 Volt Thunderbolt") TURN { EXPECT_MOVE(opponent, MOVE_THUNDERBOLT, gimmick: GIMMICK_Z_MOVE); } } } +AI_SINGLE_BATTLE_TEST("AI uses Z-Moves -- Z-Conversion") +{ + u32 ability; + PARAMETRIZE { ability = ABILITY_NONE; } + PARAMETRIZE { ability = ABILITY_OPPORTUNIST; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT ); + ASSUME(GetMoveType(MOVE_CONVERSION) == TYPE_NORMAL); + PLAYER(SPECIES_WOBBUFFET) { Ability(ability); } + OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_NORMALIUM_Z); Ability(ABILITY_ADAPTABILITY); Moves(MOVE_THUNDERBOLT, MOVE_CONVERSION); } + } WHEN { + if (ability == ABILITY_OPPORTUNIST) + TURN { EXPECT_MOVE(opponent, MOVE_CONVERSION, gimmick: GIMMICK_NONE); } + else + TURN { EXPECT_MOVE(opponent, MOVE_CONVERSION, gimmick: GIMMICK_Z_MOVE); } + } +} + AI_SINGLE_BATTLE_TEST("AI uses Z-Moves -- Z-Destiny Bond is not used in singles") { @@ -120,3 +108,94 @@ AI_DOUBLE_BATTLE_TEST("AI uses Z-Moves -- Z-Destiny Bond is used when about to d } } +AI_SINGLE_BATTLE_TEST("AI uses Z-Moves -- Z-Detect") +{ + u32 move; + PARAMETRIZE { move = MOVE_THUNDERBOLT; } + PARAMETRIZE { move = MOVE_CLOSE_COMBAT; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT | AI_FLAG_PREDICT_MOVE ); + PLAYER(SPECIES_WOBBUFFET) { Moves(MOVE_CELEBRATE, MOVE_FAKE_OUT); } + OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_FIGHTINIUM_Z); Moves(MOVE_DETECT, move); } + } WHEN { + if (move == MOVE_CLOSE_COMBAT) + TURN { EXPECT_MOVE(opponent, MOVE_DETECT, gimmick: GIMMICK_NONE); } + else + TURN { EXPECT_MOVE(opponent, MOVE_DETECT, gimmick: GIMMICK_Z_MOVE); } + } +} + +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); } + } +} + +TO_DO_BATTLE_TEST("TODO: AI uses Z-Moves -- Z-Haze") + +TO_DO_BATTLE_TEST("TODO: AI uses Z-Moves -- Z-Mirror Move") + +AI_SINGLE_BATTLE_TEST("AI uses Z-Moves -- Z-Nature Power") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT | AI_FLAG_PREDICT_MOVE); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_NORMALIUM_Z); Moves(MOVE_NATURE_POWER, MOVE_HEADBUTT); } + } WHEN { + TURN { EXPECT_MOVE(opponent, MOVE_NATURE_POWER, gimmick: GIMMICK_Z_MOVE); } + } +} + +// Requires handling for Wish passing/Healing Wish/other ways to determine what pokemon to heal via switching into. +TO_DO_BATTLE_TEST("TODO: AI uses Z-Moves -- Z-Parting Shot") + +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); } + } +} + +TO_DO_BATTLE_TEST("TODO: AI uses Z-Moves -- Z-Tailwind") + +AI_SINGLE_BATTLE_TEST("AI uses Z-Moves -- Z-Transform") +{ + u32 currentHP, move; + PARAMETRIZE { currentHP = 1; move = MOVE_HEADBUTT; } + PARAMETRIZE { currentHP = 1; move = MOVE_THUNDERBOLT; } + PARAMETRIZE { currentHP = 500; move = MOVE_HEADBUTT; } + PARAMETRIZE { currentHP = 500; move = MOVE_THUNDERBOLT; } + + 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, MOVE_CELEBRATE); } + OPPONENT(SPECIES_WOBBUFFET) { HP(currentHP); Item(ITEM_NORMALIUM_Z); Moves(MOVE_TRANSFORM); } + } WHEN { + if (currentHP == 1 || move == MOVE_THUNDERBOLT) + TURN { EXPECT_MOVE(opponent, MOVE_TRANSFORM, gimmick: GIMMICK_Z_MOVE); } + else + TURN { EXPECT_MOVE(opponent, MOVE_TRANSFORM, gimmick: GIMMICK_NONE); } + } +} + +TO_DO_BATTLE_TEST("TODO: AI uses Z-Moves -- Z-Trick Room") + +