mirror of
https://github.com/pret/pmd-red.git
synced 2026-09-15 04:25:54 -05:00
925 lines
33 KiB
C
925 lines
33 KiB
C
#include "global.h"
|
|
#include "dungeon_ai_attack.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 "charge_move.h"
|
|
#include "dungeon_action.h"
|
|
#include "dungeon_ai_targeting.h"
|
|
#include "dungeon_ai_targeting_1.h"
|
|
#include "dungeon_ai_targeting_2.h"
|
|
#include "dungeon_capabilities_1.h"
|
|
#include "dungeon_global_data.h"
|
|
#include "dungeon_map_access.h"
|
|
#include "dungeon_pokemon_attributes.h"
|
|
#include "dungeon_random.h"
|
|
#include "dungeon_random_1.h"
|
|
#include "dungeon_util.h"
|
|
#include "dungeon_visibility.h"
|
|
#include "move_util.h"
|
|
#include "moves.h"
|
|
#include "position_util.h"
|
|
#include "status_checker.h"
|
|
#include "status_checks.h"
|
|
#include "status_checks_1.h"
|
|
#include "targeting.h"
|
|
#include "targeting_flags.h"
|
|
#include "type_effectiveness.h"
|
|
|
|
#define REGULAR_ATTACK_INDEX 4
|
|
|
|
const s16 gRegularAttackWeights[] = {100, 20, 30, 40, 50};
|
|
|
|
extern bool8 gCanAttackInDirection[NUM_DIRECTIONS];
|
|
extern s32 gNumPotentialTargets;
|
|
extern s32 gPotentialAttackTargetWeights[NUM_DIRECTIONS];
|
|
extern u8 gPotentialAttackTargetDirections[NUM_DIRECTIONS];
|
|
extern struct DungeonEntity *gPotentialTargets[NUM_DIRECTIONS];
|
|
|
|
void DecideAttack(struct DungeonEntity *pokemon)
|
|
{
|
|
struct DungeonEntityData *pokemonData = pokemon->entityData;
|
|
s32 i;
|
|
s32 chargeStatus = CHARGING_STATUS_CHARGE;
|
|
struct MoveTargetResults moveTargetResults[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) ||
|
|
ShouldAvoidEnemiesAndShowEffect(pokemon, TRUE) ||
|
|
HasTactic(pokemon, TACTIC_KEEP_YOUR_DISTANCE) ||
|
|
(pokemonData->volatileStatus == VOLATILE_STATUS_CONFUSED && RollPercentChance(gConfusedAttackChance)))
|
|
{
|
|
return;
|
|
}
|
|
if (pokemonData->chargingStatus != CHARGING_STATUS_NONE)
|
|
{
|
|
for (i = 0; i < MAX_MON_MOVES; i++)
|
|
{
|
|
if (pokemonData->moves[i].moveFlags & MOVE_FLAG_EXISTS &&
|
|
MoveMatchesChargingStatus(pokemon, &pokemonData->moves[i]) &&
|
|
pokemonData->chargingStatusMoveIndex == i)
|
|
{
|
|
s32 chosenMoveIndex;
|
|
SetAction(&pokemonData->action, DUNGEON_ACTION_USE_MOVE_AI);
|
|
chosenMoveIndex = i;
|
|
if (i > 0 && pokemonData->moves[i].moveFlags & MOVE_FLAG_LINKED)
|
|
{
|
|
while (--chosenMoveIndex > 0)
|
|
{
|
|
if (!(pokemonData->moves[chosenMoveIndex].moveFlags & MOVE_FLAG_LINKED))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
pokemonData->action.actionUseIndex = chosenMoveIndex;
|
|
TargetTileInFront(pokemon);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
total = 0;
|
|
numUsableMoves = 0;
|
|
for (i = 0; i < MAX_MON_MOVES; i++)
|
|
{
|
|
struct PokemonMove *move = &pokemonData->moves[i];
|
|
if (pokemonData->moves[i].moveFlags & MOVE_FLAG_EXISTS)
|
|
{
|
|
if (pokemonData->moves[i].moveFlags & MOVE_FLAG_ENABLED)
|
|
{
|
|
numUsableMoves++;
|
|
}
|
|
total += move->PP;
|
|
}
|
|
}
|
|
if (total == 0)
|
|
{
|
|
struct PokemonMove struggle;
|
|
InitPokemonMove(&struggle, MOVE_STRUGGLE);
|
|
FindMoveTarget(&moveTargetResults[0], pokemon, &struggle);
|
|
if (moveTargetResults[0].moveUsable)
|
|
{
|
|
SetAction(&pokemonData->action, DUNGEON_ACTION_STRUGGLE);
|
|
pokemonData->action.facingDir = moveTargetResults[0].targetDir & DIRECTION_MASK;
|
|
TargetTileInFront(pokemon);
|
|
}
|
|
return;
|
|
}
|
|
hasWeakTypePicker = HasIQSkill(pokemon, IQ_SKILL_WEAK_TYPE_PICKER);
|
|
hasPPChecker = HasIQSkill(pokemon, IQ_SKILL_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++)
|
|
{
|
|
struct PokemonMove *move = &pokemonData->moves[i];
|
|
if (!(move->moveFlags & MOVE_FLAG_EXISTS))
|
|
{
|
|
break;
|
|
}
|
|
if (i != 0 && !(move->moveFlags & MOVE_FLAG_LINKED))
|
|
{
|
|
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++)
|
|
{
|
|
struct PokemonMove *move;
|
|
moveTargetResults[i].moveUsable = FALSE;
|
|
move = &pokemonData->moves[i];
|
|
if (move->moveFlags & MOVE_FLAG_EXISTS &&
|
|
willNotUnlinkMove[i] &&
|
|
IsMoveIndexUsable(pokemon, i, hasPPChecker) &&
|
|
move->moveFlags & MOVE_FLAG_ENABLED)
|
|
{
|
|
moveTargetResults[i].moveUsable = TRUE;
|
|
if (pokemonData->chargingStatus == chargeStatus)
|
|
{
|
|
if (move->moveID == MOVE_CHARGE)
|
|
{
|
|
moveTargetResults[i].moveWeight = 0;
|
|
continue;
|
|
}
|
|
else if (GetMoveTypeForPokemon(pokemon, move) == TYPE_ELECTRIC)
|
|
{
|
|
moveTargetResults[i].moveWeight = GetMoveWeight(move);
|
|
}
|
|
else
|
|
{
|
|
moveTargetResults[i].moveWeight = 1;
|
|
}
|
|
}
|
|
else if (hasWeakTypePicker)
|
|
{
|
|
struct MoveTargetResults *results = &moveTargetResults[i];
|
|
results->moveWeight = FindMoveTarget(results, pokemon, move);
|
|
}
|
|
else
|
|
{
|
|
moveTargetResults[i].moveWeight = GetMoveWeight(move);
|
|
}
|
|
total += moveTargetResults[i].moveWeight;
|
|
}
|
|
}
|
|
moveTargetResults[REGULAR_ATTACK_INDEX].moveWeight = 0;
|
|
if (!HasIQSkill(pokemon, IQ_SKILL_EXCLUSIVE_MOVE_USER) && pokemonData->chargingStatus != chargeStatus)
|
|
{
|
|
moveTargetResults[REGULAR_ATTACK_INDEX].moveUsable = TRUE;
|
|
if (pokemonData->chargingStatus == chargeStatus)
|
|
{
|
|
// Never reached? Charge already skips the regular attack in the outer block.
|
|
moveTargetResults[REGULAR_ATTACK_INDEX].moveWeight = 1;
|
|
}
|
|
else if (hasWeakTypePicker)
|
|
{
|
|
moveTargetResults[REGULAR_ATTACK_INDEX].moveWeight = 2;
|
|
}
|
|
else
|
|
{
|
|
moveTargetResults[REGULAR_ATTACK_INDEX].moveWeight = gRegularAttackWeights[numUsableMoves];
|
|
}
|
|
total += moveTargetResults[REGULAR_ATTACK_INDEX].moveWeight;
|
|
}
|
|
if (hasWeakTypePicker)
|
|
{
|
|
s32 i;
|
|
maxWeight = 0;
|
|
total = 0;
|
|
for (i = 0; i < MAX_MON_MOVES + 1; i++)
|
|
{
|
|
if (!moveTargetResults[i].moveUsable)
|
|
{
|
|
moveTargetResults[i].moveWeight = 0;
|
|
}
|
|
else if (maxWeight < moveTargetResults[i].moveWeight)
|
|
{
|
|
maxWeight = moveTargetResults[i].moveWeight;
|
|
}
|
|
}
|
|
for (i = 0; i < MAX_MON_MOVES + 1; i++)
|
|
{
|
|
if (moveTargetResults[i].moveUsable)
|
|
{
|
|
if (maxWeight != moveTargetResults[i].moveWeight)
|
|
{
|
|
moveTargetResults[i].moveWeight = 0;
|
|
}
|
|
total += moveTargetResults[i].moveWeight;
|
|
}
|
|
}
|
|
}
|
|
if (total == 0)
|
|
{
|
|
return;
|
|
}
|
|
randomWeight = DungeonRandomCapped(total);
|
|
weightCounter = 0;
|
|
if (!HasIQSkill(pokemon, IQ_SKILL_EXCLUSIVE_MOVE_USER))
|
|
{
|
|
canTargetRegularAttack = TargetRegularAttack(pokemon, ®ularAttackTargetDir, TRUE);
|
|
}
|
|
else
|
|
{
|
|
canTargetRegularAttack = FALSE;
|
|
regularAttackTargetDir = DIRECTION_SOUTH;
|
|
}
|
|
for (i = 0; i <= REGULAR_ATTACK_INDEX; i++)
|
|
{
|
|
if (moveTargetResults[i].moveUsable && moveTargetResults[i].moveWeight != 0)
|
|
{
|
|
weightCounter += moveTargetResults[i].moveWeight;
|
|
if (weightCounter >= randomWeight)
|
|
{
|
|
if (i == REGULAR_ATTACK_INDEX)
|
|
{
|
|
if (canTargetRegularAttack)
|
|
{
|
|
SetAction(&pokemonData->action, DUNGEON_ACTION_REGULAR_ATTACK);
|
|
pokemonData->action.facingDir = regularAttackTargetDir & DIRECTION_MASK;
|
|
TargetTileInFront(pokemon);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FindMoveTarget(&moveTargetResults[i], pokemon, &pokemonData->moves[i]);
|
|
if (moveTargetResults[i].moveUsable)
|
|
{
|
|
s32 chosenMoveIndex;
|
|
SetAction(&pokemonData->action, DUNGEON_ACTION_USE_MOVE_AI);
|
|
chosenMoveIndex = i;
|
|
if (i > 0 && pokemonData->moves[i].moveFlags & MOVE_FLAG_LINKED)
|
|
{
|
|
while (--chosenMoveIndex > 0)
|
|
{
|
|
if (!(pokemonData->moves[chosenMoveIndex].moveFlags & MOVE_FLAG_LINKED))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
pokemonData->action.facingDir = moveTargetResults[i].targetDir & DIRECTION_MASK;
|
|
pokemonData->action.actionUseIndex = chosenMoveIndex;
|
|
TargetTileInFront(pokemon);
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (canTargetRegularAttack)
|
|
{
|
|
SetAction(&pokemonData->action, DUNGEON_ACTION_REGULAR_ATTACK);
|
|
pokemonData->action.facingDir = regularAttackTargetDir & DIRECTION_MASK;
|
|
TargetTileInFront(pokemon);
|
|
}
|
|
}
|
|
|
|
s32 FindMoveTarget(struct MoveTargetResults *moveTargetResults, struct DungeonEntity *pokemon, struct PokemonMove *move)
|
|
{
|
|
s32 targetingFlags;
|
|
s32 moveWeight = 1;
|
|
struct DungeonEntityData *pokemonData = pokemon->entityData;
|
|
s32 numPotentialTargets = 0;
|
|
s32 i;
|
|
bool8 hasStatusChecker;
|
|
s32 rangeTargetingFlags;
|
|
s32 rangeTargetingFlags2;
|
|
for (i = 0; i < NUM_DIRECTIONS; i++)
|
|
{
|
|
gCanAttackInDirection[i] = FALSE;
|
|
}
|
|
targetingFlags = GetMoveTargetingFlagsForPokemon(pokemon, move, TRUE);
|
|
hasStatusChecker = HasIQSkill(pokemon, IQ_SKILL_STATUS_CHECKER);
|
|
moveTargetResults->moveUsable = FALSE;
|
|
if ((pokemonData->volatileStatus == VOLATILE_STATUS_TAUNTED && !MoveDealsDirectDamage(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 (pokemonData->eyesightStatus == EYESIGHT_STATUS_BLINKER)
|
|
{
|
|
u8 facingDir = pokemonData->action.facingDir;
|
|
i = facingDir; // 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;
|
|
struct MapTile *adjacentTile = GetMapTile_1(pokemon->posWorld.x + gAdjacentTileOffsets[i].x,
|
|
pokemon->posWorld.y + gAdjacentTileOffsets[i].y);
|
|
struct DungeonEntity *adjacentPokemon = adjacentTile->pokemon;
|
|
if (adjacentPokemon != NULL && GetEntityType(adjacentPokemon) == ENTITY_POKEMON)
|
|
{
|
|
if (rangeTargetingFlags != TARGETING_FLAG_TARGET_FRONTAL_CONE &&
|
|
rangeTargetingFlags != TARGETING_FLAG_TARGET_AROUND)
|
|
{
|
|
if (!CanAttackInFront(pokemon, i))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
numPotentialTargets = WeightMoveIfUsable(numPotentialTargets, targetingFlags, pokemon, adjacentPokemon, move, hasStatusChecker);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (rangeTargetingFlags == TARGETING_FLAG_TARGET_ROOM)
|
|
{
|
|
s32 i;
|
|
for (i = 0; i < DUNGEON_MAX_POKEMON; i++)
|
|
{
|
|
struct DungeonEntity *target = gDungeonGlobalData->allPokemon[i];
|
|
if (EntityExists(target) && CanSee(pokemon, target))
|
|
{
|
|
numPotentialTargets = WeightMoveIfUsable(numPotentialTargets, targetingFlags, pokemon, target, move, hasStatusChecker);
|
|
}
|
|
}
|
|
}
|
|
else if (rangeTargetingFlags == TARGETING_FLAG_TARGET_2_TILES_AHEAD)
|
|
{
|
|
for (i = 0; i < NUM_DIRECTIONS; i++)
|
|
{
|
|
struct MapTile *targetTile = GetMapTile_1(pokemon->posWorld.x + gAdjacentTileOffsets[i].x,
|
|
pokemon->posWorld.y + gAdjacentTileOffsets[i].y);
|
|
if (CanAttackInFront(pokemon, i))
|
|
{
|
|
struct DungeonEntity *targetPokemon = targetTile->pokemon;
|
|
if (targetPokemon != NULL && GetEntityType(targetPokemon) == ENTITY_POKEMON)
|
|
{
|
|
s32 prevNumPotentialTargets = numPotentialTargets;
|
|
numPotentialTargets = WeightMoveIfUsable(numPotentialTargets, targetingFlags, pokemon, targetPokemon, move, hasStatusChecker);
|
|
if (prevNumPotentialTargets != numPotentialTargets)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
targetTile = GetMapTile_1(pokemon->posWorld.x + gAdjacentTileOffsets[i].x * 2,
|
|
pokemon->posWorld.y + gAdjacentTileOffsets[i].y * 2);
|
|
targetPokemon = targetTile->pokemon;
|
|
if (targetPokemon != NULL && GetEntityType(targetPokemon) == ENTITY_POKEMON)
|
|
{
|
|
numPotentialTargets = WeightMoveIfUsable(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++)
|
|
{
|
|
struct DungeonEntity *target = gDungeonGlobalData->allPokemon[i];
|
|
if (EntityExists(target) && pokemon != target)
|
|
{
|
|
s32 facingDir = CalculateFacingDir(&pokemon->posWorld, &target->posWorld);
|
|
if (!gCanAttackInDirection[facingDir] &&
|
|
CanSee(pokemon, target) &&
|
|
IsTargetInLineRange(pokemon, target, maxRange) &&
|
|
CanUseStatusMove(targetingFlags, pokemon, target, move, hasStatusChecker) &&
|
|
IsTargetStraightAhead(pokemon, target, facingDir, maxRange))
|
|
{
|
|
gCanAttackInDirection[facingDir] = TRUE;
|
|
gPotentialAttackTargetDirections[numPotentialTargets] = facingDir;
|
|
gPotentialAttackTargetWeights[numPotentialTargets] = WeightMove(pokemon, targetingFlags, target, GetMoveTypeForPokemon(pokemon, move));
|
|
gPotentialTargets[numPotentialTargets] = target;
|
|
numPotentialTargets++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (rangeTargetingFlags == TARGETING_FLAG_TARGET_FLOOR)
|
|
{
|
|
s32 i;
|
|
for (i = 0; i < DUNGEON_MAX_POKEMON; i++)
|
|
{
|
|
struct DungeonEntity *target = gDungeonGlobalData->allPokemon[i];
|
|
if (EntityExists(target))
|
|
{
|
|
numPotentialTargets = WeightMoveIfUsable(numPotentialTargets, targetingFlags, pokemon, target, move, hasStatusChecker);
|
|
}
|
|
}
|
|
}
|
|
else if (rangeTargetingFlags == TARGETING_FLAG_TARGET_SELF)
|
|
{
|
|
numPotentialTargets = WeightMoveIfUsable(numPotentialTargets, targetingFlags, pokemon, pokemon, move, hasStatusChecker);
|
|
}
|
|
if (numPotentialTargets == 0)
|
|
{
|
|
moveTargetResults->moveUsable = 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 = DungeonRandomCapped(totalWeight);
|
|
for (i = 0; i < numPotentialTargets; i++)
|
|
{
|
|
weightCounter -= gPotentialAttackTargetWeights[i];
|
|
if (weightCounter < 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
moveTargetResults->moveUsable = TRUE;
|
|
moveTargetResults->targetDir = gPotentialAttackTargetDirections[i];
|
|
moveTargetResults->moveWeight = 8;
|
|
}
|
|
return moveWeight;
|
|
}
|
|
|
|
bool8 IsTargetInLineRange(struct DungeonEntity *user, struct DungeonEntity *target, s32 range)
|
|
{
|
|
s32 posDiffX = user->posWorld.x - target->posWorld.x;
|
|
s32 posDiffY, maxPosDiff;
|
|
s32 direction;
|
|
if (posDiffX < 0)
|
|
{
|
|
posDiffX = -posDiffX;
|
|
}
|
|
posDiffY = user->posWorld.y - target->posWorld.y;
|
|
if (posDiffY < 0)
|
|
{
|
|
posDiffY = -posDiffY;
|
|
}
|
|
maxPosDiff = posDiffY;
|
|
if (posDiffY < posDiffX)
|
|
{
|
|
maxPosDiff = posDiffX;
|
|
}
|
|
if (maxPosDiff > RANGED_ATTACK_RANGE || maxPosDiff > range)
|
|
{
|
|
return FALSE;
|
|
}
|
|
direction = -1;
|
|
if (posDiffX == posDiffY)
|
|
{
|
|
if (user->posWorld.x < target->posWorld.x &&
|
|
(user->posWorld.y < target->posWorld.y || user->posWorld.y > target->posWorld.y))
|
|
{
|
|
returnTrue:
|
|
return TRUE;
|
|
}
|
|
if (user->posWorld.x > target->posWorld.x); // Fixes register loading order.
|
|
direction = DIRECTION_SOUTHWEST;
|
|
if (user->posWorld.x <= target->posWorld.x || user->posWorld.y <= target->posWorld.y)
|
|
{
|
|
goto checkDirectionSet;
|
|
}
|
|
goto returnTrue;
|
|
}
|
|
else if (user->posWorld.x == target->posWorld.x && user->posWorld.y < target->posWorld.y)
|
|
{
|
|
return TRUE;
|
|
}
|
|
else if (user->posWorld.x < target->posWorld.x && user->posWorld.y == target->posWorld.y)
|
|
{
|
|
return TRUE;
|
|
}
|
|
else if (user->posWorld.x == target->posWorld.x && user->posWorld.y > target->posWorld.y)
|
|
{
|
|
return TRUE;
|
|
}
|
|
else if (user->posWorld.x > target->posWorld.x && user->posWorld.y == target->posWorld.y)
|
|
{
|
|
direction = DIRECTION_WEST;
|
|
}
|
|
checkDirectionSet:
|
|
if (direction < 0)
|
|
{
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
s32 WeightMoveIfUsable(s32 numPotentialTargets, s32 targetingFlags, struct DungeonEntity *user, struct DungeonEntity *target, struct PokemonMove *move, bool32 hasStatusChecker)
|
|
{
|
|
s32 facingDir;
|
|
s32 targetingFlags2 = (s16) targetingFlags;
|
|
bool8 hasStatusChecker2 = hasStatusChecker;
|
|
struct DungeonEntityData *userData = user->entityData;
|
|
if ((user->posWorld.x == target->posWorld.x && user->posWorld.y == target->posWorld.y) ||
|
|
(targetingFlags2 & 0xF0) == TARGETING_FLAG_TARGET_ROOM ||
|
|
(targetingFlags2 & 0xF0) == TARGETING_FLAG_TARGET_FLOOR ||
|
|
(targetingFlags2 & 0xF0) == TARGETING_FLAG_TARGET_SELF)
|
|
{
|
|
facingDir = userData->action.facingDir;
|
|
}
|
|
else
|
|
{
|
|
facingDir = CalculateFacingDir(&user->posWorld, &target->posWorld);
|
|
}
|
|
if (!gCanAttackInDirection[facingDir] &&
|
|
CanUseStatusMove(targetingFlags2, user, target, move, hasStatusChecker2))
|
|
{
|
|
gCanAttackInDirection[facingDir] = TRUE;
|
|
do { gPotentialAttackTargetDirections[numPotentialTargets] = facingDir; } while (0);
|
|
gPotentialAttackTargetWeights[numPotentialTargets] = WeightMove(user, targetingFlags2, target, GetMoveTypeForPokemon(user, move));
|
|
gPotentialTargets[numPotentialTargets] = target;
|
|
numPotentialTargets++;
|
|
}
|
|
return numPotentialTargets;
|
|
}
|
|
|
|
bool8 CanUseStatusMove(s32 targetingFlags, struct DungeonEntity *user, struct DungeonEntity *target, struct PokemonMove *move, bool32 hasStatusChecker)
|
|
{
|
|
struct DungeonEntityData *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 (CanTarget(user, target, FALSE, TRUE) == TARGET_CAPABILITY_CAN_TARGET)
|
|
{
|
|
hasTarget = TRUE;
|
|
}
|
|
}
|
|
else if (categoryTargetingFlags == TARGETING_FLAG_HEAL_TEAM)
|
|
{
|
|
goto checkCanTarget;
|
|
}
|
|
else if (categoryTargetingFlags == TARGETING_FLAG_LONG_RANGE)
|
|
{
|
|
targetData = target->entityData;
|
|
goto checkThirdParty;
|
|
}
|
|
else if (categoryTargetingFlags == TARGETING_FLAG_ATTACK_ALL)
|
|
{
|
|
targetData = target->entityData;
|
|
if (user == target)
|
|
{
|
|
goto returnFalse;
|
|
}
|
|
checkThirdParty:
|
|
hasTarget = TRUE;
|
|
if (targetData->shopkeeperMode == SHOPKEEPER_FRIENDLY ||
|
|
targetData->clientType == CLIENT_TYPE_DONT_MOVE ||
|
|
targetData->clientType == CLIENT_TYPE_CLIENT)
|
|
{
|
|
returnFalse:
|
|
return FALSE;
|
|
}
|
|
}
|
|
else if (categoryTargetingFlags == TARGETING_FLAG_BOOST_TEAM)
|
|
{
|
|
if (user == target)
|
|
{
|
|
goto returnFalse;
|
|
}
|
|
checkCanTarget:
|
|
if (CanTarget(user, target, FALSE, TRUE) == TARGET_CAPABILITY_CANNOT_ATTACK)
|
|
{
|
|
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 (!HasQuarterHPOrLess(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 = target->entityData;
|
|
if ((targetData->types[0] != TYPE_GHOST && targetData->types[1] != TYPE_GHOST) || targetData->exposedStatus)
|
|
{
|
|
if (*categoryTargetingFlags2); // Flips the conditional.
|
|
goto returnFalse;
|
|
}
|
|
}
|
|
else if ((targetingFlags2 & 0xF00) == TARGETING_FLAG_HEAL_ALL)
|
|
{
|
|
if (!HasNegativeStatus(target) && !HasQuarterHPOrLess(target))
|
|
{
|
|
if (*categoryTargetingFlags2); // Flips the conditional.
|
|
goto returnFalse;
|
|
}
|
|
}
|
|
}
|
|
else if ((targetingFlags2 & 0xF00) == TARGETING_FLAG_SET_TRAP)
|
|
{
|
|
s32 useChance;
|
|
rollMoveUseChance:
|
|
useChance = GetMoveAccuracy(move, ACCURACY_TYPE_USE_CHANCE);
|
|
if (DungeonRandomCapped(100) >= useChance)
|
|
{
|
|
goto returnFalse;
|
|
}
|
|
}
|
|
}
|
|
return hasTarget;
|
|
}
|
|
|
|
s32 WeightMove(struct DungeonEntity *user, s32 targetingFlags, struct DungeonEntity *target, u32 moveType)
|
|
{
|
|
#ifndef NONMATCHING
|
|
register struct DungeonEntityData *targetData asm("r4");
|
|
#else
|
|
struct DungeonEntityData *targetData;
|
|
#endif
|
|
s32 targetingFlags2 = (s16) targetingFlags;
|
|
u8 moveType2 = moveType;
|
|
u8 weight = 1;
|
|
struct DungeonEntityData *targetData2;
|
|
targetData2 = targetData = target->entityData;
|
|
if (!targetData->isEnemy || (targetingFlags2 & 0xF) != TARGETING_FLAG_TARGET_OTHER)
|
|
{
|
|
return 1;
|
|
}
|
|
else if (HasIQSkill(user, IQ_SKILL_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 = gDungeonGlobalData->expYieldRankings[targetData->entityID];
|
|
}
|
|
else if (HasIQSkill(user, IQ_SKILL_EFFICIENCY_EXPERT))
|
|
{
|
|
weight = -12 - targetData2->HP;
|
|
if (weight == 0)
|
|
{
|
|
weight = 1;
|
|
}
|
|
}
|
|
else if (HasIQSkill(user, IQ_SKILL_WEAK_TYPE_PICKER))
|
|
{
|
|
weight = WeightWeakTypePicker(user, target, moveType2) + 1;
|
|
}
|
|
return weight;
|
|
}
|
|
|
|
bool8 TargetRegularAttack(struct DungeonEntity *pokemon, u32 *targetDir, bool8 checkPetrified)
|
|
{
|
|
struct DungeonEntityData *pokemonData = pokemon->entityData;
|
|
s32 numPotentialTargets = 0;
|
|
s32 facingDir = pokemonData->action.facingDir;
|
|
s32 faceTurnLimit = pokemonData->eyesightStatus == EYESIGHT_STATUS_BLINKER ? 1 : 8;
|
|
s32 i;
|
|
s32 potentialAttackTargetDirections[NUM_DIRECTIONS];
|
|
s32 potentialAttackTargetWeights[NUM_DIRECTIONS];
|
|
bool8 hasTargetingIQ = HasIQSkill(pokemon, IQ_SKILL_EXP_GO_GETTER) || HasIQSkill(pokemon, IQ_SKILL_EFFICIENCY_EXPERT);
|
|
bool8 hasStatusChecker = HasIQSkill(pokemon, IQ_SKILL_STATUS_CHECKER);
|
|
for (i = 0; i < faceTurnLimit; i++, facingDir++)
|
|
{
|
|
struct DungeonEntity *target;
|
|
facingDir &= DIRECTION_MASK;
|
|
target = GetMapTile_1(pokemon->posWorld.x + gAdjacentTileOffsets[facingDir].x,
|
|
pokemon->posWorld.y + gAdjacentTileOffsets[facingDir].y)->pokemon;
|
|
if (target != NULL &&
|
|
GetEntityType(target) == ENTITY_POKEMON &&
|
|
CanAttackInFront(pokemon, facingDir) &&
|
|
CanTarget(pokemon, target, FALSE, checkPetrified) == TARGET_CAPABILITY_CAN_TARGET &&
|
|
(!hasStatusChecker || target->entityData->immobilizeStatus != IMMOBILIZE_STATUS_FROZEN))
|
|
{
|
|
potentialAttackTargetDirections[numPotentialTargets] = facingDir;
|
|
potentialAttackTargetWeights[numPotentialTargets] = WeightMove(pokemon, TARGETING_FLAG_TARGET_OTHER, target, TYPE_NONE);
|
|
if (!hasTargetingIQ)
|
|
{
|
|
*targetDir = facingDir;
|
|
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 = DungeonRandomCapped(totalWeight);
|
|
for (i = 0; i < numPotentialTargets; i++)
|
|
{
|
|
weightCounter -= potentialAttackTargetWeights[i];
|
|
if (weightCounter < 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
*targetDir = potentialAttackTargetDirections[i];
|
|
return TRUE;
|
|
}
|
|
|
|
}
|
|
|
|
bool8 IsTargetStraightAhead(struct DungeonEntity *pokemon, struct DungeonEntity *targetPokemon, s32 facingDir, s32 maxRange)
|
|
{
|
|
s32 posDiffX = pokemon->posWorld.x - targetPokemon->posWorld.x;
|
|
s32 effectiveMaxRange;
|
|
if (posDiffX < 0)
|
|
{
|
|
posDiffX = -posDiffX;
|
|
}
|
|
effectiveMaxRange = pokemon->posWorld.y - targetPokemon->posWorld.y;
|
|
if (effectiveMaxRange < 0)
|
|
{
|
|
effectiveMaxRange = -effectiveMaxRange;
|
|
}
|
|
if (effectiveMaxRange < posDiffX)
|
|
{
|
|
effectiveMaxRange = posDiffX;
|
|
}
|
|
if (effectiveMaxRange > maxRange)
|
|
{
|
|
effectiveMaxRange = maxRange;
|
|
}
|
|
if (!HasIQSkill(pokemon, IQ_SKILL_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->posWorld.x;
|
|
s32 currentPosY = pokemon->posWorld.y;
|
|
s32 adjacentTileOffsetX = gAdjacentTileOffsets[facingDir].x;
|
|
s32 adjacentTileOffsetY = gAdjacentTileOffsets[facingDir].y;
|
|
s32 i;
|
|
for (i = 0; i <= effectiveMaxRange; i++)
|
|
{
|
|
struct MapTile *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 = GetMapTile_1(currentPosX, currentPosY);
|
|
if (!(mapTile->tileType & (TILE_TYPE_FLOOR | TILE_TYPE_LIQUID)))
|
|
{
|
|
break;
|
|
}
|
|
if (mapTile->pokemon == targetPokemon)
|
|
{
|
|
return TRUE;
|
|
}
|
|
if (mapTile->pokemon != NULL)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return FALSE;
|
|
}
|