mirror of
https://github.com/pret/pmd-red.git
synced 2026-09-28 22:50:05 -04:00
1074 lines
37 KiB
C
1074 lines
37 KiB
C
#include "global.h"
|
|
#include "dungeon_ai_attack.h"
|
|
#include "charge_move.h"
|
|
#include "code_8045A00.h"
|
|
#include "dungeon_message.h"
|
|
#include "code_806CD90.h"
|
|
#include "constants/direction.h"
|
|
#include "constants/dungeon_action.h"
|
|
#include "constants/iq_skill.h"
|
|
#include "constants/move_id.h"
|
|
#include "constants/status.h"
|
|
#include "constants/tactic.h"
|
|
#include "constants/targeting.h"
|
|
#include "constants/type.h"
|
|
#include "dungeon_action.h"
|
|
#include "dungeon_ai_targeting.h"
|
|
#include "dungeon_ai.h"
|
|
#include "dungeon_capabilities.h"
|
|
#include "dungeon_config.h"
|
|
#include "dungeon_engine.h"
|
|
#include "dungeon_map_access.h"
|
|
#include "dungeon_pokemon_attributes.h"
|
|
#include "dungeon_random.h"
|
|
#include "dungeon_util.h"
|
|
#include "dungeon_visibility.h"
|
|
#include "items.h"
|
|
#include "move_checks.h"
|
|
#include "move_util.h"
|
|
#include "moves.h"
|
|
#include "position_util.h"
|
|
#include "status_checks_1.h"
|
|
#include "status_checks.h"
|
|
#include "structs/str_dungeon.h"
|
|
#include "targeting_flags.h"
|
|
#include "targeting.h"
|
|
#include "type_effectiveness.h"
|
|
|
|
#define REGULAR_ATTACK_INDEX 4
|
|
|
|
extern const u8 *const gItemStickyDoesntWorkText[];
|
|
extern const u8 *const gPtrMysteriousPowerPreventedUseMessage[];
|
|
extern const u8 *const gUnknown_80FC6A8[];
|
|
extern const u8 *const gUnknown_80FC714[];
|
|
extern const u8 *const gUnknown_80FC718[];
|
|
|
|
EWRAM_DATA bool8 gCanAttackInDirection[NUM_DIRECTIONS] = {0};
|
|
EWRAM_DATA u8 gPotentialAttackTargetDirections[NUM_DIRECTIONS] = {0};
|
|
EWRAM_DATA s32 gPotentialAttackTargetWeights[NUM_DIRECTIONS] = {0};
|
|
EWRAM_DATA Entity *gPotentialTargets[NUM_DIRECTIONS] = {0};
|
|
|
|
extern void sub_8055A00(Entity *, u8, u32, u32, u32);
|
|
extern void sub_806A9B4(Entity *, u8);
|
|
extern bool8 sub_8044B28(void);
|
|
extern void sub_8057588(Entity *, u32);
|
|
extern void sub_806A1B0(Entity *);
|
|
extern Item *sub_8044D90(Entity *, s32, u32);
|
|
extern bool8 sub_8044D40(ActionContainer *, u32);
|
|
extern bool8 TryUseChosenMove(struct Entity *, u32, u32, u32, u32, struct Move *);
|
|
extern void sub_8045BF8(u8 *, struct Item *);
|
|
extern void sub_8044DF0(struct Entity *, u32, u32);
|
|
extern void sub_8071DA4(struct Entity *);
|
|
extern void sub_804AC20(DungeonPos *);
|
|
extern void sub_807EC28(bool8);
|
|
extern void sub_806A5B8(struct Entity *entity);
|
|
|
|
void ChooseAIMove(Entity *pokemon)
|
|
{
|
|
EntityInfo *pokemonInfo = GetEntInfo(pokemon);
|
|
s32 i;
|
|
s32 chargeStatus = STATUS_CHARGING;
|
|
struct AIPossibleMove aiPossibleMove[MAX_MON_MOVES + 1];
|
|
bool8 willNotUnlinkMove[MAX_MON_MOVES];
|
|
s32 randomWeight;
|
|
bool8 hasPPChecker;
|
|
s32 numUsableMoves;
|
|
s32 total;
|
|
s32 weightCounter;
|
|
bool8 hasWeakTypePicker;
|
|
s32 regularAttackTargetDir;
|
|
bool8 canTargetRegularAttack;
|
|
s32 maxWeight;
|
|
if (CannotAttack(pokemon, FALSE) ||
|
|
ShouldMonsterRunAwayAndShowEffect(pokemon, TRUE) ||
|
|
IsTacticSet(pokemon, TACTIC_KEEP_YOUR_DISTANCE) ||
|
|
(pokemonInfo->cringeClassStatus.status == STATUS_CONFUSED && DungeonRandOutcome(gAIConfusedAttackChance)))
|
|
{
|
|
return;
|
|
}
|
|
if (pokemonInfo->bideClassStatus.status != STATUS_NONE)
|
|
{
|
|
for (i = 0; i < MAX_MON_MOVES; i++)
|
|
{
|
|
if (pokemonInfo->moves.moves[i].moveFlags & MOVE_FLAG_EXISTS &&
|
|
MoveMatchesBideClassStatus(pokemon, &pokemonInfo->moves.moves[i]) &&
|
|
pokemonInfo->bideClassStatus.moveSlot == i)
|
|
{
|
|
s32 chosenMoveIndex;
|
|
SetMonsterActionFields(&pokemonInfo->action, ACTION_USE_MOVE_AI);
|
|
chosenMoveIndex = i;
|
|
if (i > 0 && pokemonInfo->moves.moves[i].moveFlags & MOVE_FLAG_SUBSEQUENT_IN_LINK_CHAIN)
|
|
{
|
|
while (--chosenMoveIndex > 0)
|
|
{
|
|
if (!(pokemonInfo->moves.moves[chosenMoveIndex].moveFlags & MOVE_FLAG_SUBSEQUENT_IN_LINK_CHAIN))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
pokemonInfo->action.actionParameters[0].actionUseIndex = chosenMoveIndex;
|
|
TargetTileInFront(pokemon);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
total = 0;
|
|
numUsableMoves = 0;
|
|
for (i = 0; i < MAX_MON_MOVES; i++)
|
|
{
|
|
Move *move = &pokemonInfo->moves.moves[i];
|
|
if (pokemonInfo->moves.moves[i].moveFlags & MOVE_FLAG_EXISTS)
|
|
{
|
|
if (pokemonInfo->moves.moves[i].moveFlags & MOVE_FLAG_ENABLED_FOR_AI)
|
|
{
|
|
numUsableMoves++;
|
|
}
|
|
total += move->PP;
|
|
}
|
|
}
|
|
if (total == 0)
|
|
{
|
|
Move struggle;
|
|
InitPokemonMove(&struggle, MOVE_STRUGGLE);
|
|
AIConsiderMove(&aiPossibleMove[0], pokemon, &struggle);
|
|
if (aiPossibleMove[0].canBeUsed)
|
|
{
|
|
SetMonsterActionFields(&pokemonInfo->action, ACTION_STRUGGLE);
|
|
pokemonInfo->action.direction = aiPossibleMove[0].direction & DIRECTION_MASK;
|
|
TargetTileInFront(pokemon);
|
|
}
|
|
return;
|
|
}
|
|
hasWeakTypePicker = IQSkillIsEnabled(pokemon, IQ_WEAK_TYPE_PICKER);
|
|
hasPPChecker = IQSkillIsEnabled(pokemon, IQ_PP_CHECKER) != FALSE;
|
|
total = 0;
|
|
for (i = 0; i < MAX_MON_MOVES; i++)
|
|
{
|
|
willNotUnlinkMove[i] = TRUE;
|
|
}
|
|
if (hasPPChecker)
|
|
{
|
|
s32 minPP = 99;
|
|
s32 linkedMoveStartIndex = 0;
|
|
// Linked moves unlink if any move in the chain runs out of PP.
|
|
// With PP Checker, the AI avoids this by not using linked moves if any move in the chain has 1 PP left.
|
|
// This requires a separate check from the 0-PP check used for unlinked moves.
|
|
for (i = 0; i < MAX_MON_MOVES; i++)
|
|
{
|
|
Move *move = &pokemonInfo->moves.moves[i];
|
|
if (!(move->moveFlags & MOVE_FLAG_EXISTS))
|
|
{
|
|
break;
|
|
}
|
|
if (i != 0 && !(move->moveFlags & MOVE_FLAG_SUBSEQUENT_IN_LINK_CHAIN))
|
|
{
|
|
if (linkedMoveStartIndex + 1 < i && minPP <= 1 && linkedMoveStartIndex + 1 <= i)
|
|
{
|
|
s32 j;
|
|
for (j = linkedMoveStartIndex; j < i; j++)
|
|
{
|
|
willNotUnlinkMove[j] = FALSE;
|
|
}
|
|
}
|
|
minPP = move->PP;
|
|
linkedMoveStartIndex = i;
|
|
}
|
|
else
|
|
{
|
|
s32 newMinPP = move->PP;
|
|
if (newMinPP > minPP)
|
|
{
|
|
newMinPP = minPP;
|
|
}
|
|
minPP = newMinPP;
|
|
}
|
|
}
|
|
if (linkedMoveStartIndex + 1 < i && minPP <= 1 && linkedMoveStartIndex + 1 <= i)
|
|
{
|
|
s32 j;
|
|
for (j = linkedMoveStartIndex; j < i; j++)
|
|
{
|
|
willNotUnlinkMove[j] = FALSE;
|
|
}
|
|
}
|
|
}
|
|
for (i = 0; i < MAX_MON_MOVES; i++)
|
|
{
|
|
Move *move;
|
|
aiPossibleMove[i].canBeUsed = FALSE;
|
|
move = &pokemonInfo->moves.moves[i];
|
|
if (move->moveFlags & MOVE_FLAG_EXISTS &&
|
|
willNotUnlinkMove[i] &&
|
|
CanAIUseMove(pokemon, i, hasPPChecker) &&
|
|
move->moveFlags & MOVE_FLAG_ENABLED_FOR_AI)
|
|
{
|
|
aiPossibleMove[i].canBeUsed = TRUE;
|
|
if (pokemonInfo->bideClassStatus.status == chargeStatus)
|
|
{
|
|
if (move->id == MOVE_CHARGE)
|
|
{
|
|
aiPossibleMove[i].weight = 0;
|
|
continue;
|
|
}
|
|
else if (GetMoveTypeForMonster(pokemon, move) == TYPE_ELECTRIC)
|
|
{
|
|
aiPossibleMove[i].weight = GetMoveAIWeight(move);
|
|
}
|
|
else
|
|
{
|
|
aiPossibleMove[i].weight = 1;
|
|
}
|
|
}
|
|
else if (hasWeakTypePicker)
|
|
{
|
|
struct AIPossibleMove *results = &aiPossibleMove[i];
|
|
results->weight = AIConsiderMove(results, pokemon, move);
|
|
}
|
|
else
|
|
{
|
|
aiPossibleMove[i].weight = GetMoveAIWeight(move);
|
|
}
|
|
total += aiPossibleMove[i].weight;
|
|
}
|
|
}
|
|
aiPossibleMove[REGULAR_ATTACK_INDEX].weight = 0;
|
|
if (!IQSkillIsEnabled(pokemon, IQ_EXCLUSIVE_MOVE_USER) && pokemonInfo->bideClassStatus.status != chargeStatus)
|
|
{
|
|
aiPossibleMove[REGULAR_ATTACK_INDEX].canBeUsed = TRUE;
|
|
if (pokemonInfo->bideClassStatus.status == chargeStatus)
|
|
{
|
|
// Never reached? Charge already skips the regular attack in the outer block.
|
|
aiPossibleMove[REGULAR_ATTACK_INDEX].weight = 1;
|
|
}
|
|
else if (hasWeakTypePicker)
|
|
{
|
|
aiPossibleMove[REGULAR_ATTACK_INDEX].weight = 2;
|
|
}
|
|
else
|
|
{
|
|
aiPossibleMove[REGULAR_ATTACK_INDEX].weight = gAiRegularAttackWeights[numUsableMoves];
|
|
}
|
|
total += aiPossibleMove[REGULAR_ATTACK_INDEX].weight;
|
|
}
|
|
if (hasWeakTypePicker)
|
|
{
|
|
s32 i;
|
|
maxWeight = 0;
|
|
total = 0;
|
|
for (i = 0; i < MAX_MON_MOVES + 1; i++)
|
|
{
|
|
if (!aiPossibleMove[i].canBeUsed)
|
|
{
|
|
aiPossibleMove[i].weight = 0;
|
|
}
|
|
else if (maxWeight < aiPossibleMove[i].weight)
|
|
{
|
|
maxWeight = aiPossibleMove[i].weight;
|
|
}
|
|
}
|
|
for (i = 0; i < MAX_MON_MOVES + 1; i++)
|
|
{
|
|
if (aiPossibleMove[i].canBeUsed)
|
|
{
|
|
if (maxWeight != aiPossibleMove[i].weight)
|
|
{
|
|
// Only the move(s) with the highest weight can be used, instead of a weighted random.
|
|
// This has the side effect of making the AI use a STAB ranged move consistently when at a distance.
|
|
aiPossibleMove[i].weight = 0;
|
|
}
|
|
total += aiPossibleMove[i].weight;
|
|
}
|
|
}
|
|
}
|
|
if (total == 0)
|
|
{
|
|
return;
|
|
}
|
|
randomWeight = DungeonRandInt(total);
|
|
weightCounter = 0;
|
|
if (!IQSkillIsEnabled(pokemon, IQ_EXCLUSIVE_MOVE_USER))
|
|
{
|
|
canTargetRegularAttack = TargetRegularAttack(pokemon, ®ularAttackTargetDir, TRUE);
|
|
}
|
|
else
|
|
{
|
|
canTargetRegularAttack = FALSE;
|
|
regularAttackTargetDir = DIRECTION_SOUTH;
|
|
}
|
|
for (i = 0; i <= REGULAR_ATTACK_INDEX; i++)
|
|
{
|
|
if (aiPossibleMove[i].canBeUsed && aiPossibleMove[i].weight != 0)
|
|
{
|
|
weightCounter += aiPossibleMove[i].weight;
|
|
if (weightCounter >= randomWeight)
|
|
{
|
|
if (i == REGULAR_ATTACK_INDEX)
|
|
{
|
|
if (canTargetRegularAttack)
|
|
{
|
|
SetMonsterActionFields(&pokemonInfo->action, ACTION_REGULAR_ATTACK);
|
|
pokemonInfo->action.direction = regularAttackTargetDir & DIRECTION_MASK;
|
|
TargetTileInFront(pokemon);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AIConsiderMove(&aiPossibleMove[i], pokemon, &pokemonInfo->moves.moves[i]);
|
|
if (aiPossibleMove[i].canBeUsed)
|
|
{
|
|
s32 chosenMoveIndex;
|
|
SetMonsterActionFields(&pokemonInfo->action, ACTION_USE_MOVE_AI);
|
|
chosenMoveIndex = i;
|
|
if (i > 0 && pokemonInfo->moves.moves[i].moveFlags & MOVE_FLAG_SUBSEQUENT_IN_LINK_CHAIN)
|
|
{
|
|
while (--chosenMoveIndex > 0)
|
|
{
|
|
if (!(pokemonInfo->moves.moves[chosenMoveIndex].moveFlags & MOVE_FLAG_SUBSEQUENT_IN_LINK_CHAIN))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
pokemonInfo->action.direction = aiPossibleMove[i].direction & DIRECTION_MASK;
|
|
pokemonInfo->action.actionParameters[0].actionUseIndex = chosenMoveIndex;
|
|
TargetTileInFront(pokemon);
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (canTargetRegularAttack)
|
|
{
|
|
SetMonsterActionFields(&pokemonInfo->action, ACTION_REGULAR_ATTACK);
|
|
pokemonInfo->action.direction = regularAttackTargetDir & DIRECTION_MASK;
|
|
TargetTileInFront(pokemon);
|
|
}
|
|
}
|
|
|
|
s32 AIConsiderMove(struct AIPossibleMove *aiPossibleMove, Entity *pokemon, Move *move)
|
|
{
|
|
s32 targetingFlags;
|
|
s32 moveWeight = 1;
|
|
EntityInfo *pokemonInfo = GetEntInfo(pokemon);
|
|
s32 numPotentialTargets = 0;
|
|
s32 i;
|
|
bool8 hasStatusChecker;
|
|
s32 rangeTargetingFlags;
|
|
s32 rangeTargetingFlags2;
|
|
for (i = 0; i < NUM_DIRECTIONS; i++)
|
|
{
|
|
gCanAttackInDirection[i] = FALSE;
|
|
}
|
|
targetingFlags = GetMoveTargetAndRangeForPokemon(pokemon, move, TRUE);
|
|
hasStatusChecker = IQSkillIsEnabled(pokemon, IQ_STATUS_CHECKER);
|
|
aiPossibleMove->canBeUsed = FALSE;
|
|
if ((pokemonInfo->cringeClassStatus.status == STATUS_TAUNTED && !MoveIgnoresTaunted(move)) ||
|
|
(hasStatusChecker && !CanUseOnSelfWithStatusChecker(pokemon, move)))
|
|
{
|
|
return 1;
|
|
}
|
|
rangeTargetingFlags = targetingFlags & 0xF0;
|
|
if (rangeTargetingFlags == TARGETING_FLAG_TARGET_OTHER ||
|
|
rangeTargetingFlags == TARGETING_FLAG_TARGET_FRONTAL_CONE ||
|
|
rangeTargetingFlags == TARGETING_FLAG_TARGET_AROUND)
|
|
{
|
|
if (pokemonInfo->blinkerClassStatus.status == STATUS_BLINKER)
|
|
{
|
|
u8 direction = pokemonInfo->action.direction;
|
|
i = direction; // Fixes a regswap.
|
|
if (!gCanAttackInDirection[i])
|
|
{
|
|
gCanAttackInDirection[i] = TRUE;
|
|
gPotentialAttackTargetDirections[numPotentialTargets] = i;
|
|
gPotentialAttackTargetWeights[numPotentialTargets] = 99;
|
|
gPotentialTargets[numPotentialTargets] = NULL;
|
|
numPotentialTargets++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (i = 0; i < NUM_DIRECTIONS; i++)
|
|
{
|
|
// Double assignment to fix a regswap.
|
|
s16 rangeTargetingFlags = rangeTargetingFlags2 = targetingFlags & 0xF0;
|
|
const Tile *adjacentTile = GetTile(pokemon->pos.x + gAdjacentTileOffsets[i].x,
|
|
pokemon->pos.y + gAdjacentTileOffsets[i].y);
|
|
Entity *adjacentPokemon = adjacentTile->monster;
|
|
if (adjacentPokemon != NULL && GetEntityType(adjacentPokemon) == ENTITY_MONSTER)
|
|
{
|
|
if (rangeTargetingFlags != TARGETING_FLAG_TARGET_FRONTAL_CONE &&
|
|
rangeTargetingFlags != TARGETING_FLAG_TARGET_AROUND)
|
|
{
|
|
if (!CanAttackInDirection(pokemon, i))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
numPotentialTargets = TryAddTargetToAITargetList(numPotentialTargets, targetingFlags, pokemon, adjacentPokemon, move, hasStatusChecker);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (rangeTargetingFlags == TARGETING_FLAG_TARGET_ROOM)
|
|
{
|
|
s32 i;
|
|
for (i = 0; i < DUNGEON_MAX_POKEMON; i++)
|
|
{
|
|
Entity *target = gDungeon->activePokemon[i];
|
|
if (EntityIsValid(target) && CanSeeTarget(pokemon, target))
|
|
{
|
|
numPotentialTargets = TryAddTargetToAITargetList(numPotentialTargets, targetingFlags, pokemon, target, move, hasStatusChecker);
|
|
}
|
|
}
|
|
}
|
|
else if (rangeTargetingFlags == TARGETING_FLAG_TARGET_2_TILES_AHEAD)
|
|
{
|
|
for (i = 0; i < NUM_DIRECTIONS; i++)
|
|
{
|
|
const Tile *targetTile = GetTile(pokemon->pos.x + gAdjacentTileOffsets[i].x,
|
|
pokemon->pos.y + gAdjacentTileOffsets[i].y);
|
|
if (CanAttackInDirection(pokemon, i))
|
|
{
|
|
Entity *targetPokemon = targetTile->monster;
|
|
if (targetPokemon != NULL && GetEntityType(targetPokemon) == ENTITY_MONSTER)
|
|
{
|
|
s32 prevNumPotentialTargets = numPotentialTargets;
|
|
numPotentialTargets = TryAddTargetToAITargetList(numPotentialTargets, targetingFlags, pokemon, targetPokemon, move, hasStatusChecker);
|
|
if (prevNumPotentialTargets != numPotentialTargets)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
targetTile = GetTile(pokemon->pos.x + gAdjacentTileOffsets[i].x * 2,
|
|
pokemon->pos.y + gAdjacentTileOffsets[i].y * 2);
|
|
targetPokemon = targetTile->monster;
|
|
if (targetPokemon != NULL && GetEntityType(targetPokemon) == ENTITY_MONSTER)
|
|
{
|
|
numPotentialTargets = TryAddTargetToAITargetList(numPotentialTargets, targetingFlags, pokemon, targetPokemon, move, hasStatusChecker);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (rangeTargetingFlags == TARGETING_FLAG_TARGET_LINE || rangeTargetingFlags == TARGETING_FLAG_CUT_CORNERS)
|
|
{
|
|
s32 maxRange = 1;
|
|
s32 i;
|
|
if (rangeTargetingFlags == TARGETING_FLAG_TARGET_LINE)
|
|
{
|
|
maxRange = 10;
|
|
}
|
|
for (i = 0; i < DUNGEON_MAX_POKEMON; i++)
|
|
{
|
|
Entity *target = gDungeon->activePokemon[i];
|
|
if (EntityIsValid(target) && pokemon != target)
|
|
{
|
|
s32 direction = GetDirectionTowardsPosition(&pokemon->pos, &target->pos);
|
|
if (!gCanAttackInDirection[direction] &&
|
|
CanSeeTarget(pokemon, target) &&
|
|
IsTargetInLineRange(pokemon, target, maxRange) &&
|
|
IsAITargetEligible(targetingFlags, pokemon, target, move, hasStatusChecker) &&
|
|
IsTargetInRange(pokemon, target, direction, maxRange))
|
|
{
|
|
gCanAttackInDirection[direction] = TRUE;
|
|
gPotentialAttackTargetDirections[numPotentialTargets] = direction;
|
|
gPotentialAttackTargetWeights[numPotentialTargets] = WeightMove(pokemon, targetingFlags, target, GetMoveTypeForMonster(pokemon, move));
|
|
gPotentialTargets[numPotentialTargets] = target;
|
|
numPotentialTargets++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (rangeTargetingFlags == TARGETING_FLAG_TARGET_FLOOR)
|
|
{
|
|
s32 i;
|
|
for (i = 0; i < DUNGEON_MAX_POKEMON; i++)
|
|
{
|
|
Entity *target = gDungeon->activePokemon[i];
|
|
if (EntityIsValid(target))
|
|
{
|
|
numPotentialTargets = TryAddTargetToAITargetList(numPotentialTargets, targetingFlags, pokemon, target, move, hasStatusChecker);
|
|
}
|
|
}
|
|
}
|
|
else if (rangeTargetingFlags == TARGETING_FLAG_TARGET_SELF)
|
|
{
|
|
numPotentialTargets = TryAddTargetToAITargetList(numPotentialTargets, targetingFlags, pokemon, pokemon, move, hasStatusChecker);
|
|
}
|
|
if (numPotentialTargets == 0)
|
|
{
|
|
aiPossibleMove->canBeUsed = FALSE;
|
|
}
|
|
else
|
|
{
|
|
s32 totalWeight = 0;
|
|
s32 maxWeight = 0;
|
|
s32 weightCounter;
|
|
s32 i;
|
|
for (i = 0; i < numPotentialTargets; i++)
|
|
{
|
|
if (maxWeight < gPotentialAttackTargetWeights[i])
|
|
{
|
|
maxWeight = gPotentialAttackTargetWeights[i];
|
|
}
|
|
}
|
|
for (i = 0; i < numPotentialTargets; i++)
|
|
{
|
|
if (maxWeight != gPotentialAttackTargetWeights[i])
|
|
{
|
|
gPotentialAttackTargetWeights[i] = 0;
|
|
}
|
|
}
|
|
moveWeight = maxWeight;
|
|
for (i = 0; i < numPotentialTargets; i++)
|
|
{
|
|
totalWeight += gPotentialAttackTargetWeights[i];
|
|
}
|
|
weightCounter = DungeonRandInt(totalWeight);
|
|
for (i = 0; i < numPotentialTargets; i++)
|
|
{
|
|
weightCounter -= gPotentialAttackTargetWeights[i];
|
|
if (weightCounter < 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
aiPossibleMove->canBeUsed = TRUE;
|
|
aiPossibleMove->direction = gPotentialAttackTargetDirections[i];
|
|
aiPossibleMove->weight = 8;
|
|
}
|
|
return moveWeight;
|
|
}
|
|
|
|
bool8 IsTargetInLineRange(Entity *user, Entity *target, s32 range)
|
|
{
|
|
s32 distanceX = user->pos.x - target->pos.x;
|
|
s32 distanceY, distance;
|
|
s32 direction;
|
|
if (distanceX < 0)
|
|
{
|
|
distanceX = -distanceX;
|
|
}
|
|
distanceY = user->pos.y - target->pos.y;
|
|
if (distanceY < 0)
|
|
{
|
|
distanceY = -distanceY;
|
|
}
|
|
distance = distanceY;
|
|
if (distanceY < distanceX)
|
|
{
|
|
distance = distanceX;
|
|
}
|
|
if (distance > RANGED_ATTACK_RANGE || distance > range)
|
|
{
|
|
return FALSE;
|
|
}
|
|
direction = -1;
|
|
if (distanceX == distanceY)
|
|
{
|
|
if (user->pos.x < target->pos.x &&
|
|
(user->pos.y < target->pos.y || user->pos.y > target->pos.y))
|
|
{
|
|
returnTrue:
|
|
return TRUE;
|
|
}
|
|
if (user->pos.x > target->pos.x); // Fixes register loading order.
|
|
direction = DIRECTION_SOUTHWEST;
|
|
if (user->pos.x <= target->pos.x || user->pos.y <= target->pos.y)
|
|
{
|
|
goto checkDirectionSet;
|
|
}
|
|
goto returnTrue;
|
|
}
|
|
else if (user->pos.x == target->pos.x && user->pos.y < target->pos.y)
|
|
{
|
|
return TRUE;
|
|
}
|
|
else if (user->pos.x < target->pos.x && user->pos.y == target->pos.y)
|
|
{
|
|
return TRUE;
|
|
}
|
|
else if (user->pos.x == target->pos.x && user->pos.y > target->pos.y)
|
|
{
|
|
return TRUE;
|
|
}
|
|
else if (user->pos.x > target->pos.x && user->pos.y == target->pos.y)
|
|
{
|
|
direction = DIRECTION_WEST;
|
|
}
|
|
checkDirectionSet:
|
|
if (direction < 0)
|
|
{
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
s32 TryAddTargetToAITargetList(s32 numPotentialTargets, s32 targetingFlags, Entity *user, Entity *target, Move *move, bool32 hasStatusChecker)
|
|
{
|
|
s32 direction;
|
|
s32 targetingFlags2 = (s16) targetingFlags;
|
|
bool8 hasStatusChecker2 = hasStatusChecker;
|
|
EntityInfo *userData = GetEntInfo(user);
|
|
if ((user->pos.x == target->pos.x && user->pos.y == target->pos.y) ||
|
|
(targetingFlags2 & 0xF0) == TARGETING_FLAG_TARGET_ROOM ||
|
|
(targetingFlags2 & 0xF0) == TARGETING_FLAG_TARGET_FLOOR ||
|
|
(targetingFlags2 & 0xF0) == TARGETING_FLAG_TARGET_SELF)
|
|
{
|
|
direction = userData->action.direction;
|
|
}
|
|
else
|
|
{
|
|
direction = GetDirectionTowardsPosition(&user->pos, &target->pos);
|
|
}
|
|
if (!gCanAttackInDirection[direction] &&
|
|
IsAITargetEligible(targetingFlags2, user, target, move, hasStatusChecker2))
|
|
{
|
|
gCanAttackInDirection[direction] = TRUE;
|
|
do { gPotentialAttackTargetDirections[numPotentialTargets] = direction; } while (0);
|
|
gPotentialAttackTargetWeights[numPotentialTargets] = WeightMove(user, targetingFlags2, target, GetMoveTypeForMonster(user, move));
|
|
gPotentialTargets[numPotentialTargets] = target;
|
|
numPotentialTargets++;
|
|
}
|
|
return numPotentialTargets;
|
|
}
|
|
|
|
bool8 IsAITargetEligible(s32 targetingFlags, Entity *user, Entity *target, Move *move, bool32 hasStatusChecker)
|
|
{
|
|
EntityInfo *targetData;
|
|
s32 targetingFlags2 = (s16) targetingFlags;
|
|
bool8 hasStatusChecker2 = hasStatusChecker;
|
|
bool8 hasTarget = FALSE;
|
|
u32 categoryTargetingFlags = targetingFlags2 & 0xF;
|
|
u32 *categoryTargetingFlags2 = &categoryTargetingFlags; // Fixes a regswap.
|
|
if (*categoryTargetingFlags2 == TARGETING_FLAG_TARGET_OTHER)
|
|
{
|
|
if (GetTreatmentBetweenMonsters(user, target, FALSE, TRUE) == TREATMENT_TREAT_AS_ENEMY)
|
|
{
|
|
hasTarget = TRUE;
|
|
}
|
|
}
|
|
else if (categoryTargetingFlags == TARGETING_FLAG_HEAL_TEAM)
|
|
{
|
|
goto checkCanTarget;
|
|
}
|
|
else if (categoryTargetingFlags == TARGETING_FLAG_LONG_RANGE)
|
|
{
|
|
targetData = GetEntInfo(target);
|
|
goto checkThirdParty;
|
|
}
|
|
else if (categoryTargetingFlags == TARGETING_FLAG_ATTACK_ALL)
|
|
{
|
|
targetData = GetEntInfo(target);
|
|
if (user == target)
|
|
{
|
|
goto returnFalse;
|
|
}
|
|
checkThirdParty:
|
|
hasTarget = TRUE;
|
|
if (targetData->shopkeeper == SHOPKEEPER_MODE_SHOPKEEPER ||
|
|
targetData->monsterBehavior == BEHAVIOR_DIGLETT ||
|
|
targetData->monsterBehavior == BEHAVIOR_RESCUE_TARGET)
|
|
{
|
|
returnFalse:
|
|
return FALSE;
|
|
}
|
|
}
|
|
else if (categoryTargetingFlags == TARGETING_FLAG_BOOST_TEAM)
|
|
{
|
|
if (user == target)
|
|
{
|
|
goto returnFalse;
|
|
}
|
|
checkCanTarget:
|
|
if (GetTreatmentBetweenMonsters(user, target, FALSE, TRUE) == TREATMENT_TREAT_AS_ALLY)
|
|
{
|
|
hasTarget = TRUE;
|
|
}
|
|
}
|
|
else if ((u16) (categoryTargetingFlags - 3) <= 1) // categoryTargetingFlags == TARGETING_FLAG_ITEM
|
|
{
|
|
hasTarget = TRUE;
|
|
}
|
|
|
|
if (hasTarget)
|
|
{
|
|
if (hasStatusChecker2)
|
|
{
|
|
if (!CanUseOnTargetWithStatusChecker(user, target, move))
|
|
{
|
|
goto returnFalse;
|
|
}
|
|
if ((targetingFlags2 & 0xF00) == TARGETING_FLAG_SET_TRAP)
|
|
{
|
|
goto rollMoveUseChance;
|
|
}
|
|
else if ((targetingFlags2 & 0xF00) == TARGETING_FLAG_HEAL_HP)
|
|
{
|
|
if (!HasLowHealth(target))
|
|
{
|
|
if (*categoryTargetingFlags2);
|
|
goto returnFalse;
|
|
}
|
|
}
|
|
else if ((targetingFlags2 & 0xF00) == TARGETING_FLAG_HEAL_STATUS)
|
|
{
|
|
if (!HasNegativeStatus(target))
|
|
{
|
|
if (*categoryTargetingFlags2); // Flips the conditional.
|
|
goto returnFalse;
|
|
}
|
|
}
|
|
else if ((targetingFlags2 & 0xF00) == TARGETING_FLAG_DREAM_EATER)
|
|
{
|
|
if (!IsSleeping(target))
|
|
{
|
|
if (*categoryTargetingFlags2); // Flips the conditional.
|
|
goto returnFalse;
|
|
}
|
|
}
|
|
else if ((targetingFlags2 & 0xF00) == TARGETING_FLAG_EXPOSE)
|
|
{
|
|
targetData = GetEntInfo(target);
|
|
if ((targetData->types[0] != TYPE_GHOST && targetData->types[1] != TYPE_GHOST) || targetData->exposed)
|
|
{
|
|
if (*categoryTargetingFlags2); // Flips the conditional.
|
|
goto returnFalse;
|
|
}
|
|
}
|
|
else if ((targetingFlags2 & 0xF00) == TARGETING_FLAG_HEAL_ALL)
|
|
{
|
|
if (!HasNegativeStatus(target) && !HasLowHealth(target))
|
|
{
|
|
if (*categoryTargetingFlags2); // Flips the conditional.
|
|
goto returnFalse;
|
|
}
|
|
}
|
|
}
|
|
else if ((targetingFlags2 & 0xF00) == TARGETING_FLAG_SET_TRAP)
|
|
{
|
|
s32 useChance;
|
|
rollMoveUseChance:
|
|
useChance = GetMoveAccuracyOrAIChance(move, ACCURACY_AI_CONDITION_RANDOM_CHANCE);
|
|
if (DungeonRandInt(100) >= useChance)
|
|
{
|
|
goto returnFalse;
|
|
}
|
|
}
|
|
}
|
|
return hasTarget;
|
|
}
|
|
|
|
s32 WeightMove(Entity *user, s32 targetingFlags, Entity *target, u32 moveType)
|
|
{
|
|
EntityInfo *targetData;
|
|
s32 targetingFlags2 = (s16) targetingFlags;
|
|
u8 moveType2 = moveType;
|
|
u8 weight = 1;
|
|
targetData = GetEntInfo(target);
|
|
if (!targetData->isNotTeamMember)
|
|
return 1;
|
|
if ((targetingFlags2 & 0xF) != TARGETING_FLAG_TARGET_OTHER)
|
|
return 1;
|
|
|
|
if (IQSkillIsEnabled(user, IQ_EXP_GO_GETTER))
|
|
{
|
|
// BUG: expYieldRankings has lower values as the Pokémon's experience yield increases.
|
|
// This causes Exp. Go-Getter to prioritize Pokémon worth less experience
|
|
// instead of Pokémon worth more experience.
|
|
weight = gDungeon->expYieldRankings[targetData->id];
|
|
}
|
|
else if (IQSkillIsEnabled(user, IQ_EFFICIENCY_EXPERT))
|
|
{
|
|
weight = -12 - targetData->HP;
|
|
if (weight == 0)
|
|
{
|
|
weight = 1;
|
|
}
|
|
}
|
|
else if (IQSkillIsEnabled(user, IQ_WEAK_TYPE_PICKER))
|
|
{
|
|
weight = WeightWeakTypePicker(user, target, moveType2) + 1;
|
|
}
|
|
return weight;
|
|
}
|
|
|
|
bool8 TargetRegularAttack(Entity *pokemon, u32 *targetDir, bool8 checkPetrified)
|
|
{
|
|
EntityInfo *pokemonInfo = GetEntInfo(pokemon);
|
|
s32 numPotentialTargets = 0;
|
|
s32 direction = pokemonInfo->action.direction;
|
|
s32 faceTurnLimit = pokemonInfo->blinkerClassStatus.status == STATUS_BLINKER ? 1 : 8;
|
|
s32 i;
|
|
s32 potentialAttackTargetDirections[NUM_DIRECTIONS];
|
|
s32 potentialAttackTargetWeights[NUM_DIRECTIONS];
|
|
bool8 hasTargetingIQ = IQSkillIsEnabled(pokemon, IQ_EXP_GO_GETTER) || IQSkillIsEnabled(pokemon, IQ_EFFICIENCY_EXPERT);
|
|
bool8 hasStatusChecker = IQSkillIsEnabled(pokemon, IQ_STATUS_CHECKER);
|
|
for (i = 0; i < faceTurnLimit; i++, direction++)
|
|
{
|
|
Entity *target;
|
|
direction &= DIRECTION_MASK;
|
|
target = GetTile(pokemon->pos.x + gAdjacentTileOffsets[direction].x,
|
|
pokemon->pos.y + gAdjacentTileOffsets[direction].y)->monster;
|
|
if (target != NULL &&
|
|
GetEntityType(target) == ENTITY_MONSTER &&
|
|
CanAttackInDirection(pokemon, direction) &&
|
|
GetTreatmentBetweenMonsters(pokemon, target, FALSE, checkPetrified) == TREATMENT_TREAT_AS_ENEMY &&
|
|
(!hasStatusChecker || GetEntInfo(target)->frozenClassStatus.status != STATUS_FROZEN))
|
|
{
|
|
potentialAttackTargetDirections[numPotentialTargets] = direction;
|
|
potentialAttackTargetWeights[numPotentialTargets] = WeightMove(pokemon, TARGETING_FLAG_TARGET_OTHER, target, TYPE_NONE);
|
|
if (!hasTargetingIQ)
|
|
{
|
|
*targetDir = direction;
|
|
return TRUE;
|
|
}
|
|
numPotentialTargets++;
|
|
}
|
|
}
|
|
if (numPotentialTargets == 0)
|
|
{
|
|
return FALSE;
|
|
}
|
|
else
|
|
{
|
|
s32 totalWeight = 0;
|
|
s32 maxWeight = 0;
|
|
s32 weightCounter;
|
|
s32 i;
|
|
for (i = 0; i < numPotentialTargets; i++)
|
|
{
|
|
if (maxWeight < potentialAttackTargetWeights[i])
|
|
{
|
|
maxWeight = potentialAttackTargetWeights[i];
|
|
}
|
|
}
|
|
for (i = 0; i < numPotentialTargets; i++)
|
|
{
|
|
if (maxWeight != potentialAttackTargetWeights[i])
|
|
{
|
|
potentialAttackTargetWeights[i] = 0;
|
|
}
|
|
}
|
|
for (i = 0; i < numPotentialTargets; i++)
|
|
{
|
|
totalWeight += potentialAttackTargetWeights[i];
|
|
}
|
|
weightCounter = DungeonRandInt(totalWeight);
|
|
for (i = 0; i < numPotentialTargets; i++)
|
|
{
|
|
weightCounter -= potentialAttackTargetWeights[i];
|
|
if (weightCounter < 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
*targetDir = potentialAttackTargetDirections[i];
|
|
return TRUE;
|
|
}
|
|
|
|
}
|
|
|
|
bool8 IsTargetInRange(Entity *pokemon, Entity *targetPokemon, s32 direction, s32 maxRange)
|
|
{
|
|
s32 distanceX = pokemon->pos.x - targetPokemon->pos.x;
|
|
s32 effectiveMaxRange;
|
|
if (distanceX < 0)
|
|
{
|
|
distanceX = -distanceX;
|
|
}
|
|
effectiveMaxRange = pokemon->pos.y - targetPokemon->pos.y;
|
|
if (effectiveMaxRange < 0)
|
|
{
|
|
effectiveMaxRange = -effectiveMaxRange;
|
|
}
|
|
if (effectiveMaxRange < distanceX)
|
|
{
|
|
effectiveMaxRange = distanceX;
|
|
}
|
|
if (effectiveMaxRange > maxRange)
|
|
{
|
|
effectiveMaxRange = maxRange;
|
|
}
|
|
if (!IQSkillIsEnabled(pokemon, IQ_COURSE_CHECKER))
|
|
{
|
|
// BUG: effectiveMaxRange is already capped at maxRange, so this condition always evaluates to TRUE.
|
|
// The AI also has range checks elsewhere, so this doesn't become an issue in most cases.
|
|
// If the AI has the Long Toss or Pierce statuses and Course Checker is disabled,
|
|
// this incorrect check causes the AI to throw items at targets further than 10 tiles away.
|
|
if (effectiveMaxRange <= maxRange)
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
s32 currentPosX = pokemon->pos.x;
|
|
s32 currentPosY = pokemon->pos.y;
|
|
s32 adjacentTileOffsetX = gAdjacentTileOffsets[direction].x;
|
|
s32 adjacentTileOffsetY = gAdjacentTileOffsets[direction].y;
|
|
s32 i;
|
|
for (i = 0; i <= effectiveMaxRange; i++)
|
|
{
|
|
const Tile *mapTile;
|
|
currentPosX += adjacentTileOffsetX;
|
|
currentPosY += adjacentTileOffsetY;
|
|
if (currentPosX <= 0 || currentPosY <= 0 ||
|
|
currentPosX >= DUNGEON_MAX_SIZE_X - 1 || currentPosY >= DUNGEON_MAX_SIZE_Y - 1)
|
|
{
|
|
break;
|
|
}
|
|
while (0); // Extra label needed to swap branch locations in ASM.
|
|
mapTile = GetTile(currentPosX, currentPosY);
|
|
if (!(mapTile->terrainType & (TERRAIN_TYPE_NORMAL | TERRAIN_TYPE_SECONDARY)))
|
|
{
|
|
break;
|
|
}
|
|
if (mapTile->monster == targetPokemon)
|
|
{
|
|
return TRUE;
|
|
}
|
|
if (mapTile->monster != NULL)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
void HandleUseMoveAIAction(Entity *target)
|
|
{
|
|
EntityInfo *entityInfo;
|
|
s32 counter;
|
|
|
|
counter = 0;
|
|
while (TRUE) {
|
|
if (counter >= sub_8070828(target, TRUE))
|
|
break;
|
|
entityInfo = GetEntInfo(target);
|
|
sub_8055A00(target, entityInfo->action.actionParameters[0].actionUseIndex, 1, 0, 0);
|
|
if (!EntityIsValid(target))
|
|
break;
|
|
if (sub_8044B28())
|
|
break;
|
|
if (entityInfo->unk159)
|
|
break;
|
|
counter++;
|
|
}
|
|
|
|
sub_8057588(target, 1);
|
|
if (EntityIsValid(target))
|
|
sub_806A9B4(target, GetEntInfo(target)->action.actionParameters[0].actionUseIndex);
|
|
|
|
sub_806A1B0(target);
|
|
}
|
|
|
|
void HandleUseOrbAction(Entity *pokemon)
|
|
{
|
|
bool8 r4;
|
|
Item *item;
|
|
Item IVar5;
|
|
EntityInfo *entityInfo; // r7
|
|
ActionContainer act;
|
|
Move move;
|
|
struct AIPossibleMove sp28;
|
|
bool8 r8;
|
|
|
|
entityInfo = GetEntInfo(pokemon);
|
|
item = sub_8044D90(pokemon, 0, 21);
|
|
IVar5 = *item;
|
|
|
|
if (item->flags & ITEM_FLAG_STICKY) {
|
|
sub_8045BF8(gFormatBuffer_Items[0], item);
|
|
LogMessageByIdWithPopupCheckUser(pokemon, *gItemStickyDoesntWorkText);
|
|
return;
|
|
}
|
|
|
|
act = entityInfo->action;
|
|
|
|
if (IsBossFight()) {
|
|
LogMessageByIdWithPopupCheckUser(pokemon, *gPtrMysteriousPowerPreventedUseMessage);
|
|
r4 = TRUE;
|
|
}
|
|
else {
|
|
r8 = TRUE;
|
|
InitPokemonMove(&move, GetItemMoveID(IVar5.id));
|
|
|
|
if (!entityInfo->isTeamLeader) {
|
|
sp28.canBeUsed = r8;
|
|
sp28.weight = 10;
|
|
AIConsiderMove(&sp28, pokemon, entityInfo->moves.moves);
|
|
|
|
if (sp28.canBeUsed) {
|
|
entityInfo->action.direction = sp28.direction & 7;
|
|
TargetTileInFront(pokemon);
|
|
}
|
|
}
|
|
|
|
if (entityInfo->cringeClassStatus.status == 1) {
|
|
SubstitutePlaceholderStringTags(gFormatBuffer_Monsters[0], pokemon, 0);
|
|
LogMessageByIdWithPopupCheckUser(pokemon, *gUnknown_80FC714);
|
|
r4 = FALSE;
|
|
r8 = FALSE;
|
|
}
|
|
else if (entityInfo->cringeClassStatus.status == 7) {
|
|
SubstitutePlaceholderStringTags(gFormatBuffer_Monsters[0], pokemon, 0);
|
|
LogMessageByIdWithPopupCheckUser(pokemon, *gUnknown_80FC718);
|
|
r4 = FALSE;
|
|
r8 = FALSE;
|
|
}
|
|
else if (entityInfo->burnClassStatus.status == 4)
|
|
{
|
|
SubstitutePlaceholderStringTags(gFormatBuffer_Monsters[0], pokemon, 0);
|
|
LogMessageByIdWithPopupCheckUser(pokemon, *gUnknown_80FC6A8);
|
|
r4 = FALSE;
|
|
r8 = FALSE;
|
|
}
|
|
|
|
if (r8) {
|
|
if ((IVar5.id == ITEM_SWITCHER_ORB) || (IVar5.id == ITEM_POUNCE_ORB)) {
|
|
sub_8044DF0(pokemon, 0, 122);
|
|
move.moveFlags |= MOVE_FLAG_SET;
|
|
move.moveFlags |= MOVE_FLAG_ENABLED_FOR_AI;
|
|
move.PP = 10;
|
|
TryUseChosenMove(pokemon, 0, IVar5.id, 0, 0, &move);
|
|
r4 = FALSE;
|
|
}
|
|
else {
|
|
r4 = TryUseChosenMove(pokemon, 0, IVar5.id, 0, 0, &move);
|
|
}
|
|
}
|
|
else {
|
|
r4 = FALSE;
|
|
}
|
|
|
|
}
|
|
|
|
if (EntityIsValid(pokemon)) {
|
|
if (r4)
|
|
sub_8044DF0(pokemon, 0, 122);
|
|
|
|
sub_8071DA4(pokemon);
|
|
sub_806CE68(pokemon, 8);
|
|
|
|
if (entityInfo->isTeamLeader) {
|
|
sub_804AC20(&pokemon->pos);
|
|
sub_807EC28(FALSE);
|
|
}
|
|
|
|
sub_806A5B8(pokemon);
|
|
sub_8075900(pokemon, gDungeon->forceMonsterHouse);
|
|
}
|
|
else if (r4)
|
|
sub_8044D40(&act, 0);
|
|
|
|
if (!sub_8044B28())
|
|
sub_806A1B0(pokemon);
|
|
}
|