Add Dynamic Switch AI Function (#8629)

This commit is contained in:
Pawkkie 2025-12-24 20:02:01 -05:00 committed by GitHub
parent 9d62b2327f
commit 0551fcf408
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 130 additions and 155 deletions

View File

@ -2129,6 +2129,11 @@
.4byte \func
.endm
.macro setdynamicswitchaifunc func:req
callnative ScriptSetDynamicAiSwitchFunc, requests_effects=1
.4byte \func
.endm
@ Set up a totem boost for the next battle.
@ 'battler' is the position of the mon you want to gain a boost. see B_POSITION_xx in include/constants/battle.h.
@ The rest of the arguments are the stat change values to each stat.

View File

@ -25,6 +25,7 @@
- [Tutorials]()
- [What are AI Flags?](tutorials/ai_flags.md)
- [How to add new AI Flags](tutorials/ai_logic.md)
- [What are Dynamic AI Functions?](tutorials/ai_dynamic_functions.md)
- [How to add new battle script commands/macros](tutorials/how_to_battle_script_command_macro.md)
- [How to add a new move](tutorials/how_to_new_move.md)
- [How to add a new trainer class]()

View File

@ -0,0 +1,27 @@
# What are Dynamic AI Functions?
Dynamic AI functions enable AI behaviour to be controlled on a per-battle basis by being set directly ahead of a particular battle when scripting. They allow for unique move scoring or switching decisions that are not applied at scale to multiple trainers or the entire AI as a whole.
As such they're really useful for one-shot battle setups like boss fights or totem Pokemon or narrative tie-ins like switch happy Jugglers that would benefit from specialized AI that only applies during those specific fights.
There are currently two different types of dynamic AI functions, one that affects move scoring and one that affects switching.
# How do I use the dynamic move scoring function?
There are a few steps involved:
- Be sure to set `AI_FLAG_DYNAMIC_FUNC` on the trainer you'll be using your unique behaviour for
- Write your custom AI logic. Our example for this is `AI_TagBattlePreferFoe`, and you should match its arguments and return structure in your own custom function.
- In the script the triggers the battle, add a call to `setdynamicaifunc` specifying your function, as in:
```
setdynamicaifunc AI_TagBattlePreferFoe
multi_2_vs_2 TRAINER_SIRIUS_NOVA_HYPERION_TAG, Text_NovaInsurgence_Arrival_Hyperion_Loss, TRAINER_SIRIUS_NOVA_DEIMOS_RECRUIT_TAG, Text_NovaInsurgence_Arrival_DeimosRecruit_Loss, TRAINER_SIRIUS_WHARF_TRITON_PARTNER, TRAINER_BACK_PIC_TRITON
```
That's it! The scoring function will be used in the battle immediately following it, and automatically cleared at the end of the battle. You can then use `setdynamicaifunc` with the same or a different AI scoring function as you see fit.
# How do I use the dynamic switching function?
There are a few steps involved:
- Write your custom AI logic. Our example for this is `ShouldSwitchDynFuncExample`, and you should match its arguments and return structure in your own custom function.
- In the script that triggers the battle, add a call to `setdynamicswitchaifunc` specifying your function, as in:
```
setdynamicswitchaifunc ShouldSwitchDynFuncExample
trainerbattle_single TRAINER_TIANA, Route102_Text_TianaIntro, Route102_Text_TianaDefeated
```
That's it! The switching function will be used in the battle immediately following it, and automatically cleared at the end of the battle. You can then use `setdynamicswitchaifunc` with the same or a different AI scoring function as you see fit.

View File

@ -3,6 +3,7 @@
typedef s32 (*AiScoreFunc)(u32, u32, u32, s32);
typedef bool32 (*AiSwitchFunc)(u32);
#define UNKNOWN_NO_OF_HITS UINT32_MAX
@ -132,8 +133,10 @@ void Ai_InitPartyStruct(void);
void Ai_UpdateSwitchInData(u32 battler);
void Ai_UpdateFaintData(u32 battler);
void SetAiLogicDataForTurn(struct AiLogicData *aiData);
void ResetDynamicAiFunc(void);
void ResetDynamicAiFunctions(void);
void AI_TrySwitchOrUseItem(u32 battler);
void CalcBattlerAiMovesData(struct AiLogicData *aiData, u32 battlerAtk, u32 battlerDef, u32 weather, u32 fieldStatus);
extern AiSwitchFunc gDynamicAiSwitchFunc;
#endif // GUARD_BATTLE_AI_MAIN_H

View File

@ -33,6 +33,7 @@ enum ShouldSwitchScenario
SHOULD_SWITCH_ATTACKING_STAT_MINUS_TWO,
SHOULD_SWITCH_ATTACKING_STAT_MINUS_THREE_PLUS,
SHOULD_SWITCH_ALL_SCORES_BAD,
SHOULD_SWITCH_DYN_FUNC,
};
enum SwitchType

