From 260b12337e8ef6c3d380f40ada2e8d3d10791b0d Mon Sep 17 00:00:00 2001 From: surskitty Date: Wed, 13 Aug 2025 10:21:13 -0400 Subject: [PATCH] Helping Hand logic (#7504) --- include/battle_ai_util.h | 1 + src/battle_ai_main.c | 55 ++++++++++++++++++++++++++++++++++--- src/battle_ai_util.c | 23 ++++++++++++++++ test/battle/ai/ai_doubles.c | 37 +++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 4 deletions(-) diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index 2e62a1340b..1b23dd7e1a 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -160,6 +160,7 @@ bool32 AI_IsDamagedByRecoil(u32 battler); u32 GetNoOfHitsToKO(u32 dmg, s32 hp); u32 GetNoOfHitsToKOBattlerDmg(u32 dmg, u32 battlerDef); u32 GetNoOfHitsToKOBattler(u32 battlerAtk, u32 battlerDef, u32 moveIndex, enum DamageCalcContext calcContext); +u32 GetBestNoOfHitsToKO(u32 battlerAtk, u32 battlerDef, enum DamageCalcContext calcContext); u32 GetCurrDamageHpPercent(u32 battlerAtk, u32 battlerDef, enum DamageCalcContext calcContext); uq4_12_t AI_GetMoveEffectiveness(u32 move, u32 battlerAtk, u32 battlerDef); u16 *GetMovesArray(u32 battler); diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 9789aa8339..1bd9b0fb8d 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -211,7 +211,7 @@ static u64 GetAiFlags(u16 trainerId) flags = GetTrainerAIFlagsFromId(trainerId); } - if (IsDoubleBattle()) + if (IsDoubleBattle() && flags != 0) { flags |= AI_FLAG_DOUBLE_BATTLE; } @@ -2042,7 +2042,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) || DoesPartnerHaveSameMoveEffect(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove) || (aiData->partnerMove != MOVE_NONE && IsBattleMoveStatus(aiData->partnerMove)) || gBattleStruct->monToSwitchIntoId[BATTLE_PARTNER(battlerAtk)] != PARTY_SIZE) //Partner is switching out. - ADJUST_SCORE(-10); + ADJUST_SCORE(-20); break; case EFFECT_TRICK: case EFFECT_KNOCK_OFF: @@ -3030,6 +3030,7 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) SetTypeBeforeUsingMove(move, battlerAtk); moveType = GetBattleMoveType(move); + bool32 hasTwoOpponents = HasTwoOpponents(battlerAtk); bool32 hasPartner = HasPartner(battlerAtk); u32 friendlyFireThreshold = GetFriendlyFireKOThreshold(battlerAtk); u32 noOfHitsToKOPartner = GetNoOfHitsToKOBattler(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING); @@ -3088,8 +3089,54 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) switch (effect) { case EFFECT_HELPING_HAND: - if (!hasPartner || !HasDamagingMove(battlerAtkPartner)) - ADJUST_SCORE(-20); + if (!hasPartner + || !HasDamagingMove(battlerAtkPartner) + || (aiData->partnerMove != MOVE_NONE && IsBattleMoveStatus(aiData->partnerMove))) + { + ADJUST_SCORE(WORST_EFFECT); + } + else + { + u32 ownHitsToKOFoe1 = GetBestNoOfHitsToKO(battlerAtk, FOE(battlerAtk), AI_ATTACKING); + u32 partnerHitsToKOFoe1 = GetBestNoOfHitsToKO(battlerAtkPartner, FOE(battlerAtk), AI_ATTACKING); + u32 ownHitsToKOFoe2 = GetBestNoOfHitsToKO(battlerAtk, FOE(battlerAtkPartner), AI_ATTACKING); + u32 partnerHitsToKOFoe2 = GetBestNoOfHitsToKO(battlerAtkPartner, FOE(battlerAtkPartner), AI_ATTACKING); + + if (hasTwoOpponents) + { + // Might be about to die + if (CanTargetFaintAi(FOE(battlerAtk), battlerAtk) && CanTargetFaintAi(FOE(battlerAtkPartner), battlerAtk) + && AI_IsSlower(battlerAtk, FOE(battlerAtk), move, predictedMove, DONT_CONSIDER_PRIORITY) + && AI_IsSlower(battlerAtk, FOE(battlerAtkPartner), move, predictedMove, DONT_CONSIDER_PRIORITY)) + ADJUST_SCORE(GOOD_EFFECT); + + if (ownHitsToKOFoe1 > partnerHitsToKOFoe1 && partnerHitsToKOFoe1 > 1 + && ownHitsToKOFoe2 > partnerHitsToKOFoe2 && partnerHitsToKOFoe2 > 1) + ADJUST_SCORE(GOOD_EFFECT); + } + else if (IsBattlerAlive(FOE(battlerAtk))) + { + // Might be about to die + if (CanTargetFaintAi(FOE(battlerAtk), battlerAtk) + && AI_IsSlower(battlerAtk, FOE(battlerAtk), move, predictedMove, DONT_CONSIDER_PRIORITY)) + ADJUST_SCORE(GOOD_EFFECT); + + if (ownHitsToKOFoe1 > partnerHitsToKOFoe1 && partnerHitsToKOFoe1 > 1) + ADJUST_SCORE(GOOD_EFFECT); + } + else if (IsBattlerAlive(FOE(battlerAtkPartner))) + { + // Might be about to die + if (CanTargetFaintAi(FOE(battlerAtkPartner), battlerAtk) + && AI_IsSlower(battlerAtk, FOE(battlerAtkPartner), move, predictedMove, DONT_CONSIDER_PRIORITY)) + ADJUST_SCORE(GOOD_EFFECT); + + if (ownHitsToKOFoe2 > partnerHitsToKOFoe2 && partnerHitsToKOFoe2 > 1) + ADJUST_SCORE(GOOD_EFFECT); + + } + + } break; case EFFECT_PERISH_SONG: if (aiData->partnerMove != 0 && HasTrappingMoveEffect(battlerAtkPartner)) diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index a0e5e7716b..2b31bdf98d 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -1252,6 +1252,29 @@ u32 GetNoOfHitsToKOBattler(u32 battlerAtk, u32 battlerDef, u32 moveIndex, enum D return GetNoOfHitsToKOBattlerDmg(AI_GetDamage(battlerAtk, battlerDef, moveIndex, calcContext, gAiLogicData), battlerDef); } +u32 GetBestNoOfHitsToKO(u32 battlerAtk, u32 battlerDef, enum DamageCalcContext calcContext) +{ + u32 result = 100; + u32 tempResult = 0; + + struct AiLogicData *aiData = gAiLogicData; + s32 moveIndex; + u16 *moves = GetMovesArray(battlerAtk); + u32 moveLimitations = aiData->moveLimitations[battlerAtk]; + + for (moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++) + { + if (IsMoveUnusable(moveIndex, moves[moveIndex], moveLimitations)) + continue; + + tempResult = GetNoOfHitsToKOBattler(battlerAtk, battlerDef, moveIndex, calcContext); + if (tempResult != 0 && tempResult < result) + result = tempResult; + } + + return result; +} + u32 GetCurrDamageHpPercent(u32 battlerAtk, u32 battlerDef, enum DamageCalcContext calcContext) { int bestDmg = AI_GetDamage(battlerAtk, battlerDef, gAiThinkingStruct->movesetIndex, calcContext, gAiLogicData); diff --git a/test/battle/ai/ai_doubles.c b/test/battle/ai/ai_doubles.c index 9e6edc4c56..ff880fb2e2 100644 --- a/test/battle/ai/ai_doubles.c +++ b/test/battle/ai/ai_doubles.c @@ -595,6 +595,43 @@ AI_DOUBLE_BATTLE_TEST("AI uses Trick Room intelligently") } } +AI_DOUBLE_BATTLE_TEST("AI uses Helping Hand if it's about to die") +{ + u32 hp; + + PARAMETRIZE { hp = 1; } + PARAMETRIZE { hp = 500; } + + GIVEN { + ASSUME(GetMoveEffect(MOVE_HELPING_HAND) == EFFECT_HELPING_HAND); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_OMNISCIENT); + PLAYER(SPECIES_WOBBUFFET) { Moves(MOVE_TACKLE, MOVE_CELEBRATE); } + PLAYER(SPECIES_WOBBUFFET) { Moves(MOVE_TACKLE, MOVE_CELEBRATE); } + OPPONENT(SPECIES_WOBBUFFET) { HP(hp); Moves(MOVE_HELPING_HAND, MOVE_MUDDY_WATER); } + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_MUDDY_WATER); } + } WHEN { + if (hp == 1) + TURN { EXPECT_MOVE(opponentLeft, MOVE_HELPING_HAND); } + else + TURN { NOT_EXPECT_MOVE(opponentLeft, MOVE_HELPING_HAND); } + } +} + +AI_DOUBLE_BATTLE_TEST("AI uses Helping Hand if the ally does notably more damage") +{ + + GIVEN { + ASSUME(GetMoveEffect(MOVE_HELPING_HAND) == EFFECT_HELPING_HAND); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_OMNISCIENT); + PLAYER(SPECIES_WOBBUFFET) { Moves(MOVE_TACKLE, MOVE_CELEBRATE); } + PLAYER(SPECIES_WOBBUFFET) { Moves(MOVE_TACKLE, MOVE_CELEBRATE); } + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_HELPING_HAND, MOVE_MUD_SLAP); } + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_MUDDY_WATER); } + } WHEN { + TURN { EXPECT_MOVE(opponentLeft, MOVE_HELPING_HAND); } + } +} + AI_DOUBLE_BATTLE_TEST("AI uses Tailwind") { u32 speed1, speed2, speed3, speed4;