2022-05-19 01:51:26 -07:00
|
|
|
#include "combat_ai.h"
|
|
|
|
|
2022-09-15 02:38:23 -07:00
|
|
|
#include <stdio.h>
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <string.h>
|
|
|
|
|
2022-05-19 01:51:26 -07:00
|
|
|
#include "actions.h"
|
|
|
|
#include "animation.h"
|
2022-06-19 03:32:42 -07:00
|
|
|
#include "art.h"
|
2022-05-19 01:51:26 -07:00
|
|
|
#include "combat.h"
|
|
|
|
#include "config.h"
|
|
|
|
#include "critter.h"
|
|
|
|
#include "debug.h"
|
|
|
|
#include "display_monitor.h"
|
|
|
|
#include "game.h"
|
|
|
|
#include "game_sound.h"
|
2022-10-04 23:23:27 -07:00
|
|
|
#include "input.h"
|
2022-05-19 01:51:26 -07:00
|
|
|
#include "interface.h"
|
|
|
|
#include "item.h"
|
|
|
|
#include "light.h"
|
|
|
|
#include "map.h"
|
|
|
|
#include "memory.h"
|
2022-06-19 01:32:07 -07:00
|
|
|
#include "message.h"
|
2022-05-19 01:51:26 -07:00
|
|
|
#include "object.h"
|
2022-06-19 01:32:07 -07:00
|
|
|
#include "party_member.h"
|
2022-05-28 01:57:32 -07:00
|
|
|
#include "platform_compat.h"
|
2022-05-19 01:51:26 -07:00
|
|
|
#include "proto.h"
|
|
|
|
#include "proto_instance.h"
|
|
|
|
#include "random.h"
|
|
|
|
#include "scripts.h"
|
2022-10-06 06:32:46 -07:00
|
|
|
#include "settings.h"
|
2022-05-19 01:51:26 -07:00
|
|
|
#include "skill.h"
|
|
|
|
#include "stat.h"
|
2022-10-05 00:35:05 -07:00
|
|
|
#include "svga.h"
|
2022-05-19 01:51:26 -07:00
|
|
|
#include "text_object.h"
|
|
|
|
#include "tile.h"
|
|
|
|
|
2022-09-23 05:43:44 -07:00
|
|
|
namespace fallout {
|
|
|
|
|
2022-06-19 01:32:07 -07:00
|
|
|
#define AI_PACKET_CHEM_PRIMARY_DESIRE_COUNT (3)
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
#define AI_MESSAGE_SIZE 260
|
|
|
|
|
2022-12-27 15:19:43 -08:00
|
|
|
static constexpr int kChemUseStimsWhenHurtLittleHpRatio = 60;
|
|
|
|
static constexpr int kChemUseStimsWhenHurtLotsHpRatio = 30;
|
|
|
|
static constexpr int kChemUseStimsHpRatio = 50;
|
|
|
|
|
|
|
|
static constexpr int kChemUseSometimesChance = 25;
|
|
|
|
static constexpr int kChemUseAnytimeChance = 75;
|
|
|
|
static constexpr int kChemUseAlwaysChance = 100;
|
|
|
|
|
|
|
|
static constexpr int kRandomDrugPickingArraySize = 3;
|
|
|
|
|
2022-06-19 01:32:07 -07:00
|
|
|
typedef struct AiMessageRange {
|
|
|
|
int start;
|
|
|
|
int end;
|
|
|
|
} AiMessageRange;
|
|
|
|
|
|
|
|
typedef struct AiPacket {
|
|
|
|
char* name;
|
|
|
|
int packet_num;
|
|
|
|
int max_dist;
|
|
|
|
int min_to_hit;
|
|
|
|
int min_hp;
|
|
|
|
int aggression;
|
|
|
|
int hurt_too_much;
|
|
|
|
int secondary_freq;
|
|
|
|
int called_freq;
|
|
|
|
int font;
|
|
|
|
int color;
|
|
|
|
int outline_color;
|
|
|
|
int chance;
|
|
|
|
AiMessageRange run;
|
|
|
|
AiMessageRange move;
|
|
|
|
AiMessageRange attack;
|
|
|
|
AiMessageRange miss;
|
|
|
|
AiMessageRange hit[HIT_LOCATION_SPECIFIC_COUNT];
|
|
|
|
int area_attack_mode;
|
|
|
|
int run_away_mode;
|
|
|
|
int best_weapon;
|
|
|
|
int distance;
|
|
|
|
int attack_who;
|
|
|
|
int chem_use;
|
|
|
|
int chem_primary_desire[AI_PACKET_CHEM_PRIMARY_DESIRE_COUNT];
|
|
|
|
int disposition;
|
|
|
|
char* body_type;
|
|
|
|
char* general_type;
|
|
|
|
} AiPacket;
|
|
|
|
|
2022-08-02 03:31:26 -07:00
|
|
|
typedef struct AiRetargetData {
|
|
|
|
Object* source;
|
|
|
|
Object* target;
|
|
|
|
Object* critterList[100];
|
|
|
|
int ratingList[100];
|
|
|
|
int critterCount;
|
|
|
|
int sourceTeam;
|
|
|
|
int sourceRating;
|
|
|
|
bool notSameTile;
|
|
|
|
int* tiles;
|
|
|
|
int currentTileIndex;
|
|
|
|
int sourceIntelligence;
|
|
|
|
} AiRetargetData;
|
2022-06-19 01:32:07 -07:00
|
|
|
|
|
|
|
static void _parse_hurt_str(char* str, int* out_value);
|
|
|
|
static int _cai_match_str_to_list(const char* str, const char** list, int count, int* out_value);
|
|
|
|
static void aiPacketInit(AiPacket* ai);
|
|
|
|
static int aiPacketRead(File* stream, AiPacket* ai);
|
|
|
|
static int aiPacketWrite(File* stream, AiPacket* ai);
|
|
|
|
static AiPacket* aiGetPacket(Object* obj);
|
|
|
|
static AiPacket* aiGetPacketByNum(int aiPacketNum);
|
|
|
|
static int _ai_magic_hands(Object* a1, Object* a2, int num);
|
|
|
|
static int _ai_check_drugs(Object* critter);
|
|
|
|
static void _ai_run_away(Object* a1, Object* a2);
|
|
|
|
static int _ai_move_away(Object* a1, Object* a2, int a3);
|
|
|
|
static bool _ai_find_friend(Object* a1, int a2, int a3);
|
|
|
|
static int _compare_nearer(const void* a1, const void* a2);
|
2022-09-01 08:41:37 -07:00
|
|
|
static void _ai_sort_list_distance(Object** critterList, int length, Object* origin);
|
2022-12-24 03:30:03 -08:00
|
|
|
static int _compare_strength(const void* a1, const void* a2);
|
2022-09-01 08:41:37 -07:00
|
|
|
static void _ai_sort_list_strength(Object** critterList, int length);
|
2022-12-24 03:30:03 -08:00
|
|
|
static int _compare_weakness(const void* a1, const void* a2);
|
2022-09-01 08:41:37 -07:00
|
|
|
static void _ai_sort_list_weakness(Object** critterList, int length);
|
2022-12-24 03:30:03 -08:00
|
|
|
static Object* _ai_find_nearest_team(Object* a1, Object* a2, int flags);
|
|
|
|
static Object* _ai_find_nearest_team_in_combat(Object* a1, Object* a2, int flags);
|
2022-12-28 08:45:17 -08:00
|
|
|
static int aiFindAttackers(Object* critter, Object** whoHitMePtr, Object** whoHitFriendPtr, Object** whoHitByFriendPtr);
|
2022-06-19 01:32:07 -07:00
|
|
|
static Object* _ai_danger_source(Object* a1);
|
2022-08-31 02:05:56 -07:00
|
|
|
static bool aiHaveAmmo(Object* critter, Object* weapon, Object** ammoPtr);
|
2022-06-19 01:32:07 -07:00
|
|
|
static bool _caiHasWeapPrefType(AiPacket* ai, int attackType);
|
|
|
|
static Object* _ai_best_weapon(Object* a1, Object* a2, Object* a3, Object* a4);
|
|
|
|
static bool _ai_can_use_weapon(Object* critter, Object* weapon, int hitMode);
|
|
|
|
static bool aiCanUseItem(Object* obj, Object* a2);
|
|
|
|
static Object* _ai_search_environ(Object* critter, int itemType);
|
2022-12-24 03:30:03 -08:00
|
|
|
static Object* _ai_retrieve_object(Object* critter, Object* item);
|
|
|
|
static int _ai_pick_hit_mode(Object* attacker, Object* weapon, Object* defender);
|
|
|
|
static int _ai_move_steps_closer(Object* a1, Object* a2, int actionPoints, bool taunt);
|
|
|
|
static int _ai_move_closer(Object* a1, Object* a2, bool taunt);
|
2022-08-02 03:31:26 -07:00
|
|
|
static int _cai_retargetTileFromFriendlyFire(Object* source, Object* target, int* tilePtr);
|
|
|
|
static int _cai_retargetTileFromFriendlyFireSubFunc(AiRetargetData* aiRetargetData, int tile);
|
2022-09-23 04:01:39 -07:00
|
|
|
static bool _cai_attackWouldIntersect(Object* attacker, Object* defender, Object* attackerFriend, int tile, int* distance);
|
2022-06-19 01:32:07 -07:00
|
|
|
static int _ai_switch_weapons(Object* a1, int* hitMode, Object** weapon, Object* a4);
|
2022-12-24 03:30:03 -08:00
|
|
|
static int _ai_called_shot(Object* attacker, Object* defender, int hitMode);
|
|
|
|
static int _ai_attack(Object* attacker, Object* defender, int hitMode);
|
2022-06-19 01:32:07 -07:00
|
|
|
static int _ai_try_attack(Object* a1, Object* a2);
|
|
|
|
static int _cai_get_min_hp(AiPacket* ai);
|
|
|
|
static int _ai_print_msg(Object* critter, int type);
|
|
|
|
static int _combatai_rating(Object* obj);
|
|
|
|
static int aiMessageListInit();
|
|
|
|
static int aiMessageListFree();
|
|
|
|
|
2022-05-19 01:51:26 -07:00
|
|
|
// 0x51805C
|
2022-06-19 01:32:07 -07:00
|
|
|
static Object* _combat_obj = NULL;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
// 0x518060
|
2022-06-19 01:32:07 -07:00
|
|
|
static int gAiPacketsLength = 0;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
// 0x518064
|
2022-06-19 01:32:07 -07:00
|
|
|
static AiPacket* gAiPackets = NULL;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
// 0x518068
|
2022-06-19 01:32:07 -07:00
|
|
|
static bool gAiInitialized = false;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
// 0x51806C
|
|
|
|
const char* gAreaAttackModeKeys[AREA_ATTACK_MODE_COUNT] = {
|
|
|
|
"always",
|
|
|
|
"sometimes",
|
|
|
|
"be_sure",
|
|
|
|
"be_careful",
|
|
|
|
"be_absolutely_sure",
|
|
|
|
};
|
|
|
|
|
|
|
|
// 0x5180D0
|
|
|
|
const char* gAttackWhoKeys[ATTACK_WHO_COUNT] = {
|
|
|
|
"whomever_attacking_me",
|
|
|
|
"strongest",
|
|
|
|
"weakest",
|
|
|
|
"whomever",
|
|
|
|
"closest",
|
|
|
|
};
|
|
|
|
|
|
|
|
// 0x51809C
|
|
|
|
const char* gBestWeaponKeys[BEST_WEAPON_COUNT] = {
|
|
|
|
"no_pref",
|
|
|
|
"melee",
|
|
|
|
"melee_over_ranged",
|
|
|
|
"ranged_over_melee",
|
|
|
|
"ranged",
|
|
|
|
"unarmed",
|
|
|
|
"unarmed_over_thrown",
|
|
|
|
"random",
|
|
|
|
};
|
|
|
|
|
|
|
|
// 0x5180E4
|
|
|
|
const char* gChemUseKeys[CHEM_USE_COUNT] = {
|
|
|
|
"clean",
|
|
|
|
"stims_when_hurt_little",
|
|
|
|
"stims_when_hurt_lots",
|
|
|
|
"sometimes",
|
|
|
|
"anytime",
|
|
|
|
"always",
|
|
|
|
};
|
|
|
|
|
|
|
|
// 0x5180BC
|
|
|
|
const char* gDistanceModeKeys[DISTANCE_COUNT] = {
|
|
|
|
"stay_close",
|
|
|
|
"charge",
|
|
|
|
"snipe",
|
|
|
|
"on_your_own",
|
|
|
|
"stay",
|
|
|
|
};
|
|
|
|
|
|
|
|
// 0x518080
|
|
|
|
const char* gRunAwayModeKeys[RUN_AWAY_MODE_COUNT] = {
|
|
|
|
"none",
|
|
|
|
"coward",
|
|
|
|
"finger_hurts",
|
|
|
|
"bleeding",
|
|
|
|
"not_feeling_good",
|
|
|
|
"tourniquet",
|
|
|
|
"never",
|
|
|
|
};
|
|
|
|
|
|
|
|
// 0x5180FC
|
|
|
|
const char* gDispositionKeys[DISPOSITION_COUNT] = {
|
|
|
|
"none",
|
|
|
|
"custom",
|
|
|
|
"coward",
|
|
|
|
"defensive",
|
|
|
|
"aggressive",
|
|
|
|
"berserk",
|
|
|
|
};
|
|
|
|
|
|
|
|
// 0x518114
|
|
|
|
const char* gHurtTooMuchKeys[HURT_COUNT] = {
|
|
|
|
"blind",
|
|
|
|
"crippled",
|
|
|
|
"crippled_legs",
|
|
|
|
"crippled_arms",
|
|
|
|
};
|
|
|
|
|
|
|
|
// hurt_too_much
|
|
|
|
//
|
|
|
|
// 0x518124
|
2022-06-19 01:32:07 -07:00
|
|
|
static const int _rmatchHurtVals[5] = {
|
2022-05-19 01:51:26 -07:00
|
|
|
DAM_BLIND,
|
|
|
|
DAM_CRIP_LEG_LEFT | DAM_CRIP_LEG_RIGHT | DAM_CRIP_ARM_LEFT | DAM_CRIP_ARM_RIGHT,
|
|
|
|
DAM_CRIP_LEG_LEFT | DAM_CRIP_LEG_RIGHT,
|
|
|
|
DAM_CRIP_ARM_LEFT | DAM_CRIP_ARM_RIGHT,
|
|
|
|
0,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Hit points in percent to choose run away mode.
|
|
|
|
//
|
|
|
|
// 0x518138
|
2022-06-19 01:32:07 -07:00
|
|
|
static const int _hp_run_away_value[6] = {
|
2022-05-19 01:51:26 -07:00
|
|
|
0,
|
|
|
|
25,
|
|
|
|
40,
|
|
|
|
60,
|
|
|
|
75,
|
|
|
|
100,
|
|
|
|
};
|
|
|
|
|
|
|
|
// 0x518150
|
2022-06-19 01:32:07 -07:00
|
|
|
static Object* _attackerTeamObj = NULL;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
// 0x518154
|
2022-06-19 01:32:07 -07:00
|
|
|
static Object* _targetTeamObj = NULL;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
// 0x518158
|
2022-12-24 03:30:03 -08:00
|
|
|
static const int _weapPrefOrderings[BEST_WEAPON_COUNT + 1][ATTACK_TYPE_COUNT] = {
|
2022-05-19 01:51:26 -07:00
|
|
|
{ ATTACK_TYPE_RANGED, ATTACK_TYPE_THROW, ATTACK_TYPE_MELEE, ATTACK_TYPE_UNARMED, 0 },
|
|
|
|
{ ATTACK_TYPE_RANGED, ATTACK_TYPE_THROW, ATTACK_TYPE_MELEE, ATTACK_TYPE_UNARMED, 0 }, // BEST_WEAPON_NO_PREF
|
|
|
|
{ ATTACK_TYPE_MELEE, 0, 0, 0, 0 }, // BEST_WEAPON_MELEE
|
|
|
|
{ ATTACK_TYPE_MELEE, ATTACK_TYPE_RANGED, 0, 0, 0 }, // BEST_WEAPON_MELEE_OVER_RANGED
|
|
|
|
{ ATTACK_TYPE_RANGED, ATTACK_TYPE_MELEE, 0, 0, 0 }, // BEST_WEAPON_RANGED_OVER_MELEE
|
|
|
|
{ ATTACK_TYPE_RANGED, 0, 0, 0, 0 }, // BEST_WEAPON_RANGED
|
|
|
|
{ ATTACK_TYPE_UNARMED, 0, 0, 0, 0 }, // BEST_WEAPON_UNARMED
|
|
|
|
{ ATTACK_TYPE_UNARMED, ATTACK_TYPE_THROW, 0, 0, 0 }, // BEST_WEAPON_UNARMED_OVER_THROW
|
|
|
|
{ 0, 0, 0, 0, 0 }, // BEST_WEAPON_RANDOM
|
|
|
|
};
|
|
|
|
|
|
|
|
// 0x518220
|
2022-06-19 01:32:07 -07:00
|
|
|
static int gLanguageFilter = -1;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
// ai.msg
|
|
|
|
//
|
|
|
|
// 0x56D510
|
2022-06-19 01:32:07 -07:00
|
|
|
static MessageList gCombatAiMessageList;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
// 0x56D518
|
2022-12-24 03:30:03 -08:00
|
|
|
static char _target_str[AI_MESSAGE_SIZE];
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
// 0x56D61C
|
2022-06-19 01:32:07 -07:00
|
|
|
static int _curr_crit_num;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
// 0x56D620
|
2022-06-19 01:32:07 -07:00
|
|
|
static Object** _curr_crit_list;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
// 0x56D624
|
2022-12-24 03:30:03 -08:00
|
|
|
static char _attack_str[AI_MESSAGE_SIZE];
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
// parse hurt_too_much
|
2022-06-19 01:32:07 -07:00
|
|
|
static void _parse_hurt_str(char* str, int* valuePtr)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
*valuePtr = 0;
|
|
|
|
|
2022-05-28 01:57:32 -07:00
|
|
|
str = compat_strlwr(str);
|
2022-05-19 01:51:26 -07:00
|
|
|
while (*str) {
|
2022-12-24 03:30:03 -08:00
|
|
|
str += strspn(str, " ");
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
size_t delimeterPos = strcspn(str, ",");
|
|
|
|
char tmp = str[delimeterPos];
|
|
|
|
str[delimeterPos] = '\0';
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
int index;
|
|
|
|
for (index = 0; index < HURT_COUNT; index++) {
|
|
|
|
if (strcmp(str, gHurtTooMuchKeys[index]) == 0) {
|
|
|
|
*valuePtr |= _rmatchHurtVals[index];
|
2022-05-19 01:51:26 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (index == HURT_COUNT) {
|
2022-05-19 01:51:26 -07:00
|
|
|
debugPrint("Unrecognized flag: %s\n", str);
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
str[delimeterPos] = tmp;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
if (tmp == '\0') {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
str += delimeterPos + 1;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// parse behaviour entry
|
2022-06-19 01:32:07 -07:00
|
|
|
static int _cai_match_str_to_list(const char* str, const char** list, int count, int* valuePtr)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
*valuePtr = -1;
|
|
|
|
for (int index = 0; index < count; index++) {
|
2022-05-28 01:57:32 -07:00
|
|
|
if (compat_stricmp(str, list[index]) == 0) {
|
2022-05-19 01:51:26 -07:00
|
|
|
*valuePtr = index;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x426FE0
|
2022-06-19 01:32:07 -07:00
|
|
|
static void aiPacketInit(AiPacket* ai)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
ai->name = NULL;
|
|
|
|
|
|
|
|
ai->area_attack_mode = -1;
|
|
|
|
ai->run_away_mode = -1;
|
|
|
|
ai->best_weapon = -1;
|
|
|
|
ai->distance = -1;
|
|
|
|
ai->attack_who = -1;
|
|
|
|
ai->chem_use = -1;
|
|
|
|
|
|
|
|
for (int index = 0; index < AI_PACKET_CHEM_PRIMARY_DESIRE_COUNT; index++) {
|
|
|
|
ai->chem_primary_desire[index] = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
ai->disposition = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// ai_init
|
|
|
|
// 0x42703C
|
|
|
|
int aiInit()
|
|
|
|
{
|
|
|
|
int index;
|
|
|
|
|
|
|
|
if (aiMessageListInit() == -1) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
gAiPacketsLength = 0;
|
|
|
|
|
|
|
|
Config config;
|
|
|
|
if (!configInit(&config)) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!configRead(&config, "data\\ai.txt", true)) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-05-21 08:22:03 -07:00
|
|
|
gAiPackets = (AiPacket*)internal_malloc(sizeof(*gAiPackets) * config.entriesLength);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (gAiPackets == NULL) {
|
|
|
|
goto err;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (index = 0; index < config.entriesLength; index++) {
|
|
|
|
aiPacketInit(&(gAiPackets[index]));
|
|
|
|
}
|
|
|
|
|
|
|
|
for (index = 0; index < config.entriesLength; index++) {
|
|
|
|
DictionaryEntry* sectionEntry = &(config.entries[index]);
|
|
|
|
AiPacket* ai = &(gAiPackets[index]);
|
|
|
|
char* stringValue;
|
|
|
|
|
|
|
|
ai->name = internal_strdup(sectionEntry->key);
|
|
|
|
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "packet_num", &(ai->packet_num))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "max_dist", &(ai->max_dist))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "min_to_hit", &(ai->min_to_hit))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "min_hp", &(ai->min_hp))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "aggression", &(ai->aggression))) goto err;
|
|
|
|
|
|
|
|
if (configGetString(&config, sectionEntry->key, "hurt_too_much", &stringValue)) {
|
|
|
|
_parse_hurt_str(stringValue, &(ai->hurt_too_much));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "secondary_freq", &(ai->secondary_freq))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "called_freq", &(ai->called_freq))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "font", &(ai->font))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "color", &(ai->color))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "outline_color", &(ai->outline_color))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "chance", &(ai->chance))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "run_start", &(ai->run.start))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "run_end", &(ai->run.end))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "move_start", &(ai->move.start))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "move_end", &(ai->move.end))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "attack_start", &(ai->attack.start))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "attack_end", &(ai->attack.end))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "miss_start", &(ai->miss.start))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "miss_end", &(ai->miss.end))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "hit_head_start", &(ai->hit[HIT_LOCATION_HEAD].start))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "hit_head_end", &(ai->hit[HIT_LOCATION_HEAD].end))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "hit_left_arm_start", &(ai->hit[HIT_LOCATION_LEFT_ARM].start))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "hit_left_arm_end", &(ai->hit[HIT_LOCATION_LEFT_ARM].end))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "hit_right_arm_start", &(ai->hit[HIT_LOCATION_RIGHT_ARM].start))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "hit_right_arm_end", &(ai->hit[HIT_LOCATION_RIGHT_ARM].end))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "hit_torso_start", &(ai->hit[HIT_LOCATION_TORSO].start))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "hit_torso_end", &(ai->hit[HIT_LOCATION_TORSO].end))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "hit_right_leg_start", &(ai->hit[HIT_LOCATION_RIGHT_LEG].start))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "hit_right_leg_end", &(ai->hit[HIT_LOCATION_RIGHT_LEG].end))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "hit_left_leg_start", &(ai->hit[HIT_LOCATION_LEFT_LEG].start))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "hit_left_leg_end", &(ai->hit[HIT_LOCATION_LEFT_LEG].end))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "hit_eyes_start", &(ai->hit[HIT_LOCATION_EYES].start))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "hit_eyes_end", &(ai->hit[HIT_LOCATION_EYES].end))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "hit_groin_start", &(ai->hit[HIT_LOCATION_GROIN].start))) goto err;
|
|
|
|
if (!configGetInt(&config, sectionEntry->key, "hit_groin_end", &(ai->hit[HIT_LOCATION_GROIN].end))) goto err;
|
|
|
|
|
|
|
|
ai->hit[HIT_LOCATION_GROIN].end++;
|
|
|
|
|
|
|
|
if (configGetString(&config, sectionEntry->key, "area_attack_mode", &stringValue)) {
|
|
|
|
_cai_match_str_to_list(stringValue, gAreaAttackModeKeys, AREA_ATTACK_MODE_COUNT, &(ai->area_attack_mode));
|
|
|
|
} else {
|
2022-12-24 03:30:03 -08:00
|
|
|
ai->area_attack_mode = -1;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (configGetString(&config, sectionEntry->key, "run_away_mode", &stringValue)) {
|
|
|
|
_cai_match_str_to_list(stringValue, gRunAwayModeKeys, RUN_AWAY_MODE_COUNT, &(ai->run_away_mode));
|
|
|
|
|
|
|
|
if (ai->run_away_mode >= 0) {
|
|
|
|
ai->run_away_mode--;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (configGetString(&config, sectionEntry->key, "best_weapon", &stringValue)) {
|
|
|
|
_cai_match_str_to_list(stringValue, gBestWeaponKeys, BEST_WEAPON_COUNT, &(ai->best_weapon));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (configGetString(&config, sectionEntry->key, "distance", &stringValue)) {
|
|
|
|
_cai_match_str_to_list(stringValue, gDistanceModeKeys, DISTANCE_COUNT, &(ai->distance));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (configGetString(&config, sectionEntry->key, "attack_who", &stringValue)) {
|
|
|
|
_cai_match_str_to_list(stringValue, gAttackWhoKeys, ATTACK_WHO_COUNT, &(ai->attack_who));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (configGetString(&config, sectionEntry->key, "chem_use", &stringValue)) {
|
|
|
|
_cai_match_str_to_list(stringValue, gChemUseKeys, CHEM_USE_COUNT, &(ai->chem_use));
|
|
|
|
}
|
|
|
|
|
|
|
|
configGetIntList(&config, sectionEntry->key, "chem_primary_desire", ai->chem_primary_desire, AI_PACKET_CHEM_PRIMARY_DESIRE_COUNT);
|
|
|
|
|
|
|
|
if (configGetString(&config, sectionEntry->key, "disposition", &stringValue)) {
|
|
|
|
_cai_match_str_to_list(stringValue, gDispositionKeys, DISPOSITION_COUNT, &(ai->disposition));
|
|
|
|
ai->disposition--;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (configGetString(&config, sectionEntry->key, "body_type", &stringValue)) {
|
|
|
|
ai->body_type = internal_strdup(stringValue);
|
|
|
|
} else {
|
|
|
|
ai->body_type = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (configGetString(&config, sectionEntry->key, "general_type", &stringValue)) {
|
|
|
|
ai->general_type = internal_strdup(stringValue);
|
|
|
|
} else {
|
|
|
|
ai->general_type = NULL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (index < config.entriesLength) {
|
|
|
|
goto err;
|
|
|
|
}
|
|
|
|
|
|
|
|
gAiPacketsLength = config.entriesLength;
|
|
|
|
|
|
|
|
configFree(&config);
|
|
|
|
|
|
|
|
gAiInitialized = true;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
err:
|
|
|
|
|
|
|
|
if (gAiPackets != NULL) {
|
|
|
|
for (index = 0; index < config.entriesLength; index++) {
|
|
|
|
AiPacket* ai = &(gAiPackets[index]);
|
|
|
|
if (ai->name != NULL) {
|
|
|
|
internal_free(ai->name);
|
|
|
|
}
|
|
|
|
|
|
|
|
// FIXME: leaking ai->body_type and ai->general_type, does not matter
|
|
|
|
// because it halts further processing
|
|
|
|
}
|
|
|
|
internal_free(gAiPackets);
|
|
|
|
}
|
|
|
|
|
|
|
|
debugPrint("Error processing ai.txt");
|
|
|
|
|
|
|
|
configFree(&config);
|
|
|
|
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4279F8
|
|
|
|
void aiReset()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4279FC
|
|
|
|
int aiExit()
|
|
|
|
{
|
|
|
|
for (int index = 0; index < gAiPacketsLength; index++) {
|
|
|
|
AiPacket* ai = &(gAiPackets[index]);
|
|
|
|
|
|
|
|
if (ai->name != NULL) {
|
|
|
|
internal_free(ai->name);
|
|
|
|
ai->name = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ai->general_type != NULL) {
|
|
|
|
internal_free(ai->general_type);
|
|
|
|
ai->general_type = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ai->body_type != NULL) {
|
|
|
|
internal_free(ai->body_type);
|
|
|
|
ai->body_type = NULL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
internal_free(gAiPackets);
|
|
|
|
gAiPacketsLength = 0;
|
|
|
|
|
|
|
|
gAiInitialized = false;
|
|
|
|
|
|
|
|
// NOTE: Uninline.
|
|
|
|
if (aiMessageListFree() != 0) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x427AD8
|
|
|
|
int aiLoad(File* stream)
|
|
|
|
{
|
|
|
|
for (int index = 0; index < gPartyMemberDescriptionsLength; index++) {
|
|
|
|
int pid = gPartyMemberPids[index];
|
2022-07-29 06:04:05 -07:00
|
|
|
if (pid != -1 && PID_TYPE(pid) == OBJ_TYPE_CRITTER) {
|
2022-05-19 01:51:26 -07:00
|
|
|
Proto* proto;
|
|
|
|
if (protoGetProto(pid, &proto) == -1) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
AiPacket* ai = aiGetPacketByNum(proto->critter.aiPacket);
|
|
|
|
if (ai->disposition == 0) {
|
|
|
|
aiPacketRead(stream, ai);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x427B50
|
|
|
|
int aiSave(File* stream)
|
|
|
|
{
|
|
|
|
for (int index = 0; index < gPartyMemberDescriptionsLength; index++) {
|
|
|
|
int pid = gPartyMemberPids[index];
|
2022-07-29 06:04:05 -07:00
|
|
|
if (pid != -1 && PID_TYPE(pid) == OBJ_TYPE_CRITTER) {
|
2022-05-19 01:51:26 -07:00
|
|
|
Proto* proto;
|
|
|
|
if (protoGetProto(pid, &proto) == -1) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
AiPacket* ai = aiGetPacketByNum(proto->critter.aiPacket);
|
|
|
|
if (ai->disposition == 0) {
|
|
|
|
aiPacketWrite(stream, ai);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x427BC8
|
2022-06-19 01:32:07 -07:00
|
|
|
static int aiPacketRead(File* stream, AiPacket* ai)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
if (fileReadInt32(stream, &(ai->packet_num)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->max_dist)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->min_to_hit)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->min_hp)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->aggression)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->hurt_too_much)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->secondary_freq)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->called_freq)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->font)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->color)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->outline_color)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->chance)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->run.start)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->run.end)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->move.start)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->move.end)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->attack.start)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->attack.end)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->miss.start)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->miss.end)) == -1) return -1;
|
|
|
|
|
|
|
|
for (int index = 0; index < HIT_LOCATION_SPECIFIC_COUNT; index++) {
|
|
|
|
AiMessageRange* range = &(ai->hit[index]);
|
|
|
|
if (fileReadInt32(stream, &(range->start)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(range->end)) == -1) return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (fileReadInt32(stream, &(ai->area_attack_mode)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->best_weapon)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->distance)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->attack_who)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->chem_use)) == -1) return -1;
|
|
|
|
if (fileReadInt32(stream, &(ai->run_away_mode)) == -1) return -1;
|
|
|
|
|
|
|
|
for (int index = 0; index < AI_PACKET_CHEM_PRIMARY_DESIRE_COUNT; index++) {
|
|
|
|
if (fileReadInt32(stream, &(ai->chem_primary_desire[index])) == -1) return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x427E1C
|
2022-06-19 01:32:07 -07:00
|
|
|
static int aiPacketWrite(File* stream, AiPacket* ai)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
if (fileWriteInt32(stream, ai->packet_num) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->max_dist) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->min_to_hit) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->min_hp) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->aggression) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->hurt_too_much) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->secondary_freq) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->called_freq) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->font) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->color) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->outline_color) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->chance) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->run.start) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->run.end) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->move.start) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->move.end) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->attack.start) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->attack.end) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->miss.start) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->miss.end) == -1) return -1;
|
|
|
|
|
|
|
|
for (int index = 0; index < HIT_LOCATION_SPECIFIC_COUNT; index++) {
|
|
|
|
AiMessageRange* range = &(ai->hit[index]);
|
|
|
|
if (fileWriteInt32(stream, range->start) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, range->end) == -1) return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (fileWriteInt32(stream, ai->area_attack_mode) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->best_weapon) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->distance) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->attack_who) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->chem_use) == -1) return -1;
|
|
|
|
if (fileWriteInt32(stream, ai->run_away_mode) == -1) return -1;
|
|
|
|
|
|
|
|
for (int index = 0; index < AI_PACKET_CHEM_PRIMARY_DESIRE_COUNT; index++) {
|
|
|
|
// TODO: Check, probably writes chem_primary_desire[0] three times,
|
|
|
|
// might be a bug in original source code.
|
|
|
|
if (fileWriteInt32(stream, ai->chem_primary_desire[index]) == -1) return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get ai from object
|
|
|
|
//
|
|
|
|
// 0x4280B4
|
2022-06-19 01:32:07 -07:00
|
|
|
static AiPacket* aiGetPacket(Object* obj)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
// NOTE: Uninline.
|
|
|
|
AiPacket* ai = aiGetPacketByNum(obj->data.critter.combat.aiPacket);
|
|
|
|
return ai;
|
|
|
|
}
|
|
|
|
|
|
|
|
// get ai packet by num
|
|
|
|
//
|
|
|
|
// 0x42811C
|
2022-06-19 01:32:07 -07:00
|
|
|
static AiPacket* aiGetPacketByNum(int aiPacketId)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
for (int index = 0; index < gAiPacketsLength; index++) {
|
|
|
|
AiPacket* ai = &(gAiPackets[index]);
|
|
|
|
if (aiPacketId == ai->packet_num) {
|
|
|
|
return ai;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
debugPrint("Missing AI Packet\n");
|
|
|
|
|
|
|
|
return gAiPackets;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x428184
|
|
|
|
int aiGetAreaAttackMode(Object* obj)
|
|
|
|
{
|
|
|
|
AiPacket* ai = aiGetPacket(obj);
|
|
|
|
return ai->area_attack_mode;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x428190
|
|
|
|
int aiGetRunAwayMode(Object* obj)
|
|
|
|
{
|
2022-12-24 03:30:03 -08:00
|
|
|
int runAwayMode = -1;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
AiPacket* ai = aiGetPacket(obj);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (ai->run_away_mode != -1) {
|
|
|
|
return ai->run_away_mode;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
int hpRatio = 100 * ai->min_hp / critterGetStat(obj, STAT_MAXIMUM_HIT_POINTS);
|
|
|
|
for (int index = 0; index < 6; index++) {
|
|
|
|
if (hpRatio >= _hp_run_away_value[index]) {
|
|
|
|
runAwayMode = index;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
return runAwayMode;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4281FC
|
|
|
|
int aiGetBestWeapon(Object* obj)
|
|
|
|
{
|
|
|
|
AiPacket* ai = aiGetPacket(obj);
|
|
|
|
return ai->best_weapon;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x428208
|
|
|
|
int aiGetDistance(Object* obj)
|
|
|
|
{
|
|
|
|
AiPacket* ai = aiGetPacket(obj);
|
|
|
|
return ai->distance;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x428214
|
|
|
|
int aiGetAttackWho(Object* obj)
|
|
|
|
{
|
|
|
|
AiPacket* ai = aiGetPacket(obj);
|
|
|
|
return ai->attack_who;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x428220
|
|
|
|
int aiGetChemUse(Object* obj)
|
|
|
|
{
|
|
|
|
AiPacket* ai = aiGetPacket(obj);
|
|
|
|
return ai->chem_use;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42822C
|
|
|
|
int aiSetAreaAttackMode(Object* critter, int areaAttackMode)
|
|
|
|
{
|
|
|
|
if (areaAttackMode >= AREA_ATTACK_MODE_COUNT) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
AiPacket* ai = aiGetPacket(critter);
|
|
|
|
ai->area_attack_mode = areaAttackMode;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x428248
|
|
|
|
int aiSetRunAwayMode(Object* obj, int runAwayMode)
|
|
|
|
{
|
|
|
|
if (runAwayMode >= 6) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
AiPacket* ai = aiGetPacket(obj);
|
|
|
|
ai->run_away_mode = runAwayMode;
|
|
|
|
|
|
|
|
int maximumHp = critterGetStat(obj, STAT_MAXIMUM_HIT_POINTS);
|
|
|
|
ai->min_hp = maximumHp - maximumHp * _hp_run_away_value[runAwayMode] / 100;
|
|
|
|
|
|
|
|
int currentHp = critterGetStat(obj, STAT_CURRENT_HIT_POINTS);
|
|
|
|
const char* name = critterGetName(obj);
|
|
|
|
|
|
|
|
debugPrint("\n%s minHp = %d; curHp = %d", name, ai->min_hp, currentHp);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4282D0
|
|
|
|
int aiSetBestWeapon(Object* critter, int bestWeapon)
|
|
|
|
{
|
|
|
|
if (bestWeapon >= BEST_WEAPON_COUNT) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
AiPacket* ai = aiGetPacket(critter);
|
|
|
|
ai->best_weapon = bestWeapon;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4282EC
|
|
|
|
int aiSetDistance(Object* critter, int distance)
|
|
|
|
{
|
|
|
|
if (distance >= DISTANCE_COUNT) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
AiPacket* ai = aiGetPacket(critter);
|
|
|
|
ai->distance = distance;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x428308
|
|
|
|
int aiSetAttackWho(Object* critter, int attackWho)
|
|
|
|
{
|
|
|
|
if (attackWho >= ATTACK_WHO_COUNT) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
AiPacket* ai = aiGetPacket(critter);
|
|
|
|
ai->attack_who = attackWho;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x428324
|
|
|
|
int aiSetChemUse(Object* critter, int chemUse)
|
|
|
|
{
|
|
|
|
if (chemUse >= CHEM_USE_COUNT) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
AiPacket* ai = aiGetPacket(critter);
|
|
|
|
ai->chem_use = chemUse;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x428340
|
|
|
|
int aiGetDisposition(Object* obj)
|
|
|
|
{
|
|
|
|
if (obj == NULL) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
AiPacket* ai = aiGetPacket(obj);
|
|
|
|
return ai->disposition;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x428354
|
|
|
|
int aiSetDisposition(Object* obj, int disposition)
|
|
|
|
{
|
|
|
|
if (obj == NULL) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (disposition == -1 || disposition >= 5) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
AiPacket* ai = aiGetPacket(obj);
|
|
|
|
obj->data.critter.combat.aiPacket = ai->packet_num - (disposition - ai->disposition);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x428398
|
2022-06-19 01:32:07 -07:00
|
|
|
static int _ai_magic_hands(Object* critter, Object* item, int num)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-07-29 06:04:05 -07:00
|
|
|
reg_anim_begin(ANIMATION_REQUEST_RESERVED);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-07-29 06:04:05 -07:00
|
|
|
animationRegisterAnimate(critter, ANIM_MAGIC_HANDS_MIDDLE, 0);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
if (reg_anim_end() == 0) {
|
|
|
|
if (isInCombat()) {
|
|
|
|
_combat_turn_run();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (num != -1) {
|
|
|
|
MessageListItem messageListItem;
|
|
|
|
messageListItem.num = num;
|
|
|
|
if (messageListGetItem(&gMiscMessageList, &messageListItem)) {
|
|
|
|
const char* critterName = objectGetName(critter);
|
|
|
|
|
|
|
|
char text[200];
|
|
|
|
if (item != NULL) {
|
|
|
|
const char* itemName = objectGetName(item);
|
2022-12-08 12:05:50 -08:00
|
|
|
snprintf(text, sizeof(text), "%s %s %s.", critterName, messageListItem.text, itemName);
|
2022-05-19 01:51:26 -07:00
|
|
|
} else {
|
2022-12-08 12:05:50 -08:00
|
|
|
snprintf(text, sizeof(text), "%s %s.", critterName, messageListItem.text);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
displayMonitorAddMessage(text);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// ai using drugs
|
|
|
|
// 0x428480
|
2022-06-19 01:32:07 -07:00
|
|
|
static int _ai_check_drugs(Object* critter)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
if (critterGetBodyType(critter) != BODY_TYPE_BIPED) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
bool searchCompleted = false;
|
|
|
|
bool drugUsed = false;
|
|
|
|
int drugCount = 0;
|
|
|
|
Object* lastItem = aiInfoGetLastItem(critter);
|
|
|
|
if (lastItem == NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
AiPacket* ai = aiGetPacket(critter);
|
|
|
|
if (ai == NULL) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2022-12-27 15:19:43 -08:00
|
|
|
int hpRatio = kChemUseStimsHpRatio;
|
2022-12-24 03:30:03 -08:00
|
|
|
int chemUseChance = 0;
|
|
|
|
switch (ai->chem_use) {
|
|
|
|
case CHEM_USE_CLEAN:
|
2022-05-19 01:51:26 -07:00
|
|
|
return 0;
|
2022-12-24 03:30:03 -08:00
|
|
|
case CHEM_USE_STIMS_WHEN_HURT_LITTLE:
|
2022-12-27 15:19:43 -08:00
|
|
|
hpRatio = kChemUseStimsWhenHurtLittleHpRatio;
|
2022-05-19 01:51:26 -07:00
|
|
|
break;
|
2022-12-24 03:30:03 -08:00
|
|
|
case CHEM_USE_STIMS_WHEN_HURT_LOTS:
|
2022-12-27 15:19:43 -08:00
|
|
|
hpRatio = kChemUseStimsWhenHurtLotsHpRatio;
|
2022-05-19 01:51:26 -07:00
|
|
|
break;
|
2022-12-24 03:30:03 -08:00
|
|
|
case CHEM_USE_SOMETIMES:
|
2022-05-19 01:51:26 -07:00
|
|
|
if ((_combatNumTurns % 3) == 0) {
|
2022-12-27 15:19:43 -08:00
|
|
|
chemUseChance = kChemUseSometimesChance;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
break;
|
2022-12-24 03:30:03 -08:00
|
|
|
case CHEM_USE_ANYTIME:
|
2022-05-19 01:51:26 -07:00
|
|
|
if ((_combatNumTurns % 3) == 0) {
|
2022-12-27 15:19:43 -08:00
|
|
|
chemUseChance = kChemUseAnytimeChance;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
break;
|
2022-12-24 03:30:03 -08:00
|
|
|
case CHEM_USE_ALWAYS:
|
2022-12-27 15:19:43 -08:00
|
|
|
chemUseChance = kChemUseAlwaysChance;
|
2022-05-19 01:51:26 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
int minHp = critterGetStat(critter, STAT_MAXIMUM_HIT_POINTS) * hpRatio / 100;
|
|
|
|
int inventoryItemIndex = -1;
|
|
|
|
while (critterGetStat(critter, STAT_CURRENT_HIT_POINTS) < minHp && critter->data.critter.combat.ap >= 2) {
|
|
|
|
Object* drug = _inven_find_type(critter, ITEM_TYPE_DRUG, &inventoryItemIndex);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (drug == NULL) {
|
2022-12-24 03:30:03 -08:00
|
|
|
searchCompleted = true;
|
2022-05-19 01:51:26 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
int drugPid = drug->pid;
|
2022-12-24 03:30:03 -08:00
|
|
|
if (itemIsHealing(drugPid)) {
|
|
|
|
if (itemRemove(critter, drug, 1) == 0) {
|
|
|
|
if (_item_d_take_drug(critter, drug) == -1) {
|
|
|
|
itemAdd(critter, drug, 1);
|
|
|
|
} else {
|
|
|
|
_ai_magic_hands(critter, drug, 5000);
|
|
|
|
_obj_connect(drug, critter->tile, critter->elevation, NULL);
|
|
|
|
_obj_destroy(drug);
|
|
|
|
drugUsed = true;
|
|
|
|
}
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (critter->data.critter.combat.ap < 2) {
|
|
|
|
critter->data.critter.combat.ap = 0;
|
|
|
|
} else {
|
|
|
|
critter->data.critter.combat.ap -= 2;
|
|
|
|
}
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
inventoryItemIndex = -1;
|
|
|
|
}
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (!drugUsed) {
|
|
|
|
if (chemUseChance > 0 && randomBetween(0, 100) < chemUseChance) {
|
2022-12-27 15:19:43 -08:00
|
|
|
// CE: Slightly improve and randomize drug picking.
|
|
|
|
inventoryItemIndex = -1;
|
|
|
|
searchCompleted = false;
|
|
|
|
|
|
|
|
Object* primaryDrugs[kRandomDrugPickingArraySize];
|
|
|
|
int primaryDrugsCount = 0;
|
|
|
|
|
|
|
|
Object* secondaryDrugs[kRandomDrugPickingArraySize];
|
|
|
|
int secondaryDrugsCount = 0;
|
|
|
|
|
|
|
|
// Collect drugs into buckets - primary desires and everything
|
|
|
|
// else.
|
|
|
|
//
|
|
|
|
// Strictly speaking NPCs can have more drugs than buckets can
|
|
|
|
// store. Together with `CHEM_USE_ALWAYS` it can lead to
|
|
|
|
// unexpected behaviour. In vanilla NPCs will eat drugs on their
|
|
|
|
// turns while they have action points. With this implementation
|
|
|
|
// NPCs will eat at most 2x `kRandomDrugPickingArraySize` drugs
|
|
|
|
// every turn (exhausting both primary and secondary buckets)
|
|
|
|
// and then continue to other activities (assuming they have
|
|
|
|
// action points to do so). In practice this sitation is very
|
|
|
|
// rare if even exists in vanilla or popular mods.
|
|
|
|
//
|
|
|
|
// The alternative is to use a pair of `std::vector`s and let
|
|
|
|
// them manage the memory.
|
|
|
|
while (true) {
|
2022-12-24 03:30:03 -08:00
|
|
|
Object* drug = _inven_find_type(critter, ITEM_TYPE_DRUG, &inventoryItemIndex);
|
|
|
|
if (drug == NULL) {
|
|
|
|
searchCompleted = true;
|
2022-05-19 01:51:26 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2022-12-27 15:19:43 -08:00
|
|
|
if (!itemIsHealing(drug->pid)) {
|
|
|
|
bool isPrimary = false;
|
|
|
|
for (int index = 0; index < AI_PACKET_CHEM_PRIMARY_DESIRE_COUNT; index++) {
|
|
|
|
if (ai->chem_primary_desire[index] == drug->pid) {
|
|
|
|
isPrimary = true;
|
|
|
|
break;
|
|
|
|
}
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
2022-12-27 15:19:43 -08:00
|
|
|
if (isPrimary) {
|
|
|
|
if (primaryDrugsCount < kRandomDrugPickingArraySize) {
|
|
|
|
primaryDrugs[primaryDrugsCount++] = drug;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (secondaryDrugsCount < kRandomDrugPickingArraySize) {
|
|
|
|
secondaryDrugs[secondaryDrugsCount++] = drug;
|
2022-12-24 03:30:03 -08:00
|
|
|
}
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-12-27 15:19:43 -08:00
|
|
|
|
|
|
|
// Consume drugs one-by-one in random order with primary desires
|
|
|
|
// first.
|
|
|
|
while (critter->data.critter.combat.ap >= 2) {
|
|
|
|
Object** availableDrugs;
|
|
|
|
int* availableDrugsCountPtr;
|
|
|
|
|
|
|
|
if (primaryDrugsCount > 0) {
|
|
|
|
availableDrugs = primaryDrugs;
|
|
|
|
availableDrugsCountPtr = &primaryDrugsCount;
|
|
|
|
} else if (secondaryDrugsCount > 0) {
|
|
|
|
availableDrugs = secondaryDrugs;
|
|
|
|
availableDrugsCountPtr = &secondaryDrugsCount;
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
int index = randomBetween(0, *availableDrugsCountPtr - 1);
|
|
|
|
Object* drug = availableDrugs[index];
|
|
|
|
availableDrugs[index] = availableDrugs[*availableDrugsCountPtr - 1];
|
|
|
|
*availableDrugsCountPtr -= 1;
|
|
|
|
|
|
|
|
if (itemRemove(critter, drug, 1) == 0) {
|
|
|
|
if (_item_d_take_drug(critter, drug) == -1) {
|
|
|
|
itemAdd(critter, drug, 1);
|
|
|
|
} else {
|
|
|
|
_ai_magic_hands(critter, drug, 5000);
|
|
|
|
_obj_connect(drug, critter->tile, critter->elevation, NULL);
|
|
|
|
_obj_destroy(drug);
|
|
|
|
drugUsed = true;
|
|
|
|
drugCount += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (critter->data.critter.combat.ap < 2) {
|
|
|
|
critter->data.critter.combat.ap = 0;
|
|
|
|
} else {
|
|
|
|
critter->data.critter.combat.ap -= 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ai->chem_use == CHEM_USE_SOMETIMES || (ai->chem_use == CHEM_USE_ANYTIME && drugCount >= 2)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (lastItem != NULL || (!drugUsed && searchCompleted)) {
|
2022-05-19 01:51:26 -07:00
|
|
|
do {
|
2022-12-24 03:30:03 -08:00
|
|
|
if (lastItem == NULL) {
|
|
|
|
lastItem = _ai_search_environ(critter, ITEM_TYPE_DRUG);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (lastItem == NULL) {
|
|
|
|
lastItem = _ai_search_environ(critter, ITEM_TYPE_MISC);
|
|
|
|
|
|
|
|
if (lastItem == NULL) {
|
|
|
|
break;
|
|
|
|
}
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
lastItem = _ai_retrieve_object(critter, lastItem);
|
|
|
|
if (lastItem == NULL) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (itemRemove(critter, lastItem, 1) == 0) {
|
|
|
|
if (_item_d_take_drug(critter, lastItem) == -1) {
|
|
|
|
itemAdd(critter, lastItem, 1);
|
2022-05-19 01:51:26 -07:00
|
|
|
} else {
|
2022-12-24 03:30:03 -08:00
|
|
|
_ai_magic_hands(critter, lastItem, 5000);
|
|
|
|
_obj_connect(lastItem, critter->tile, critter->elevation, NULL);
|
|
|
|
_obj_destroy(lastItem);
|
|
|
|
lastItem = NULL;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (critter->data.critter.combat.ap < 2) {
|
|
|
|
critter->data.critter.combat.ap = 0;
|
|
|
|
} else {
|
|
|
|
critter->data.critter.combat.ap -= 2;
|
|
|
|
}
|
|
|
|
}
|
2022-12-24 03:30:03 -08:00
|
|
|
} while (lastItem != NULL && critter->data.critter.combat.ap >= 2);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x428868
|
2022-06-19 01:32:07 -07:00
|
|
|
static void _ai_run_away(Object* a1, Object* a2)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
if (a2 == NULL) {
|
|
|
|
a2 = gDude;
|
|
|
|
}
|
|
|
|
|
|
|
|
CritterCombatData* combatData = &(a1->data.critter.combat);
|
|
|
|
|
|
|
|
AiPacket* ai = aiGetPacket(a1);
|
|
|
|
int distance = objectGetDistanceBetween(a1, a2);
|
|
|
|
if (distance < ai->max_dist) {
|
|
|
|
combatData->maneuver |= CRITTER_MANUEVER_FLEEING;
|
|
|
|
|
|
|
|
int rotation = tileGetRotationTo(a2->tile, a1->tile);
|
|
|
|
|
|
|
|
int destination;
|
|
|
|
int actionPoints = combatData->ap;
|
|
|
|
for (; actionPoints > 0; actionPoints -= 1) {
|
|
|
|
destination = tileGetTileInDirection(a1->tile, rotation, actionPoints);
|
|
|
|
if (_make_path(a1, a1->tile, destination, NULL, 1) > 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
destination = tileGetTileInDirection(a1->tile, (rotation + 1) % ROTATION_COUNT, actionPoints);
|
|
|
|
if (_make_path(a1, a1->tile, destination, NULL, 1) > 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
destination = tileGetTileInDirection(a1->tile, (rotation + 5) % ROTATION_COUNT, actionPoints);
|
|
|
|
if (_make_path(a1, a1->tile, destination, NULL, 1) > 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (actionPoints > 0) {
|
2022-07-29 06:04:05 -07:00
|
|
|
reg_anim_begin(ANIMATION_REQUEST_RESERVED);
|
2022-05-19 01:51:26 -07:00
|
|
|
_combatai_msg(a1, NULL, AI_MESSAGE_TYPE_RUN, 0);
|
2022-07-29 06:04:05 -07:00
|
|
|
animationRegisterRunToTile(a1, destination, a1->elevation, combatData->ap, 0);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (reg_anim_end() == 0) {
|
|
|
|
_combat_turn_run();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2023-01-16 05:49:50 -08:00
|
|
|
combatData->maneuver |= CRITTER_MANEUVER_DISENGAGING;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42899C
|
2022-06-19 01:32:07 -07:00
|
|
|
static int _ai_move_away(Object* a1, Object* a2, int a3)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
if (aiGetPacket(a1)->distance == DISTANCE_STAY) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (objectGetDistanceBetween(a1, a2) <= a3) {
|
|
|
|
int actionPoints = a1->data.critter.combat.ap;
|
|
|
|
if (a3 < actionPoints) {
|
|
|
|
actionPoints = a3;
|
|
|
|
}
|
|
|
|
|
|
|
|
int rotation = tileGetRotationTo(a2->tile, a1->tile);
|
|
|
|
|
|
|
|
int destination;
|
|
|
|
int actionPointsLeft = actionPoints;
|
|
|
|
for (; actionPointsLeft > 0; actionPointsLeft -= 1) {
|
|
|
|
destination = tileGetTileInDirection(a1->tile, rotation, actionPointsLeft);
|
|
|
|
if (_make_path(a1, a1->tile, destination, NULL, 1) > 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
destination = tileGetTileInDirection(a1->tile, (rotation + 1) % ROTATION_COUNT, actionPointsLeft);
|
|
|
|
if (_make_path(a1, a1->tile, destination, NULL, 1) > 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
destination = tileGetTileInDirection(a1->tile, (rotation + 5) % ROTATION_COUNT, actionPointsLeft);
|
|
|
|
if (_make_path(a1, a1->tile, destination, NULL, 1) > 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (actionPoints > 0) {
|
2022-07-29 06:04:05 -07:00
|
|
|
reg_anim_begin(ANIMATION_REQUEST_RESERVED);
|
|
|
|
animationRegisterMoveToTile(a1, destination, a1->elevation, actionPoints, 0);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (reg_anim_end() == 0) {
|
|
|
|
_combat_turn_run();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x428AC4
|
2022-06-19 01:32:07 -07:00
|
|
|
static bool _ai_find_friend(Object* a1, int a2, int a3)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
Object* v1 = _ai_find_nearest_team(a1, a1, 1);
|
|
|
|
if (v1 == NULL) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
int distance = objectGetDistanceBetween(a1, v1);
|
|
|
|
if (distance > a2) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (a3 > distance) {
|
|
|
|
int v2 = objectGetDistanceBetween(a1, v1) - a3;
|
2022-12-24 03:30:03 -08:00
|
|
|
_ai_move_steps_closer(a1, v1, v2, false);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Compare objects by distance to origin.
|
|
|
|
//
|
|
|
|
// 0x428B1C
|
2022-06-19 01:32:07 -07:00
|
|
|
static int _compare_nearer(const void* a1, const void* a2)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-12-24 03:30:03 -08:00
|
|
|
Object* object1 = *(Object**)a1;
|
|
|
|
Object* object2 = *(Object**)a2;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (object1 == NULL && object2 == NULL) {
|
|
|
|
return 0;
|
2022-12-28 06:54:09 -08:00
|
|
|
} else if (object1 != NULL && object2 == NULL) {
|
2022-12-24 03:30:03 -08:00
|
|
|
return -1;
|
2022-12-28 06:54:09 -08:00
|
|
|
} else if (object1 == NULL && object2 != NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
int distance1 = objectGetDistanceBetween(object1, _combat_obj);
|
|
|
|
int distance2 = objectGetDistanceBetween(object2, _combat_obj);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
if (distance1 < distance2) {
|
|
|
|
return -1;
|
|
|
|
} else if (distance1 > distance2) {
|
|
|
|
return 1;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-01 08:41:37 -07:00
|
|
|
// NOTE: Inlined.
|
|
|
|
//
|
|
|
|
// 0x428B74
|
|
|
|
static void _ai_sort_list_distance(Object** critterList, int length, Object* origin)
|
|
|
|
{
|
|
|
|
_combat_obj = origin;
|
|
|
|
qsort(critterList, length, sizeof(*critterList), _compare_nearer);
|
|
|
|
}
|
|
|
|
|
2022-05-19 01:51:26 -07:00
|
|
|
// qsort compare function - melee then ranged.
|
|
|
|
//
|
|
|
|
// 0x428B8C
|
2022-12-24 03:30:03 -08:00
|
|
|
static int _compare_strength(const void* a1, const void* a2)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-12-24 03:30:03 -08:00
|
|
|
Object* object1 = *(Object**)a1;
|
|
|
|
Object* object2 = *(Object**)a2;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (object1 == NULL && object2 == NULL) {
|
|
|
|
return 0;
|
2022-12-28 06:54:09 -08:00
|
|
|
} else if (object1 != NULL && object2 == NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
2022-12-28 06:54:09 -08:00
|
|
|
} else if (object1 == NULL && object2 != NULL) {
|
2022-12-24 03:30:03 -08:00
|
|
|
return 1;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
int rating1 = _combatai_rating(object1);
|
|
|
|
int rating2 = _combatai_rating(object2);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (rating1 < rating2) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
2022-12-24 03:30:03 -08:00
|
|
|
} else if (rating1 > rating2) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return 1;
|
2022-12-24 03:30:03 -08:00
|
|
|
} else {
|
|
|
|
return 0;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-01 08:41:37 -07:00
|
|
|
// NOTE: Inlined.
|
|
|
|
//
|
|
|
|
// 0x428BD0
|
|
|
|
static void _ai_sort_list_strength(Object** critterList, int length)
|
|
|
|
{
|
|
|
|
qsort(critterList, length, sizeof(*critterList), _compare_strength);
|
|
|
|
}
|
|
|
|
|
2022-05-19 01:51:26 -07:00
|
|
|
// qsort compare unction - ranged then melee
|
|
|
|
//
|
|
|
|
// 0x428BE4
|
2022-12-24 03:30:03 -08:00
|
|
|
static int _compare_weakness(const void* a1, const void* a2)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-12-24 03:30:03 -08:00
|
|
|
Object* object1 = *(Object**)a1;
|
|
|
|
Object* object2 = *(Object**)a2;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (object1 == NULL && object2 == NULL) {
|
|
|
|
return 0;
|
2022-12-28 06:54:09 -08:00
|
|
|
} else if (object1 != NULL && object2 == NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
2022-12-28 06:54:09 -08:00
|
|
|
} else if (object1 == NULL && object2 != NULL) {
|
2022-12-24 03:30:03 -08:00
|
|
|
return 1;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
int rating1 = _combatai_rating(object1);
|
|
|
|
int rating2 = _combatai_rating(object2);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (rating1 < rating2) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return 1;
|
2022-12-24 03:30:03 -08:00
|
|
|
} else if (rating1 > rating2) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
2022-12-24 03:30:03 -08:00
|
|
|
} else {
|
|
|
|
return 0;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-01 08:41:37 -07:00
|
|
|
// NOTE: Inlined.
|
|
|
|
//
|
|
|
|
// 0x428C28
|
|
|
|
static void _ai_sort_list_weakness(Object** critterList, int length)
|
|
|
|
{
|
|
|
|
qsort(critterList, length, sizeof(*critterList), _compare_weakness);
|
|
|
|
}
|
|
|
|
|
2022-05-19 01:51:26 -07:00
|
|
|
// 0x428C3C
|
2022-12-24 03:30:03 -08:00
|
|
|
static Object* _ai_find_nearest_team(Object* a1, Object* a2, int flags)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
if (a2 == NULL) {
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
int team = a2->data.critter.combat.team;
|
|
|
|
|
2022-05-19 01:51:26 -07:00
|
|
|
if (_curr_crit_num == 0) {
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2022-09-01 08:41:37 -07:00
|
|
|
// NOTE: Uninline.
|
|
|
|
_ai_sort_list_distance(_curr_crit_list, _curr_crit_num, a1);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
for (int index = 0; index < _curr_crit_num; index++) {
|
|
|
|
Object* obj = _curr_crit_list[index];
|
|
|
|
if (a1 != obj
|
|
|
|
&& (obj->data.critter.combat.results & DAM_DEAD) == 0
|
|
|
|
&& (((flags & 0x02) && team != obj->data.critter.combat.team)
|
|
|
|
|| ((flags & 0x01) && team == obj->data.critter.combat.team))) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x428CF4
|
2022-12-24 03:30:03 -08:00
|
|
|
static Object* _ai_find_nearest_team_in_combat(Object* a1, Object* a2, int flags)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
if (a2 == NULL) {
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
int team = a2->data.critter.combat.team;
|
|
|
|
|
2022-05-19 01:51:26 -07:00
|
|
|
if (_curr_crit_num == 0) {
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2022-09-01 08:41:37 -07:00
|
|
|
// NOTE: Uninline.
|
|
|
|
_ai_sort_list_distance(_curr_crit_list, _curr_crit_num, a1);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
for (int index = 0; index < _curr_crit_num; index++) {
|
|
|
|
Object* obj = _curr_crit_list[index];
|
|
|
|
if (obj != a1
|
|
|
|
&& (obj->data.critter.combat.results & DAM_DEAD) == 0
|
2022-12-24 03:30:03 -08:00
|
|
|
&& (((flags & 0x02) != 0 && team != obj->data.critter.combat.team)
|
|
|
|
|| ((flags & 0x01) != 0 && team == obj->data.critter.combat.team))) {
|
2022-05-19 01:51:26 -07:00
|
|
|
if (obj->data.critter.combat.whoHitMe != NULL) {
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x428DB0
|
2022-12-28 08:45:17 -08:00
|
|
|
static int aiFindAttackers(Object* critter, Object** whoHitMePtr, Object** whoHitFriendPtr, Object** whoHitByFriendPtr)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-12-28 08:45:17 -08:00
|
|
|
if (whoHitMePtr != NULL) {
|
|
|
|
*whoHitMePtr = NULL;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
2022-12-28 08:45:17 -08:00
|
|
|
if (whoHitFriendPtr != NULL) {
|
|
|
|
*whoHitFriendPtr = NULL;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
2022-12-28 08:45:17 -08:00
|
|
|
if (*whoHitByFriendPtr != NULL) {
|
|
|
|
*whoHitByFriendPtr = NULL;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (_curr_crit_num == 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2022-09-01 08:41:37 -07:00
|
|
|
// NOTE: Uninline.
|
2022-12-28 08:45:17 -08:00
|
|
|
_ai_sort_list_distance(_curr_crit_list, _curr_crit_num, critter);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
int foundTargetCount = 0;
|
2022-12-28 08:45:17 -08:00
|
|
|
int team = critter->data.critter.combat.team;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-28 08:45:17 -08:00
|
|
|
// SFALL: Add `continue` to fix for one candidate being reported in more
|
|
|
|
// than one category.
|
2022-05-19 01:51:26 -07:00
|
|
|
for (int index = 0; foundTargetCount < 3 && index < _curr_crit_num; index++) {
|
|
|
|
Object* candidate = _curr_crit_list[index];
|
2022-12-28 08:45:17 -08:00
|
|
|
if (candidate != critter) {
|
|
|
|
if (whoHitMePtr != NULL && *whoHitMePtr == NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
if ((candidate->data.critter.combat.results & DAM_DEAD) == 0
|
2022-12-28 08:45:17 -08:00
|
|
|
&& candidate->data.critter.combat.whoHitMe == critter) {
|
2022-05-19 01:51:26 -07:00
|
|
|
foundTargetCount++;
|
2022-12-28 08:45:17 -08:00
|
|
|
*whoHitMePtr = candidate;
|
|
|
|
continue;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-28 08:45:17 -08:00
|
|
|
if (whoHitFriendPtr != NULL && *whoHitFriendPtr == NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
if (team == candidate->data.critter.combat.team) {
|
|
|
|
Object* whoHitCandidate = candidate->data.critter.combat.whoHitMe;
|
|
|
|
if (whoHitCandidate != NULL
|
2022-12-28 08:45:17 -08:00
|
|
|
&& whoHitCandidate != critter
|
2022-05-19 01:51:26 -07:00
|
|
|
&& team != whoHitCandidate->data.critter.combat.team
|
|
|
|
&& (whoHitCandidate->data.critter.combat.results & DAM_DEAD) == 0) {
|
|
|
|
foundTargetCount++;
|
2022-12-28 08:45:17 -08:00
|
|
|
*whoHitFriendPtr = whoHitCandidate;
|
|
|
|
continue;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-28 08:45:17 -08:00
|
|
|
if (whoHitByFriendPtr != NULL && *whoHitByFriendPtr == NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
if (candidate->data.critter.combat.team != team
|
|
|
|
&& (candidate->data.critter.combat.results & DAM_DEAD) == 0) {
|
|
|
|
Object* whoHitCandidate = candidate->data.critter.combat.whoHitMe;
|
|
|
|
if (whoHitCandidate != NULL
|
|
|
|
&& whoHitCandidate->data.critter.combat.team == team) {
|
|
|
|
foundTargetCount++;
|
2022-12-28 08:45:17 -08:00
|
|
|
*whoHitByFriendPtr = candidate;
|
|
|
|
continue;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// ai_danger_source
|
|
|
|
// 0x428F4C
|
2022-06-19 01:32:07 -07:00
|
|
|
static Object* _ai_danger_source(Object* a1)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
if (a1 == NULL) {
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2022-12-28 22:37:51 -08:00
|
|
|
bool ignoreFleeingCritters = false;
|
2022-05-19 01:51:26 -07:00
|
|
|
int attackWho;
|
|
|
|
|
|
|
|
Object* targets[4];
|
|
|
|
targets[0] = NULL;
|
|
|
|
|
|
|
|
if (objectIsPartyMember(a1)) {
|
2022-12-24 03:30:03 -08:00
|
|
|
int disposition = aiGetDisposition(a1);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
switch (disposition + 1) {
|
2022-12-28 22:37:51 -08:00
|
|
|
case DISPOSITION_CUSTOM:
|
|
|
|
case DISPOSITION_COWARD:
|
|
|
|
case DISPOSITION_DEFENSIVE:
|
|
|
|
case DISPOSITION_AGGRESSIVE:
|
|
|
|
ignoreFleeingCritters = true;
|
2022-05-19 01:51:26 -07:00
|
|
|
break;
|
2022-12-28 22:37:51 -08:00
|
|
|
case DISPOSITION_NONE:
|
|
|
|
case DISPOSITION_BERKSERK:
|
|
|
|
ignoreFleeingCritters = false;
|
2022-05-19 01:51:26 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2022-12-28 22:37:51 -08:00
|
|
|
if (ignoreFleeingCritters && aiGetDistance(a1) == DISTANCE_CHARGE) {
|
|
|
|
ignoreFleeingCritters = false;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
attackWho = aiGetAttackWho(a1);
|
2022-05-19 01:51:26 -07:00
|
|
|
switch (attackWho) {
|
2022-12-28 22:37:51 -08:00
|
|
|
case ATTACK_WHO_WHOMEVER_ATTACKING_ME:
|
|
|
|
if (1) {
|
|
|
|
// CE: Slightly improve "Whomever is attacking me" targeting.
|
|
|
|
//
|
|
|
|
// First attempt to continue attack our previous target. This
|
|
|
|
// prevents jumping from target to target thus spending precious
|
|
|
|
// action points on meaningless movements.
|
|
|
|
Object* candidate = aiInfoGetLastTarget(a1);
|
|
|
|
if (candidate != NULL) {
|
|
|
|
// Check if candidate is still a valid target:
|
|
|
|
// - not dead and not knocked out
|
|
|
|
// - not on the same team
|
|
|
|
// - still attacking dude
|
|
|
|
Object* critter = candidate;
|
|
|
|
if ((critter->data.critter.combat.results & (DAM_DEAD | DAM_KNOCKED_OUT)) != 0
|
|
|
|
|| a1->data.critter.combat.team == critter->data.critter.combat.team
|
|
|
|
|| aiInfoGetLastTarget(critter) != gDude) {
|
|
|
|
candidate = NULL;
|
|
|
|
}
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-28 22:37:51 -08:00
|
|
|
// NOTE: I'm not sure if we need to revalidate candidate's
|
|
|
|
// reachability and shot conditions.
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-28 22:37:51 -08:00
|
|
|
// Do not chase fleeing critter.
|
|
|
|
if (ignoreFleeingCritters && critterIsFleeing(critter)) {
|
|
|
|
candidate = NULL;
|
|
|
|
}
|
|
|
|
}
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-28 22:37:51 -08:00
|
|
|
if (candidate == NULL) {
|
|
|
|
// Previous target is invalid. Pick nearest candidate who's
|
|
|
|
// attacking dude.
|
|
|
|
_ai_sort_list_distance(_curr_crit_list, _curr_crit_num, a1);
|
|
|
|
|
|
|
|
for (int index = 0; index < _curr_crit_num; index++) {
|
|
|
|
Object* critter = _curr_crit_list[index];
|
|
|
|
if (critter != a1) {
|
|
|
|
// Check if it's valid target (same conditions as
|
|
|
|
// above).
|
|
|
|
if ((critter->data.critter.combat.results & (DAM_DEAD | DAM_KNOCKED_OUT)) != 0
|
|
|
|
|| a1->data.critter.combat.team == critter->data.critter.combat.team
|
|
|
|
|| aiInfoGetLastTarget(critter) != gDude) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure critter is reachable.
|
|
|
|
if (pathfinderFindPath(a1, a1->tile, critter->tile, NULL, 0, _obj_blocking_at) == 0) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if we can attack it. No ammo and out of
|
|
|
|
// range are ok results, since we can reload weapon
|
|
|
|
// move closer if necessary.
|
|
|
|
int badShot = _combat_check_bad_shot(a1, critter, HIT_MODE_RIGHT_WEAPON_PRIMARY, false);
|
|
|
|
if (badShot != COMBAT_BAD_SHOT_OK
|
|
|
|
&& badShot != COMBAT_BAD_SHOT_NO_AMMO
|
|
|
|
&& badShot != COMBAT_BAD_SHOT_OUT_OF_RANGE) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do not chase fleeing critter.
|
|
|
|
if (ignoreFleeingCritters && critterIsFleeing(critter)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
candidate = critter;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (candidate != NULL) {
|
|
|
|
return candidate;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
2022-05-19 01:51:26 -07:00
|
|
|
case ATTACK_WHO_STRONGEST:
|
|
|
|
case ATTACK_WHO_WEAKEST:
|
|
|
|
case ATTACK_WHO_CLOSEST:
|
|
|
|
a1->data.critter.combat.whoHitMe = NULL;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
attackWho = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
Object* whoHitMe = a1->data.critter.combat.whoHitMe;
|
|
|
|
if (whoHitMe == NULL || a1 == whoHitMe) {
|
|
|
|
targets[0] = NULL;
|
|
|
|
} else {
|
|
|
|
if ((whoHitMe->data.critter.combat.results & DAM_DEAD) == 0) {
|
|
|
|
if (attackWho == ATTACK_WHO_WHOMEVER || attackWho == -1) {
|
|
|
|
return whoHitMe;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (whoHitMe->data.critter.combat.team != a1->data.critter.combat.team) {
|
|
|
|
targets[0] = _ai_find_nearest_team(a1, whoHitMe, 1);
|
|
|
|
} else {
|
|
|
|
targets[0] = NULL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-28 08:45:17 -08:00
|
|
|
aiFindAttackers(a1, &(targets[1]), &(targets[2]), &(targets[3]));
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-28 22:37:51 -08:00
|
|
|
if (ignoreFleeingCritters) {
|
2022-05-19 01:51:26 -07:00
|
|
|
for (int index = 0; index < 4; index++) {
|
|
|
|
if (targets[index] != NULL && critterIsFleeing(targets[index])) {
|
|
|
|
targets[index] = NULL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (attackWho) {
|
|
|
|
case ATTACK_WHO_STRONGEST:
|
2022-09-01 08:41:37 -07:00
|
|
|
// NOTE: Uninline.
|
|
|
|
_ai_sort_list_strength(targets, 4);
|
2022-05-19 01:51:26 -07:00
|
|
|
break;
|
|
|
|
case ATTACK_WHO_WEAKEST:
|
2022-09-01 08:41:37 -07:00
|
|
|
// NOTE: Uninline.
|
|
|
|
_ai_sort_list_weakness(targets, 4);
|
2022-05-19 01:51:26 -07:00
|
|
|
break;
|
|
|
|
default:
|
2022-09-01 08:41:37 -07:00
|
|
|
// NOTE: Uninline.
|
|
|
|
_ai_sort_list_distance(targets, 4, a1);
|
2022-05-19 01:51:26 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int index = 0; index < 4; index++) {
|
|
|
|
Object* candidate = targets[index];
|
2022-10-05 04:25:36 -07:00
|
|
|
if (candidate != NULL && isWithinPerception(a1, candidate)) {
|
2022-05-19 01:51:26 -07:00
|
|
|
if (pathfinderFindPath(a1, a1->tile, candidate->tile, NULL, 0, _obj_blocking_at) != 0
|
2022-08-31 08:29:46 -07:00
|
|
|
|| _combat_check_bad_shot(a1, candidate, HIT_MODE_RIGHT_WEAPON_PRIMARY, false) == COMBAT_BAD_SHOT_OK) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return candidate;
|
|
|
|
}
|
|
|
|
debugPrint("\nai_danger_source: I couldn't get at my target! Picking alternate!");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4291C4
|
2022-12-24 03:30:03 -08:00
|
|
|
int _caiSetupTeamCombat(Object* attackerTeam, Object* defenderTeam)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-12-24 03:30:03 -08:00
|
|
|
Object* obj = objectFindFirstAtElevation(attackerTeam->elevation);
|
2022-05-19 01:51:26 -07:00
|
|
|
while (obj != NULL) {
|
2022-07-29 06:04:05 -07:00
|
|
|
if (PID_TYPE(obj->pid) == OBJ_TYPE_CRITTER && obj != gDude) {
|
2023-01-16 05:49:50 -08:00
|
|
|
obj->data.critter.combat.maneuver |= CRITTER_MANEUVER_ENGAGING;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
obj = objectFindNextAtElevation();
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
_attackerTeamObj = attackerTeam;
|
|
|
|
_targetTeamObj = defenderTeam;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x_429210
|
2022-12-24 03:30:03 -08:00
|
|
|
int _caiTeamCombatInit(Object** crittersList, int crittersListLength)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-12-24 03:30:03 -08:00
|
|
|
if (crittersList == NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (crittersListLength == 0) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_attackerTeamObj == NULL) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
int attackerTeam = _attackerTeamObj->data.critter.combat.team;
|
|
|
|
int defenderTeam = _targetTeamObj->data.critter.combat.team;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
for (int index = 0; index < crittersListLength; index++) {
|
|
|
|
int team = crittersList[index]->data.critter.combat.team;
|
|
|
|
if (team == attackerTeam) {
|
|
|
|
crittersList[index]->data.critter.combat.whoHitMe = _ai_find_nearest_team(crittersList[index], _targetTeamObj, 1);
|
|
|
|
} else if (team == defenderTeam) {
|
|
|
|
crittersList[index]->data.critter.combat.whoHitMe = _ai_find_nearest_team(crittersList[index], _attackerTeamObj, 1);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_attackerTeamObj = NULL;
|
|
|
|
_targetTeamObj = NULL;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4292C0
|
|
|
|
void _caiTeamCombatExit()
|
|
|
|
{
|
2022-12-24 03:30:03 -08:00
|
|
|
_targetTeamObj = NULL;
|
|
|
|
_attackerTeamObj = NULL;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4292D4
|
2022-08-31 02:05:56 -07:00
|
|
|
static bool aiHaveAmmo(Object* critter, Object* weapon, Object** ammoPtr)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-08-31 02:05:56 -07:00
|
|
|
if (ammoPtr != NULL) {
|
|
|
|
*ammoPtr = NULL;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
2022-08-31 02:05:56 -07:00
|
|
|
if (weapon->pid == PROTO_ID_SOLAR_SCORCHER) {
|
2023-01-03 12:56:52 -08:00
|
|
|
return lightGetAmbientIntensity() > LIGHT_INTENSITY_MAX * 0.95;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
2022-08-31 02:05:56 -07:00
|
|
|
int inventoryItemIndex = -1;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
while (1) {
|
2022-08-31 02:05:56 -07:00
|
|
|
Object* ammo = _inven_find_type(critter, ITEM_TYPE_AMMO, &inventoryItemIndex);
|
|
|
|
if (ammo == NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2022-08-31 02:05:56 -07:00
|
|
|
if (weaponCanBeReloadedWith(weapon, ammo)) {
|
|
|
|
if (ammoPtr != NULL) {
|
|
|
|
*ammoPtr = ammo;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
2022-08-31 02:05:56 -07:00
|
|
|
return true;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
2022-08-31 02:05:56 -07:00
|
|
|
if (weaponGetAnimationCode(weapon)) {
|
|
|
|
if (weaponGetRange(critter, HIT_MODE_RIGHT_WEAPON_PRIMARY) < 3) {
|
|
|
|
_inven_unwield(critter, HAND_RIGHT);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
} else {
|
2022-08-31 02:05:56 -07:00
|
|
|
_inven_unwield(critter, HAND_RIGHT);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-31 02:05:56 -07:00
|
|
|
return false;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42938C
|
2022-06-19 01:32:07 -07:00
|
|
|
static bool _caiHasWeapPrefType(AiPacket* ai, int attackType)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
int bestWeapon = ai->best_weapon + 1;
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
for (int index = 0; index < ATTACK_TYPE_COUNT; index++) {
|
2022-05-19 01:51:26 -07:00
|
|
|
if (attackType == _weapPrefOrderings[bestWeapon][index]) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4293BC
|
2022-06-19 01:32:07 -07:00
|
|
|
static Object* _ai_best_weapon(Object* attacker, Object* weapon1, Object* weapon2, Object* defender)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
if (attacker == NULL) {
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
AiPacket* ai = aiGetPacket(attacker);
|
|
|
|
if (ai->best_weapon == BEST_WEAPON_RANDOM) {
|
|
|
|
return randomBetween(1, 100) <= 50 ? weapon1 : weapon2;
|
|
|
|
}
|
|
|
|
int minDamage;
|
|
|
|
int maxDamage;
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
bool ignoreWeapon1 = false;
|
|
|
|
int order2 = 999;
|
|
|
|
int order1 = 999;
|
2022-05-19 01:51:26 -07:00
|
|
|
int avgDamage1 = 0;
|
|
|
|
|
|
|
|
Attack attack;
|
|
|
|
attackInit(&attack, attacker, defender, HIT_MODE_RIGHT_WEAPON_PRIMARY, HIT_LOCATION_TORSO);
|
|
|
|
|
|
|
|
int attackType1;
|
|
|
|
int distance;
|
|
|
|
int attackType2;
|
|
|
|
int avgDamage2 = 0;
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
bool ignoreWeapon2 = false;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
// NOTE: weaponClass1 and weaponClass2 both use ESI but they are not
|
|
|
|
// initialized. I'm not sure if this is right, but at least it doesn't
|
|
|
|
// crash.
|
|
|
|
attackType1 = -1;
|
|
|
|
attackType2 = -1;
|
|
|
|
|
|
|
|
if (weapon1 != NULL) {
|
|
|
|
attackType1 = weaponGetAttackTypeForHitMode(weapon1, HIT_MODE_RIGHT_WEAPON_PRIMARY);
|
|
|
|
if (weaponGetDamageMinMax(weapon1, &minDamage, &maxDamage) == -1) {
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2022-08-14 07:48:59 -07:00
|
|
|
// SFALL: Fix avg damage calculation.
|
|
|
|
avgDamage1 = (maxDamage + minDamage) / 2;
|
2022-08-17 22:41:15 -07:00
|
|
|
if (weaponGetDamageRadius(weapon1, HIT_MODE_RIGHT_WEAPON_PRIMARY) > 0 && defender != NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
attack.weapon = weapon1;
|
2022-07-29 06:04:05 -07:00
|
|
|
_compute_explosion_on_extras(&attack, 0, weaponIsGrenade(weapon1), 1);
|
2022-05-19 01:51:26 -07:00
|
|
|
avgDamage1 *= attack.extrasLength + 1;
|
|
|
|
}
|
|
|
|
|
2022-08-14 07:48:59 -07:00
|
|
|
// SFALL: Fix for the incorrect item being checked.
|
|
|
|
if (weaponGetPerk(weapon1) != -1) {
|
|
|
|
// SFALL: Lower weapon score multiplier for having perk.
|
|
|
|
avgDamage1 *= 2;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (defender != NULL) {
|
|
|
|
if (_combat_safety_invalidate_weapon(attacker, weapon1, HIT_MODE_RIGHT_WEAPON_PRIMARY, defender, NULL)) {
|
2022-12-24 03:30:03 -08:00
|
|
|
ignoreWeapon1 = true;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-17 22:41:15 -07:00
|
|
|
if (itemIsHidden(weapon1)) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return weapon1;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
distance = objectGetDistanceBetween(attacker, defender);
|
2022-08-17 22:41:15 -07:00
|
|
|
if (weaponGetRange(attacker, HIT_MODE_PUNCH) >= distance) {
|
2022-05-19 01:51:26 -07:00
|
|
|
attackType1 = ATTACK_TYPE_UNARMED;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (!ignoreWeapon1) {
|
2022-05-19 01:51:26 -07:00
|
|
|
for (int index = 0; index < ATTACK_TYPE_COUNT; index++) {
|
|
|
|
if (_weapPrefOrderings[ai->best_weapon + 1][index] == attackType1) {
|
2022-12-24 03:30:03 -08:00
|
|
|
order1 = index;
|
2022-05-19 01:51:26 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (weapon2 != NULL) {
|
|
|
|
attackType2 = weaponGetAttackTypeForHitMode(weapon2, HIT_MODE_RIGHT_WEAPON_PRIMARY);
|
|
|
|
if (weaponGetDamageMinMax(weapon2, &minDamage, &maxDamage) == -1) {
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2022-08-14 07:48:59 -07:00
|
|
|
// SFALL: Fix avg damage calculation.
|
|
|
|
avgDamage2 = (maxDamage + minDamage) / 2;
|
2022-08-17 22:41:15 -07:00
|
|
|
if (weaponGetDamageRadius(weapon2, HIT_MODE_RIGHT_WEAPON_PRIMARY) > 0 && defender != NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
attack.weapon = weapon2;
|
2022-07-29 06:04:05 -07:00
|
|
|
_compute_explosion_on_extras(&attack, 0, weaponIsGrenade(weapon2), 1);
|
2022-05-19 01:51:26 -07:00
|
|
|
avgDamage2 *= attack.extrasLength + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (weaponGetPerk(weapon2) != -1) {
|
2022-08-14 07:48:59 -07:00
|
|
|
// SFALL: Lower weapon score multiplier for having perk.
|
|
|
|
avgDamage2 *= 2;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (defender != NULL) {
|
|
|
|
if (_combat_safety_invalidate_weapon(attacker, weapon2, HIT_MODE_RIGHT_WEAPON_PRIMARY, defender, NULL)) {
|
2022-12-24 03:30:03 -08:00
|
|
|
ignoreWeapon2 = true;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-17 22:41:15 -07:00
|
|
|
if (itemIsHidden(weapon2)) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return weapon2;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (distance == 0) {
|
|
|
|
distance = objectGetDistanceBetween(attacker, weapon1);
|
|
|
|
}
|
|
|
|
|
2022-08-17 22:41:15 -07:00
|
|
|
if (weaponGetRange(attacker, HIT_MODE_PUNCH) >= distance) {
|
2022-05-19 01:51:26 -07:00
|
|
|
attackType2 = ATTACK_TYPE_UNARMED;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (!ignoreWeapon2) {
|
2022-05-19 01:51:26 -07:00
|
|
|
for (int index = 0; index < ATTACK_TYPE_COUNT; index++) {
|
|
|
|
if (_weapPrefOrderings[ai->best_weapon + 1][index] == attackType2) {
|
2022-12-24 03:30:03 -08:00
|
|
|
order2 = index;
|
2022-05-19 01:51:26 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (order1 == order2) {
|
|
|
|
if (order1 == 999) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (abs(avgDamage2 - avgDamage1) <= 5) {
|
|
|
|
return itemGetCost(weapon2) > itemGetCost(weapon1) ? weapon2 : weapon1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return avgDamage2 > avgDamage1 ? weapon2 : weapon1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (weapon1 != NULL && weapon1->pid == PROTO_ID_FLARE && weapon2 != NULL) {
|
|
|
|
return weapon2;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (weapon2 != NULL && weapon2->pid == PROTO_ID_FLARE && weapon1 != NULL) {
|
|
|
|
return weapon1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((ai->best_weapon == -1 || ai->best_weapon >= BEST_WEAPON_UNARMED_OVER_THROW)
|
|
|
|
&& abs(avgDamage2 - avgDamage1) > 5) {
|
|
|
|
return avgDamage2 > avgDamage1 ? weapon2 : weapon1;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
return order1 > order2 ? weapon2 : weapon1;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4298EC
|
2022-06-19 01:32:07 -07:00
|
|
|
static bool _ai_can_use_weapon(Object* critter, Object* weapon, int hitMode)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
int damageFlags = critter->data.critter.combat.results;
|
|
|
|
if ((damageFlags & DAM_CRIP_ARM_LEFT) != 0 && (damageFlags & DAM_CRIP_ARM_RIGHT) != 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-07-06 04:23:06 -07:00
|
|
|
if ((damageFlags & DAM_CRIP_ARM_ANY) != 0 && weaponIsTwoHanded(weapon)) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
int rotation = critter->rotation + 1;
|
|
|
|
int animationCode = weaponGetAnimationCode(weapon);
|
2022-12-24 03:30:03 -08:00
|
|
|
int weaponAnimationCode = weaponGetAnimationForHitMode(weapon, hitMode);
|
|
|
|
int fid = buildFid(OBJ_TYPE_CRITTER, critter->fid & 0xFFF, weaponAnimationCode, animationCode, rotation);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (!artExists(fid)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
int skill = weaponGetSkillForHitMode(weapon, hitMode);
|
|
|
|
AiPacket* ai = aiGetPacket(critter);
|
|
|
|
if (skillGetValue(critter, skill) < ai->min_to_hit) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
int attackType = weaponGetAttackTypeForHitMode(weapon, HIT_MODE_RIGHT_WEAPON_PRIMARY);
|
|
|
|
return _caiHasWeapPrefType(ai, attackType) != 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4299A0
|
2022-12-24 03:30:03 -08:00
|
|
|
Object* _ai_search_inven_weap(Object* critter, bool checkRequiredActionPoints, Object* defender)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
int bodyType = critterGetBodyType(critter);
|
|
|
|
if (bodyType != BODY_TYPE_BIPED
|
|
|
|
&& bodyType != BODY_TYPE_ROBOTIC
|
|
|
|
&& critter->pid != PROTO_ID_0x1000098) {
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
int token = -1;
|
|
|
|
Object* bestWeapon = NULL;
|
|
|
|
Object* rightHandWeapon = critterGetItem2(critter);
|
|
|
|
while (true) {
|
|
|
|
Object* weapon = _inven_find_type(critter, ITEM_TYPE_WEAPON, &token);
|
|
|
|
if (weapon == NULL) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (weapon == rightHandWeapon) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (checkRequiredActionPoints) {
|
2022-08-17 22:41:15 -07:00
|
|
|
if (weaponGetPrimaryActionPointCost(weapon) > critter->data.critter.combat.ap) {
|
2022-05-19 01:51:26 -07:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!_ai_can_use_weapon(critter, weapon, HIT_MODE_RIGHT_WEAPON_PRIMARY)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (weaponGetAttackTypeForHitMode(weapon, HIT_MODE_RIGHT_WEAPON_PRIMARY) == ATTACK_TYPE_RANGED) {
|
|
|
|
if (ammoGetQuantity(weapon) == 0) {
|
2022-08-31 02:05:56 -07:00
|
|
|
if (!aiHaveAmmo(critter, weapon, NULL)) {
|
2022-05-19 01:51:26 -07:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
bestWeapon = _ai_best_weapon(critter, bestWeapon, weapon, defender);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return bestWeapon;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Finds new best armor (other than what's already equipped) based on the armor score.
|
|
|
|
//
|
|
|
|
// 0x429A6C
|
|
|
|
Object* _ai_search_inven_armor(Object* critter)
|
|
|
|
{
|
|
|
|
if (!objectIsPartyMember(critter)) {
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calculate armor score - it's a unitless combination of armor class and bonuses across
|
|
|
|
// all damage types.
|
|
|
|
int armorScore = 0;
|
|
|
|
Object* armor = critterGetArmor(critter);
|
|
|
|
if (armor != NULL) {
|
|
|
|
armorScore = armorGetArmorClass(armor);
|
|
|
|
|
|
|
|
for (int damageType = 0; damageType < DAMAGE_TYPE_COUNT; damageType++) {
|
|
|
|
armorScore += armorGetDamageResistance(armor, damageType);
|
|
|
|
armorScore += armorGetDamageThreshold(armor, damageType);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
armorScore = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
Object* bestArmor = NULL;
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
int inventoryItemIndex = -1;
|
2022-05-19 01:51:26 -07:00
|
|
|
while (true) {
|
2022-12-24 03:30:03 -08:00
|
|
|
Object* candidate = _inven_find_type(critter, ITEM_TYPE_ARMOR, &inventoryItemIndex);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (candidate == NULL) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (armor != candidate) {
|
|
|
|
int candidateScore = armorGetArmorClass(candidate);
|
|
|
|
for (int damageType = 0; damageType < DAMAGE_TYPE_COUNT; damageType++) {
|
|
|
|
candidateScore += armorGetDamageResistance(candidate, damageType);
|
|
|
|
candidateScore += armorGetDamageThreshold(candidate, damageType);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (candidateScore > armorScore) {
|
|
|
|
armorScore = candidateScore;
|
|
|
|
bestArmor = candidate;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return bestArmor;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns true if critter can use given item.
|
|
|
|
//
|
|
|
|
// That means the item is one of it's primary desires,
|
|
|
|
// or it's a humanoid being with intelligence at least 3,
|
|
|
|
// and the iteam is a something healing.
|
|
|
|
//
|
|
|
|
// 0x429B44
|
2022-06-19 01:32:07 -07:00
|
|
|
static bool aiCanUseItem(Object* critter, Object* item)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
if (critter == NULL) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (item == NULL) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
AiPacket* ai = aiGetPacketByNum(critter->data.critter.combat.aiPacket);
|
|
|
|
if (ai == NULL) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int index = 0; index < AI_PACKET_CHEM_PRIMARY_DESIRE_COUNT; index++) {
|
|
|
|
if (item->pid == ai->chem_primary_desire[index]) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (critterGetBodyType(critter) != BODY_TYPE_BIPED) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
int killType = critterGetKillType(critter);
|
|
|
|
if (killType != KILL_TYPE_MAN
|
|
|
|
&& killType != KILL_TYPE_WOMAN
|
|
|
|
&& killType != KILL_TYPE_SUPER_MUTANT
|
|
|
|
&& killType != KILL_TYPE_GHOUL
|
|
|
|
&& killType != KILL_TYPE_CHILD) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (critterGetStat(critter, STAT_INTELLIGENCE) < 3) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-12-27 15:19:43 -08:00
|
|
|
// SFALL: Check healing items.
|
|
|
|
if (!itemIsHealing(item->pid)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// CE: Make sure critter actually need healing item.
|
|
|
|
//
|
|
|
|
// Sfall has similar fix implemented differently in `ai_check_drugs`. It
|
|
|
|
// does so after healing item is returned from `ai_search_environ`, so it
|
|
|
|
// does not a have a chance to look for other items.
|
|
|
|
int hpRatio = kChemUseStimsHpRatio;
|
|
|
|
switch (aiGetChemUse(critter)) {
|
|
|
|
case CHEM_USE_CLEAN:
|
|
|
|
hpRatio = 0;
|
|
|
|
break;
|
|
|
|
case CHEM_USE_STIMS_WHEN_HURT_LITTLE:
|
|
|
|
hpRatio = kChemUseStimsWhenHurtLittleHpRatio;
|
|
|
|
break;
|
|
|
|
case CHEM_USE_STIMS_WHEN_HURT_LOTS:
|
|
|
|
hpRatio = kChemUseStimsWhenHurtLotsHpRatio;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
int currentHp = critterGetStat(critter, STAT_CURRENT_HIT_POINTS);
|
|
|
|
int stimsHp = critterGetStat(critter, STAT_MAXIMUM_HIT_POINTS) * hpRatio / 100;
|
|
|
|
if (currentHp > stimsHp) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find best item type to use?
|
|
|
|
//
|
|
|
|
// 0x429C18
|
2022-06-19 01:32:07 -07:00
|
|
|
static Object* _ai_search_environ(Object* critter, int itemType)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
if (critterGetBodyType(critter) != BODY_TYPE_BIPED) {
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
Object** objects;
|
|
|
|
int count = objectListCreate(-1, gElevation, OBJ_TYPE_ITEM, &objects);
|
|
|
|
if (count == 0) {
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2022-09-01 08:41:37 -07:00
|
|
|
// NOTE: Uninline.
|
|
|
|
_ai_sort_list_distance(objects, count, critter);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
int perception = critterGetStat(critter, STAT_PERCEPTION) + 5;
|
|
|
|
Object* item2 = critterGetItem2(critter);
|
|
|
|
|
|
|
|
Object* foundItem = NULL;
|
|
|
|
|
|
|
|
for (int index = 0; index < count; index++) {
|
|
|
|
Object* item = objects[index];
|
|
|
|
int distance = objectGetDistanceBetween(critter, item);
|
|
|
|
if (distance > perception) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (itemGetType(item) == itemType) {
|
|
|
|
switch (itemType) {
|
|
|
|
case ITEM_TYPE_WEAPON:
|
|
|
|
if (_ai_can_use_weapon(critter, item, HIT_MODE_RIGHT_WEAPON_PRIMARY)) {
|
|
|
|
foundItem = item;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case ITEM_TYPE_AMMO:
|
|
|
|
if (weaponCanBeReloadedWith(item2, item)) {
|
|
|
|
foundItem = item;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case ITEM_TYPE_DRUG:
|
|
|
|
case ITEM_TYPE_MISC:
|
|
|
|
if (aiCanUseItem(critter, item)) {
|
|
|
|
foundItem = item;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (foundItem != NULL) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
objectListFree(objects);
|
|
|
|
|
|
|
|
return foundItem;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x429D60
|
2022-12-24 03:30:03 -08:00
|
|
|
static Object* _ai_retrieve_object(Object* critter, Object* item)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-12-24 03:30:03 -08:00
|
|
|
if (actionPickUp(critter, item) != 0) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
// Run animation so that NPC can actually move and pick up the item.
|
2022-05-19 01:51:26 -07:00
|
|
|
_combat_turn_run();
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
// Find the item in NPC's inventory. This step is needed because NPC could
|
|
|
|
// not get the item on this turn.
|
|
|
|
Object* retrievedItem = _inven_find_id(critter, item->id);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (retrievedItem != NULL || item->owner != NULL) {
|
|
|
|
// Either NPC have the item, or someone else have picked it up.
|
|
|
|
item = NULL;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
// Save item NPC wants to pick up on the next turn.
|
|
|
|
aiInfoSetLastItem(retrievedItem, item);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
return retrievedItem;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// 0x429DB4
|
2022-12-24 03:30:03 -08:00
|
|
|
static int _ai_pick_hit_mode(Object* attacker, Object* weapon, Object* defender)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-12-24 03:30:03 -08:00
|
|
|
if (weapon == NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return HIT_MODE_PUNCH;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (itemGetType(weapon) != ITEM_TYPE_WEAPON) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return HIT_MODE_PUNCH;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
int attackType = weaponGetAttackTypeForHitMode(weapon, HIT_MODE_RIGHT_WEAPON_SECONDARY);
|
|
|
|
int intelligence = critterGetStat(attacker, STAT_INTELLIGENCE);
|
|
|
|
if (attackType == ATTACK_TYPE_NONE || !_ai_can_use_weapon(attacker, weapon, HIT_MODE_RIGHT_WEAPON_SECONDARY)) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return HIT_MODE_RIGHT_WEAPON_PRIMARY;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool useSecondaryMode = false;
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
AiPacket* ai = aiGetPacket(attacker);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (ai == NULL) {
|
|
|
|
return HIT_MODE_PUNCH;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ai->area_attack_mode != -1) {
|
|
|
|
switch (ai->area_attack_mode) {
|
|
|
|
case AREA_ATTACK_MODE_ALWAYS:
|
|
|
|
useSecondaryMode = true;
|
|
|
|
break;
|
|
|
|
case AREA_ATTACK_MODE_SOMETIMES:
|
|
|
|
if (randomBetween(1, ai->secondary_freq) == 1) {
|
|
|
|
useSecondaryMode = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case AREA_ATTACK_MODE_BE_SURE:
|
2022-12-24 03:30:03 -08:00
|
|
|
if (_determine_to_hit(attacker, defender, HIT_LOCATION_TORSO, HIT_MODE_RIGHT_WEAPON_SECONDARY) >= 85
|
|
|
|
&& !_combat_safety_invalidate_weapon(attacker, weapon, 3, defender, 0)) {
|
2022-05-19 01:51:26 -07:00
|
|
|
useSecondaryMode = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case AREA_ATTACK_MODE_BE_CAREFUL:
|
2022-12-24 03:30:03 -08:00
|
|
|
if (_determine_to_hit(attacker, defender, HIT_LOCATION_TORSO, HIT_MODE_RIGHT_WEAPON_SECONDARY) >= 50
|
|
|
|
&& !_combat_safety_invalidate_weapon(attacker, weapon, 3, defender, 0)) {
|
2022-05-19 01:51:26 -07:00
|
|
|
useSecondaryMode = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case AREA_ATTACK_MODE_BE_ABSOLUTELY_SURE:
|
2022-12-24 03:30:03 -08:00
|
|
|
if (_determine_to_hit(attacker, defender, HIT_LOCATION_TORSO, HIT_MODE_RIGHT_WEAPON_SECONDARY) >= 95
|
|
|
|
&& !_combat_safety_invalidate_weapon(attacker, weapon, 3, defender, 0)) {
|
2022-05-19 01:51:26 -07:00
|
|
|
useSecondaryMode = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else {
|
2022-12-24 03:30:03 -08:00
|
|
|
if (intelligence < 6 || objectGetDistanceBetween(attacker, defender) < 10) {
|
2022-05-19 01:51:26 -07:00
|
|
|
if (randomBetween(1, ai->secondary_freq) == 1) {
|
|
|
|
useSecondaryMode = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (useSecondaryMode) {
|
|
|
|
if (!_caiHasWeapPrefType(ai, attackType)) {
|
|
|
|
useSecondaryMode = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-31 10:25:40 -07:00
|
|
|
// SFALL: Add a check for the weapon range and the AP cost when AI is
|
|
|
|
// choosing weapon attack modes.
|
|
|
|
if (useSecondaryMode) {
|
2022-12-24 03:30:03 -08:00
|
|
|
if (objectGetDistanceBetween(attacker, defender) > weaponGetRange(attacker, HIT_MODE_RIGHT_WEAPON_SECONDARY)) {
|
2022-08-31 10:25:40 -07:00
|
|
|
useSecondaryMode = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (useSecondaryMode) {
|
2022-12-24 03:30:03 -08:00
|
|
|
if (attacker->data.critter.combat.ap < weaponGetActionPointCost(attacker, HIT_MODE_RIGHT_WEAPON_SECONDARY, false)) {
|
2022-08-31 10:25:40 -07:00
|
|
|
useSecondaryMode = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-19 01:51:26 -07:00
|
|
|
if (useSecondaryMode) {
|
|
|
|
if (attackType != ATTACK_TYPE_THROW
|
2022-12-24 03:30:03 -08:00
|
|
|
|| _ai_search_inven_weap(attacker, false, defender) != NULL
|
|
|
|
|| statRoll(attacker, STAT_INTELLIGENCE, 0, NULL) <= 1) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return HIT_MODE_RIGHT_WEAPON_SECONDARY;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return HIT_MODE_RIGHT_WEAPON_PRIMARY;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x429FC8
|
2022-12-24 03:30:03 -08:00
|
|
|
static int _ai_move_steps_closer(Object* a1, Object* a2, int actionPoints, bool taunt)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
if (actionPoints <= 0) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
int distance = aiGetDistance(a1);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (distance == DISTANCE_STAY) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (distance == DISTANCE_STAY_CLOSE) {
|
|
|
|
if (a2 != gDude) {
|
2022-12-24 03:30:03 -08:00
|
|
|
int currentDistance = objectGetDistanceBetween(a1, gDude);
|
|
|
|
if (currentDistance > 5
|
|
|
|
&& objectGetDistanceBetween(a2, gDude) > 5
|
|
|
|
&& currentDistance + actionPoints > 5) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (objectGetDistanceBetween(a1, a2) <= 1) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-07-29 06:04:05 -07:00
|
|
|
reg_anim_begin(ANIMATION_REQUEST_RESERVED);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (taunt) {
|
2022-05-19 01:51:26 -07:00
|
|
|
_combatai_msg(a1, NULL, AI_MESSAGE_TYPE_MOVE, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
Object* v18 = a2;
|
|
|
|
|
|
|
|
bool shouldUnhide;
|
2022-12-24 03:30:03 -08:00
|
|
|
if ((a2->flags & OBJECT_MULTIHEX) != 0) {
|
2022-05-19 01:51:26 -07:00
|
|
|
shouldUnhide = true;
|
|
|
|
a2->flags |= OBJECT_HIDDEN;
|
|
|
|
} else {
|
|
|
|
shouldUnhide = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pathfinderFindPath(a1, a1->tile, a2->tile, NULL, 0, _obj_blocking_at) == 0) {
|
|
|
|
_moveBlockObj = NULL;
|
|
|
|
if (pathfinderFindPath(a1, a1->tile, a2->tile, NULL, 0, _obj_ai_blocking_at) == 0
|
|
|
|
&& _moveBlockObj != NULL
|
2022-07-29 06:04:05 -07:00
|
|
|
&& PID_TYPE(_moveBlockObj->pid) == OBJ_TYPE_CRITTER) {
|
2022-05-19 01:51:26 -07:00
|
|
|
if (shouldUnhide) {
|
|
|
|
a2->flags &= ~OBJECT_HIDDEN;
|
|
|
|
}
|
|
|
|
|
|
|
|
a2 = _moveBlockObj;
|
2022-12-24 03:30:03 -08:00
|
|
|
if ((a2->flags & OBJECT_MULTIHEX) != 0) {
|
2022-05-19 01:51:26 -07:00
|
|
|
shouldUnhide = true;
|
|
|
|
a2->flags |= OBJECT_HIDDEN;
|
|
|
|
} else {
|
|
|
|
shouldUnhide = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (shouldUnhide) {
|
|
|
|
a2->flags &= ~OBJECT_HIDDEN;
|
|
|
|
}
|
|
|
|
|
|
|
|
int tile = a2->tile;
|
|
|
|
if (a2 == v18) {
|
|
|
|
_cai_retargetTileFromFriendlyFire(a1, a2, &tile);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (actionPoints >= critterGetStat(a1, STAT_MAXIMUM_ACTION_POINTS) / 2 && artCritterFidShouldRun(a1->fid)) {
|
2022-06-08 10:38:46 -07:00
|
|
|
if ((a2->flags & OBJECT_MULTIHEX) != 0) {
|
2022-07-29 06:04:05 -07:00
|
|
|
animationRegisterRunToObject(a1, a2, actionPoints, 0);
|
2022-05-19 01:51:26 -07:00
|
|
|
} else {
|
2022-07-29 06:04:05 -07:00
|
|
|
animationRegisterRunToTile(a1, tile, a1->elevation, actionPoints, 0);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
} else {
|
2022-06-08 10:38:46 -07:00
|
|
|
if ((a2->flags & OBJECT_MULTIHEX) != 0) {
|
2022-07-29 06:04:05 -07:00
|
|
|
animationRegisterMoveToObject(a1, a2, actionPoints, 0);
|
2022-05-19 01:51:26 -07:00
|
|
|
} else {
|
2022-07-29 06:04:05 -07:00
|
|
|
animationRegisterMoveToTile(a1, tile, a1->elevation, actionPoints, 0);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (reg_anim_end() != 0) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
_combat_turn_run();
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2022-09-01 08:41:37 -07:00
|
|
|
// NOTE: Inlined.
|
|
|
|
//
|
|
|
|
// 0x42A1C0
|
2022-12-24 03:30:03 -08:00
|
|
|
static int _ai_move_closer(Object* a1, Object* a2, bool taunt)
|
2022-09-01 08:41:37 -07:00
|
|
|
{
|
2022-12-24 03:30:03 -08:00
|
|
|
return _ai_move_steps_closer(a1, a2, a1->data.critter.combat.ap, taunt);
|
2022-09-01 08:41:37 -07:00
|
|
|
}
|
|
|
|
|
2022-05-19 01:51:26 -07:00
|
|
|
// 0x42A1D4
|
2022-08-02 03:31:26 -07:00
|
|
|
static int _cai_retargetTileFromFriendlyFire(Object* source, Object* target, int* tilePtr)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-08-02 03:31:26 -07:00
|
|
|
if (source == NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-08-02 03:31:26 -07:00
|
|
|
if (target == NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-08-02 03:31:26 -07:00
|
|
|
if (tilePtr == NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-08-02 03:31:26 -07:00
|
|
|
if (*tilePtr == -1) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_curr_crit_num == 0) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
int tiles[32];
|
|
|
|
|
2022-08-02 03:31:26 -07:00
|
|
|
AiRetargetData aiRetargetData;
|
|
|
|
aiRetargetData.source = source;
|
|
|
|
aiRetargetData.target = target;
|
|
|
|
aiRetargetData.sourceTeam = source->data.critter.combat.team;
|
|
|
|
aiRetargetData.sourceRating = _combatai_rating(source);
|
|
|
|
aiRetargetData.critterCount = 0;
|
|
|
|
aiRetargetData.tiles = tiles;
|
|
|
|
aiRetargetData.notSameTile = *tilePtr != source->tile;
|
|
|
|
aiRetargetData.currentTileIndex = 0;
|
|
|
|
aiRetargetData.sourceIntelligence = critterGetStat(source, STAT_INTELLIGENCE);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
for (int index = 0; index < 32; index++) {
|
|
|
|
tiles[index] = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int index = 0; index < _curr_crit_num; index++) {
|
|
|
|
Object* obj = _curr_crit_list[index];
|
|
|
|
if ((obj->data.critter.combat.results & DAM_DEAD) == 0
|
2022-08-02 03:31:26 -07:00
|
|
|
&& obj->data.critter.combat.team == aiRetargetData.sourceTeam
|
|
|
|
&& aiInfoGetLastTarget(obj) == aiRetargetData.target
|
|
|
|
&& obj != aiRetargetData.source) {
|
|
|
|
int rating = _combatai_rating(obj);
|
|
|
|
if (rating >= aiRetargetData.sourceRating) {
|
|
|
|
aiRetargetData.critterList[aiRetargetData.critterCount] = obj;
|
|
|
|
aiRetargetData.ratingList[aiRetargetData.critterCount] = rating;
|
|
|
|
aiRetargetData.critterCount += 1;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-01 08:41:37 -07:00
|
|
|
// NOTE: Uninline.
|
|
|
|
_ai_sort_list_distance(aiRetargetData.critterList, aiRetargetData.critterCount, source);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-08-02 03:31:26 -07:00
|
|
|
if (_cai_retargetTileFromFriendlyFireSubFunc(&aiRetargetData, *tilePtr) == 0) {
|
2022-05-19 01:51:26 -07:00
|
|
|
int minDistance = 99999;
|
|
|
|
int minDistanceIndex = -1;
|
|
|
|
|
|
|
|
for (int index = 0; index < 32; index++) {
|
|
|
|
int tile = tiles[index];
|
|
|
|
if (tile == -1) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2022-08-02 03:31:26 -07:00
|
|
|
if (_obj_blocking_at(NULL, tile, source->elevation) == 0) {
|
|
|
|
int distance = tileDistanceBetween(*tilePtr, tile);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (distance < minDistance) {
|
|
|
|
minDistance = distance;
|
|
|
|
minDistanceIndex = index;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (minDistanceIndex != -1) {
|
2022-08-02 03:31:26 -07:00
|
|
|
*tilePtr = tiles[minDistanceIndex];
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42A410
|
2022-08-02 03:31:26 -07:00
|
|
|
static int _cai_retargetTileFromFriendlyFireSubFunc(AiRetargetData* aiRetargetData, int tile)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-08-02 03:31:26 -07:00
|
|
|
if (aiRetargetData->sourceIntelligence <= 0) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
int distance = 1;
|
|
|
|
|
2022-08-02 03:31:26 -07:00
|
|
|
for (int index = 0; index < aiRetargetData->critterCount; index++) {
|
|
|
|
Object* obj = aiRetargetData->critterList[index];
|
|
|
|
if (_cai_attackWouldIntersect(obj, aiRetargetData->target, aiRetargetData->source, tile, &distance)) {
|
2022-05-19 01:51:26 -07:00
|
|
|
debugPrint("In the way!");
|
|
|
|
|
2022-08-02 03:31:26 -07:00
|
|
|
aiRetargetData->tiles[aiRetargetData->currentTileIndex] = tileGetTileInDirection(tile, (obj->rotation + 1) % ROTATION_COUNT, distance);
|
|
|
|
aiRetargetData->tiles[aiRetargetData->currentTileIndex + 1] = tileGetTileInDirection(tile, (obj->rotation + 5) % ROTATION_COUNT, distance);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-08-02 03:31:26 -07:00
|
|
|
aiRetargetData->sourceIntelligence -= 2;
|
|
|
|
aiRetargetData->currentTileIndex += 2;
|
2022-05-19 01:51:26 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42A518
|
2022-09-23 04:01:39 -07:00
|
|
|
static bool _cai_attackWouldIntersect(Object* attacker, Object* defender, Object* attackerFriend, int tile, int* distance)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
int hitMode = HIT_MODE_RIGHT_WEAPON_PRIMARY;
|
|
|
|
bool aiming = false;
|
2022-09-23 04:01:39 -07:00
|
|
|
if (attacker == gDude) {
|
2022-05-19 01:51:26 -07:00
|
|
|
interfaceGetCurrentHitMode(&hitMode, &aiming);
|
|
|
|
}
|
|
|
|
|
2022-09-23 04:01:39 -07:00
|
|
|
Object* weapon = critterGetWeaponForHitMode(attacker, hitMode);
|
|
|
|
if (weapon == NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-09-23 04:01:39 -07:00
|
|
|
if (weaponGetRange(attacker, hitMode) < 1) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
Object* object = NULL;
|
2022-09-23 04:01:39 -07:00
|
|
|
_make_straight_path_func(attacker, attacker->tile, defender->tile, NULL, &object, 32, _obj_shoot_blocking_at);
|
|
|
|
if (object != attackerFriend) {
|
|
|
|
if (!_combatTestIncidentalHit(attacker, defender, attackerFriend, weapon)) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42A5B8
|
2022-12-24 03:30:03 -08:00
|
|
|
static int _ai_switch_weapons(Object* attacker, int* hitMode, Object** weapon, Object* defender)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
*weapon = NULL;
|
|
|
|
*hitMode = HIT_MODE_PUNCH;
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
Object* bestWeapon = _ai_search_inven_weap(attacker, true, defender);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (bestWeapon != NULL) {
|
|
|
|
*weapon = bestWeapon;
|
2022-12-24 03:30:03 -08:00
|
|
|
*hitMode = _ai_pick_hit_mode(attacker, bestWeapon, defender);
|
2022-05-19 01:51:26 -07:00
|
|
|
} else {
|
2022-12-24 03:30:03 -08:00
|
|
|
Object* nearbyWeapon = _ai_search_environ(attacker, ITEM_TYPE_WEAPON);
|
|
|
|
if (nearbyWeapon == NULL) {
|
|
|
|
if (weaponGetActionPointCost(attacker, *hitMode, 0) <= attacker->data.critter.combat.ap) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
Object* retrievedWeapon = _ai_retrieve_object(attacker, nearbyWeapon);
|
|
|
|
if (retrievedWeapon != NULL) {
|
|
|
|
*weapon = retrievedWeapon;
|
|
|
|
*hitMode = _ai_pick_hit_mode(attacker, retrievedWeapon, defender);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (*weapon != NULL) {
|
2022-12-24 03:30:03 -08:00
|
|
|
_inven_wield(attacker, *weapon, 1);
|
2022-05-19 01:51:26 -07:00
|
|
|
_combat_turn_run();
|
2022-12-24 03:30:03 -08:00
|
|
|
if (weaponGetActionPointCost(attacker, *hitMode, 0) <= attacker->data.critter.combat.ap) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42A670
|
2022-12-24 03:30:03 -08:00
|
|
|
static int _ai_called_shot(Object* attacker, Object* defender, int hitMode)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-12-24 03:30:03 -08:00
|
|
|
int hitLocation = HIT_LOCATION_TORSO;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (weaponGetActionPointCost(attacker, hitMode, true) <= attacker->data.critter.combat.ap) {
|
|
|
|
if (critterCanAim(attacker, hitMode)) {
|
|
|
|
AiPacket* ai = aiGetPacket(attacker);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (randomBetween(1, ai->called_freq) == 1) {
|
2022-12-24 03:30:03 -08:00
|
|
|
int intelligenceRequired;
|
|
|
|
switch (settings.preferences.combat_difficulty) {
|
|
|
|
case COMBAT_DIFFICULTY_EASY:
|
|
|
|
intelligenceRequired = 7;
|
|
|
|
break;
|
|
|
|
case COMBAT_DIFFICULTY_NORMAL:
|
|
|
|
intelligenceRequired = 5;
|
|
|
|
break;
|
|
|
|
case COMBAT_DIFFICULTY_HARD:
|
|
|
|
intelligenceRequired = 3;
|
|
|
|
break;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (critterGetStat(attacker, STAT_INTELLIGENCE) >= intelligenceRequired) {
|
|
|
|
hitLocation = randomBetween(0, HIT_LOCATION_SPECIFIC_COUNT);
|
|
|
|
int chanceToHit = _determine_to_hit(attacker, defender, hitMode, hitLocation);
|
|
|
|
if (chanceToHit < ai->min_to_hit) {
|
|
|
|
hitLocation = HIT_LOCATION_TORSO;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
return hitLocation;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42A748
|
2022-12-24 03:30:03 -08:00
|
|
|
static int _ai_attack(Object* attacker, Object* defender, int hitMode)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-12-24 03:30:03 -08:00
|
|
|
if (attacker->data.critter.combat.maneuver & CRITTER_MANUEVER_FLEEING) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-07-29 06:04:05 -07:00
|
|
|
reg_anim_begin(ANIMATION_REQUEST_RESERVED);
|
2022-12-24 03:30:03 -08:00
|
|
|
animationRegisterRotateToTile(attacker, defender->tile);
|
2022-05-19 01:51:26 -07:00
|
|
|
reg_anim_end();
|
|
|
|
_combat_turn_run();
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
int hitLocation = _ai_called_shot(attacker, defender, hitMode);
|
|
|
|
if (_combat_attack(attacker, defender, hitMode, hitLocation)) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
_combat_turn_run();
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42A7D8
|
2022-06-19 01:32:07 -07:00
|
|
|
static int _ai_try_attack(Object* a1, Object* a2)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
_critter_set_who_hit_me(a1, a2);
|
|
|
|
|
|
|
|
CritterCombatData* combatData = &(a1->data.critter.combat);
|
2022-12-24 03:30:03 -08:00
|
|
|
bool taunt = true;
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
Object* weapon = critterGetItem2(a1);
|
|
|
|
if (weapon != NULL && itemGetType(weapon) != ITEM_TYPE_WEAPON) {
|
|
|
|
weapon = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
int hitMode = _ai_pick_hit_mode(a1, weapon, a2);
|
|
|
|
int minToHit = aiGetPacket(a1)->min_to_hit;
|
|
|
|
|
|
|
|
int actionPoints = a1->data.critter.combat.ap;
|
2022-12-24 03:30:03 -08:00
|
|
|
int safeDistance = 0;
|
2022-05-19 01:51:26 -07:00
|
|
|
int v42 = 0;
|
2022-08-31 09:53:12 -07:00
|
|
|
if (weapon != NULL
|
|
|
|
|| (critterGetBodyType(a2) == BODY_TYPE_BIPED
|
|
|
|
&& ((a2->fid & 0xF000) >> 12 == 0)
|
|
|
|
&& artExists(buildFid(OBJ_TYPE_CRITTER, a1->fid & 0xFFF, ANIM_THROW_PUNCH, 0, a1->rotation + 1)))) {
|
2022-08-31 09:56:09 -07:00
|
|
|
// SFALL: Check the safety of weapons based on the selected attack mode
|
|
|
|
// instead of always the primary weapon hit mode.
|
2022-12-24 03:30:03 -08:00
|
|
|
if (_combat_safety_invalidate_weapon(a1, weapon, hitMode, a2, &safeDistance)) {
|
2022-05-19 01:51:26 -07:00
|
|
|
_ai_switch_weapons(a1, &hitMode, &weapon, a2);
|
|
|
|
}
|
2022-08-31 09:53:12 -07:00
|
|
|
} else {
|
|
|
|
_ai_switch_weapons(a1, &hitMode, &weapon, a2);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
unsigned char rotations[800];
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
Object* ammo = NULL;
|
|
|
|
for (int attempt = 0; attempt < 10; attempt++) {
|
|
|
|
if ((combatData->results & (DAM_KNOCKED_OUT | DAM_DEAD | DAM_LOSE_TURN)) != 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
int reason = _combat_check_bad_shot(a1, a2, hitMode, false);
|
2022-08-31 08:29:46 -07:00
|
|
|
if (reason == COMBAT_BAD_SHOT_NO_AMMO) {
|
2022-05-19 01:51:26 -07:00
|
|
|
// out of ammo
|
2022-08-31 02:05:56 -07:00
|
|
|
if (aiHaveAmmo(a1, weapon, &ammo)) {
|
2022-12-24 03:30:03 -08:00
|
|
|
int remainingAmmoQuantity = weaponReload(weapon, ammo);
|
|
|
|
if (remainingAmmoQuantity == 0 && ammo != NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
_obj_destroy(ammo);
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (remainingAmmoQuantity != -1) {
|
2022-05-19 01:51:26 -07:00
|
|
|
int volume = _gsound_compute_relative_volume(a1);
|
|
|
|
const char* sfx = sfxBuildWeaponName(WEAPON_SOUND_EFFECT_READY, weapon, hitMode, NULL);
|
|
|
|
_gsound_play_sfx_file_volume(sfx, volume);
|
|
|
|
_ai_magic_hands(a1, weapon, 5002);
|
|
|
|
|
2022-08-31 07:41:50 -07:00
|
|
|
// SFALL: Fix incorrect AP cost when AI reloads a weapon.
|
|
|
|
// CE: There is a commented out code which checks
|
|
|
|
// available action points before performing reload. Not
|
|
|
|
// sure why it was commented, probably needs additional
|
|
|
|
// testing.
|
|
|
|
int actionPointsRequired = weaponGetActionPointCost(a1, HIT_MODE_RIGHT_WEAPON_RELOAD, false);
|
|
|
|
if (a1->data.critter.combat.ap >= actionPointsRequired) {
|
|
|
|
a1->data.critter.combat.ap -= actionPointsRequired;
|
2022-05-19 01:51:26 -07:00
|
|
|
} else {
|
|
|
|
a1->data.critter.combat.ap = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ammo = _ai_search_environ(a1, ITEM_TYPE_AMMO);
|
|
|
|
if (ammo != NULL) {
|
|
|
|
ammo = _ai_retrieve_object(a1, ammo);
|
|
|
|
if (ammo != NULL) {
|
2022-12-24 03:30:03 -08:00
|
|
|
int remainingAmmoQuantity = weaponReload(weapon, ammo);
|
|
|
|
if (remainingAmmoQuantity == 0) {
|
2022-05-19 01:51:26 -07:00
|
|
|
_obj_destroy(ammo);
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (remainingAmmoQuantity != -1) {
|
2022-05-19 01:51:26 -07:00
|
|
|
int volume = _gsound_compute_relative_volume(a1);
|
|
|
|
const char* sfx = sfxBuildWeaponName(WEAPON_SOUND_EFFECT_READY, weapon, hitMode, NULL);
|
|
|
|
_gsound_play_sfx_file_volume(sfx, volume);
|
|
|
|
_ai_magic_hands(a1, weapon, 5002);
|
|
|
|
|
2022-08-31 07:41:50 -07:00
|
|
|
// SFALL: Fix incorrect AP cost when AI reloads a
|
|
|
|
// weapon.
|
|
|
|
// CE: See note above, probably need to check
|
|
|
|
// available action points before performing
|
|
|
|
// reload.
|
|
|
|
int actionPointsRequired = weaponGetActionPointCost(a1, HIT_MODE_RIGHT_WEAPON_RELOAD, false);
|
|
|
|
if (a1->data.critter.combat.ap >= actionPointsRequired) {
|
|
|
|
a1->data.critter.combat.ap -= actionPointsRequired;
|
2022-05-19 01:51:26 -07:00
|
|
|
} else {
|
|
|
|
a1->data.critter.combat.ap = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
int volume = _gsound_compute_relative_volume(a1);
|
|
|
|
const char* sfx = sfxBuildWeaponName(WEAPON_SOUND_EFFECT_OUT_OF_AMMO, weapon, hitMode, NULL);
|
|
|
|
_gsound_play_sfx_file_volume(sfx, volume);
|
|
|
|
_ai_magic_hands(a1, weapon, 5001);
|
|
|
|
|
|
|
|
if (_inven_unwield(a1, 1) == 0) {
|
|
|
|
_combat_turn_run();
|
|
|
|
}
|
|
|
|
|
|
|
|
_ai_switch_weapons(a1, &hitMode, &weapon, a2);
|
|
|
|
}
|
|
|
|
}
|
2022-08-31 08:29:46 -07:00
|
|
|
} else if (reason == COMBAT_BAD_SHOT_NOT_ENOUGH_AP || reason == COMBAT_BAD_SHOT_ARM_CRIPPLED || reason == COMBAT_BAD_SHOT_BOTH_ARMS_CRIPPLED) {
|
2022-05-19 01:51:26 -07:00
|
|
|
// 3 - not enough action points
|
|
|
|
// 6 - crippled one arm for two-handed weapon
|
|
|
|
// 7 - both hands crippled
|
|
|
|
if (_ai_switch_weapons(a1, &hitMode, &weapon, a2) == -1) {
|
|
|
|
return -1;
|
|
|
|
}
|
2022-08-31 08:29:46 -07:00
|
|
|
} else if (reason == COMBAT_BAD_SHOT_OUT_OF_RANGE) {
|
2022-05-19 01:51:26 -07:00
|
|
|
// target out of range
|
2022-12-24 03:30:03 -08:00
|
|
|
int accuracy = _determine_to_hit_no_range(a1, a2, HIT_LOCATION_UNCALLED, hitMode, rotations);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (accuracy < minToHit) {
|
2022-12-24 03:30:03 -08:00
|
|
|
debugPrint("%s: FLEEING: Can't possibly Hit Target!", critterGetName(a1));
|
2022-05-19 01:51:26 -07:00
|
|
|
_ai_run_away(a1, a2);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (weapon != NULL) {
|
2022-12-24 03:30:03 -08:00
|
|
|
if (_ai_move_steps_closer(a1, a2, actionPoints, taunt) == -1) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
2022-12-24 03:30:03 -08:00
|
|
|
taunt = false;
|
2022-05-19 01:51:26 -07:00
|
|
|
} else {
|
|
|
|
if (_ai_switch_weapons(a1, &hitMode, &weapon, a2) == -1 || weapon == NULL) {
|
2022-09-01 08:41:37 -07:00
|
|
|
// NOTE: Uninline.
|
2022-12-24 03:30:03 -08:00
|
|
|
if (_ai_move_closer(a1, a2, taunt) == -1) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
2022-12-24 03:30:03 -08:00
|
|
|
taunt = false;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
2022-08-31 08:29:46 -07:00
|
|
|
} else if (reason == COMBAT_BAD_SHOT_AIM_BLOCKED) {
|
2022-05-19 01:51:26 -07:00
|
|
|
// aim is blocked
|
2022-12-24 03:30:03 -08:00
|
|
|
if (_ai_move_steps_closer(a1, a2, a1->data.critter.combat.ap, taunt) == -1) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
2022-12-24 03:30:03 -08:00
|
|
|
taunt = false;
|
2022-08-31 08:29:46 -07:00
|
|
|
} else if (reason == COMBAT_BAD_SHOT_OK) {
|
2022-05-19 01:51:26 -07:00
|
|
|
int accuracy = _determine_to_hit(a1, a2, HIT_LOCATION_UNCALLED, hitMode);
|
2022-12-24 03:30:03 -08:00
|
|
|
if (safeDistance != 0) {
|
|
|
|
if (_ai_move_away(a1, a2, safeDistance) == -1) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (accuracy < minToHit) {
|
2022-12-24 03:30:03 -08:00
|
|
|
int accuracyNoRange = _determine_to_hit_no_range(a1, a2, HIT_LOCATION_UNCALLED, hitMode, rotations);
|
|
|
|
if (accuracyNoRange < minToHit) {
|
|
|
|
debugPrint("%s: FLEEING: Can't possibly Hit Target!", critterGetName(a1));
|
2022-05-19 01:51:26 -07:00
|
|
|
_ai_run_away(a1, a2);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (actionPoints > 0) {
|
2022-12-24 03:30:03 -08:00
|
|
|
int v24 = pathfinderFindPath(a1, a1->tile, a2->tile, rotations, 0, _obj_blocking_at);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (v24 == 0) {
|
|
|
|
v42 = actionPoints;
|
|
|
|
} else {
|
|
|
|
if (v24 < actionPoints) {
|
|
|
|
actionPoints = v24;
|
|
|
|
}
|
|
|
|
|
|
|
|
int tile = a1->tile;
|
|
|
|
int index;
|
|
|
|
for (index = 0; index < actionPoints; index++) {
|
2022-12-24 03:30:03 -08:00
|
|
|
tile = tileGetTileInDirection(tile, rotations[index], 1);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
v42++;
|
|
|
|
|
|
|
|
int v27 = _determine_to_hit_from_tile(a1, tile, a2, HIT_LOCATION_UNCALLED, hitMode);
|
|
|
|
if (v27 >= minToHit) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (index == actionPoints) {
|
|
|
|
v42 = actionPoints;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (_ai_move_steps_closer(a1, a2, v42, taunt) == -1) {
|
|
|
|
debugPrint("%s: FLEEING: Can't possibly get closer to Target!", critterGetName(a1));
|
2022-05-19 01:51:26 -07:00
|
|
|
_ai_run_away(a1, a2);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
taunt = false;
|
2022-08-17 22:41:15 -07:00
|
|
|
if (_ai_attack(a1, a2, hitMode) == -1 || weaponGetActionPointCost(a1, hitMode, 0) > a1->data.critter.combat.ap) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
} else {
|
2022-08-17 22:41:15 -07:00
|
|
|
if (_ai_attack(a1, a2, hitMode) == -1 || weaponGetActionPointCost(a1, hitMode, 0) > a1->data.critter.combat.ap) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Something with using flare
|
|
|
|
//
|
|
|
|
// 0x42AE90
|
|
|
|
int _cAIPrepWeaponItem(Object* critter, Object* item)
|
|
|
|
{
|
2023-01-03 12:56:52 -08:00
|
|
|
if (item != NULL && critterGetStat(critter, STAT_INTELLIGENCE) >= 3 && item->pid == PROTO_ID_FLARE && lightGetAmbientIntensity() < LIGHT_INTENSITY_MAX * 0.85) {
|
2022-05-19 01:51:26 -07:00
|
|
|
_protinst_use_item(critter, item);
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42AECC
|
2022-08-31 00:05:00 -07:00
|
|
|
void aiAttemptWeaponReload(Object* critter, int animate)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-08-31 00:05:00 -07:00
|
|
|
Object* weapon = critterGetItem2(critter);
|
|
|
|
if (weapon == NULL) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-08-31 00:05:00 -07:00
|
|
|
int ammoQuantity = ammoGetQuantity(weapon);
|
|
|
|
int ammoCapacity = ammoGetCapacity(weapon);
|
|
|
|
if (ammoQuantity < ammoCapacity) {
|
|
|
|
Object* ammo;
|
2022-08-31 02:05:56 -07:00
|
|
|
if (aiHaveAmmo(critter, weapon, &ammo)) {
|
2022-08-31 00:05:00 -07:00
|
|
|
int rc = weaponReload(weapon, ammo);
|
|
|
|
if (rc == 0) {
|
|
|
|
_obj_destroy(ammo);
|
|
|
|
}
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-08-31 00:05:00 -07:00
|
|
|
if (rc != -1 && objectIsPartyMember(critter)) {
|
|
|
|
int volume = _gsound_compute_relative_volume(critter);
|
|
|
|
const char* sfx = sfxBuildWeaponName(WEAPON_SOUND_EFFECT_READY, weapon, HIT_MODE_RIGHT_WEAPON_PRIMARY, NULL);
|
|
|
|
_gsound_play_sfx_file_volume(sfx, volume);
|
|
|
|
|
|
|
|
if (animate) {
|
|
|
|
_ai_magic_hands(critter, weapon, 5002);
|
|
|
|
}
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42AF78
|
|
|
|
void _combat_ai_begin(int a1, void* a2)
|
|
|
|
{
|
|
|
|
_curr_crit_num = a1;
|
|
|
|
|
|
|
|
if (a1 != 0) {
|
2022-05-21 08:22:03 -07:00
|
|
|
_curr_crit_list = (Object**)internal_malloc(sizeof(Object*) * a1);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (_curr_crit_list) {
|
|
|
|
memcpy(_curr_crit_list, a2, sizeof(Object*) * a1);
|
|
|
|
} else {
|
|
|
|
_curr_crit_num = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42AFBC
|
|
|
|
void _combat_ai_over()
|
|
|
|
{
|
|
|
|
if (_curr_crit_num) {
|
|
|
|
internal_free(_curr_crit_list);
|
|
|
|
}
|
|
|
|
|
|
|
|
_curr_crit_num = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42AFDC
|
|
|
|
int _cai_perform_distance_prefs(Object* a1, Object* a2)
|
|
|
|
{
|
|
|
|
if (a1->data.critter.combat.ap <= 0) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
int distance = aiGetPacket(a1)->distance;
|
|
|
|
|
|
|
|
if (a2 != NULL) {
|
|
|
|
if ((a2->data.critter.combat.ap & DAM_DEAD) != 0) {
|
|
|
|
a2 = NULL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (distance) {
|
|
|
|
case DISTANCE_STAY_CLOSE:
|
|
|
|
if (a1->data.critter.combat.whoHitMe != gDude) {
|
|
|
|
int distance = objectGetDistanceBetween(a1, gDude);
|
|
|
|
if (distance > 5) {
|
2022-12-24 03:30:03 -08:00
|
|
|
_ai_move_steps_closer(a1, gDude, distance - 5, false);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case DISTANCE_CHARGE:
|
|
|
|
if (a2 != NULL) {
|
2022-09-01 08:41:37 -07:00
|
|
|
// NOTE: Uninline.
|
|
|
|
_ai_move_closer(a1, a2, 1);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case DISTANCE_SNIPE:
|
|
|
|
if (a2 != NULL) {
|
2022-08-31 12:02:08 -07:00
|
|
|
// SFALL: Fix AI behavior for "Snipe" distance preference.
|
|
|
|
int distance = objectGetDistanceBetween(a1, a2);
|
|
|
|
if (distance < 10) {
|
|
|
|
int attackCost = weaponGetActionPointCost(a1, HIT_MODE_RIGHT_WEAPON_PRIMARY, false);
|
|
|
|
int movementPoints = a1->data.critter.combat.ap - attackCost;
|
|
|
|
if (movementPoints > 0) {
|
|
|
|
if (movementPoints + distance - 1 < 5) {
|
|
|
|
int attackerRating = _combatai_rating(a1);
|
|
|
|
int defenderRating = _combatai_rating(a2);
|
|
|
|
if (attackerRating < defenderRating) {
|
|
|
|
_ai_move_away(a1, a2, 10);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
_ai_move_away(a1, a2, 10);
|
|
|
|
}
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
int tile = a1->tile;
|
|
|
|
if (_cai_retargetTileFromFriendlyFire(a1, a2, &tile) == 0 && tile != a1->tile) {
|
2022-07-29 06:04:05 -07:00
|
|
|
reg_anim_begin(ANIMATION_REQUEST_RESERVED);
|
|
|
|
animationRegisterMoveToTile(a1, tile, a1->elevation, a1->data.critter.combat.ap, 0);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (reg_anim_end() != 0) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
_combat_turn_run();
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42B100
|
2022-06-19 01:32:07 -07:00
|
|
|
static int _cai_get_min_hp(AiPacket* ai)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
if (ai == NULL) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
int run_away_mode = ai->run_away_mode;
|
|
|
|
if (run_away_mode >= 0 && run_away_mode < RUN_AWAY_MODE_COUNT) {
|
|
|
|
return _hp_run_away_value[run_away_mode];
|
|
|
|
} else if (run_away_mode == -1) {
|
|
|
|
return ai->min_hp;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42B130
|
|
|
|
void _combat_ai(Object* a1, Object* a2)
|
|
|
|
{
|
2022-12-24 03:30:03 -08:00
|
|
|
// 0x51820C
|
|
|
|
static const int aiPartyMemberDistances[DISTANCE_COUNT] = {
|
|
|
|
5,
|
|
|
|
7,
|
|
|
|
7,
|
|
|
|
7,
|
|
|
|
50000,
|
|
|
|
};
|
|
|
|
|
2022-05-19 01:51:26 -07:00
|
|
|
AiPacket* ai = aiGetPacket(a1);
|
|
|
|
int hpRatio = _cai_get_min_hp(ai);
|
|
|
|
if (ai->run_away_mode != -1) {
|
|
|
|
int v7 = critterGetStat(a1, STAT_MAXIMUM_HIT_POINTS) * hpRatio / 100;
|
|
|
|
int minimumHitPoints = critterGetStat(a1, STAT_MAXIMUM_HIT_POINTS) - v7;
|
|
|
|
int currentHitPoints = critterGetStat(a1, STAT_CURRENT_HIT_POINTS);
|
|
|
|
const char* name = critterGetName(a1);
|
|
|
|
debugPrint("\n%s minHp = %d; curHp = %d", name, minimumHitPoints, currentHitPoints);
|
|
|
|
}
|
|
|
|
|
|
|
|
CritterCombatData* combatData = &(a1->data.critter.combat);
|
|
|
|
if ((combatData->maneuver & CRITTER_MANUEVER_FLEEING) != 0
|
|
|
|
|| (combatData->results & ai->hurt_too_much) != 0
|
|
|
|
|| critterGetStat(a1, STAT_CURRENT_HIT_POINTS) < ai->min_hp) {
|
2022-12-24 03:30:03 -08:00
|
|
|
debugPrint("%s: FLEEING: I'm Hurt!", critterGetName(a1));
|
2022-05-19 01:51:26 -07:00
|
|
|
_ai_run_away(a1, a2);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_ai_check_drugs(a1)) {
|
2022-12-24 03:30:03 -08:00
|
|
|
debugPrint("%s: FLEEING: I need DRUGS!", critterGetName(a1));
|
2022-05-19 01:51:26 -07:00
|
|
|
_ai_run_away(a1, a2);
|
|
|
|
} else {
|
|
|
|
if (a2 == NULL) {
|
|
|
|
a2 = _ai_danger_source(a1);
|
|
|
|
}
|
|
|
|
|
|
|
|
_cai_perform_distance_prefs(a1, a2);
|
|
|
|
|
|
|
|
if (a2 != NULL) {
|
|
|
|
_ai_try_attack(a1, a2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (a2 != NULL
|
|
|
|
&& (a1->data.critter.combat.results & DAM_DEAD) == 0
|
|
|
|
&& a1->data.critter.combat.ap != 0
|
|
|
|
&& objectGetDistanceBetween(a1, a2) > ai->max_dist) {
|
2022-12-24 03:30:03 -08:00
|
|
|
Object* friendlyDead = aiInfoGetFriendlyDead(a1);
|
|
|
|
if (friendlyDead != NULL) {
|
|
|
|
_ai_move_away(a1, friendlyDead, 10);
|
2022-08-02 03:31:26 -07:00
|
|
|
aiInfoSetFriendlyDead(a1, NULL);
|
2022-05-19 01:51:26 -07:00
|
|
|
} else {
|
|
|
|
int perception = critterGetStat(a1, STAT_PERCEPTION);
|
|
|
|
if (!_ai_find_friend(a1, perception * 2, 5)) {
|
2023-01-16 05:49:50 -08:00
|
|
|
combatData->maneuver |= CRITTER_MANEUVER_DISENGAGING;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (a2 == NULL && !objectIsPartyMember(a1)) {
|
|
|
|
Object* whoHitMe = combatData->whoHitMe;
|
|
|
|
if (whoHitMe != NULL) {
|
|
|
|
if ((whoHitMe->data.critter.combat.results & DAM_DEAD) == 0 && combatData->damageLastTurn > 0) {
|
2022-12-24 03:30:03 -08:00
|
|
|
Object* friendlyDead = aiInfoGetFriendlyDead(a1);
|
|
|
|
if (friendlyDead != NULL) {
|
|
|
|
_ai_move_away(a1, friendlyDead, 10);
|
2022-08-02 03:31:26 -07:00
|
|
|
aiInfoSetFriendlyDead(a1, NULL);
|
2022-05-19 01:51:26 -07:00
|
|
|
} else {
|
2022-12-24 03:30:03 -08:00
|
|
|
debugPrint("%s: FLEEING: Somebody is shooting at me that I can't see!", critterGetName(a1));
|
2022-05-19 01:51:26 -07:00
|
|
|
_ai_run_away(a1, NULL);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
Object* friendlyDead = aiInfoGetFriendlyDead(a1);
|
|
|
|
if (friendlyDead != NULL) {
|
|
|
|
_ai_move_away(a1, friendlyDead, 10);
|
|
|
|
if (objectGetDistanceBetween(a1, friendlyDead) >= 10) {
|
2022-08-02 03:31:26 -07:00
|
|
|
aiInfoSetFriendlyDead(a1, NULL);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
Object* nearestTeammate;
|
|
|
|
int maxTeammateDistance = 5;
|
2022-05-19 01:51:26 -07:00
|
|
|
if (a1->data.critter.combat.team != 0) {
|
2022-12-24 03:30:03 -08:00
|
|
|
nearestTeammate = _ai_find_nearest_team_in_combat(a1, a1, 1);
|
2022-05-19 01:51:26 -07:00
|
|
|
} else {
|
2022-12-24 03:30:03 -08:00
|
|
|
nearestTeammate = gDude;
|
2022-05-19 01:51:26 -07:00
|
|
|
if (objectIsPartyMember(a1)) {
|
|
|
|
// NOTE: Uninline
|
|
|
|
int distance = aiGetDistance(a1);
|
|
|
|
if (distance != -1) {
|
2022-12-24 03:30:03 -08:00
|
|
|
maxTeammateDistance = aiPartyMemberDistances[distance];
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (a2 == NULL && nearestTeammate != NULL && objectGetDistanceBetween(a1, nearestTeammate) > maxTeammateDistance) {
|
|
|
|
int currentDistance = objectGetDistanceBetween(a1, nearestTeammate);
|
|
|
|
_ai_move_steps_closer(a1, nearestTeammate, currentDistance - maxTeammateDistance, false);
|
2022-05-19 01:51:26 -07:00
|
|
|
} else {
|
|
|
|
if (a1->data.critter.combat.ap > 0) {
|
|
|
|
debugPrint("\n>>>NOTE: %s had extra AP's to use!<<<", critterGetName(a1));
|
|
|
|
_cai_perform_distance_prefs(a1, a2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42B3FC
|
|
|
|
bool _combatai_want_to_join(Object* a1)
|
|
|
|
{
|
|
|
|
_process_bk();
|
|
|
|
|
|
|
|
if ((a1->flags & OBJECT_HIDDEN) != 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (a1->elevation != gDude->elevation) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((a1->data.critter.combat.results & (DAM_DEAD | DAM_KNOCKED_OUT)) != 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (a1->data.critter.combat.damageLastTurn > 0) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (a1->sid != -1) {
|
|
|
|
scriptSetObjects(a1->sid, NULL, NULL);
|
|
|
|
scriptSetFixedParam(a1->sid, 5);
|
|
|
|
scriptExecProc(a1->sid, SCRIPT_PROC_COMBAT);
|
|
|
|
}
|
|
|
|
|
2023-01-16 05:49:50 -08:00
|
|
|
if ((a1->data.critter.combat.maneuver & CRITTER_MANEUVER_ENGAGING) != 0) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-01-16 05:49:50 -08:00
|
|
|
if ((a1->data.critter.combat.maneuver & CRITTER_MANEUVER_DISENGAGING) != 0) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-01-16 05:42:50 -08:00
|
|
|
if ((a1->data.critter.combat.maneuver & CRITTER_MANUEVER_FLEEING) != 0) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_ai_danger_source(a1) == NULL) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42B4A8
|
|
|
|
bool _combatai_want_to_stop(Object* a1)
|
|
|
|
{
|
|
|
|
_process_bk();
|
|
|
|
|
2023-01-16 05:49:50 -08:00
|
|
|
if ((a1->data.critter.combat.maneuver & CRITTER_MANEUVER_DISENGAGING) != 0) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((a1->data.critter.combat.results & (DAM_KNOCKED_OUT | DAM_DEAD)) != 0) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((a1->data.critter.combat.maneuver & CRITTER_MANUEVER_FLEEING) != 0) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
Object* enemy = _ai_danger_source(a1);
|
|
|
|
return enemy == NULL || !isWithinPerception(a1, enemy);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42B504
|
|
|
|
int critterSetTeam(Object* obj, int team)
|
|
|
|
{
|
2022-07-29 06:04:05 -07:00
|
|
|
if (PID_TYPE(obj->pid) != OBJ_TYPE_CRITTER) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
obj->data.critter.combat.team = team;
|
|
|
|
|
|
|
|
if (obj->data.critter.combat.whoHitMeCid == -1) {
|
|
|
|
_critter_set_who_hit_me(obj, NULL);
|
|
|
|
debugPrint("\nError: CombatData found with invalid who_hit_me!");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
Object* whoHitMe = obj->data.critter.combat.whoHitMe;
|
|
|
|
if (whoHitMe != NULL) {
|
|
|
|
if (whoHitMe->data.critter.combat.team == team) {
|
|
|
|
_critter_set_who_hit_me(obj, NULL);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-02 03:31:26 -07:00
|
|
|
aiInfoSetLastTarget(obj, NULL);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
if (isInCombat()) {
|
|
|
|
bool outlineWasEnabled = obj->outline != 0 && (obj->outline & OUTLINE_DISABLED) == 0;
|
|
|
|
|
|
|
|
objectClearOutline(obj, NULL);
|
|
|
|
|
|
|
|
int outlineType;
|
|
|
|
if (obj->data.critter.combat.team == gDude->data.critter.combat.team) {
|
|
|
|
outlineType = OUTLINE_TYPE_2;
|
|
|
|
} else {
|
|
|
|
outlineType = OUTLINE_TYPE_HOSTILE;
|
|
|
|
}
|
|
|
|
|
|
|
|
objectSetOutline(obj, outlineType, NULL);
|
|
|
|
|
|
|
|
if (outlineWasEnabled) {
|
|
|
|
Rect rect;
|
|
|
|
objectEnableOutline(obj, &rect);
|
|
|
|
tileWindowRefreshRect(&rect, obj->elevation);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42B5D4
|
|
|
|
int critterSetAiPacket(Object* object, int aiPacket)
|
|
|
|
{
|
2022-07-29 06:04:05 -07:00
|
|
|
if (PID_TYPE(object->pid) != OBJ_TYPE_CRITTER) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
object->data.critter.combat.aiPacket = aiPacket;
|
|
|
|
|
|
|
|
if (_isPotentialPartyMember(object)) {
|
|
|
|
Proto* proto;
|
|
|
|
if (protoGetProto(object->pid, &proto) == -1) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
proto->critter.aiPacket = aiPacket;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// combatai_msg
|
|
|
|
// 0x42B634
|
2022-12-24 03:30:03 -08:00
|
|
|
int _combatai_msg(Object* critter, Attack* attack, int type, int delay)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-12-24 03:30:03 -08:00
|
|
|
if (PID_TYPE(critter->pid) != OBJ_TYPE_CRITTER) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-10-06 06:32:46 -07:00
|
|
|
if (!settings.preferences.combat_taunts) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if (critter == gDude) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
if ((critter->data.critter.combat.results & (DAM_DEAD | DAM_KNOCKED_OUT)) != 0) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
AiPacket* ai = aiGetPacket(critter);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
debugPrint("%s is using %s packet with a %d%% chance to taunt\n", objectGetName(critter), ai->name, ai->chance);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
if (randomBetween(1, 100) > ai->chance) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
int start;
|
|
|
|
int end;
|
|
|
|
char* string;
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
case AI_MESSAGE_TYPE_RUN:
|
|
|
|
start = ai->run.start;
|
|
|
|
end = ai->run.end;
|
|
|
|
string = _attack_str;
|
|
|
|
break;
|
|
|
|
case AI_MESSAGE_TYPE_MOVE:
|
|
|
|
start = ai->move.start;
|
|
|
|
end = ai->move.end;
|
|
|
|
string = _attack_str;
|
|
|
|
break;
|
|
|
|
case AI_MESSAGE_TYPE_ATTACK:
|
|
|
|
start = ai->attack.start;
|
|
|
|
end = ai->attack.end;
|
|
|
|
string = _attack_str;
|
|
|
|
break;
|
|
|
|
case AI_MESSAGE_TYPE_MISS:
|
|
|
|
start = ai->miss.start;
|
|
|
|
end = ai->miss.end;
|
|
|
|
string = _target_str;
|
|
|
|
break;
|
|
|
|
case AI_MESSAGE_TYPE_HIT:
|
|
|
|
start = ai->hit[attack->defenderHitLocation].start;
|
|
|
|
end = ai->hit[attack->defenderHitLocation].end;
|
|
|
|
string = _target_str;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (end < start) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
MessageListItem messageListItem;
|
|
|
|
messageListItem.num = randomBetween(start, end);
|
|
|
|
if (!messageListGetItem(&gCombatAiMessageList, &messageListItem)) {
|
2022-12-24 03:30:03 -08:00
|
|
|
debugPrint("\nERROR: combatai_msg: Couldn't find message # %d for %s", messageListItem.num, critterGetName(critter));
|
2022-05-19 01:51:26 -07:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-12-24 03:30:03 -08:00
|
|
|
debugPrint("%s said message %d\n", objectGetName(critter), messageListItem.num);
|
|
|
|
snprintf(string, AI_MESSAGE_SIZE, "%s", messageListItem.text);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
// TODO: Get rid of casts.
|
2022-12-24 03:30:03 -08:00
|
|
|
return animationRegisterCallback(critter, (void*)type, (AnimationCallback*)_ai_print_msg, delay);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42B80C
|
2022-06-19 01:32:07 -07:00
|
|
|
static int _ai_print_msg(Object* critter, int type)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
if (textObjectsGetCount() > 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
char* string;
|
|
|
|
switch (type) {
|
|
|
|
case AI_MESSAGE_TYPE_HIT:
|
|
|
|
case AI_MESSAGE_TYPE_MISS:
|
|
|
|
string = _target_str;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
string = _attack_str;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
AiPacket* ai = aiGetPacket(critter);
|
|
|
|
|
|
|
|
Rect rect;
|
|
|
|
if (textObjectAdd(critter, string, ai->font, ai->color, ai->outline_color, &rect) == 0) {
|
|
|
|
tileWindowRefreshRect(&rect, critter->elevation);
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns random critter for attacking as a result of critical weapon failure.
|
|
|
|
//
|
|
|
|
// 0x42B868
|
|
|
|
Object* _combat_ai_random_target(Attack* attack)
|
|
|
|
{
|
|
|
|
// Looks like this function does nothing because it's result is not used. I
|
|
|
|
// suppose it was planned to use range as a condition below, but it was
|
|
|
|
// later moved into 0x426614, but remained here.
|
2022-08-17 22:41:15 -07:00
|
|
|
weaponGetRange(attack->attacker, attack->hitMode);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
Object* critter = NULL;
|
|
|
|
|
|
|
|
if (_curr_crit_num != 0) {
|
|
|
|
// Randomize starting critter.
|
|
|
|
int start = randomBetween(0, _curr_crit_num - 1);
|
|
|
|
int index = start;
|
|
|
|
while (true) {
|
|
|
|
Object* obj = _curr_crit_list[index];
|
|
|
|
if (obj != attack->attacker
|
|
|
|
&& obj != attack->defender
|
|
|
|
&& _can_see(attack->attacker, obj)
|
2022-08-31 08:31:23 -07:00
|
|
|
&& _combat_check_bad_shot(attack->attacker, obj, attack->hitMode, false) == COMBAT_BAD_SHOT_OK) {
|
2022-05-19 01:51:26 -07:00
|
|
|
critter = obj;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
index += 1;
|
|
|
|
if (index == _curr_crit_num) {
|
|
|
|
index = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (index == start) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return critter;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42B90C
|
2022-06-19 01:32:07 -07:00
|
|
|
static int _combatai_rating(Object* obj)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
int melee_damage;
|
|
|
|
Object* item;
|
|
|
|
int weapon_damage_min;
|
|
|
|
int weapon_damage_max;
|
|
|
|
|
|
|
|
if (obj == NULL) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2022-07-29 06:04:05 -07:00
|
|
|
if (FID_TYPE(obj->fid) != OBJ_TYPE_CRITTER) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((obj->data.critter.combat.results & (DAM_DEAD | DAM_KNOCKED_OUT)) != 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
melee_damage = critterGetStat(obj, STAT_MELEE_DAMAGE);
|
|
|
|
|
|
|
|
item = critterGetItem2(obj);
|
|
|
|
if (item != NULL && itemGetType(item) == ITEM_TYPE_WEAPON && weaponGetDamageMinMax(item, &weapon_damage_min, &weapon_damage_max) != -1 && melee_damage < weapon_damage_max) {
|
|
|
|
melee_damage = weapon_damage_max;
|
|
|
|
}
|
|
|
|
|
|
|
|
item = critterGetItem1(obj);
|
|
|
|
if (item != NULL && itemGetType(item) == ITEM_TYPE_WEAPON && weaponGetDamageMinMax(item, &weapon_damage_min, &weapon_damage_max) != -1 && melee_damage < weapon_damage_max) {
|
|
|
|
melee_damage = weapon_damage_max;
|
|
|
|
}
|
|
|
|
|
|
|
|
return melee_damage + critterGetStat(obj, STAT_ARMOR_CLASS);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42B9D4
|
2022-12-24 03:30:03 -08:00
|
|
|
void _combatai_check_retaliation(Object* a1, Object* a2)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
Object* whoHitMe = a1->data.critter.combat.whoHitMe;
|
|
|
|
if (whoHitMe != NULL) {
|
2022-12-24 03:30:03 -08:00
|
|
|
int candidateRating = _combatai_rating(a2);
|
|
|
|
int whoHitMeRating = _combatai_rating(whoHitMe);
|
|
|
|
if (candidateRating > whoHitMeRating) {
|
|
|
|
_critter_set_who_hit_me(a1, a2);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
2022-12-24 03:30:03 -08:00
|
|
|
} else {
|
|
|
|
_critter_set_who_hit_me(a1, a2);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42BA04
|
2022-10-05 04:25:36 -07:00
|
|
|
bool isWithinPerception(Object* a1, Object* a2)
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
if (a2 == NULL) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
int distance = objectGetDistanceBetween(a2, a1);
|
|
|
|
int perception = critterGetStat(a1, STAT_PERCEPTION);
|
|
|
|
int sneak = skillGetValue(a2, SKILL_SNEAK);
|
|
|
|
if (_can_see(a1, a2)) {
|
2022-10-05 04:25:36 -07:00
|
|
|
int maxDistance = perception * 5;
|
2022-06-08 10:38:46 -07:00
|
|
|
if ((a2->flags & OBJECT_TRANS_GLASS) != 0) {
|
2022-10-05 04:25:36 -07:00
|
|
|
maxDistance /= 2;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (a2 == gDude) {
|
|
|
|
if (dudeIsSneaking()) {
|
2022-10-05 04:25:36 -07:00
|
|
|
maxDistance /= 4;
|
2022-05-19 01:51:26 -07:00
|
|
|
if (sneak > 120) {
|
2022-10-05 04:25:36 -07:00
|
|
|
maxDistance -= 1;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
2022-10-05 04:25:36 -07:00
|
|
|
} else if (dudeHasState(DUDE_STATE_SNEAKING)) {
|
|
|
|
maxDistance = maxDistance * 2 / 3;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-05 04:25:36 -07:00
|
|
|
if (distance <= maxDistance) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-05 04:25:36 -07:00
|
|
|
int maxDistance;
|
2022-05-19 01:51:26 -07:00
|
|
|
if (isInCombat()) {
|
2022-10-05 04:25:36 -07:00
|
|
|
maxDistance = perception * 2;
|
2022-05-19 01:51:26 -07:00
|
|
|
} else {
|
2022-10-05 04:25:36 -07:00
|
|
|
maxDistance = perception;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (a2 == gDude) {
|
|
|
|
if (dudeIsSneaking()) {
|
2022-10-05 04:25:36 -07:00
|
|
|
maxDistance /= 4;
|
2022-05-19 01:51:26 -07:00
|
|
|
if (sneak > 120) {
|
2022-10-05 04:25:36 -07:00
|
|
|
maxDistance -= 1;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
2022-10-05 04:25:36 -07:00
|
|
|
} else if (dudeHasState(DUDE_STATE_SNEAKING)) {
|
|
|
|
maxDistance = maxDistance * 2 / 3;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-05 04:25:36 -07:00
|
|
|
if (distance <= maxDistance) {
|
2022-05-19 01:51:26 -07:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load combatai.msg and apply language filter.
|
|
|
|
//
|
|
|
|
// 0x42BB34
|
2022-06-19 01:32:07 -07:00
|
|
|
static int aiMessageListInit()
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
|
|
|
if (!messageListInit(&gCombatAiMessageList)) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-05-28 02:34:49 -07:00
|
|
|
char path[COMPAT_MAX_PATH];
|
2022-12-08 12:05:50 -08:00
|
|
|
snprintf(path, sizeof(path), "%s%s", asc_5186C8, "combatai.msg");
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
if (!messageListLoad(&gCombatAiMessageList, path)) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-10-06 06:32:46 -07:00
|
|
|
if (settings.preferences.language_filter) {
|
2022-05-19 01:51:26 -07:00
|
|
|
messageListFilterBadwords(&gCombatAiMessageList);
|
|
|
|
}
|
|
|
|
|
2022-11-10 07:07:23 -08:00
|
|
|
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_COMBAT_AI, &gCombatAiMessageList);
|
|
|
|
|
2022-05-19 01:51:26 -07:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE: Inlined.
|
|
|
|
//
|
|
|
|
// 0x42BBD8
|
2022-06-19 01:32:07 -07:00
|
|
|
static int aiMessageListFree()
|
2022-05-19 01:51:26 -07:00
|
|
|
{
|
2022-11-10 07:07:23 -08:00
|
|
|
messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_COMBAT_AI, nullptr);
|
2022-05-19 01:51:26 -07:00
|
|
|
if (!messageListFree(&gCombatAiMessageList)) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42BBF0
|
|
|
|
void aiMessageListReloadIfNeeded()
|
|
|
|
{
|
2022-10-06 06:32:46 -07:00
|
|
|
int languageFilter = static_cast<int>(settings.preferences.language_filter);
|
2022-05-19 01:51:26 -07:00
|
|
|
|
|
|
|
if (languageFilter != gLanguageFilter) {
|
|
|
|
gLanguageFilter = languageFilter;
|
|
|
|
|
2022-07-07 00:52:54 -07:00
|
|
|
if (languageFilter == 1) {
|
2022-05-19 01:51:26 -07:00
|
|
|
messageListFilterBadwords(&gCombatAiMessageList);
|
|
|
|
} else {
|
|
|
|
// NOTE: Uninline.
|
|
|
|
aiMessageListFree();
|
|
|
|
|
|
|
|
aiMessageListInit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42BC60
|
|
|
|
void _combatai_notify_onlookers(Object* a1)
|
|
|
|
{
|
|
|
|
for (int index = 0; index < _curr_crit_num; index++) {
|
|
|
|
Object* obj = _curr_crit_list[index];
|
2023-01-16 05:49:50 -08:00
|
|
|
if ((obj->data.critter.combat.maneuver & CRITTER_MANEUVER_ENGAGING) == 0) {
|
2022-10-05 04:25:36 -07:00
|
|
|
if (isWithinPerception(obj, a1)) {
|
2023-01-16 05:49:50 -08:00
|
|
|
obj->data.critter.combat.maneuver |= CRITTER_MANEUVER_ENGAGING;
|
2022-05-19 01:51:26 -07:00
|
|
|
if ((a1->data.critter.combat.results & DAM_DEAD) != 0) {
|
2022-10-05 04:25:36 -07:00
|
|
|
if (!isWithinPerception(obj, obj->data.critter.combat.whoHitMe)) {
|
2022-05-19 01:51:26 -07:00
|
|
|
debugPrint("\nSomebody Died and I don't know why! Run!!!");
|
2022-08-02 03:31:26 -07:00
|
|
|
aiInfoSetFriendlyDead(obj, a1);
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42BCD4
|
|
|
|
void _combatai_notify_friends(Object* a1)
|
|
|
|
{
|
|
|
|
int team = a1->data.critter.combat.team;
|
|
|
|
|
|
|
|
for (int index = 0; index < _curr_crit_num; index++) {
|
|
|
|
Object* obj = _curr_crit_list[index];
|
2023-01-16 05:49:50 -08:00
|
|
|
if ((obj->data.critter.combat.maneuver & CRITTER_MANEUVER_ENGAGING) == 0 && team == obj->data.critter.combat.team) {
|
2022-10-05 04:25:36 -07:00
|
|
|
if (isWithinPerception(obj, a1)) {
|
2023-01-16 05:49:50 -08:00
|
|
|
obj->data.critter.combat.maneuver |= CRITTER_MANEUVER_ENGAGING;
|
2022-05-19 01:51:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x42BD28
|
|
|
|
void _combatai_delete_critter(Object* obj)
|
|
|
|
{
|
|
|
|
// TODO: Check entire function.
|
|
|
|
for (int i = 0; i < _curr_crit_num; i++) {
|
|
|
|
if (obj == _curr_crit_list[i]) {
|
|
|
|
_curr_crit_num--;
|
|
|
|
_curr_crit_list[i] = _curr_crit_list[_curr_crit_num];
|
|
|
|
_curr_crit_list[_curr_crit_num] = obj;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-09-23 05:43:44 -07:00
|
|
|
|
|
|
|
} // namespace fallout
|