View File

@ -302,6 +302,8 @@ bool32 IsPartyFullyHealedExceptBattler(u32 battler);
bool32 PartyHasMoveCategory(u32 battlerId, enum DamageCategory category);
bool32 SideHasMoveCategory(u32 battlerId, enum DamageCategory category);
void GetAIPartyIndexes(u32 battlerId, s32 *firstId, s32 *lastId);
u32 GetActiveBattlerIds(u32 battler, u32 *battlerIn1, u32 *battlerIn2);
bool32 IsPartyMonOnFieldOrChosenToSwitch(u32 partyIndex, u32 battlerIn1, u32 battlerIn2);
// score increases
u32 IncreaseStatUpScore(u32 battlerAtk, u32 battlerDef, enum StatChange statId);

View File

@ -24,6 +24,7 @@
#define SHOULD_SWITCH_ATTACKING_STAT_MINUS_TWO_PERCENTAGE 50
#define SHOULD_SWITCH_ATTACKING_STAT_MINUS_THREE_PLUS_PERCENTAGE 100
#define SHOULD_SWITCH_ALL_SCORES_BAD_PERCENTAGE 100
#define SHOULD_SWITCH_DYN_FUNC_PERCENTAGE 50 // Dynamic switching function switch chance
// AI smart switching chances for bad statuses
#define SHOULD_SWITCH_PERISH_SONG_PERCENTAGE 100

View File

@ -194,6 +194,7 @@ enum RandomTag
RNG_AI_SWITCH_TRAPPER,
RNG_AI_SWITCH_FREE_TURN,
RNG_AI_SWITCH_ALL_MOVES_BAD,
RNG_AI_SWITCH_DYN_FUNC,
RNG_AI_CONSERVE_TERA,
RNG_AI_SWITCH_ALL_SCORES_BAD,
RNG_AI_SWITCH_ABSORBING_HIDDEN_POWER,

View File

