From be2517415b841114594b5ecd6514b0217529cc62 Mon Sep 17 00:00:00 2001 From: Pawkkie <61265402+Pawkkie@users.noreply.github.com> Date: Tue, 28 May 2024 04:21:25 -0400 Subject: [PATCH] Damage roll selection in AI_CalcDamage (#4615) * Damage roll selection and AI_FLAG_CONSERVATIVE * Clarify enum names * Simplify AverageRollDmg line * Change u8s to u32s * Turns out Boomburst does a lot of damage lol * Spacing --- include/battle_ai_util.h | 13 +++++++++--- include/constants/battle_ai.h | 3 ++- src/battle_ai_main.c | 7 ++++++- src/battle_ai_switch_items.c | 8 ++++---- src/battle_ai_util.c | 38 +++++++++++++++++++++++++++-------- test/battle/ai.c | 2 +- 6 files changed, 53 insertions(+), 18 deletions(-) diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index 03fa1e5f315c..b6319b7f66a5 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -5,6 +5,13 @@ #define AI_STRIKES_FIRST(battlerAi, battlerDef, move)((AI_WhoStrikesFirst(battlerAi, battlerDef, move) == AI_IS_FASTER)) +enum +{ + DMG_ROLL_LOWEST, + DMG_ROLL_AVERAGE, + DMG_ROLL_HIGHEST, +}; + bool32 AI_RandLessThan(u32 val); bool32 IsAiVsAiBattle(void); bool32 BattlerHasAi(u32 battlerId); @@ -82,8 +89,8 @@ bool32 ShouldLowerEvasion(u32 battlerAtk, u32 battlerDef, u32 defAbility); bool32 IsAffectedByPowder(u32 battler, u32 ability, u32 holdEffect); bool32 MovesWithCategoryUnusable(u32 attacker, u32 target, u32 category); s32 AI_WhichMoveBetter(u32 move1, u32 move2, u32 battlerAtk, u32 battlerDef, s32 noOfHitsToKo); -s32 AI_CalcDamageSaveBattlers(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectiveness, bool32 considerZPower); -s32 AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectiveness, bool32 considerZPower, u32 weather); +s32 AI_CalcDamageSaveBattlers(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectiveness, bool32 considerZPower, u32 dmgRoll); +s32 AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectiveness, bool32 considerZPower, u32 weather, u32 dmgRoll); bool32 AI_IsDamagedByRecoil(u32 battler); u32 GetNoOfHitsToKO(u32 dmg, s32 hp); u32 GetNoOfHitsToKOBattlerDmg(u32 dmg, u32 battlerDef); @@ -187,7 +194,7 @@ void IncreaseSleepScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score); void IncreaseConfusionScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score); void IncreaseFrostbiteScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score); -s32 AI_CalcPartyMonDamage(u32 move, u32 battlerAtk, u32 battlerDef, struct BattlePokemon switchinCandidate, bool8 isPartyMonAttacker); +s32 AI_CalcPartyMonDamage(u32 move, u32 battlerAtk, u32 battlerDef, struct BattlePokemon switchinCandidate, bool32 isPartyMonAttacker, u32 dmgRoll); s32 AI_CheckMoveEffects(u32 battlerAtk, u32 battlerDef, u32 move, s32 score, struct AiLogicData *aiData, u32 predictedMove, bool32 isDoubleBattle); s32 AI_TryToClearStats(u32 battlerAtk, u32 battlerDef, bool32 isDoubleBattle); bool32 AI_ShouldCopyStatChanges(u32 battlerAtk, u32 battlerDef); diff --git a/include/constants/battle_ai.h b/include/constants/battle_ai.h index e42403e16ace..db3051b2572e 100644 --- a/include/constants/battle_ai.h +++ b/include/constants/battle_ai.h @@ -46,8 +46,9 @@ #define AI_FLAG_ACE_POKEMON (1 << 16) // AI has an Ace Pokemon. The last Pokemon in the party will not be used until it's the last one remaining. #define AI_FLAG_OMNISCIENT (1 << 17) // AI has full knowledge of player moves, abilities, hold items #define AI_FLAG_SMART_MON_CHOICES (1 << 18) // AI will make smarter decisions when choosing which mon to send out mid-battle and after a KO, which are separate decisions. Automatically included by AI_FLAG_SMART_SWITCHING. +#define AI_FLAG_CONSERVATIVE (1 << 19) // AI assumes all moves will low roll damage -#define AI_FLAG_COUNT 19 +#define AI_FLAG_COUNT 20 // 'other' ai logic flags #define AI_FLAG_ROAMING (1 << 29) diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 09dd5b44d0f7..fda99dcafe99 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -459,7 +459,12 @@ static void SetBattlerAiMovesData(struct AiLogicData *aiData, u32 battlerAtk, u3 //&& gMovesInfo[move].power != 0 /* we want to get effectiveness and accuracy of status moves */ && !(aiData->moveLimitations[battlerAtk] & gBitTable[i])) { - dmg = AI_CalcDamage(move, battlerAtk, battlerDef, &effectiveness, TRUE, weather); + if (AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_RISKY) + dmg = AI_CalcDamage(move, battlerAtk, battlerDef, &effectiveness, TRUE, weather, DMG_ROLL_HIGHEST); + else if (AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_CONSERVATIVE) + dmg = AI_CalcDamage(move, battlerAtk, battlerDef, &effectiveness, TRUE, weather, DMG_ROLL_LOWEST); + else + dmg = AI_CalcDamage(move, battlerAtk, battlerDef, &effectiveness, TRUE, weather, DMG_ROLL_AVERAGE); aiData->moveAccuracy[battlerAtk][battlerDef][i] = Ai_SetMoveAccuracy(aiData, battlerAtk, battlerDef, move); } aiData->simulatedDmg[battlerAtk][battlerDef][i] = dmg; diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index 093db185d9c4..2dac845c47bd 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -147,7 +147,7 @@ static bool32 HasBadOdds(u32 battler, bool32 emitResult) playerMove = gBattleMons[opposingBattler].moves[i]; if (playerMove != MOVE_NONE && gMovesInfo[playerMove].power != 0) { - damageTaken = AI_CalcDamage(playerMove, opposingBattler, battler, &effectiveness, FALSE, weather); + damageTaken = AI_CalcDamage(playerMove, opposingBattler, battler, &effectiveness, FALSE, weather, DMG_ROLL_HIGHEST); if (damageTaken > maxDamageTaken) maxDamageTaken = damageTaken; } @@ -1234,7 +1234,7 @@ static u32 GetBestMonDmg(struct Pokemon *party, int firstId, int lastId, u8 inva if (aiMove != MOVE_NONE && gMovesInfo[aiMove].power != 0) { aiMove = GetMonData(&party[i], MON_DATA_MOVE1 + j); - dmg = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, AI_DATA->switchinCandidate.battleMon, TRUE); + dmg = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, AI_DATA->switchinCandidate.battleMon, TRUE, DMG_ROLL_AVERAGE); if (bestDmg < dmg) { bestDmg = dmg; @@ -1683,7 +1683,7 @@ static s32 GetMaxDamagePlayerCouldDealToSwitchin(u32 battler, u32 opposingBattle playerMove = gBattleMons[opposingBattler].moves[i]; if (playerMove != MOVE_NONE && gMovesInfo[playerMove].power != 0) { - damageTaken = AI_CalcPartyMonDamage(playerMove, opposingBattler, battler, battleMon, FALSE); + damageTaken = AI_CalcPartyMonDamage(playerMove, opposingBattler, battler, battleMon, FALSE, DMG_ROLL_HIGHEST); if (damageTaken > maxDamageTaken) maxDamageTaken = damageTaken; } @@ -1775,7 +1775,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, // Only do damage calc if switching after KO, don't need it otherwise and saves ~0.02s per turn if (isSwitchAfterKO && aiMove != MOVE_NONE && gMovesInfo[aiMove].power != 0) - damageDealt = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, AI_DATA->switchinCandidate.battleMon, TRUE); + damageDealt = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, AI_DATA->switchinCandidate.battleMon, TRUE, DMG_ROLL_AVERAGE); // Check for Baton Pass; hitsToKO requirements mean mon can boost and BP without dying whether it's slower or not if (aiMove == MOVE_BATON_PASS && ((hitsToKO > hitsToKOThreshold + 1 && AI_DATA->switchinCandidate.battleMon.speed < playerMonSpeed) || (hitsToKO > hitsToKOThreshold && AI_DATA->switchinCandidate.battleMon.speed > playerMonSpeed))) diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 345ef5e9ad46..6afc4b65ae16 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -354,14 +354,14 @@ bool32 MovesWithCategoryUnusable(u32 attacker, u32 target, u32 category) } // To save computation time this function has 2 variants. One saves, sets and restores battlers, while the other doesn't. -s32 AI_CalcDamageSaveBattlers(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectiveness, bool32 considerZPower) +s32 AI_CalcDamageSaveBattlers(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectiveness, bool32 considerZPower, u32 dmgRoll) { s32 dmg = 0; SaveBattlerData(battlerAtk); SaveBattlerData(battlerDef); SetBattlerData(battlerAtk); SetBattlerData(battlerDef); - dmg = AI_CalcDamage(move, battlerAtk, battlerDef, typeEffectiveness, considerZPower, AI_GetWeather(AI_DATA)); + dmg = AI_CalcDamage(move, battlerAtk, battlerDef, typeEffectiveness, considerZPower, AI_GetWeather(AI_DATA), dmgRoll); RestoreBattlerData(battlerAtk); RestoreBattlerData(battlerDef); return dmg; @@ -374,6 +374,18 @@ static inline s32 LowestRollDmg(s32 dmg) return dmg; } +static inline s32 HighestRollDmg(s32 dmg) +{ + return dmg; +} + +static inline s32 AverageRollDmg(s32 dmg) +{ + dmg = ((HighestRollDmg(dmg) + LowestRollDmg(dmg)) * 100) / 2; + dmg /= 100; + return dmg; +} + bool32 IsDamageMoveUnusable(u32 move, u32 battlerAtk, u32 battlerDef) { s32 moveType; @@ -459,7 +471,7 @@ bool32 IsDamageMoveUnusable(u32 move, u32 battlerAtk, u32 battlerDef) return FALSE; } -s32 AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectiveness, bool32 considerZPower, u32 weather) +s32 AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectiveness, bool32 considerZPower, u32 weather, u32 dmgRoll) { s32 dmg, moveType; uq4_12_t effectivenessMultiplier; @@ -534,11 +546,21 @@ s32 AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectivenes aiData->abilities[battlerAtk], aiData->abilities[battlerDef]); u32 critChance = GetCritHitChance(critChanceIndex); // With critChance getting closer to 1, dmg gets closer to critDmg. - dmg = LowestRollDmg((critDmg + normalDmg * (critChance - 1)) / (critChance)); + if (dmgRoll == DMG_ROLL_AVERAGE) + dmg = AverageRollDmg((critDmg + normalDmg * (critChance - 1)) / (critChance)); + else if (dmgRoll == DMG_ROLL_HIGHEST) + dmg = HighestRollDmg((critDmg + normalDmg * (critChance - 1)) / (critChance)); + else + dmg = LowestRollDmg((critDmg + normalDmg * (critChance - 1)) / (critChance)); // Default to lowest roll } else { - dmg = LowestRollDmg(normalDmg); + if (dmgRoll == DMG_ROLL_AVERAGE) + dmg = AverageRollDmg(normalDmg); + else if (dmgRoll == DMG_ROLL_HIGHEST) + dmg = HighestRollDmg(normalDmg); + else + dmg = LowestRollDmg(normalDmg); // Default to lowest roll } if (!gBattleStruct->zmove.active) @@ -3286,7 +3308,7 @@ void FreeRestoreBattleMons(struct BattlePokemon *savedBattleMons) } // party logic -s32 AI_CalcPartyMonDamage(u32 move, u32 battlerAtk, u32 battlerDef, struct BattlePokemon switchinCandidate, bool8 isPartyMonAttacker) +s32 AI_CalcPartyMonDamage(u32 move, u32 battlerAtk, u32 battlerDef, struct BattlePokemon switchinCandidate, bool32 isPartyMonAttacker, u32 dmgRoll) { s32 dmg; u8 effectiveness; @@ -3307,7 +3329,7 @@ s32 AI_CalcPartyMonDamage(u32 move, u32 battlerAtk, u32 battlerDef, struct Battl AI_THINKING_STRUCT->saved[battlerAtk].saved = FALSE; } - dmg = AI_CalcDamage(move, battlerAtk, battlerDef, &effectiveness, FALSE, AI_GetWeather(AI_DATA)); + dmg = AI_CalcDamage(move, battlerAtk, battlerDef, &effectiveness, FALSE, AI_GetWeather(AI_DATA), dmgRoll); // restores original gBattleMon struct FreeRestoreBattleMons(savedBattleMons); return dmg; @@ -3751,7 +3773,7 @@ bool32 ShouldUseZMove(u32 battlerAtk, u32 battlerDef, u32 chosenMove) else if (!IS_MOVE_STATUS(chosenMove) && IS_MOVE_STATUS(gBattleStruct->zmove.chosenZMove)) return FALSE; - if (!IS_MOVE_STATUS(chosenMove) && AI_CalcDamageSaveBattlers(chosenMove, battlerAtk, battlerDef, &effectiveness, FALSE) >= gBattleMons[battlerDef].hp) + if (!IS_MOVE_STATUS(chosenMove) && AI_CalcDamageSaveBattlers(chosenMove, battlerAtk, battlerDef, &effectiveness, FALSE, DMG_ROLL_AVERAGE) >= gBattleMons[battlerDef].hp) return FALSE; // don't waste damaging z move if can otherwise faint target return TRUE; diff --git a/test/battle/ai.c b/test/battle/ai.c index 28e541befd58..e673be4e6a3a 100644 --- a/test/battle/ai.c +++ b/test/battle/ai.c @@ -679,7 +679,7 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES); PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_WING_ATTACK, MOVE_BOOMBURST); Speed(5); } OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_NONE); Speed(4); } // Forces switchout - OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_HEADBUTT); Speed(4); } // Mid battle, AI sends out Aron + OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_HEADBUTT); Speed(4); SpDefense(41); } // Mid battle, AI sends out Aron OPPONENT(SPECIES_ELECTRODE) { Level(30); Moves(MOVE_CHARGE_BEAM); Speed(6); } } WHEN { TURN { MOVE(player, MOVE_WING_ATTACK); EXPECT_SWITCH(opponent, 1); }