Add AI contextual damage roll configs (#9568)
Some checks are pending
CI / build (push) Waiting to run
CI / docs_validate (push) Waiting to run
CI / allcontributors (push) Waiting to run

This commit is contained in:
Pawkkie 2026-03-19 11:31:20 -04:00 committed by GitHub
parent 48bf615a67
commit d12a6a46e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 151 additions and 35 deletions

View File

@ -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.

View File

@ -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);

View File

@ -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

View File

@ -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)))

View File

@ -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, ...) \

View File

@ -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

View File

@ -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;
}

View File

@ -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);

View File

@ -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); }
}

View File

@ -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); }
}
}