Skip to content

Commit

Permalink
Damage roll selection in AI_CalcDamage (#4615)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Pawkkie authored May 28, 2024
1 parent d999092 commit be25174
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 18 deletions.
13 changes: 10 additions & 3 deletions include/battle_ai_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion include/constants/battle_ai.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion src/battle_ai_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions src/battle_ai_switch_items.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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)))
Expand Down
38 changes: 30 additions & 8 deletions src/battle_ai_util.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion test/battle/ai.c
Original file line number Diff line number Diff line change
Expand Up @@ -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); }
Expand Down

0 comments on commit be25174

Please sign in to comment.