diff --git a/include/battle.h b/include/battle.h index 2a03310879..8b55b0e671 100644 --- a/include/battle.h +++ b/include/battle.h @@ -201,6 +201,7 @@ struct SimulatedDamage u16 minimum; u16 median; u16 maximum; + u16 random; }; // Ai Data used when deciding which move to use, computed only once before each turn's start. diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index f1afa95d10..fbd12b97d2 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -7,19 +7,24 @@ // Roll boundaries used by AI when scoring. Doesn't affect actual damage dealt. #define MAX_ROLL_PERCENTAGE DMG_ROLL_PERCENT_HI #define MIN_ROLL_PERCENTAGE DMG_ROLL_PERCENT_LO -#define DMG_ROLL_PERCENTAGE ((MAX_ROLL_PERCENTAGE + MIN_ROLL_PERCENTAGE + 1) / 2) // Controls the damage roll the AI sees for the default roll. By default the 9th roll is seen +#define DMG_ROLL_PERCENTAGE ((MAX_ROLL_PERCENTAGE + MIN_ROLL_PERCENTAGE + 1) / 2) // Controls the damage roll the AI sees for the median roll. By default the 9th roll is seen enum DamageRollType { DMG_ROLL_LOWEST, - DMG_ROLL_DEFAULT, + DMG_ROLL_MEDIAN, DMG_ROLL_HIGHEST, + DMG_ROLL_RANDOM, }; enum DamageCalcContext { AI_DEFENDING, AI_ATTACKING, + AI_SWITCHIN_DEFENDING, + AI_SWITCHIN_ATTACKING, + AI_SHOULD_SETUP_DEFENDING, + AI_ATTACKING_PARTNER, }; enum AiConsiderEndure @@ -104,7 +109,7 @@ bool32 AI_CanBattlerEscape(enum BattlerId battler); bool32 IsBattlerTrapped(enum BattlerId battlerAtk, enum BattlerId battlerDef); s32 AI_WhoStrikesFirst(enum BattlerId battlerAI, enum BattlerId battler, enum Move aiMoveConsidered, enum Move playerMoveConsidered, enum ConsiderPriority considerPriority); bool32 CanTargetFaintAi(enum BattlerId battlerDef, enum BattlerId battlerAtk); -u32 NoOfHitsForTargetToFaintBattler(enum BattlerId battlerDef, enum BattlerId battlerAtk, enum AiConsiderEndure considerEndure); +u32 NoOfHitsForTargetToFaintBattler(enum BattlerId battlerDef, enum BattlerId battlerAtk, enum DamageCalcContext calcContext, enum AiConsiderEndure considerEndure); void GetBestDmgMovesFromBattler(enum BattlerId battlerAtk, enum BattlerId battlerDef, enum DamageCalcContext calcContext, enum Move *bestMoves); u32 GetMoveIndex(enum BattlerId battler, enum Move move); bool32 IsBestDmgMove(enum BattlerId battlerAtk, enum BattlerId battlerDef, enum DamageCalcContext calcContext, enum Move move); diff --git a/include/config/ai.h b/include/config/ai.h index 30880b018a..ba976ebd39 100644 --- a/include/config/ai.h +++ b/include/config/ai.h @@ -84,6 +84,21 @@ #define AI_DAMAGES_THROUGH_BERRIES TRUE // AI will see through resist berries when considering a certain KO threshold for the purposes damage calcs; this is considered when comparing best moves to KO to still pick the actual OHKO if needed #define AI_IGNORE_BERRY_KO_THRESHOLD 2 // KO threshold AI must meet in order to treat it berry though it doesn't exist (ie. 2 means "If the AI can 2HKO with berry resisted attack + not-berry resisted next attack, ignore berry resistence when calcing first attack"). Requires AI_DAMAGES_THROUGH_BERRIES +// AI damage calc roll considerations +#define AI_ROLL_MIN 1 +#define AI_ROLL_MEDIAN 2 +#define AI_ROLL_MAX 3 +#define AI_ROLL_RANDOM 4 +#define AI_ROLL_TYPE_COUNT 5 + +// Define which roll type to use in each context; overridden by AI_FLAG_RISKY and AI_FLAG_CONSERVATIVE +#define AI_ROLL_ATTACKING AI_ROLL_MAX +#define AI_ROLL_DEFENDING AI_ROLL_MEDIAN +#define AI_ROLL_SWITCHIN_ATTACKING AI_ROLL_MEDIAN +#define AI_ROLL_SWITCHIN_DEFENDING AI_ROLL_MEDIAN +#define AI_ROLL_SHOULD_SETUP_DEFENDING AI_ROLL_MAX +#define AI_ROLL_ATTACKING_PARTNER AI_ROLL_MAX + // AI prediction chances #define PREDICT_SWITCH_CHANCE 50 #define PREDICT_MOVE_CHANCE 100 diff --git a/include/constants/config_changes.h b/include/constants/config_changes.h index d1ff3b87d7..1c0abe77ee 100644 --- a/include/constants/config_changes.h +++ b/include/constants/config_changes.h @@ -233,6 +233,7 @@ F(POKERUS_WEAK_VARIANT, pokerusWeakVariant, (u32, TRUE)) \ #define AI_CONFIG_DEFINITIONS(F) \ + F(AI_ROLL_ATTACKING, aiRollAttacking, (u32, AI_ROLL_TYPE_COUNT - 1)) \ #define GET_CONFIG_MAXIMUM(_typeMaxValue, ...) INVOKE_WITH_B(GET_CONFIG_MAXIMUM_, _typeMaxValue) #define GET_CONFIG_MAXIMUM_(_type, ...) FIRST(__VA_OPT__(FIRST(__VA_ARGS__),) MAX_BITS((sizeof(_type) * 8))) diff --git a/include/random.h b/include/random.h index 00a1e4e866..eedf6854f3 100644 --- a/include/random.h +++ b/include/random.h @@ -244,6 +244,7 @@ enum RandomTag RNG_FISHING_GEN3_STICKY, RNG_WILD_MON_TARGET, RNG_AI_FAKE_OUT_SAVE_ALLY, + RNG_AI_DMG_ROLL_RANDOM, }; #define RandomWeighted(tag, ...) \ diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index e8d45f37d3..e8c7781510 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -3203,8 +3203,8 @@ static s32 AI_DoubleBattle(enum BattlerId battlerAtk, enum BattlerId battlerDef, bool32 hasTwoOpponents = HasTwoOpponents(battlerAtk); bool32 hasPartner = HasPartner(battlerAtk); u32 friendlyFireThreshold = GetFriendlyFireKOThreshold(battlerAtk); - u32 noOfHitsToKOPartner = GetNoOfHitsToKOBattler(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING, CONSIDER_ENDURE); - bool32 wouldPartnerFaint = hasPartner && CanIndexMoveFaintTarget(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING) && !partnerProtecting; + u32 noOfHitsToKOPartner = GetNoOfHitsToKOBattler(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING_PARTNER, CONSIDER_ENDURE); + bool32 wouldPartnerFaint = hasPartner && CanIndexMoveFaintTarget(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING_PARTNER) && !partnerProtecting; bool32 isFriendlyFireOK = !wouldPartnerFaint && (noOfHitsToKOPartner == 0 || noOfHitsToKOPartner > friendlyFireThreshold); // check what effect partner is using @@ -4104,7 +4104,7 @@ static enum MoveComparisonResult CompareMoveTwoTurnEffect(enum BattlerId battler static inline bool32 ShouldUseSpreadDamageMove(enum BattlerId battlerAtk, enum Move move, u32 moveIndex, u32 hitsToFaintOpposingBattler) { enum BattlerId partnerBattler = BATTLE_PARTNER(battlerAtk); - u32 noOfHitsToFaintPartner = GetNoOfHitsToKOBattler(battlerAtk, partnerBattler, moveIndex, AI_ATTACKING, CONSIDER_ENDURE); + u32 noOfHitsToFaintPartner = GetNoOfHitsToKOBattler(battlerAtk, partnerBattler, moveIndex, AI_ATTACKING_PARTNER, CONSIDER_ENDURE); u32 friendlyFireThreshold = GetFriendlyFireKOThreshold(battlerAtk); return (HasPartnerIgnoreFlags(battlerAtk) && noOfHitsToFaintPartner != 0 // Immunity check diff --git a/src/battle_ai_switch.c b/src/battle_ai_switch.c index 532091a00e..a788dcc24f 100644 --- a/src/battle_ai_switch.c +++ b/src/battle_ai_switch.c @@ -378,7 +378,7 @@ static bool32 ShouldSwitchIfHasBadOdds(enum BattlerId battler) hasSuperEffectiveMove = TRUE; // Check if can win 1v1 - hitsToKOPlayer = GetNoOfHitsToKOBattler(battler, opposingBattler, moveIndex, AI_ATTACKING, CONSIDER_ENDURE); + hitsToKOPlayer = GetNoOfHitsToKOBattler(battler, opposingBattler, moveIndex, AI_SWITCHIN_ATTACKING, CONSIDER_ENDURE); if (!canBattlerWin1v1 ) // Once we can win a 1v1 we don't need to track this, but want to run the rest of the function to keep the runtime the same regardless of when we find the winning move { isBattlerFirst = AI_IsFaster(battler, opposingBattler, aiMove, expectedMove, CONSIDER_PRIORITY); @@ -625,7 +625,7 @@ static bool32 FindMonThatAbsorbsOpponentsMove(enum BattlerId battler) // Only check damage if it's a damaging move if (!IsBattleMoveStatus(aiMove)) { - if (!AI_DoesChoiceEffectBlockMove(battler, aiMove) && AI_GetDamage(battler, opposingBattler, moveIndex, AI_ATTACKING, gAiLogicData) > gBattleMons[opposingBattler].hp) + if (!AI_DoesChoiceEffectBlockMove(battler, aiMove) && AI_GetDamage(battler, opposingBattler, moveIndex, AI_SWITCHIN_ATTACKING, gAiLogicData) > gBattleMons[opposingBattler].hp) return FALSE; } } @@ -2024,7 +2024,7 @@ static s32 GetMaxDamagePlayerCouldDealToSwitchin(enum BattlerId battler, enum Ba playerMove = SMART_SWITCHING_OMNISCIENT ? gBattleMons[opposingBattler].moves[moveIndex] : playerMoves[moveIndex]; if (playerMove != MOVE_NONE && !IsBattleMoveStatus(playerMove) && GetMoveEffect(playerMove) != EFFECT_FOCUS_PUNCH && gBattleMons[opposingBattler].pp[moveIndex] > 0) { - damageTaken = AI_GetDamage(opposingBattler, battler, moveIndex, AI_DEFENDING, gAiLogicData); + damageTaken = AI_GetDamage(opposingBattler, battler, moveIndex, AI_SWITCHIN_DEFENDING, gAiLogicData); if (playerMove == gBattleStruct->choicedMove[opposingBattler]) // If player is choiced, only care about the choice locked move { *bestPlayerMove = playerMove; @@ -2055,7 +2055,7 @@ static s32 GetMaxPriorityDamagePlayerCouldDealToSwitchin(enum BattlerId battler, if (GetBattleMovePriority(opposingBattler, gAiLogicData->abilities[opposingBattler], playerMove) > 0 && playerMove != MOVE_NONE && !IsBattleMoveStatus(playerMove) && GetMoveEffect(playerMove) != EFFECT_FOCUS_PUNCH && gBattleMons[opposingBattler].pp[moveIndex] > 0) { - damageTaken = AI_GetDamage(opposingBattler, battler, moveIndex, AI_DEFENDING, gAiLogicData); + damageTaken = AI_GetDamage(opposingBattler, battler, moveIndex, AI_SWITCHIN_DEFENDING, gAiLogicData); if (playerMove == gBattleStruct->choicedMove[opposingBattler]) // If player is choiced, only care about the choice locked move { *bestPlayerPriorityMove = playerMove; @@ -2221,8 +2221,8 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, continue; aiMove = gBattleMons[battler].moves[moveIndex]; - damageDealt = AI_GetDamage(battler, opposingBattler, moveIndex, AI_ATTACKING, gAiLogicData); - hitsToKOPlayer = GetNoOfHitsToKOBattler(battler, opposingBattler, moveIndex, AI_ATTACKING, CONSIDER_ENDURE); + damageDealt = AI_GetDamage(battler, opposingBattler, moveIndex, AI_SWITCHIN_ATTACKING, gAiLogicData); + hitsToKOPlayer = GetNoOfHitsToKOBattler(battler, opposingBattler, moveIndex, AI_SWITCHIN_ATTACKING, CONSIDER_ENDURE); // Offensive switchin decisions are based on which whether switchin moves first and whether it can win a 1v1 isSwitchinFirst = AI_IsFaster(battler, opposingBattler, aiMove, bestPlayerMove, CONSIDER_PRIORITY); @@ -2480,7 +2480,7 @@ static u32 GetBestMonVanilla(struct Pokemon *party, int firstId, int lastId, enu // Best damage if (aiMove != MOVE_NONE && !IsBattleMoveStatus(aiMove)) { - u32 aiDmg = AI_GetDamage(battler, opposingBattler, moveIndex, AI_ATTACKING, gAiLogicData); + u32 aiDmg = AI_GetDamage(battler, opposingBattler, moveIndex, AI_SWITCHIN_ATTACKING, gAiLogicData); if (aiDmg > bestDamage) { bestDamage = aiDmg; @@ -2625,7 +2625,7 @@ u32 AI_SelectRevivalBlessingMon(enum BattlerId battler) if (aiMove == MOVE_NONE || gBattleMons[battler].pp[moveIndex] == 0) continue; - s32 damage = AI_GetDamage(battler, opposingBattler, moveIndex, AI_ATTACKING, gAiLogicData); + s32 damage = AI_GetDamage(battler, opposingBattler, moveIndex, AI_SWITCHIN_ATTACKING, gAiLogicData); if (damage > bestDamage) bestDamage = damage; } diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index ff6218b7f4..534608c7df 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -91,6 +91,23 @@ enum MoveTarget AI_GetBattlerMoveTargetType(enum BattlerId battler, enum Move mo return GetMoveTarget(move); } +u32 AI_GetDefaultDamageRollForContext(enum BattlerId battlerAtk, enum BattlerId battlerDef, u32 moveIndex, struct AiLogicData *aiData, u32 aiRoll) +{ + switch (aiRoll) + { + case AI_ROLL_MIN: + return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].minimum; + case AI_ROLL_MEDIAN: + return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].median; + case AI_ROLL_MAX: + return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].maximum; + case AI_ROLL_RANDOM: + return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].random; + default: + return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].median; // Default assumes it deals median damage + } +} + u32 AI_GetDamage(enum BattlerId battlerAtk, enum BattlerId battlerDef, u32 moveIndex, enum DamageCalcContext calcContext, struct AiLogicData *aiData) { if (calcContext == AI_ATTACKING && BattlerHasAi(battlerAtk)) @@ -99,7 +116,7 @@ u32 AI_GetDamage(enum BattlerId battlerAtk, enum BattlerId battlerDef, u32 moveI return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].maximum; if ((gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_CONSERVATIVE) && !(gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_RISKY)) // Conservative assumes it deals min damage return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].minimum; - return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].median; // Default assumes it deals median damage + return AI_GetDefaultDamageRollForContext(battlerAtk, battlerDef, moveIndex, aiData, GetConfig(AI_ROLL_ATTACKING)); } else if (calcContext == AI_DEFENDING && BattlerHasAi(battlerDef)) { @@ -107,7 +124,39 @@ u32 AI_GetDamage(enum BattlerId battlerAtk, enum BattlerId battlerDef, u32 moveI return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].minimum; if ((gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_CONSERVATIVE) && !(gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_RISKY)) // Conservative assumes it takes max damage return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].maximum; - return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].median; // Default assumes it takes median damage + return AI_GetDefaultDamageRollForContext(battlerAtk, battlerDef, moveIndex, aiData, AI_ROLL_DEFENDING); + } + else if (calcContext == AI_SWITCHIN_ATTACKING && BattlerHasAi(battlerAtk)) + { + if ((gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_RISKY) && !(gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_CONSERVATIVE)) // Risky assumes it deals max damage + return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].maximum; + if ((gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_CONSERVATIVE) && !(gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_RISKY)) // Conservative assumes it deals min damage + return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].minimum; + return AI_GetDefaultDamageRollForContext(battlerAtk, battlerDef, moveIndex, aiData, AI_ROLL_SWITCHIN_ATTACKING); + } + else if (calcContext == AI_SWITCHIN_DEFENDING && BattlerHasAi(battlerDef)) + { + if ((gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_RISKY) && !(gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_CONSERVATIVE)) // Risky assumes it takes min damage + return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].minimum; + if ((gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_CONSERVATIVE) && !(gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_RISKY)) // Conservative assumes it takes max damage + return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].maximum; + return AI_GetDefaultDamageRollForContext(battlerAtk, battlerDef, moveIndex, aiData, AI_ROLL_SWITCHIN_DEFENDING); + } + else if (calcContext == AI_SHOULD_SETUP_DEFENDING && BattlerHasAi(battlerDef)) + { + if ((gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_RISKY) && !(gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_CONSERVATIVE)) // Risky assumes it takes min damage + return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].minimum; + if ((gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_CONSERVATIVE) && !(gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_RISKY)) // Conservative assumes it takes max damage + return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].maximum; + return AI_GetDefaultDamageRollForContext(battlerAtk, battlerDef, moveIndex, aiData, AI_ROLL_SHOULD_SETUP_DEFENDING); + } + else if (calcContext == AI_ATTACKING_PARTNER && BattlerHasAi(battlerAtk)) + { + if ((gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_RISKY) && !(gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_CONSERVATIVE)) // Risky assumes it deals max damage + return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].maximum; + if ((gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_CONSERVATIVE) && !(gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_RISKY)) // Conservative assumes it deals min damage + return aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex].minimum; + return AI_GetDefaultDamageRollForContext(battlerAtk, battlerDef, moveIndex, aiData, AI_ROLL_ATTACKING_PARTNER); } else { @@ -572,6 +621,14 @@ static inline s32 DmgRoll(s32 dmg) return dmg; } +static inline s32 RandomRollDmg(s32 dmg) +{ + u32 randomRollPercentage = RandomUniform(RNG_AI_DMG_ROLL_RANDOM, MIN_ROLL_PERCENTAGE, MAX_ROLL_PERCENTAGE); + dmg *= randomRollPercentage; + dmg /= 100; + return dmg; +} + bool32 IsDamageMoveUnusable(struct BattleContext *ctx) { enum Ability battlerDefAbility; @@ -682,6 +739,8 @@ static inline s32 GetDamageByRollType(s32 dmg, enum DamageRollType rollType) return LowestRollDmg(dmg); else if (rollType == DMG_ROLL_HIGHEST) return HighestRollDmg(dmg); + else if (rollType == DMG_ROLL_RANDOM) + return RandomRollDmg(dmg); else return DmgRoll(dmg); } @@ -700,12 +759,13 @@ static inline void AI_RestoreBattlerTypes(enum BattlerId battlerAtk, enum Type * gBattleMons[battlerAtk].types[2] = types[2]; } -static inline void CalcDynamicMoveDamage(struct BattleContext *ctx, u16 *medianDamage, u16 *minimumDamage, u16 *maximumDamage) +static inline void CalcDynamicMoveDamage(struct BattleContext *ctx, u16 *medianDamage, u16 *minimumDamage, u16 *maximumDamage, u16 *randomDamage) { enum BattleMoveEffects effect = GetMoveEffect(ctx->move); u16 median = *medianDamage; u16 minimum = *minimumDamage; u16 maximum = *maximumDamage; + u16 random = *randomDamage; u32 strikeCount = GetMoveStrikeCount(ctx->move); @@ -719,7 +779,7 @@ static inline void CalcDynamicMoveDamage(struct BattleContext *ctx, u16 *medianD median = 0; for (i = 0; i < partyCount; i++) median += CalculateMoveDamage(ctx); - maximum = minimum = median; + maximum = minimum = median = random; gBattleStruct->beatUpSlot = 0; } else if (strikeCount > 1 && effect != EFFECT_TRIPLE_KICK) @@ -727,6 +787,7 @@ static inline void CalcDynamicMoveDamage(struct BattleContext *ctx, u16 *medianD median *= strikeCount; minimum *= strikeCount; maximum *= strikeCount; + random *= strikeCount; } else if (IsMultiHitMove(ctx->move)) { @@ -735,12 +796,14 @@ static inline void CalcDynamicMoveDamage(struct BattleContext *ctx, u16 *medianD median *= GetMoveSpeciesPowerOverride_NumOfHits(ctx->move); minimum *= GetMoveSpeciesPowerOverride_NumOfHits(ctx->move); maximum *= GetMoveSpeciesPowerOverride_NumOfHits(ctx->move); + random *= GetMoveSpeciesPowerOverride_NumOfHits(ctx->move); } else if (ctx->abilityAtk == ABILITY_SKILL_LINK) { median *= 5; minimum *= 5; maximum *= 5; + random *= 5; } else if (ctx->holdEffectAtk == HOLD_EFFECT_LOADED_DICE) { @@ -748,12 +811,14 @@ static inline void CalcDynamicMoveDamage(struct BattleContext *ctx, u16 *medianD median /= 2; minimum *= 4; maximum *= 5; + random *= RandomUniform(RNG_AI_DMG_ROLL_RANDOM, 4, 5); } else { median *= 3; minimum *= 2; maximum *= 5; + random *= RandomUniform(RNG_AI_DMG_ROLL_RANDOM, 2, 5); } } else if (ctx->abilityAtk == ABILITY_PARENTAL_BOND @@ -763,6 +828,7 @@ static inline void CalcDynamicMoveDamage(struct BattleContext *ctx, u16 *medianD median += median / (B_PARENTAL_BOND_DMG >= GEN_7 ? 4 : 2); minimum += minimum / (B_PARENTAL_BOND_DMG >= GEN_7 ? 4 : 2); maximum += maximum / (B_PARENTAL_BOND_DMG >= GEN_7 ? 4 : 2); + random += random / (B_PARENTAL_BOND_DMG >= GEN_7 ? 4 : 2); } if (median == 0) @@ -771,10 +837,13 @@ static inline void CalcDynamicMoveDamage(struct BattleContext *ctx, u16 *medianD minimum = 1; if (maximum == 0) maximum = 1; + if (random == 0) + random = 1; *medianDamage = median; *minimumDamage = minimum; *maximumDamage = maximum; + *randomDamage = random; } static inline bool32 ShouldCalcCritDamage(struct BattleContext *ctx) @@ -903,7 +972,7 @@ struct SimulatedDamage AI_CalcDamage(enum Move move, enum BattlerId battlerAtk, s32 fixedDamage = DoFixedDamageMoveCalc(&ctx); if (fixedDamage != INT32_MAX) { - simDamage.minimum = simDamage.median = simDamage.maximum = fixedDamage; + simDamage.minimum = simDamage.median = simDamage.maximum = simDamage.random = fixedDamage; } else if (moveEffect == EFFECT_TRIPLE_KICK) { @@ -916,11 +985,14 @@ struct SimulatedDamage AI_CalcDamage(enum Move move, enum BattlerId battlerAtk, damageByRollType = GetDamageByRollType(oneTripleKickHit, DMG_ROLL_LOWEST); simDamage.minimum += AI_ApplyModifiersAfterDmgRoll(&ctx, damageByRollType); - damageByRollType = GetDamageByRollType(oneTripleKickHit, DMG_ROLL_DEFAULT); + damageByRollType = GetDamageByRollType(oneTripleKickHit, DMG_ROLL_MEDIAN); simDamage.median += AI_ApplyModifiersAfterDmgRoll(&ctx, damageByRollType); damageByRollType = GetDamageByRollType(oneTripleKickHit, DMG_ROLL_HIGHEST); simDamage.maximum += AI_ApplyModifiersAfterDmgRoll(&ctx, damageByRollType); + + damageByRollType = GetDamageByRollType(oneTripleKickHit, DMG_ROLL_RANDOM); + simDamage.random += AI_ApplyModifiersAfterDmgRoll(&ctx, damageByRollType); } } else @@ -930,15 +1002,18 @@ struct SimulatedDamage AI_CalcDamage(enum Move move, enum BattlerId battlerAtk, simDamage.minimum = GetDamageByRollType(damage, DMG_ROLL_LOWEST); simDamage.minimum = AI_ApplyModifiersAfterDmgRoll(&ctx, simDamage.minimum); - simDamage.median = GetDamageByRollType(damage, DMG_ROLL_DEFAULT); + simDamage.median = GetDamageByRollType(damage, DMG_ROLL_MEDIAN); simDamage.median = AI_ApplyModifiersAfterDmgRoll(&ctx, simDamage.median); simDamage.maximum = GetDamageByRollType(damage, DMG_ROLL_HIGHEST); simDamage.maximum = AI_ApplyModifiersAfterDmgRoll(&ctx, simDamage.maximum); + + simDamage.random = GetDamageByRollType(damage, DMG_ROLL_RANDOM); + simDamage.random = AI_ApplyModifiersAfterDmgRoll(&ctx, simDamage.random); } if (GetActiveGimmick(battlerAtk) != GIMMICK_Z_MOVE) - CalcDynamicMoveDamage(&ctx, &simDamage.median, &simDamage.minimum, &simDamage.maximum); + CalcDynamicMoveDamage(&ctx, &simDamage.median, &simDamage.minimum, &simDamage.maximum, &simDamage.random); AI_RestoreBattlerTypes(battlerAtk, types); } @@ -947,6 +1022,7 @@ struct SimulatedDamage AI_CalcDamage(enum Move move, enum BattlerId battlerAtk, simDamage.minimum = 0; simDamage.median = 0; simDamage.maximum = 0; + simDamage.random = 0; } // convert multiper to AI_EFFECTIVENESS_xX @@ -1492,14 +1568,14 @@ bool32 CanTargetFaintAi(enum BattlerId battlerDef, enum BattlerId battlerAtk) return FALSE; } -u32 NoOfHitsForTargetToFaintBattler(enum BattlerId battlerDef, enum BattlerId battlerAtk, enum AiConsiderEndure considerEndure) +u32 NoOfHitsForTargetToFaintBattler(enum BattlerId battlerDef, enum BattlerId battlerAtk, enum DamageCalcContext calcContext, enum AiConsiderEndure considerEndure) { u32 currNumberOfHits; u32 leastNumberOfHits = UNKNOWN_NO_OF_HITS; for (u32 moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++) { - currNumberOfHits = GetNoOfHitsToKOBattler(battlerDef, battlerAtk, moveIndex, AI_DEFENDING, considerEndure); + currNumberOfHits = GetNoOfHitsToKOBattler(battlerDef, battlerAtk, moveIndex, calcContext, considerEndure); if (currNumberOfHits != 0) { if (currNumberOfHits < leastNumberOfHits) @@ -1546,7 +1622,7 @@ void GetBestDmgMovesFromBattler(enum BattlerId battlerAtk, enum BattlerId battle { for (u32 moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++) { - if (CanIndexMoveFaintTarget(battlerAtk, battlerDef, moveIndex, AI_ATTACKING)) + if (CanIndexMoveFaintTarget(battlerAtk, battlerDef, moveIndex, calcContext)) bestMoves[countBestMoves++] = moves[moveIndex]; } } @@ -1592,7 +1668,7 @@ bool32 IsBestDmgMove(enum BattlerId battlerAtk, enum BattlerId battlerDef, enum enum Move bestMoves[MAX_MON_MOVES] = {MOVE_NONE}; u32 index = GetMoveIndex(battlerAtk, move); - if (CanIndexMoveFaintTarget(battlerAtk, battlerDef, index, AI_ATTACKING)) + if (CanIndexMoveFaintTarget(battlerAtk, battlerDef, index, calcContext)) return TRUE; GetBestDmgMovesFromBattler(battlerAtk, battlerDef, calcContext, bestMoves); @@ -2463,7 +2539,7 @@ bool32 CanIndexMoveFaintTarget(enum BattlerId battlerAtk, enum BattlerId battler enum Move *moves = gBattleMons[battlerAtk].moves; if (IsDoubleBattle() && battlerDef == BATTLE_PARTNER(battlerAtk)) - dmg = gAiLogicData->simulatedDmg[battlerAtk][battlerDef][moveIndex].maximum; // Attacking partner, be careful + dmg = AI_GetDamage(battlerAtk, battlerDef, moveIndex, AI_ATTACKING_PARTNER, gAiLogicData); // Attacking partner, be careful else dmg = AI_GetDamage(battlerAtk, battlerDef, moveIndex, calcContext, gAiLogicData); @@ -3948,7 +4024,7 @@ static inline bool32 RecoveryEnablesWinning1v1(enum BattlerId battlerAtk, enum B { if (!CanTargetFaintAi(battlerDef, battlerAtk) && GetBestDmgFromBattler(battlerDef, battlerAtk, AI_DEFENDING) < healAmount - && NoOfHitsForTargetToFaintBattler(battlerDef, battlerAtk, CONSIDER_ENDURE) < NoOfHitsForTargetToFaintBattlerWithMod(battlerDef, battlerAtk, healAmount)) + && NoOfHitsForTargetToFaintBattler(battlerDef, battlerAtk, AI_DEFENDING, CONSIDER_ENDURE) < NoOfHitsForTargetToFaintBattlerWithMod(battlerDef, battlerAtk, healAmount)) return TRUE; // target can't faint attacker and is dealing less damage than we're healing else if (!CanTargetFaintAi(battlerDef, battlerAtk) && gAiLogicData->hpPercents[battlerAtk] < ENABLE_RECOVERY_THRESHOLD && RandomPercentage(RNG_AI_SHOULD_RECOVER, SHOULD_RECOVER_CHANCE)) return TRUE; // target can't faint attacker at all, generally safe @@ -4810,7 +4886,7 @@ static u32 GetStagesOfStatChange(enum StatChange statChange) static enum AIScore IncreaseStatUpScoreInternal(enum BattlerId battlerAtk, enum BattlerId battlerDef, enum StatChange statChange, bool32 considerContrary) { enum AIScore tempScore = NO_INCREASE; - u32 noOfHitsToFaint = NoOfHitsForTargetToFaintBattler(battlerDef, battlerAtk, DONT_CONSIDER_ENDURE); + u32 noOfHitsToFaint = NoOfHitsForTargetToFaintBattler(battlerDef, battlerAtk, AI_SHOULD_SETUP_DEFENDING, DONT_CONSIDER_ENDURE); enum Move predictedMoveSpeedCheck = GetIncomingMoveSpeedCheck(battlerAtk, battlerDef, gAiLogicData); bool32 aiIsFaster = AI_IsFaster(battlerAtk, battlerDef, MOVE_NONE, predictedMoveSpeedCheck, DONT_CONSIDER_PRIORITY); // Don't care about the priority of our setup move, care about outspeeding otherwise bool32 shouldSetUp = ((noOfHitsToFaint >= 2 && aiIsFaster) || (noOfHitsToFaint >= 3 && !aiIsFaster) || noOfHitsToFaint == UNKNOWN_NO_OF_HITS); diff --git a/test/battle/ai/ai.c b/test/battle/ai/ai.c index cfb91ff771..81c2deda27 100644 --- a/test/battle/ai/ai.c +++ b/test/battle/ai/ai.c @@ -197,14 +197,14 @@ AI_SINGLE_BATTLE_TEST("AI prefers Earthquake over Drill Run if both require the } } -AI_SINGLE_BATTLE_TEST("AI prefers a weaker move over a one with a downside effect if both require the same number of hits to ko") +AI_SINGLE_BATTLE_TEST("AI prefers a weaker move over one with a downside effect if both require the same number of hits to ko") { enum Move move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE; enum Move expectedMove; u16 hp, turns; // Both moves require the same number of turns but Flamethrower will be chosen over Overheat (powerful effect) - PARAMETRIZE { move1 = MOVE_OVERHEAT; move2 = MOVE_FLAMETHROWER; hp = 300; expectedMove = MOVE_FLAMETHROWER; turns = 2; } + PARAMETRIZE { move1 = MOVE_OVERHEAT; move2 = MOVE_FLAMETHROWER; hp = 320; expectedMove = MOVE_FLAMETHROWER; turns = 2; } // Overheat kill in least amount of turns PARAMETRIZE { move1 = MOVE_OVERHEAT; move2 = MOVE_FLAMETHROWER; hp = 250; expectedMove = MOVE_OVERHEAT; turns = 1; } @@ -1007,7 +1007,7 @@ AI_SINGLE_BATTLE_TEST("AI has a chance to prioritize last chance priority damage GIVEN { AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT); PLAYER(SPECIES_CAMERUPT) { Speed(2); Moves(MOVE_FLAMETHROWER, MOVE_CELEBRATE); } - OPPONENT(SPECIES_FLOATZEL) { Level(90); Speed(1); HP(1); Moves(MOVE_WAVE_CRASH, MOVE_AQUA_JET); } + OPPONENT(SPECIES_FLOATZEL) { Level(85); Speed(1); HP(1); Moves(MOVE_WAVE_CRASH, MOVE_AQUA_JET); } } WHEN { TURN { MOVE(player, MOVE_CELEBRATE); EXPECT_MOVE(opponent, MOVE_AQUA_JET); } } diff --git a/test/battle/ai/ai_doubles.c b/test/battle/ai/ai_doubles.c index 0a276ca7cc..6ee7b6ec53 100644 --- a/test/battle/ai/ai_doubles.c +++ b/test/battle/ai/ai_doubles.c @@ -708,16 +708,33 @@ AI_DOUBLE_BATTLE_TEST("AI sees corresponding absorbing abilities on partners") GIVEN { ASSUME(GetMoveTarget(MOVE_DISCHARGE) == TARGET_FOES_AND_ALLY); - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_HP_AWARE); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); PLAYER(SPECIES_ZIGZAGOON); PLAYER(SPECIES_ZIGZAGOON); - OPPONENT(SPECIES_SLAKING) { Moves(move, MOVE_SCRATCH); } + OPPONENT(SPECIES_SLAKING) { Moves(move, MOVE_CONSTRICT); } OPPONENT(species) { HP(1); Ability(ability); Moves(MOVE_POUND, MOVE_EMBER, MOVE_ROUND); } } WHEN { if (ability != ABILITY_CLOUD_NINE) TURN { EXPECT_MOVE(opponentLeft, move); } else - TURN { EXPECT_MOVE(opponentLeft, MOVE_SCRATCH); } + TURN { EXPECT_MOVE(opponentLeft, MOVE_CONSTRICT); } + } +} + +AI_DOUBLE_BATTLE_TEST("AI sees random rolls correctly") +{ + PASSES_RANDOMLY(3, 15, RNG_AI_DMG_ROLL_RANDOM); // Slaking KOs with 3 rolls + GIVEN { + WITH_CONFIG(AI_ROLL_ATTACKING, AI_ROLL_RANDOM); + ASSUME(GetMoveTarget(MOVE_DISCHARGE) == TARGET_FOES_AND_ALLY); + ASSUME(GetMoveType(MOVE_DISCHARGE) == TYPE_ELECTRIC); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_ZIGZAGOON); + PLAYER(SPECIES_ZIGZAGOON); + OPPONENT(SPECIES_SLAKING) { Moves(MOVE_DISCHARGE, MOVE_SCRATCH); } + OPPONENT(SPECIES_PIKACHU) { HP(1); Ability(ABILITY_LIGHTNING_ROD); Moves(MOVE_SCRATCH); } + } WHEN { + TURN { EXPECT_MOVE(opponentLeft, MOVE_SCRATCH); } } }