@ -49,6 +49,7 @@ static void AI_CompareDamagingMoves(u32 battlerAtk, u32 battlerDef);
// ewram
EWRAM_DATA const u8 *gAIScriptPtr = NULL; // Still used in contests
EWRAM_DATA AiScoreFunc sDynamicAiFunc = NULL;
EWRAM_DATA AiSwitchFunc gDynamicAiSwitchFunc = NULL;
// const rom data
static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score);
@ -417,10 +418,9 @@ static void SetupRandomRollsForAIMoveSelection(u32 battler)
void AI_TrySwitchOrUseItem(u32 battler)
{
struct Pokemon *party;
u8 battlerIn1, battlerIn2;
u32 battlerIn1, battlerIn2;
s32 firstId;
s32 lastId; // + 1
u8 battlerPosition = GetBattlerPosition(battler);
party = GetBattlerParty(battler);
if (gBattleTypeFlags & BATTLE_TYPE_TRAINER)
@ -434,30 +434,14 @@ void AI_TrySwitchOrUseItem(u32 battler)
s32 monToSwitchId = gAiLogicData->mostSuitableMonId[battler];
if (monToSwitchId == PARTY_SIZE)
{
if (!IsDoubleBattle())
{
battlerIn1 = GetBattlerAtPosition(battlerPosition);
battlerIn2 = battlerIn1;
}
else
{
battlerIn1 = GetBattlerAtPosition(battlerPosition);
battlerIn2 = GetBattlerAtPosition(BATTLE_PARTNER(battlerPosition));
}
GetActiveBattlerIds(battler, &battlerIn1, &battlerIn2);
GetAIPartyIndexes(battler, &firstId, &lastId);
for (monToSwitchId = (lastId-1); monToSwitchId >= firstId; monToSwitchId--)
{
if (!IsValidForBattle(&party[monToSwitchId]))
continue;
if (monToSwitchId == gBattlerPartyIndexes[battlerIn1])
continue;
if (monToSwitchId == gBattlerPartyIndexes[battlerIn2])
continue;
if (monToSwitchId == gBattleStruct->monToSwitchIntoId[battlerIn1])
continue;
if (monToSwitchId == gBattleStruct->monToSwitchIntoId[battlerIn2])
if (IsPartyMonOnFieldOrChosenToSwitch(monToSwitchId, battlerIn1, battlerIn2))
continue;
if (IsAceMon(battler, monToSwitchId))
continue;
@ -7081,7 +7065,16 @@ void ScriptSetDynamicAiFunc(struct ScriptContext *ctx)
sDynamicAiFunc = func;
}
void ResetDynamicAiFunc(void)
void ScriptSetDynamicAiSwitchFunc(struct ScriptContext *ctx)
{
Script_RequestEffects(SCREFF_V1);
AiSwitchFunc func = (AiSwitchFunc)ScriptReadWord(ctx);
gDynamicAiSwitchFunc = func;
}
void ResetDynamicAiFunctions(void)
{
sDynamicAiFunc = NULL;
gDynamicAiSwitchFunc = NULL;
}

View File

@ -182,6 +182,8 @@ u32 GetSwitchChance(enum ShouldSwitchScenario shouldSwitchScenario)
return SHOULD_SWITCH_ATTACKING_STAT_MINUS_THREE_PLUS_PERCENTAGE;
case SHOULD_SWITCH_ALL_SCORES_BAD:
return SHOULD_SWITCH_ALL_SCORES_BAD_PERCENTAGE;
case SHOULD_SWITCH_DYN_FUNC:
return SHOULD_SWITCH_DYN_FUNC_PERCENTAGE;
default:
return 100;
}
@ -447,7 +449,7 @@ static bool32 ShouldSwitchIfAllMovesBad(u32 battler)
// Switch if no moves affect opponents
if (IsDoubleBattle())
{
u32 opposingPartner = GetBattlerAtPosition(BATTLE_PARTNER(opposingBattler));
u32 opposingPartner = BATTLE_PARTNER(opposingBattler);
for (moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++)
{
aiMove = gBattleMons[battler].moves[moveIndex];
@ -513,7 +515,7 @@ static bool32 ShouldSwitchIfWonderGuard(u32 battler)
static bool32 FindMonThatAbsorbsOpponentsMove(u32 battler)
{
u8 battlerIn1, battlerIn2;
u32 battlerIn1, battlerIn2;
u8 numAbsorbingAbilities = 0;
enum Ability absorbingTypeAbilities[3]; // Array size is maximum number of absorbing abilities for a single type
s32 firstId;
@ -554,20 +556,6 @@ static bool32 FindMonThatAbsorbsOpponentsMove(u32 battler)
}
}
if (IsDoubleBattle())
{
battlerIn1 = battler;
if (gAbsentBattlerFlags & (1u << GetPartnerBattler(battler)))
battlerIn2 = battler;
else
battlerIn2 = GetPartnerBattler(battler);
}
else
{
battlerIn1 = battler;
battlerIn2 = battler;
}
// Create an array of possible absorb abilities so the AI considers all of them
if (incomingType == TYPE_FIRE)
{
@ -620,10 +608,11 @@ static bool32 FindMonThatAbsorbsOpponentsMove(u32 battler)
return FALSE;
}
// Check party for mon with ability that absorbs move
GetActiveBattlerIds(battler, &battlerIn1, &battlerIn2);
GetAIPartyIndexes(battler, &firstId, &lastId);
party = GetBattlerParty(battler);
// Check party for mon with ability that absorbs move
for (u32 monIndex = firstId; monIndex < lastId; monIndex++)
{
if (!IsValidForBattle(&party[monIndex]))
@ -955,7 +944,7 @@ static bool32 CanUseSuperEffectiveMoveAgainstOpponents(u32 battler)
if (!IsDoubleBattle())
return FALSE;
opposingBattler = GetBattlerAtPosition(BATTLE_PARTNER(opposingPosition));
opposingBattler = BATTLE_PARTNER(opposingPosition);
if (!(gAbsentBattlerFlags & (1u << opposingBattler)))
{
@ -994,19 +983,7 @@ static bool32 FindMonWithFlagsAndSuperEffective(u32 battler, u16 flags, u32 perc
if (IsBattleMoveStatus(gLastLandedMoves[battler]))
return FALSE;
if (IsDoubleBattle())
{
battlerIn1 = battler;
if (gAbsentBattlerFlags & (1u << GetPartnerBattler(battler)))
battlerIn2 = battler;
else
battlerIn2 = GetPartnerBattler(battler);
}
else
{
battlerIn1 = battler;
battlerIn2 = battler;
}
GetActiveBattlerIds(battler, &battlerIn1, &battlerIn2);
GetAIPartyIndexes(battler, &firstId, &lastId);
party = GetBattlerParty(battler);
@ -1020,13 +997,7 @@ static bool32 FindMonWithFlagsAndSuperEffective(u32 battler, u16 flags, u32 perc
if (!IsValidForBattle(&party[monIndex]))
continue;
if (monIndex == gBattlerPartyIndexes[battlerIn1])
continue;
if (monIndex == gBattlerPartyIndexes[battlerIn2])
continue;
if (monIndex == gBattleStruct->monToSwitchIntoId[battlerIn1])
continue;
if (monIndex == gBattleStruct->monToSwitchIntoId[battlerIn2])
if (IsPartyMonOnFieldOrChosenToSwitch(monIndex, battlerIn1, battlerIn2))
continue;
if (IsAceMon(battler, monIndex))
continue;
@ -1070,20 +1041,7 @@ static bool32 CanMonSurviveHazardSwitchin(u32 battler)
// Battler will faint to hazards, check to see if another mon can clear them
if (hazardDamage > battlerHp)
{
if (IsDoubleBattle())
{
battlerIn1 = battler;
if (gAbsentBattlerFlags & (1u << GetPartnerBattler(battler)))
battlerIn2 = battler;
else
battlerIn2 = GetPartnerBattler(battler);
}
else
{
battlerIn1 = battler;
battlerIn2 = battler;
}
GetActiveBattlerIds(battler, &battlerIn1, &battlerIn2);
GetAIPartyIndexes(battler, &firstId, &lastId);
party = GetBattlerParty(battler);
@ -1091,13 +1049,7 @@ static bool32 CanMonSurviveHazardSwitchin(u32 battler)
{
if (!IsValidForBattle(&party[monIndex]))
continue;
if (monIndex == gBattlerPartyIndexes[battlerIn1])
continue;
if (monIndex == gBattlerPartyIndexes[battlerIn2])
continue;
if (monIndex == gBattleStruct->monToSwitchIntoId[battlerIn1])
continue;
if (monIndex == gBattleStruct->monToSwitchIntoId[battlerIn2])
if (IsPartyMonOnFieldOrChosenToSwitch(monIndex, battlerIn1, battlerIn2))
continue;
if (IsAceMon(battler, monIndex))
continue;
@ -1210,6 +1162,17 @@ static bool32 ShouldSwitchIfAttackingStatsLowered(u32 battler)
return FALSE;
}
bool32 ShouldSwitchDynFuncExample(u32 battler)
{
// Chance to switch if trainer class is Guitarist, perhaps thematic for Jugglers
if (GetTrainerClassFromId(TRAINER_BATTLE_PARAM.opponentA) == TRAINER_CLASS_GUITARIST
&& RandomPercentage(RNG_AI_SWITCH_DYN_FUNC, GetSwitchChance(SHOULD_SWITCH_DYN_FUNC)))
{
return SetSwitchinAndSwitch(battler, PARTY_SIZE);
}
return FALSE;
}
bool32 ShouldSwitch(u32 battler)
{
u32 battlerIn1, battlerIn2;
@ -1235,21 +1198,7 @@ bool32 ShouldSwitch(u32 battler)
availableToSwitch = 0;
if (IsDoubleBattle())
{
u32 partner = BATTLE_PARTNER(battler);
battlerIn1 = battler;
if (gAbsentBattlerFlags & (1u << partner))
battlerIn2 = battler;
else
battlerIn2 = partner;
}
else
{
battlerIn1 = battler;
battlerIn2 = battler;
}
GetActiveBattlerIds(battler, &battlerIn1, &battlerIn2);
GetAIPartyIndexes(battler, &firstId, &lastId);
party = GetBattlerParty(battler);
@ -1257,13 +1206,7 @@ bool32 ShouldSwitch(u32 battler)
{
if (!IsValidForBattle(&party[monIndex]))
continue;
if (monIndex == gBattlerPartyIndexes[battlerIn1])
continue;
if (monIndex == gBattlerPartyIndexes[battlerIn2])
continue;
if (monIndex == gBattleStruct->monToSwitchIntoId[battlerIn1])
continue;
if (monIndex == gBattleStruct->monToSwitchIntoId[battlerIn2])
if (IsPartyMonOnFieldOrChosenToSwitch(monIndex, battlerIn1, battlerIn2))
continue;
if (IsAceMon(battler, monIndex))
continue;
@ -1272,7 +1215,12 @@ bool32 ShouldSwitch(u32 battler)
}
if (availableToSwitch == 0)
return FALSE;
return FALSE;
// custom switching logic
// NOTE: needs to always end with `return SetSwitchinAndSwitch` or `return FALSE`
if (gDynamicAiSwitchFunc != NULL && gDynamicAiSwitchFunc(battler)) // Create custom AI functions for specific battles via "setdynamicswitchaifunc" cmd
return TRUE;
// NOTE: The sequence of the below functions matter! Do not change unless you have carefully considered the outcome.
// Since the order is sequential, and some of these functions prompt switch to specific party members.
@ -1393,21 +1341,7 @@ void ModifySwitchAfterMoveScoring(u32 battler)
availableToSwitch = 0;
if (IsDoubleBattle())
{
u32 partner = BATTLE_PARTNER(battler);
battlerIn1 = battler;
if (gAbsentBattlerFlags & (1u << partner))
battlerIn2 = battler;
else
battlerIn2 = partner;
}
else
{
battlerIn1 = battler;
battlerIn2 = battler;
}
GetActiveBattlerIds(battler, &battlerIn1, &battlerIn2);
GetAIPartyIndexes(battler, &firstId, &lastId);
party = GetBattlerParty(battler);
@ -2096,11 +2030,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
for (u32 monIndex = firstId; monIndex < lastId; monIndex++)
{
// Check mon validity
if (!IsValidForBattle(&party[monIndex])
|| gBattlerPartyIndexes[battlerIn1] == monIndex
|| gBattlerPartyIndexes[battlerIn2] == monIndex
|| monIndex == gBattleStruct->monToSwitchIntoId[battlerIn1]
|| monIndex == gBattleStruct->monToSwitchIntoId[battlerIn2])
if (!IsValidForBattle(&party[monIndex]) || IsPartyMonOnFieldOrChosenToSwitch(monIndex, battlerIn1, battlerIn2))
{
continue;
}
@ -2330,11 +2260,7 @@ static u32 GetBestMonVanilla(struct Pokemon *party, int firstId, int lastId, u32
for (u32 monIndex = firstId; monIndex < lastId; monIndex++)
{
// Check mon validity
if (!IsValidForBattle(&party[monIndex])
|| gBattlerPartyIndexes[battlerIn1] == monIndex
|| gBattlerPartyIndexes[battlerIn2] == monIndex
|| monIndex == gBattleStruct->monToSwitchIntoId[battlerIn1]
|| monIndex == gBattleStruct->monToSwitchIntoId[battlerIn2])
if (!IsValidForBattle(&party[monIndex]) || IsPartyMonOnFieldOrChosenToSwitch(monIndex, battlerIn1, battlerIn2))
{
continue;
}
@ -2421,11 +2347,7 @@ static u32 GetNextMonInParty(struct Pokemon *party, int firstId, int lastId, u32
for (u32 monIndex = firstId; monIndex < lastId; monIndex++)
{
// Check mon validity
if (!IsValidForBattle(&party[monIndex])
|| gBattlerPartyIndexes[battlerIn1] == monIndex
|| gBattlerPartyIndexes[battlerIn2] == monIndex
|| monIndex == gBattleStruct->monToSwitchIntoId[battlerIn1]
|| monIndex == gBattleStruct->monToSwitchIntoId[battlerIn2])
if (!IsValidForBattle(&party[monIndex]) || IsPartyMonOnFieldOrChosenToSwitch(monIndex, battlerIn1, battlerIn2))
{
continue;
}
@ -2448,25 +2370,7 @@ u32 GetMostSuitableMonToSwitchInto(u32 battler, enum SwitchType switchType)
if (gBattleTypeFlags & BATTLE_TYPE_ARENA)
return gBattlerPartyIndexes[battler] + 1;
if (IsDoubleBattle())
{
battlerIn1 = battler;
if (gAbsentBattlerFlags & (1u << GetPartnerBattler(battler)))
battlerIn2 = battler;
else
battlerIn2 = GetPartnerBattler(battler);
opposingBattler = BATTLE_OPPOSITE(battlerIn1);
if (gAbsentBattlerFlags & (1u << opposingBattler))
opposingBattler ^= BIT_FLANK;
}
else
{
opposingBattler = GetOppositeBattler(battler);
battlerIn1 = battler;
battlerIn2 = battler;
}
opposingBattler = GetActiveBattlerIds(battler, &battlerIn1, &battlerIn2);
GetAIPartyIndexes(battler, &firstId, &lastId);
party = GetBattlerParty(battler);

View File

@ -6320,3 +6320,40 @@ bool32 CanMoveBeBouncedBack(u32 battler, u32 move)
return FALSE;
}
u32 GetActiveBattlerIds(u32 battler, u32 *battlerIn1, u32 *battlerIn2)
{
u32 opposingBattler = 0;
u32 battlerPosition = GetBattlerPosition(battler);
if (IsDoubleBattle())
{
*battlerIn1 = battler;
if (gAbsentBattlerFlags & (1u << BATTLE_PARTNER(battler)))
*battlerIn2 = battler;
else
*battlerIn2 = GetBattlerAtPosition(BATTLE_PARTNER(battlerPosition));
opposingBattler = BATTLE_OPPOSITE(*battlerIn1);
if (gAbsentBattlerFlags & (1u << opposingBattler))
opposingBattler ^= BIT_FLANK;
}
else
{
opposingBattler = GetBattlerAtPosition(BATTLE_OPPOSITE(battlerPosition));
*battlerIn1 = battler;
*battlerIn2 = battler;
}
return opposingBattler;
}
bool32 IsPartyMonOnFieldOrChosenToSwitch(u32 partyIndex, u32 battlerIn1, u32 battlerIn2)
{
if (partyIndex == gBattlerPartyIndexes[battlerIn1]
|| partyIndex == gBattlerPartyIndexes[battlerIn2])
return TRUE;
if (partyIndex == gBattleStruct->monToSwitchIntoId[battlerIn1]
|| partyIndex == gBattleStruct->monToSwitchIntoId[battlerIn2])
return TRUE;
return FALSE;
}

View File

@ -1774,7 +1774,7 @@ static void FreeRestoreBattleData(void)
FreeMonSpritesGfx();
FreeBattleSpritesData();
FreeBattleResources();
ResetDynamicAiFunc();
ResetDynamicAiFunctions();
}
void CB2_QuitRecordedBattle(void)
@ -5612,7 +5612,7 @@ static void FreeResetData_ReturnToOvOrDoEvolutions(void)
{
ZeroEnemyPartyMons();
}
ResetDynamicAiFunc();
ResetDynamicAiFunctions();
FreeMonSpritesGfx();
FreeBattleResources();
FreeBattleSpritesData();