Files
perfect-dark/src/game/bot.c
T

3515 lines
102 KiB
C

#include <ultra64.h>
#include "constants.h"
#include "game/chraction.h"
#include "game/chr.h"
#include "game/prop.h"
#include "game/propsnd.h"
#include "game/atan2f.h"
#include "game/bondgun.h"
#include "game/game_0b0fd0.h"
#include "game/player.h"
#include "game/playermgr.h"
#include "game/bg.h"
#include "game/mplayer/setup.h"
#include "game/mplayer/scenarios.h"
#include "game/radar.h"
#include "game/bot.h"
#include "game/botcmd.h"
#include "game/botact.h"
#include "game/botinv.h"
#include "game/challenge.h"
#include "game/lang.h"
#include "game/mplayer/mplayer.h"
#include "game/pad.h"
#include "game/padhalllv.h"
#include "game/propobj.h"
#include "game/splat.h"
#include "bss.h"
#include "lib/collision.h"
#include "lib/model.h"
#include "lib/rng.h"
#include "lib/mtx.h"
#include "lib/anim.h"
#include "data.h"
#include "types.h"
#define PICKUPCRITERIA_DEFAULT 0
#define PICKUPCRITERIA_CRITICAL 1
#define PICKUPCRITERIA_ANY 2
struct chrdata *g_MpBotChrPtrs[MAX_BOTS];
u8 g_BotCount = 0;
struct botdifficulty g_BotDifficulties[] = {
// shootdelay
// | unk04
// | | unk08
// | | | unk0c
// | | | | unk10
// | | | | | unk14
// | | | | | | unk18
// | | | | | | | dizzyamount
// | | | | | | | |
/* meat */ { TICKS(90), 0.26175770163536, 0.52351540327072, TICKS(600), 10, 0.69802051782608, 0.34901025891304, TICKS(1000) },
/* easy */ { TICKS(60), 0.12215359508991, 0.24430719017982, TICKS(360), 10, 0.49733963608742, 0.13960410654545, TICKS(1000) },
/* norm */ { TICKS(30), 0.069802053272724, 0.13960410654545, TICKS(180), 4, 0.34901025891304, 0.08725256472826, TICKS(1500) },
/* hard */ { TICKS(15), 0.026175770908594, 0.069802053272724, TICKS(90), 2, 0.24430719017982, 0.034901026636362, TICKS(2500) },
/* perf */ { TICKS(0), 0, 0.034901026636362, TICKS(45), 1, 0.17450512945652, 0, TICKS(4000) },
/* dark */ { TICKS(0), 0, 0, TICKS(0), 0, 0.13960410654545, 0, TICKS(4000) },
{ 0 },
};
bool botIsDizzy(struct chrdata *chr)
{
return chr->blurdrugamount >= g_BotDifficulties[chr->aibot->config->difficulty].dizzyamount;
}
void botReset(struct chrdata *chr, u8 respawning)
{
s32 i;
u32 rand;
struct aibot *aibot = chr->aibot;
if (aibot) {
chr->fadealpha = -1;
chr->chrflags &= ~CHRCFLAG_JUST_INJURED;
chr->hidden &= ~CHRHFLAG_CLOAKED;
chr->myaction = MA_AIBOTMAINLOOP;
chr->shotbondsum = 0;
if (respawning) {
chr->numclosearghs = 0;
chr->damage = 0;
chr->target = -1;
chr->chrpreset1 = -1;
chr->cover = -1;
chrSetShield(chr, 0);
chr->cmnum = 0;
chr->cmnum2 = 0;
bgunFreeFireslot(chr->fireslots[0]);
bgunFreeFireslot(chr->fireslots[1]);
chr->unk32c_12 = 0;
chr->fireslots[0] = -1;
chr->fireslots[1] = -1;
chr->firecount[0] = 0;
chr->firecount[1] = 0;
chr->weapons_held[0] = NULL;
chr->weapons_held[1] = NULL;
chr->liftaction = 0;
chr->inlift = 0;
chr->lift = NULL;
chr->height = 185;
for (i = 0; i < 33; i++) {
aibot->ammoheld[i] = 0;
}
botinvClear(chr);
aibot->weaponnum = WEAPON_UNARMED;
aibot->gunfunc = FUNC_PRIMARY;
aibot->iscloserangeweapon = true;
aibot->loadedammo[0] = 0;
aibot->loadedammo[1] = 0;
aibot->gotoprop = NULL;
aibot->timeuntilreload60[0] = 0;
aibot->timeuntilreload60[1] = 0;
aibot->nextbullettimer60[0] = 0;
aibot->nextbullettimer60[1] = 0;
aibot->distmode = -1;
aibot->throwtimer60 = 0;
aibot->burstsdone[0] = 0;
aibot->burstsdone[1] = 0;
aibot->skrocket = NULL;
aibot->unk0a0 = 0;
aibot->hasbriefcase = false;
aibot->hascase = false;
aibot->cloakdeviceenabled = false;
aibot->rcp120cloakenabled = false;
aibot->unk04c_04 = false;
aibot->unk04c_03 = false;
aibot->hasuplink = false;
aibot->unk064 = 0;
aibot->unk04c_00 = false;
aibot->hillpadnum = -1;
aibot->hillcovernum = -1;
aibot->lastknownhill = -1;
aibot->cyclonedischarging[1] = 0;
aibot->cyclonedischarging[0] = 0;
aibot->changeguntimer60 = 0;
aibot->distmodettl60 = 0;
aibot->forcemainloop = false;
aibot->returntodefendtimer60 = 0;
aibot->punchtimer60[HAND_LEFT] = -1;
aibot->punchtimer60[HAND_RIGHT] = 0;
aibot->reaperspeed[HAND_LEFT] = 0;
aibot->reaperspeed[HAND_RIGHT] = 0;
aibot->commandtimer60 = 0;
aibot->shootdelaytimer60 = 0;
aibot->targetlastseen60 = -1;
aibot->lastseenanytarget60 = -1;
aibot->targetinsight = 0;
aibot->queryplayernum = 0;
aibot->unk040 = 0;
aibot->unk06c = 0;
aibot->unk070 = 0;
aibot->maulercharge[1] = 0;
aibot->maulercharge[0] = 0;
aibot->shotspeed.x = 0;
aibot->shotspeed.y = 0;
aibot->shotspeed.z = 0;
for (i = 0; i != MAX_MPCHRS; i++) {
aibot->chrnumsbydistanceasc[i] = -1;
aibot->chrdistances[i] = U32_MAX;
aibot->chrsinsight[i] = false;
aibot->chrslastseen60[i] = -1;
aibot->chrrooms[i] = -1;
}
aibot->waypoints[0] = NULL;
aibot->unk208 = 0;
aibot->random1 = random();
aibot->random1ttl60 = 0;
aibot->targetcloaktimer60 = 0;
aibot->canseecloaked = 0;
aibot->random2ttl60 = 0;
aibot->unk2c4 = 0;
aibot->random2 = random();
aibot->randomfrac = RANDOMFRAC();
aibot->cheap = 0;
aibot->unk078 = 0;
aibot->unk050 = 0;
aibot->unk09d = 0;
}
if (aibot->config->type == BOTTYPE_TURTLE || aibot->config->type == BOTTYPE_SHIELD) {
chr->cshield = 8;
}
if (aibot->config->difficulty == BOTDIFF_DARK) {
aibot->unk064 &= ~1;
if (mpHasShield()) {
chr->cshield = 8;
}
}
aibot->unk059 = 1;
aibot->unk058 = TICKS(120);
}
}
void botSpawn(struct chrdata *chr, u8 respawning)
{
f32 thing;
struct prop *prop;
struct defaultobj *obj;
struct aibot *aibot = chr->aibot;
struct coord pos;
s16 rooms[8];
if (chr->prop) {
prop = chr->prop->child;
while (prop) {
obj = prop->obj;
if (obj) {
obj->hidden |= OBJHFLAG_REAPABLE;
}
prop = prop->next;
}
}
if (aibot) {
botReset(chr, respawning);
splatResetChr(chr);
thing = scenarioChooseSpawnLocation(chr->radius, &pos, rooms, chr->prop);
chr->hidden |= CHRHFLAG_00100000;
chrMoveToPos(chr, &pos, rooms, thing, true);
chr->aibot->unk0a4 = model0001ae44(chr->model);
chr->aibot->angleoffset = 0;
chr->aibot->speedtheta = 0;
chr->aibot->unk0b0 = model0001ae44(chr->model);
chr->aibot->unk0b4 = 0;
chr->aibot->unk0b8 = 0;
func0f02e9a0(chr, 0);
}
}
void botSpawnAll(void)
{
s32 i;
for (i = 0; i < g_BotCount; i++) {
botSpawn(g_MpBotChrPtrs[i], false);
}
}
u32 botPickupProp(struct prop *prop, struct chrdata *chr)
{
struct defaultobj *obj = prop->obj;
if (!chr || !chr->aibot) {
return 0;
}
if (1);
obj->flags3 &= ~OBJFLAG3_ISFETCHTARGET;
switch (obj->type) {
case OBJTYPE_KEY:
// Missing break, but doesn't matter as keys don't exist in multiplayer
case OBJTYPE_AMMOCRATE:
{
struct ammocrateobj *crate = (struct ammocrateobj *)prop->obj;
s32 qty;
if (1);
qty = ammocrateGetPickupAmmoQty(crate);
if (qty) {
botactGiveAmmoByType(chr->aibot, crate->ammotype, qty);
}
// Pickup sound
propsnd0f0939f8(NULL, prop, SFX_PICKUP_AMMO, -1,
-1, 1024, 0, 0, 0, -1, 0, -1, -1, -1, -1);
objFree(obj, false, obj->hidden2 & OBJH2FLAG_CANREGEN);
}
return 2;
case OBJTYPE_MULTIAMMOCRATE:
{
struct multiammocrateobj *crate = (struct multiammocrateobj *)prop->obj;
u32 padding[1];
s32 qty;
s32 i;
for (i = 0; i != 19; i++) {
qty = crate->slots[i].quantity;
if (qty) {
botactGiveAmmoByType(chr->aibot, i + 1, qty);
}
}
// Pickup sound
propsnd0f0939f8(NULL, prop, SFX_PICKUP_AMMO, -1,
-1, 1024, 0, 0, 0, -1, 0, -1, -1, -1, -1);
objFree(obj, false, obj->hidden2 & OBJH2FLAG_CANREGEN);
}
return 2;
case OBJTYPE_WEAPON:
{
struct weaponobj *weapon = prop->weapon;
s32 itemtype = botinvGetItemType(chr, weapon->weaponnum);
s32 result;
s32 qty;
if (weapon->weaponnum == WEAPON_BRIEFCASE2) {
result = scenarioPickUpBriefcase(chr, prop);
} else if (weapon->weaponnum == WEAPON_DATAUPLINK) {
result = scenarioPickUpUplink(chr, prop);
} else {
propPlayPickupSound(prop, weapon->weaponnum);
qty = weaponGetPickupAmmoQty(weapon);
if (qty) {
botactGiveAmmoByWeapon(chr->aibot, weapon->weaponnum, weapon->gunfunc, qty);
}
if (itemtype) {
struct weapon *weapondef = weaponFindById(weapon->weaponnum);
s32 originalpad = botinvGetWeaponPad(chr, weapon->weaponnum);
s32 currentpad = obj->pad;
if (itemtype == INVITEMTYPE_WEAP
&& weapondef
&& (weapondef->flags & WEAPONFLAG_DUALWIELD)
&& originalpad != currentpad) {
botinvGiveDualWeapon(chr, weapon->weaponnum);
result = 1;
} else {
result = 2;
}
} else {
botinvGiveProp(chr, prop);
result = 1;
}
objFree(obj, false, obj->hidden2 & OBJH2FLAG_CANREGEN);
}
return result;
}
case OBJTYPE_SHIELD:
{
struct shieldobj *shield = (struct shieldobj *)prop->obj;
propsnd0f0939f8(NULL, prop, SFX_PICKUP_SHIELD, -1,
-1, 1024, 0, 0, 0, -1, 0, -1, -1, -1, -1);
chrSetShield(chr, shield->amount * 8);
objFree(obj, false, obj->hidden2 & OBJH2FLAG_CANREGEN);
}
return 3;
case OBJTYPE_BASIC:
case OBJTYPE_GLASS:
case OBJTYPE_AUTOGUN:
case OBJTYPE_TINTEDGLASS:
break;
}
return 0;
}
bool botTestPropForPickup(struct prop *prop, struct chrdata *chr)
{
struct defaultobj *obj = prop->obj;
struct weaponobj *weaponobj;
s32 itemtype;
struct weapon *weapon;
bool singleonly;
s32 i;
struct ammocrateobj *crate;
s32 weaponnum;
bool ignore1;
struct multiammocrateobj *crate2;
u32 stack1;
struct shieldobj *shield;
bool ignore2;
struct prop *chrprop;
f32 xdist;
f32 ydist;
f32 zdist;
f32 sqrange;
bool sp3c;
u32 stack2;
if (!chr || !chr->aibot || !g_Vars.lvmpbotlevel || chrIsDead(chr)) {
return false;
}
if (prop->timetoregen != 0) {
return false;
}
if (func0f085194(obj)) {
if (obj->flags & OBJFLAG_UNCOLLECTABLE) {
return false;
}
} else {
if ((obj->flags & OBJFLAG_COLLECTABLE) == 0) {
return false;
}
}
if (1);
if ((obj->hidden & OBJHFLAG_REAPABLE) || (obj->flags & OBJFLAG_THROWNLAPTOP)) {
return false;
}
if ((obj->hidden & OBJHFLAG_PROJECTILE)
&& obj->projectile
&& obj->projectile->pickuptimer240 > 0
&& obj->projectile->bouncecount == 0) {
return false;
}
if (1);
if (obj->type == OBJTYPE_WEAPON) {
weaponobj = prop->weapon;
itemtype = botinvGetItemType(chr, weaponobj->weaponnum);
weapon = weaponFindById(weaponobj->weaponnum);
singleonly = weapon && (weapon->flags & WEAPONFLAG_DUALWIELD) == 0;
if (weaponobj->weaponnum != WEAPON_BRIEFCASE2) {
// If aibot is dual wielding, or single wielding and weapon doesn't support dual,
// ignore the pickup if at max ammo already
if (itemtype == INVITEMTYPE_DUAL || (itemtype == INVITEMTYPE_WEAP && singleonly)) {
if (botactGetAmmoQuantityByWeapon(chr->aibot, weaponobj->weaponnum, weaponobj->gunfunc, false) >= bgunGetCapacityByAmmotype(botactGetAmmoTypeByFunction(weaponobj->weaponnum, weaponobj->gunfunc))) {
return false;
}
}
// Ignore rockets that are in flight
if ((weaponobj->weaponnum == WEAPON_ROCKET || weaponobj->weaponnum == WEAPON_HOMINGROCKET)
&& (obj->hidden & OBJHFLAG_PROJECTILE)) {
return false;
}
}
} else if (obj->type == OBJTYPE_AMMOCRATE) {
crate = (struct ammocrateobj *)prop->obj;
// Ignore ammo crate if at max ammo already
if (botactGetAmmoQuantityByType(chr->aibot, crate->ammotype, false) >= bgunGetCapacityByAmmotype(crate->ammotype)) {
return false;
}
} else if (obj->type == OBJTYPE_MULTIAMMOCRATE) {
crate2 = (struct multiammocrateobj *)prop->obj;
ignore1 = true;
if (objGetDestroyedLevel(obj)) {
return false;
}
for (i = 0; i < 0x13; i++) {
weaponnum = botactGetWeaponByAmmoType(i + 1);
if (crate2->slots[i].quantity > 0) {
if (botactGetAmmoQuantityByType(chr->aibot, i + 1, false) < bgunGetCapacityByAmmotype(i + 1)) {
ignore1 = false;
if (weaponnum && !botinvGetItemType(chr, weaponnum)) {
botinvGiveProp(chr, prop);
}
break;
}
}
}
if (ignore1) {
return false;
}
} else if (obj->type == OBJTYPE_SHIELD) {
shield = (struct shieldobj *)prop->obj;
ignore2 = false;
if (shield->amount <= chrGetShield(chr) * 0.125f) {
ignore2 = true;
} else if (g_MpSetup.scenario == MPSCENARIO_HOLDTHEBRIEFCASE && chr->aibot->hasbriefcase) {
ignore2 = true;
}
if (ignore2) {
return false;
}
}
chrprop = chr->prop;
xdist = prop->pos.x - chrprop->pos.x;
ydist = prop->pos.y - chrprop->pos.y;
zdist = prop->pos.z - chrprop->pos.z;
if (chr->aibot->cheap) {
if (1);
if (1);
if (1);
if (1);
if (1);
sqrange = 250 * 250;
} else {
sqrange = 100 * 100;
}
sp3c = xdist * xdist + zdist * zdist <= sqrange && ydist >= -200 && ydist <= 200;
if (sp3c) {
if ((obj->flags2 & OBJFLAG2_PICKUPWITHOUTLOS) == 0
&& !cdTestLos06(&chrprop->pos, chrprop->rooms, &prop->pos, prop->rooms, CDTYPE_DOORS | CDTYPE_BG)) {
sp3c = false;
}
}
if (sp3c) {
return botPickupProp(prop, chr);
}
return false;
}
s32 botIsObjCollectable(struct defaultobj *obj)
{
if (!obj) {
return false;
}
if (obj->type == OBJTYPE_AMMOCRATE
|| obj->type == OBJTYPE_MULTIAMMOCRATE
|| obj->type == OBJTYPE_SHIELD) {
return true;
}
if (obj->type == OBJTYPE_WEAPON) {
struct weaponobj *weapon = (struct weaponobj *)obj;
if (weapon->weaponnum == WEAPON_NBOMB
|| weapon->weaponnum == WEAPON_GRENADE
|| weapon->weaponnum == WEAPON_GRENADEROUND
|| weapon->weaponnum == WEAPON_PROXIMITYMINE
|| weapon->weaponnum == WEAPON_REMOTEMINE
|| weapon->weaponnum == WEAPON_TIMEDMINE
|| weapon->weaponnum == WEAPON_SKROCKET
|| (weapon->weaponnum == WEAPON_DRAGON && weapon->gunfunc == FUNC_SECONDARY)) {
return false;
}
return true;
}
return false;
}
/**
* Check nearby props to see if the chr is picking them up on this frame.
*/
void botCheckPickups(struct chrdata *chr)
{
s32 i;
s16 *propnumptr;
s16 propnums[260];
s16 allrooms[22];
s16 neighbours[12];
roomsCopy(chr->prop->rooms, allrooms);
for (i = 0; chr->prop->rooms[i] != -1; i++) {
roomGetNeighbours(chr->prop->rooms[i], neighbours, 10);
roomsAppend(neighbours, allrooms, 20);
}
roomGetProps(allrooms, propnums, 256);
propnumptr = propnums;
while (*propnumptr >= 0) {
struct prop *prop = &g_Vars.props[*propnumptr];
if (prop->type & (PROPTYPE_OBJ | PROPTYPE_WEAPON)) {
if (prop->timetoregen == 0) {
struct defaultobj *obj = prop->obj;
if (obj) {
if ((obj->hidden & OBJHFLAG_PROJECTILE) == 0
|| obj->projectile == NULL
|| obj->projectile->pickuptimer240 <= 0
|| obj->projectile->bouncecount != 0) {
if (botIsObjCollectable(obj)) {
if (botTestPropForPickup(prop, chr)) {
propExecuteTickOperation(prop, TICKOP_FREE);
}
}
}
}
}
}
propnumptr++;
}
}
s32 botGuessCrouchPos(struct chrdata *chr)
{
s32 crouchpos;
if (chr->height <= 90) {
crouchpos = CROUCHPOS_SQUAT;
} else if (chr->height <= 135) {
crouchpos = CROUCHPOS_DUCK;
} else {
crouchpos = CROUCHPOS_STAND;
}
return crouchpos;
}
bool botApplyMovement(struct chrdata *chr)
{
struct aibot *aibot;
u32 stack;
f32 speedforwards;
f32 speedsideways;
f32 angle;
f32 angle2;
if (!chr || !chr->aibot) {
return false;
}
aibot = chr->aibot;
angle = chrGetInverseTheta(chr) - func0f03e578(chr);
if (angle < 0) {
angle += M_BADTAU;
}
speedforwards = aibot->unk06c * cosf(angle) - sinf(angle) * aibot->unk070;
speedsideways = aibot->unk06c * sinf(angle) + cosf(angle) * aibot->unk070;
playerChooseThirdPersonAnimation(chr, botGuessCrouchPos(chr), speedsideways, speedforwards, aibot->speedtheta, &aibot->angleoffset, &aibot->unk068);
angle2 = chrGetInverseTheta(chr) - aibot->angleoffset;
if (angle2 < 0) {
angle2 += M_BADTAU;
}
if (angle2 >= M_BADTAU) {
angle2 -= M_BADTAU;
}
model0001ae90(chr->model, angle2);
return true;
}
s32 botGetWeaponNum(struct chrdata *chr)
{
if (chr->aibot) {
return chr->aibot->weaponnum;
}
return g_Vars.players[playermgrGetPlayerNumByProp(chr->prop)]->hands[HAND_RIGHT].gset.weaponnum;
}
u8 botGetTargetsWeaponNum(struct chrdata *chr)
{
struct prop *target = chrGetTargetProp(chr);
u8 weaponnum = WEAPON_NONE;
if (target) {
weaponnum = botGetWeaponNum(target->chr);
}
return weaponnum;
}
bool botIsAboutToAttack(struct chrdata *chr, bool arg1)
{
bool result = false;
struct prop *target;
u32 stack;
s32 mpindex;
if (chr->target != -1) {
target = chrGetTargetProp(chr);
mpindex = mpPlayerGetIndex(target->chr);
result = false;
if (chr->aibot->chrsinsight[mpindex]) {
result = true;
}
if (chr->aibot->config->difficulty > BOTDIFF_MEAT) {
if (chr->aibot->chrslastseen60[mpindex] >= g_Vars.lvframe60 - TICKS(240)
|| (arrayIntersects(chr->prop->rooms, target->rooms))) {
result = true;
}
if (chr->aibot->config->difficulty >= BOTDIFF_NORMAL) {
if (roomsAreNeighbours(chr->prop->rooms[0], target->rooms[0])
|| chr->aibot->chrrooms[mpindex] == target->rooms[0]
|| roomsAreNeighbours(chr->aibot->chrrooms[mpindex], target->rooms[0])) {
result = true;
}
if (chr->aibot->config->difficulty == BOTDIFF_NORMAL) {
if (chr->aibot->unk208 > 0 && chr->aibot->unk208 < 4) {
result = true;
}
} else {
if (chr->aibot->unk208 > 0 && chr->aibot->unk208 < 5) {
result = true;
}
}
}
}
if (!arg1
&& (chr->aibot->config->difficulty == BOTDIFF_MEAT || chr->aibot->config->difficulty == BOTDIFF_EASY)
&& !chrGoPosIsWaiting(chr)) {
f32 tmp = func0f03e578(chr);
f32 angle = atan2f(target->pos.x - chr->prop->pos.x, target->pos.z - chr->prop->pos.z) - tmp;
if (angle < 0) {
angle += M_BADTAU;
}
if (angle > M_PI) {
angle = M_BADTAU - angle;
}
if (chr->aibot->config->difficulty == BOTDIFF_MEAT) {
if (angle > 0.43626284599304f) {
result = false;
}
} else {
if (chr->aibot->config->difficulty == BOTDIFF_EASY && angle > 1.5705461502075f) {
result = false;
}
}
}
}
return result;
}
s32 botTick(struct prop *prop)
{
struct chrdata *chr = prop->chr;
struct aibot *aibot = chr->aibot;
s32 result = TICKOP_NONE;
bool updateable;
s32 i;
f32 diffangle;
f32 tweenangle;
f32 targetangle;
f32 oldangle;
f32 newangle;
updateable = (prop->flags & PROPFLAG_NOTYETTICKED) && g_Vars.lvupdate240;
if (aibot) {
if (updateable && aibot->unk058 > 0) {
if (aibot->unk058 > g_Vars.lvupdate60) {
aibot->unk058 -= g_Vars.lvupdate60;
} else {
aibot->unk058 = 0;
}
}
if (updateable && g_Vars.lvframe60 >= 145) {
botTickUnpaused(chr);
// Calculate cheap
aibot->cheap = true;
for (i = 0; prop->rooms[i] != -1; i++) {
if (roomIsOnscreen(prop->rooms[i]) || roomIsStandby(prop->rooms[i])) {
aibot->cheap = false;
break;
}
}
// Dampen blur
if (chr->blurdrugamount > 0) {
if (chr->blurdrugamount > TICKS(5000)) {
chr->blurdrugamount = TICKS(5000);
}
chr->blurdrugamount -= g_Vars.lvupdate60 * (chr->blurnumtimesdied + 1);
if (chr->blurdrugamount <= 0) {
chr->blurdrugamount = 0;
chr->blurnumtimesdied = 0;
}
}
// Calculate target angle
oldangle = chrGetInverseTheta(chr);
if (chrIsDead(chr)) {
targetangle = chrGetInverseTheta(chr);
} else if (aibot->skrocket) {
targetangle = chrGetInverseTheta(chr);
} else if (botIsAboutToAttack(chr, false)) {
struct prop *target = chrGetTargetProp(chr);
targetangle = chrGetAngleToPos(chr, &target->pos);
targetangle = oldangle + targetangle + aibot->unk1c0;
} else if (chr->myaction == MA_AIBOTDOWNLOAD && g_ScenarioData.htm.dlterminalnum != -1) {
targetangle = chrGetAngleToPos(chr, &g_ScenarioData.htm.terminals[g_ScenarioData.htm.dlterminalnum].prop->pos);
targetangle = oldangle + targetangle;
} else if (chr->myaction == MA_AIBOTFOLLOW
&& aibot->followingplayernum >= 0
&& aibot->chrdistances[aibot->followingplayernum] < 300
&& aibot->unk1e4 >= g_Vars.lvframe60 - TICKS(60)
&& aibot->config->difficulty != BOTDIFF_MEAT) {
targetangle = chrGetInverseTheta(g_MpAllChrPtrs[aibot->followingplayernum]);
} else if (chr->myaction == MA_AIBOTDEFEND
&& aibot->unk1e4 >= g_Vars.lvframe60 - TICKS(60)
&& aibot->config->difficulty != BOTDIFF_MEAT) {
targetangle = aibot->unk098;
} else {
targetangle = func0f03e578(chr);
}
while (targetangle >= M_BADTAU) {
targetangle -= M_BADTAU;
}
while (targetangle < 0) {
targetangle += M_BADTAU;
}
if (chr->blurdrugamount > 0 && !chrIsDead(chr) && aibot->skrocket == NULL) {
targetangle += chr->blurdrugamount * PALUPF(0.00031410926021636f) * sinf((g_Vars.lvframe60 % TICKS(120)) * PALUPF(0.052351541817188f));
if (targetangle >= M_BADTAU) {
targetangle -= M_BADTAU;
}
targetangle += M_BADTAU;
}
tweenangle = g_Vars.lvupdate60freal * 0.061590049415827f;
diffangle = targetangle - oldangle;
if (diffangle < -M_PI) {
diffangle += M_BADTAU;
} else if (diffangle >= M_PI) {
diffangle -= M_BADTAU;
}
if (diffangle >= 0) {
if (diffangle <= tweenangle) {
newangle = targetangle;
} else {
newangle = oldangle + tweenangle;
if (newangle >= M_BADTAU) {
newangle -= M_BADTAU;
}
}
} else {
if (diffangle >= -tweenangle) {
newangle = targetangle;
} else {
newangle = oldangle - tweenangle;
if (newangle < 0) {
newangle += M_BADTAU;
}
}
}
aibot->speedtheta = newangle - oldangle;
if (aibot->speedtheta < 0) {
aibot->speedtheta += M_BADTAU;
}
if (aibot->speedtheta >= M_PI) {
aibot->speedtheta -= M_BADTAU;
}
aibot->speedtheta /= g_Vars.lvupdate60freal;
aibot->speedtheta *= 16.236389160156f;
while (newangle >= M_BADTAU) {
newangle -= M_BADTAU;
}
while (newangle < 0) {
newangle += M_BADTAU;
}
chrSetLookAngle(chr, newangle);
if (chr->target != -1 && !aibot->iscloserangeweapon) {
bool left = chr->weapons_held[HAND_LEFT] ? true : false;
bool right = (0, chr->weapons_held[HAND_RIGHT] ? true : false);
func0f03e9f4(chr, aibot->unk068, left, right, 0);
} else {
chrResetAimEndProperties(chr);
}
if (chr->actiontype == ACT_DIE || chr->actiontype == ACT_DEAD) {
aibot->unk06c = 0;
aibot->unk070 = 0;
} else if (aibot->skrocket) {
aibot->unk06c = 0;
aibot->unk070 = 0;
aibot->unk1e4 = g_Vars.lvframe60;
} else if (chr->actiontype == ACT_GOPOS && (chr->act_gopos.flags & GOPOSFLAG_WAITING) == 0) {
aibot->unk06c = 1;
aibot->unk070 = 0;
} else {
aibot->unk06c = 0;
aibot->unk070 = 0;
aibot->unk1e4 = g_Vars.lvframe60;
}
}
botApplyMovement(chr);
result = chrTick(prop);
if (g_Vars.lvframe60 >= 145) {
if (updateable) {
scenarioTickChr(chr);
}
if (updateable && !chrIsDead(chr)) {
botCheckPickups(chr);
}
}
}
return result;
}
f32 botCalculateMaxSpeed(struct chrdata *chr)
{
f32 speed;
if (chr->aibot->hascase || chr->aibot->hasbriefcase) {
speed = -63.600006103516f;
} else {
speed = g_HeadsAndBodies[chr->bodynum].height * (1.0f / 159.0f);
}
speed = speed * 0.002830188954249f + 1.0f;
if (chr->aibot->config->type == BOTTYPE_TURTLE) {
speed *= 3.5f;
} else if (chr->aibot->config->type == BOTTYPE_SPEED) {
speed *= 14.0f;
} else {
switch (chr->aibot->config->difficulty) {
case BOTDIFF_MEAT:
speed *= 5.0f;
break;
case BOTDIFF_EASY:
speed *= 6.2f;
break;
default:
case BOTDIFF_NORMAL:
speed *= 7.6f;
break;
case BOTDIFF_HARD:
speed *= 9.4f;
break;
case BOTDIFF_PERFECT:
speed *= 11.2f;
break;
case BOTDIFF_DARK:
speed *= 11.2f;
break;
}
}
if (botGuessCrouchPos(chr) == CROUCHPOS_SQUAT) {
speed *= 0.35f;
} else if (botGuessCrouchPos(chr) == CROUCHPOS_DUCK) {
speed *= 0.5f;
} else if (chr->actiontype == ACT_GOPOS
&& chr->act_gopos.waypoints[chr->act_gopos.curindex] == NULL
&& chrGetSquaredLateralDistanceToCoord(chr, &chr->act_gopos.endpos) < 40000) {
speed *= 0.5f;
}
return speed;
}
void bot0f1921f8(struct chrdata *chr, f32 *move, s32 numupdates, f32 arg3)
{
s32 i;
f32 sp50;
f32 cosine;
f32 sine;
u32 stack[4];
f32 sp30[2];
f32 fVar7;
f32 fVar8;
f32 speed;
f32 tmp;
if (!chr || !chr->aibot) {
return;
}
if (g_Vars.lvframe60 < 145) {
move[0] = 0;
move[1] = 0;
return;
}
fVar7 = chr->aibot->unk070;
fVar8 = chr->aibot->unk06c;
speed = botCalculateMaxSpeed(chr);
fVar7 *= speed;
fVar8 *= speed;
sp50 = func0f03e578(chr);
cosine = cosf(sp50);
sine = sinf(sp50);
sp30[0] = fVar7 * cosine + fVar8 * sine;
sp30[1] = -fVar7 * sine + fVar8 * cosine;
move[0] = 0;
move[1] = 0;
tmp = (PAL ? 0.065f : 0.055000007152557f) * arg3 / numupdates;
for (i = 0; i < numupdates; i++) {
chr->aibot->unk0b4 = (PAL ? 0.935f : 0.945f) * chr->aibot->unk0b4 + sp30[0];
chr->aibot->unk0b8 = (PAL ? 0.935f : 0.945f) * chr->aibot->unk0b8 + sp30[1];
move[0] += chr->aibot->unk0b4 * tmp;
move[1] += chr->aibot->unk0b8 * tmp;
}
}
u32 g_MpBotCommands[NUM_MPBOTCOMMANDS] = {
L_MISC_175, // "Follow"
L_MISC_176, // "Attack"
L_MISC_177, // "Defend"
L_MISC_178, // "Hold"
L_MISC_179, // "Normal"
L_MISC_180, // "Download"
L_MISC_181, // "Get Case"
L_MISC_182, // "Tag Box"
L_MISC_209, // "Save Case"
L_MISC_210, // "Def Hill"
L_MISC_211, // "Hold Hill"
L_MISC_212, // "Get Case"
L_MISC_213, // "Pop Cap"
L_MISC_214, // "Protect"
};
char *botGetCommandName(s32 command)
{
if (command < 0 || command >= NUM_MPBOTCOMMANDS) {
return langGet(L_MISC_179); // "Normal"
}
return langGet(g_MpBotCommands[command]);
}
void botApplyAttack(struct chrdata *chr, struct prop *prop)
{
chr->aibot->command = AIBOTCMD_ATTACK;
chr->aibot->attackpropnum = prop - g_Vars.props;
chr->aibot->forcemainloop = true;
}
void botApplyFollow(struct chrdata *chr, struct prop *prop)
{
chr->aibot->command = AIBOTCMD_FOLLOW;
chr->aibot->followprotectpropnum = prop - g_Vars.props;
chr->aibot->forcemainloop = true;
}
void botApplyProtect(struct chrdata *chr, struct prop *prop)
{
chr->aibot->command = AIBOTCMD_PROTECT;
chr->aibot->followprotectpropnum = prop - g_Vars.props;
chr->aibot->forcemainloop = true;
}
void botApplyDefend(struct chrdata *chr, struct coord *pos, s16 *room, f32 arg3)
{
chr->aibot->command = AIBOTCMD_DEFEND;
chr->aibot->defendholdpos = *pos;
roomsCopy(room, chr->aibot->defendholdrooms);
chr->aibot->unk098 = arg3;
chr->aibot->forcemainloop = true;
}
void botApplyHold(struct chrdata *chr, struct coord *pos, s16 *room, f32 arg3)
{
chr->aibot->command = AIBOTCMD_HOLD;
chr->aibot->defendholdpos = *pos;
roomsCopy(room, chr->aibot->defendholdrooms);
chr->aibot->unk098 = arg3;
chr->aibot->forcemainloop = true;
}
void botApplyScenarioCommand(struct chrdata *chr, u32 command)
{
chr->aibot->command = command;
chr->aibot->forcemainloop = true;
}
void botDisarm(struct chrdata *chr, struct prop *attackerprop)
{
if (chr->aibot->weaponnum >= WEAPON_FALCON2 && chr->aibot->weaponnum != WEAPON_BRIEFCASE2) {
struct prop *prop = NULL;
struct defaultobj *obj;
if (chr->weapons_held[HAND_LEFT]) {
obj = chr->weapons_held[HAND_LEFT]->obj;
obj->hidden |= OBJHFLAG_REAPABLE;
chr->weapons_held[HAND_LEFT] = NULL;
}
if (chr->weapons_held[HAND_RIGHT]) {
prop = chr->weapons_held[HAND_RIGHT];
weaponSetGunfireVisible(prop, false, -1);
chr->weapons_held[HAND_RIGHT] = NULL;
} else {
s32 modelnum = playermgrGetModelOfWeapon(chr->aibot->weaponnum);
if (modelnum >= 0) {
prop = weaponCreateForChr(chr, modelnum, chr->aibot->weaponnum, OBJFLAG_WEAPON_AICANNOTUSE, NULL, NULL);
}
}
if (prop && prop->obj) {
obj = prop->obj;
objSetDropped(prop, DROPTYPE_DEFAULT);
chr->hidden |= CHRHFLAG_00000001;
if (obj->hidden & OBJHFLAG_PROJECTILE) {
obj->projectile->pickuptimer240 = TICKS(240);
obj->projectile->pickupby = attackerprop;
}
}
botinvRemoveItem(chr, chr->aibot->weaponnum);
chr->aibot->loadedammo[0] = 0;
chr->aibot->loadedammo[1] = 0;
botinvSwitchToWeapon(chr, WEAPON_UNARMED, FUNC_PRIMARY);
}
}
/**
* Set the bot's target and update tracking figures.
*
* This should be called on each tick even if the target hasn't changed
* because the tracking figures need to be constantly updated.
*/
void botSetTarget(struct chrdata *botchr, s32 propnum)
{
struct chrdata *otherchr = NULL;
s32 index;
if (propnum >= 0) {
otherchr = (g_Vars.props + propnum)->chr;
index = mpPlayerGetIndex(otherchr);
botchr->aibot->targetinsight = botchr->aibot->chrsinsight[index];
botchr->aibot->targetlastseen60 = botchr->aibot->chrslastseen60[index];
} else {
botchr->aibot->targetinsight = false;
botchr->aibot->targetlastseen60 = -1;
}
if (botchr->aibot->targetlastseen60 > botchr->aibot->lastseenanytarget60) {
botchr->aibot->lastseenanytarget60 = botchr->aibot->targetlastseen60;
}
if (botchr->target != propnum) {
botchr->target = propnum;
botchr->aibot->shootdelaytimer60 = 0;
botchr->aibot->waypoints[0] = NULL;
botchr->aibot->unk208 = 0;
if (botchr->aibot->targetinsight && otherchr) {
botchr->aibot->targetcloaktimer60 = TICKS(120);
} else {
botchr->aibot->targetcloaktimer60 = 0;
}
} else {
if (botchr->aibot->targetinsight) {
if (g_Vars.lvupdate240 > 0) {
botchr->aibot->shootdelaytimer60 += g_Vars.diffframe60;
}
} else {
if (g_Vars.lvupdate240 > 0) {
botchr->aibot->shootdelaytimer60 -= g_Vars.diffframe60;
}
if (botchr->aibot->shootdelaytimer60 < 0) {
botchr->aibot->shootdelaytimer60 = 0;
}
}
}
if (botchr->aibot->targetinsight && otherchr) {
if ((otherchr->hidden & CHRHFLAG_CLOAKED) == 0) {
botchr->aibot->targetcloaktimer60 = TICKS(120);
} else {
if (botchr->aibot->targetcloaktimer60 > 0) {
botchr->aibot->targetcloaktimer60 -= g_Vars.lvupdate60;
}
}
} else {
botchr->aibot->targetcloaktimer60 = 0;
}
}
bool botIsTargetInvisible(struct chrdata *botchr, struct chrdata *otherchr)
{
if (otherchr->prop->type == PROPTYPE_PLAYER && !g_Vars.bondvisible) {
return true;
}
if (otherchr->chrflags & CHRCFLAG_HIDDEN) {
return true;
}
if ((otherchr->hidden & CHRHFLAG_CLOAKED)) {
if (botchr && botchr->aibot
&& ((botchr->target != -1 && chrGetTargetProp(botchr) == otherchr->prop && botchr->aibot->targetcloaktimer60 > 0)
|| (botchr->aibot->canseecloaked && chrIsLookingAtPos(botchr, &otherchr->prop->pos, 32)))) {
return false;
}
return true;
}
return false;
}
/**
* Return true if there's ground between the chr and the death barrier.
*
* The death barrier is at -30000.
* It's assumed that no walkable ground exists below -20000.
*/
bool botHasGround(struct chrdata *chr)
{
return chr->ground >= -20000;
}
void bot0f192a74(struct chrdata *chr)
{
struct aibot *aibot = chr->aibot;
s32 diff = aibot->config->difficulty;
s32 i;
f32 fVar12;
f32 fVar11;
f32 tmp;
aibot->unk1cc -= g_Vars.lvupdate60;
if (aibot->unk1cc <= 0) {
aibot->unk1d0 = random();
aibot->unk1cc = TICKS(20) + random() % TICKS(20);
}
if (g_Vars.lvupdate240 > 0) {
if (aibot->targetinsight) {
aibot->unk1d4 += g_Vars.diffframe60;
} else {
aibot->unk1d4 -= g_Vars.diffframe60;
}
tmp = g_BotDifficulties[diff].unk10 * (aibot->speedtheta * g_Vars.lvupdate60f);
if (tmp < 0) {
tmp = -tmp;
}
aibot->unk1d4 -= tmp;
}
if (aibot->unk1d4 > aibot->shootdelaytimer60) {
aibot->unk1d4 = aibot->shootdelaytimer60;
}
if (aibot->unk1d4 < 0) {
aibot->unk1d4 = 0;
}
if (aibot->unk1d4 >= g_BotDifficulties[diff].unk0c) {
aibot->unk1d4 = g_BotDifficulties[diff].unk0c;
fVar12 = 0;
fVar11 = 0;
} else {
tmp = (g_BotDifficulties[diff].unk0c - aibot->unk1d4) / g_BotDifficulties[diff].unk0c;
fVar12 = g_BotDifficulties[diff].unk04 * tmp;
fVar11 = g_BotDifficulties[diff].unk08 * tmp;
}
if (chr->target != -1) {
struct prop *target = chrGetTargetProp(chr);
if (target->chr->hidden & CHRHFLAG_CLOAKED) {
if (fVar11 < g_BotDifficulties[diff].unk14) {
fVar11 = g_BotDifficulties[diff].unk14;
}
}
}
if (fVar11 < g_BotDifficulties[diff].unk18) {
fVar11 = g_BotDifficulties[diff].unk18;
}
aibot->unk1c8 = (fVar11 - fVar12) * (aibot->unk1d0 & 0xffff) * 0.000015259021893144f + fVar12;
if (aibot->unk1d0 & 0x10000) {
aibot->unk1c8 = -aibot->unk1c8;
}
for (i = 0; i < g_Vars.lvupdate240; i++) {
aibot->unk1c4 = aibot->unk1c4 * (PAL ? 0.97f : 0.97500002384186f) + aibot->unk1c8;
}
aibot->unk1c0 = aibot->unk1c4 * (PAL ? 0.029999971389771f : 0.024999976158142f);
}
/**
* Return true if the bot is a peacesim and is happy to fight the given chr,
* or if the bot is not a peacesim.
*/
bool botPassesPeaceCheck(struct chrdata *botchr, struct chrdata *otherchr)
{
struct aibot *aibot = botchr->aibot;
bool pass = true;
if (aibot->config->type == BOTTYPE_PEACE) {
s32 otherweaponnum = botGetWeaponNum(otherchr);
if (otherweaponnum == WEAPON_NONE || otherweaponnum == WEAPON_UNARMED) {
pass = false;
}
}
return pass;
}
/**
* Return true if the bot is a cowardsim and is happy to fight the given chr,
* or if the bot is not a cowardsim.
*/
bool botPassesCowardCheck(struct chrdata *botchr, struct chrdata *otherchr)
{
struct aibot *aibot = botchr->aibot;
bool pass = true;
s32 otherweaponnum;
s32 myscore1;
s32 myscore2;
s32 theirscore1;
s32 theirscore2;
if (aibot->config->type == BOTTYPE_COWARD) {
otherweaponnum = botGetWeaponNum(otherchr);
botinvScoreWeapon(botchr, aibot->weaponnum, FUNC_PRIMARY, 1, false, &myscore1, &myscore2, false, false);
botinvScoreWeapon(botchr, otherweaponnum, FUNC_PRIMARY, 1, false, &theirscore1, &theirscore2, false, false);
if (theirscore1 >= myscore1 - 30) {
pass = false;
}
}
return pass;
}
/**
* Choose and assign a general target to chase and attack.
*
* The function considers the distances and visibility of other chrs.
*
* The function does not compare weapons with the target, nor ammo counts,
* and does not factor in the bot types (eg. VengeSim).
*/
void botChooseGeneralTarget(struct chrdata *botchr)
{
struct aibot *aibot = botchr->aibot;
s32 i;
s32 j;
bool distancesdone[MAX_MPCHRS];
s16 room = -1;
struct chrdata *trychr;
s32 playernum;
// Advance the bot's internal pointer to the next chr
// and update stats about that chr
aibot->queryplayernum = (aibot->queryplayernum + 1) % g_MpNumChrs;
trychr = mpGetChrFromPlayerIndex(aibot->queryplayernum);
if (trychr != botchr) {
// This condition passes on average once every 4 minutes per player.
// However, the usage of canseecloaked appears to be botched.
// It is implemented in botIsTargetInvisible, but that function is not
// called here while canseecloaked is true.
if (random() % TICKS(4 * 60 * 60) < g_MpNumChrs * g_Vars.lvupdate60) {
aibot->canseecloaked = true;
}
aibot->chrdistances[aibot->queryplayernum] = chrGetDistanceToCoord(botchr, &trychr->prop->pos);
aibot->chrsinsight[aibot->queryplayernum] = chrCanSeeChr(botchr, trychr, &room);
aibot->chrrooms[aibot->queryplayernum] = room;
aibot->canseecloaked = false;
}
// Update last seen timestamps for all visible chrs
for (i = 0; i < g_MpNumChrs; i++) {
if (aibot->chrsinsight[i]) {
aibot->chrslastseen60[i] = g_Vars.lvframe60;
}
}
// Update chrnumsbydistanceasc
for (i = 0; i < g_MpNumChrs; i++) {
distancesdone[i] = false;
}
for (i = 0; i < g_MpNumChrs; i++) {
s32 closestplayernum = -1;
f32 closestdistance = 0;
for (j = 0; j < g_MpNumChrs; j++) {
if (!distancesdone[j] && (closestplayernum < 0 || aibot->chrdistances[j] < closestdistance)) {
closestplayernum = j;
closestdistance = aibot->chrdistances[j];
}
}
if (closestplayernum >= 0) {
aibot->chrnumsbydistanceasc[i] = closestplayernum;
distancesdone[closestplayernum] = true;
}
}
bot0f192a74(botchr);
// If the bot is data uplinking, clear the target
if (botchr->myaction == MA_AIBOTDOWNLOAD) {
botSetTarget(botchr, -1);
return;
}
// If the bot is attacking, keep the same target if possible
if (botchr->myaction == MA_AIBOTATTACK
&& aibot->attackingplayernum >= 0
&& aibot->chrsinsight[aibot->attackingplayernum]
&& !chrIsDead(g_MpAllChrPtrs[aibot->attackingplayernum])) {
botSetTarget(botchr, g_MpAllChrPtrs[aibot->attackingplayernum]->prop - g_Vars.props);
return;
}
// Check if existing target needs to be invalidated
if (botchr->target != -1) {
struct prop *targetprop = chrGetTargetProp(botchr);
if (chrIsDead(targetprop->chr)) {
botchr->target = -1;
}
if (!botchr->aibot->targetinsight && botIsTargetInvisible(botchr, targetprop->chr)) {
botchr->target = -1;
}
if (chrCompareTeams(botchr, targetprop->chr, COMPARE_FRIENDS)) {
botchr->target = -1;
}
if (!botPassesPeaceCheck(botchr, targetprop->chr)) {
botchr->target = -1;
}
if (!botchr->aibot->targetinsight && !botPassesCowardCheck(botchr, targetprop->chr)) {
botchr->target = -1;
}
}
// If there's no existing target, try all chrs in distance order
if (botchr->target == -1) {
s32 closestavailablechrnum = -1;
s32 tmp;
s32 stack;
for (tmp = 0; tmp < g_MpNumChrs; tmp++) {
s32 i = aibot->chrnumsbydistanceasc[tmp];
trychr = mpGetChrFromPlayerIndex(i);
if (trychr != botchr
&& !chrIsDead(trychr)
&& chrCompareTeams(botchr, trychr, COMPARE_ENEMIES)
&& botPassesPeaceCheck(botchr, trychr)) {
// If the chr is in sight, that's it
if (aibot->chrsinsight[i]) {
botSetTarget(botchr, trychr->prop - g_Vars.props);
return;
}
// Meat and easy sims will target the closest chr, even if that
// chr isn't in sight and when there are other chrs in sight who
// are further away
if (!botIsTargetInvisible(botchr, trychr)
&& (aibot->config->difficulty == BOTDIFF_MEAT || aibot->config->difficulty == BOTDIFF_EASY)) {
botSetTarget(botchr, trychr->prop - g_Vars.props);
return;
}
// Other sim types will prioritise chrs in sight, which means
// the closest out of sight chrnum must be stored for later
if (!botIsTargetInvisible(botchr, trychr) && closestavailablechrnum < 0) {
closestavailablechrnum = i;
}
}
}
// Use closest out of sight chr
if (closestavailablechrnum >= 0) {
trychr = mpGetChrFromPlayerIndex(closestavailablechrnum);
botSetTarget(botchr, trychr->prop - g_Vars.props);
return;
}
// No one available - maybe everyone else is dead or cloaked
botSetTarget(botchr, -1);
return;
}
// Bot has an existing target
// If they're still in sight, keep the target
playernum = mpPlayerGetIndex((g_Vars.props + botchr->target)->chr);
if (aibot->chrsinsight[playernum]) {
botSetTarget(botchr, botchr->target);
return;
}
// Target is no longer in sight
// Check for other chrs who are in sight, by distance
for (i = 0; i < g_MpNumChrs; i++) {
if (aibot->chrsinsight[aibot->chrnumsbydistanceasc[i]]) {
trychr = mpGetChrFromPlayerIndex(aibot->chrnumsbydistanceasc[i]);
if (trychr != botchr
&& !chrIsDead(trychr)
&& chrCompareTeams(botchr, trychr, COMPARE_ENEMIES)
&& botPassesPeaceCheck(botchr, trychr)) {
botSetTarget(botchr, trychr->prop - g_Vars.props);
return;
}
}
}
// No one else in sight - maintain original target
botSetTarget(botchr, botchr->target);
}
/**
* Check if the bot is capable of following the given chr.
*
* They are not capable if it would create a circular follow loop.
*/
bool botCanFollow(struct chrdata *botchr, struct chrdata *leader)
{
bool canfollow = true;
while (true) {
struct aibot *aibot = leader->aibot;
if (!aibot || leader->myaction != MA_AIBOTFOLLOW || aibot->followingplayernum < 0) {
// Okay to follow
break;
}
leader = g_MpAllChrPtrs[aibot->followingplayernum];
if (leader == botchr) {
// Can't follow - it would create a follow loop
canfollow = false;
break;
}
}
return canfollow;
}
s32 botFindTeammateToFollow(struct chrdata *chr, f32 range)
{
s32 result = -1;
if ((g_MpSetup.options & MPOPTION_TEAMSENABLED)
&& chr->myaction != MA_AIBOTFOLLOW
&& (random() % 100) < chr->aibot->followchance) {
f32 closestdistance = 0;
s32 closestplayernum = -1;
s32 i;
for (i = 0; i < g_MpNumChrs; i++) {
if (chr != g_MpAllChrPtrs[i]
&& !chrIsDead(g_MpAllChrPtrs[i])
&& chr->team == g_MpAllChrPtrs[i]->team
&& botCanFollow(chr, g_MpAllChrPtrs[i])) {
f32 distance = chr->aibot->chrdistances[i];
if (closestplayernum < 0 || distance < closestdistance) {
closestplayernum = i;
closestdistance = distance;
}
}
}
if (closestplayernum >= 0 && closestdistance < range) {
result = closestplayernum;
}
}
return result;
}
void botScheduleReload(struct chrdata *chr, s32 handnum)
{
chr->aibot->timeuntilreload60[handnum] = g_AibotWeaponPreferences[chr->aibot->weaponnum].reloaddelay * (PAL ? 50 : 60);
if (g_AibotWeaponPreferences[chr->aibot->weaponnum].allowpartialreloaddelay) {
s32 capacity = botactGetClipCapacityByFunction(chr->aibot->weaponnum, chr->aibot->gunfunc);
chr->aibot->timeuntilreload60[handnum] *= capacity - chr->aibot->loadedammo[handnum];
chr->aibot->timeuntilreload60[handnum] /= capacity;
}
}
#define HASENOUGHPRI(aibot, weaponnum, goal) (g_AibotWeaponPreferences[weaponnum].haspriammogoal && botactGetAmmoQuantityByWeapon(aibot, weaponnum, FUNC_PRIMARY, true) >= (goal))
#define HASENOUGHSEC(aibot, weaponnum, goal) (g_AibotWeaponPreferences[weaponnum].hassecammogoal && botactGetAmmoQuantityByWeapon(aibot, weaponnum, FUNC_SECONDARY, true) >= (goal))
/**
* Find a prop for the bot to pick up.
*
* Criteria can be:
*
* PICKUPCRITERIA_DEFAULT:
* This is the most common criteria. It is used when the bot has spawned and
* is gathering weapons, as well as when they return to their main loop.
*
* PICKUPCRITERIA_CRITICAL:
* Find props only if the bot needs them critically, eg if they are low on
* shield or ammo. This is used when the bot has other things it can be
* doing such as scenario objectives and has to decide between the two.
*
* PICKUPCRITERIA_ANY:
* Find pretty much any prop. This is used when the bot has nothing else to
* do (eg. if all opponents are cloaked) and may as well stock up on ammo.
*/
struct prop *botFindPickup(struct chrdata *chr, s32 criteria)
{
struct aibot *aibot = chr->aibot;
s32 weaponnums[6];
s32 scores1[6];
s32 scores2[6];
struct prop *weapproplist[6];
f32 weapdistlist[6];
struct prop *ammoproplist[33];
f32 ammodistlist[33];
struct invitem *invitems[6];
s32 i;
s32 j;
struct prop *prop;
struct weaponobj *weapon;
struct prop *chosenprop = NULL;
bool barelydominatinghill = false;
s32 numteam;
s32 numopponents;
struct multiammocrateobj *crate;
s32 weaponnum;
f32 sqdist1;
f32 sqdist2;
struct defaultobj *obj;
s32 ammotype;
s32 bestscore1;
bool done;
if (&aibot);
if (&criteria);
// If the hill has one or two bots from the same team in it, the bots will
// be less likely to leave the hill for pickups (barelydominatinghill = true).
// If there are three or more then this limitation is removed.
// The amount increases if there are opponents in the hill too.
if (aibot->teamisonlyai
&& g_MpSetup.scenario == MPSCENARIO_KINGOFTHEHILL
&& chr->prop->rooms[0] == g_ScenarioData.koh.hillrooms[0]) {
numteam = botGetNumTeammatesDefendingHill(chr);
numopponents = botGetNumOpponentsInHill(chr);
if (numteam >= numopponents && numteam <= numopponents + 2) {
barelydominatinghill = true;
}
}
// botinvScoreAllWeapons populates weaponnums, scores1 and scores2
// and sorts them by score1 descending
botinvScoreAllWeapons(chr, weaponnums, scores1, scores2);
for (i = 0; i < ARRAYCOUNT(weapproplist); i++) {
weapproplist[i] = NULL;
}
for (i = 0; i < ARRAYCOUNT(ammoproplist); i++) {
ammoproplist[i] = NULL;
}
for (i = 0; i < ARRAYCOUNT(weaponnums); i++) {
invitems[i] = botinvGetItem(chr, weaponnums[i]);
}
// Iterate all active props and populate the proplist and distlist arrays.
// Generally these arrays are populated with the closest prop of each weapon
// and ammotype, however there's a 1/16 chance that any prop will be skipped
// and a 1/16 chance that a further prop will overwrite the current closest.
prop = g_Vars.activeprops;
while (prop) {
if (prop->parent == NULL && prop->timetoregen == 0) {
if (prop->type == PROPTYPE_WEAPON) {
weapon = prop->weapon;
if ((weapon->base.flags3 & OBJFLAG3_ISFETCHTARGET) == 0) {
sqdist1 = chrGetSquaredDistanceToCoord(chr, &prop->pos);
for (i = 0; i < ARRAYCOUNT(weaponnums); i++) {
if (weaponnums[i] > WEAPON_UNARMED && weaponnums[i] == weapon->weaponnum) {
if (random() % 16) {
if (weapproplist[i] == NULL || sqdist1 < weapdistlist[i] || random() % 16 == 0) {
weapproplist[i] = prop;
weapdistlist[i] = sqdist1;
}
}
break;
}
}
ammotype = botactGetAmmoTypeByFunction(weapon->weaponnum, FUNC_PRIMARY);
if (ammotype > 0 && random() % 16) {
if (ammoproplist[ammotype] == NULL || sqdist1 < ammodistlist[ammotype] || random() % 16 == 0) {
ammoproplist[ammotype] = prop;
ammodistlist[ammotype] = sqdist1;
}
}
}
} else if (prop->type == PROPTYPE_OBJ) {
obj = prop->obj;
if ((obj->flags3 & OBJFLAG3_ISFETCHTARGET) == 0) {
if (obj->type == OBJTYPE_MULTIAMMOCRATE) {
crate = (struct multiammocrateobj *)prop->obj;
sqdist2 = chrGetSquaredDistanceToCoord(chr, &prop->pos);
for (i = 0; i < 19; i++) {
s32 ammotype = i + 1;
if (crate->slots[i].quantity > 0) {
weaponnum = botactGetWeaponByAmmoType(ammotype);
if (weaponnum > 0) {
for (j = 0; j < ARRAYCOUNT(weaponnums); j++) {
if (weaponnums[j] > WEAPON_UNARMED && weaponnum == weaponnums[j]) {
if (random() % 16) {
if (weapproplist[j] == NULL || sqdist2 < weapdistlist[j] || random() % 16 == 0) {
weapproplist[j] = prop;
weapdistlist[j] = sqdist2;
}
}
break;
}
}
}
if (random() % 16) {
if (ammoproplist[ammotype] == NULL || sqdist2 < ammodistlist[ammotype] || random() % 16 == 0) {
ammoproplist[ammotype] = prop;
ammodistlist[ammotype] = sqdist2;
}
}
}
}
} else if (obj->type == OBJTYPE_SHIELD) {
for (i = 0; i < ARRAYCOUNT(weaponnums); i++) {
if (weaponnums[i] == WEAPON_MPSHIELD) {
sqdist2 = chrGetSquaredDistanceToCoord(chr, &prop->pos);
if (random() % 16 == 0) {
break;
}
if (weapproplist[i] == NULL || sqdist2 < weapdistlist[i] || random() % 16 == 0) {
weapproplist[i] = prop;
weapdistlist[i] = sqdist2;
}
break;
}
}
}
}
}
}
prop = prop->next;
}
// Find the best score out of the 6 weapons, only considering ones which
// the bot is allowed to carry and which require ammo
bestscore1 = 0;
done = false;
for (i = 0; i < ARRAYCOUNT(weaponnums); i++) {
if (1);
if ((botinvAllowsWeapon(chr, weaponnums[i], FUNC_PRIMARY) || botinvAllowsWeapon(chr, weaponnums[i], FUNC_SECONDARY))
&& (g_AibotWeaponPreferences[weaponnums[i]].haspriammogoal || g_AibotWeaponPreferences[weaponnums[i]].hassecammogoal)
&& scores1[i] > bestscore1) {
bestscore1 = scores1[i];
}
}
// Decide if the bot wants to find a shield, based on the amount of health
// and shield the bot currently has. This shield logic is done prior to
// weapons and ammo, so a shield takes precedence.
// Note that max health and shield is 8 each, and that the bot must be under
// BOTH the limits for a shield to be fetched.
for (i = 0; i < ARRAYCOUNT(weaponnums) && !done; i++) {
if (weaponnums[i] == WEAPON_MPSHIELD
&& (g_MpSetup.scenario != MPSCENARIO_HOLDTHEBRIEFCASE || !chr->aibot->hasbriefcase)) {
f32 triggerathealth = 8.1f;
f32 desiredshield = 0;
s32 rand;
if (aibot->config->type == BOTTYPE_SHIELD) {
// ShieldSims are more likely to fetch shields
if (criteria == PICKUPCRITERIA_ANY) {
desiredshield = 7.9f;
} else if (criteria == PICKUPCRITERIA_DEFAULT) {
desiredshield = 6 - (aibot->randomfrac + aibot->randomfrac);
} else if (criteria == PICKUPCRITERIA_CRITICAL) {
desiredshield = 4 - (aibot->randomfrac + aibot->randomfrac);
}
} else if (barelydominatinghill) {
// Bots will be less likely to fetch shields while defending the hill
triggerathealth = 4 - (aibot->randomfrac + aibot->randomfrac);
desiredshield = 1 - aibot->randomfrac;
} else if (g_MpSetup.scenario == MPSCENARIO_CAPTURETHECASE && botShouldReturnCtcToken(chr)) {
// Bots will be less likely to fetch shields while returning a CTC case
triggerathealth = 3 - (aibot->randomfrac + aibot->randomfrac);
} else if (chr->myaction == MA_AIBOTDOWNLOAD) {
// Bots will be less likely to fetch shields while uplinking
triggerathealth = 4 - (aibot->randomfrac + aibot->randomfrac);
desiredshield = 1;
} else {
// Default behaviour
if (criteria == PICKUPCRITERIA_ANY) {
desiredshield = 7.9f;
} else if (criteria == PICKUPCRITERIA_DEFAULT) {
desiredshield = 4 - (aibot->randomfrac + aibot->randomfrac);
} else if (criteria == PICKUPCRITERIA_CRITICAL) {
desiredshield = 2 - aibot->randomfrac;
}
}
// Meat, easy and normal sims reduce the limits further,
// making them less likely to fetch shields.
if (aibot->config->difficulty == BOTDIFF_MEAT) {
rand = aibot->random2 % 8;
if (rand < 2) {
desiredshield = 0;
triggerathealth = 0;
} else if (rand < 4) {
desiredshield = 0;
triggerathealth = 2 - aibot->randomfrac;
} else {
desiredshield -= aibot->randomfrac * 16;
if (desiredshield <= 0) {
triggerathealth += desiredshield;
desiredshield = 0;
}
}
} else if (aibot->config->difficulty == BOTDIFF_EASY) {
rand = aibot->random2 % 8;
if (rand <= 0) {
desiredshield = 0;
triggerathealth = 0;
} else {
desiredshield -= aibot->randomfrac * 11;
if (desiredshield <= 0) {
triggerathealth += desiredshield;
desiredshield = 0;
}
}
} else if (aibot->config->difficulty == BOTDIFF_NORMAL) {
desiredshield -= aibot->randomfrac * 4;
if (desiredshield <= 0) {
triggerathealth += desiredshield;
desiredshield = 0;
}
}
// Actually check the limits and decide if the shield is desired
if (chr->maxdamage - chr->damage < triggerathealth
&& chr->cshield <= desiredshield
&& weapproplist[i] != NULL
&& scores2[i] >= bestscore1) {
chosenprop = weapproplist[i];
done = true;
break;
}
}
}
// Consider ammo for weapons that the bot already has.
// This loop is iterated in order of highest scoring weapons first. If the
// first iterated weapon which the bot holds has enough ammo then the lower
// scoring weapons will not be considered, nor will any new weapons be
// picked up.
for (i = 0; i < ARRAYCOUNT(weaponnums) && !done; i++) {
if (weaponnums[i] != WEAPON_MPSHIELD
&& invitems[i] != NULL
&& (g_AibotWeaponPreferences[weaponnums[i]].haspriammogoal
|| g_AibotWeaponPreferences[weaponnums[i]].hassecammogoal)
&& scores2[i] >= bestscore1) {
s32 desiredpriammo;
s32 desiredsecammo;
s32 funcnum;
bool include_equipped = true;
s32 stack;
// Don't go after ammo when returning a CTC token
if (g_MpSetup.scenario == MPSCENARIO_CAPTURETHECASE && botShouldReturnCtcToken(chr)) {
done = true;
break;
}
// Don't go after ammo when downloading in Hacker Central
if (chr->myaction == MA_AIBOTDOWNLOAD) {
done = true;
break;
}
if (barelydominatinghill) {
// If the bot's team is only barely controlling the hill,
// don't leave it unless the bot is out of ammo, and even then
// just get one ammo pickup
desiredpriammo = g_AibotWeaponPreferences[weaponnums[i]].criticalammopri;
if (desiredpriammo > 1) {
desiredpriammo = 1;
}
desiredsecammo = g_AibotWeaponPreferences[weaponnums[i]].criticalammosec;
if (desiredsecammo > 1) {
desiredsecammo = 1;
}
if (HASENOUGHPRI(aibot, weaponnums[i], desiredpriammo) || HASENOUGHSEC(aibot, weaponnums[i], desiredsecammo)) {
done = true;
break;
}
} else if (criteria == PICKUPCRITERIA_ANY) {
// If looking for any pickups at all, use the weapon's ammo
// capacities as the goal ammo
desiredpriammo = bgunGetCapacityByAmmotype(botactGetAmmoTypeByFunction(weaponnums[i], FUNC_PRIMARY));
desiredsecammo = bgunGetCapacityByAmmotype(botactGetAmmoTypeByFunction(weaponnums[i], FUNC_SECONDARY));
// If bot has max ammo for both weapon's functions
if ((g_AibotWeaponPreferences[weaponnums[i]].haspriammogoal == false
|| botactGetAmmoQuantityByWeapon(aibot, weaponnums[i], FUNC_PRIMARY, false) >= desiredpriammo)
&& (g_AibotWeaponPreferences[weaponnums[i]].hassecammogoal == false
|| botactGetAmmoQuantityByWeapon(aibot, weaponnums[i], FUNC_SECONDARY, false) >= desiredsecammo)) {
// Consider next weapon
continue;
}
include_equipped = false;
} else if (criteria == PICKUPCRITERIA_DEFAULT) {
// Default - use the target ammo amount
desiredpriammo = g_AibotWeaponPreferences[weaponnums[i]].targetammopri;
desiredsecammo = g_AibotWeaponPreferences[weaponnums[i]].targetammosec;
if (HASENOUGHPRI(aibot, weaponnums[i], desiredpriammo) || HASENOUGHSEC(aibot, weaponnums[i], desiredsecammo)) {
done = true;
break;
}
} else if (criteria == PICKUPCRITERIA_CRITICAL) {
// Critical - use the critical ammo amount
desiredpriammo = g_AibotWeaponPreferences[weaponnums[i]].criticalammopri;
desiredsecammo = g_AibotWeaponPreferences[weaponnums[i]].criticalammosec;
if (HASENOUGHPRI(aibot, weaponnums[i], desiredpriammo) || HASENOUGHSEC(aibot, weaponnums[i], desiredsecammo)) {
done = true;
break;
}
}
// Iterate both weapon functions and check
// if the bot has enough ammo for that function
for (funcnum = 0; funcnum < 2; funcnum++) {
if (botinvAllowsWeapon(chr, weaponnums[i], funcnum)) {
s32 ammotype = botactGetAmmoTypeByFunction(weaponnums[i], funcnum);
if (ammotype > 0) {
s32 goal = funcnum ? desiredsecammo : desiredpriammo;
s32 qty = botactGetAmmoQuantityByType(aibot, ammotype, include_equipped);
if (qty < goal && ammoproplist[ammotype]) {
chosenprop = ammoproplist[ammotype];
done = true;
break;
}
}
}
}
}
}
// If done is still false, the bot mustn't have any weapons.
// Consider picking up weapons that the bot doesn't have.
// Fetch the highest scoring weapon if there are any pickups for it.
for (i = 0; i < ARRAYCOUNT(weaponnums) && !done; i++) {
if (weaponnums[i] != WEAPON_MPSHIELD) {
if (g_MpSetup.scenario == MPSCENARIO_CAPTURETHECASE && botShouldReturnCtcToken(chr)) {
done = true;
break;
}
if (chr->myaction == MA_AIBOTDOWNLOAD) {
done = true;
break;
}
if (!barelydominatinghill
&& (botinvAllowsWeapon(chr, weaponnums[i], FUNC_PRIMARY) || botinvAllowsWeapon(chr, weaponnums[i], FUNC_SECONDARY))
&& invitems[i] == NULL
&& weapproplist[i] != NULL) {
chosenprop = weapproplist[i];
done = true;
break;
}
}
}
if (criteria == PICKUPCRITERIA_ANY) {
// Consider ammo even for weapons that the bot doesn't have
for (i = 0; i < ARRAYCOUNT(weaponnums) && !done; i++) {
if (weaponnums[i] != WEAPON_MPSHIELD) {
for (j = 0; j < 2; j++) {
if (botinvAllowsWeapon(chr, weaponnums[i], j)) {
s32 ammotype = botactGetAmmoTypeByFunction(weaponnums[i], j);
if (ammotype > 0
&& botactGetAmmoQuantityByType(aibot, ammotype, false) < bgunGetCapacityByAmmotype(ammotype)
&& ammoproplist[ammotype] != NULL) {
chosenprop = ammoproplist[ammotype];
done = true;
break;
}
}
}
}
}
}
return chosenprop;
}
/**
* Check if the bot wants to do a critical pickup.
*
* This returns true when the bot is low on health or ammo and there are pickups
* available.
*/
bool botCanDoCriticalPickup(struct chrdata *chr)
{
return botFindPickup(chr, PICKUPCRITERIA_CRITICAL) != NULL;
}
/**
* Find a pickup to fetch based on default criteria. Default criteria basically
* means a good amount of ammo - not lacking but not excessive either.
*/
struct prop *botFindDefaultPickup(struct chrdata *chr)
{
return botFindPickup(chr, PICKUPCRITERIA_DEFAULT);
}
/**
* Find any pickup to fetch. This is used when the bot has nothing else to do
* (eg. if all opponents are cloaked).
*/
struct prop *botFindAnyPickup(struct chrdata *chr)
{
return botFindPickup(chr, PICKUPCRITERIA_ANY);
}
s32 botGetTeamSize(struct chrdata *chr)
{
s32 count = 0;
s32 i;
for (i = 0; i < g_MpNumChrs; i++) {
if (chr->team == g_MpAllChrPtrs[i]->team) {
count++;
}
}
return count;
}
s32 botGetCountInTeamDoingCommand(struct chrdata *self, u32 command, bool includeself)
{
s32 count = 0;
s32 i;
for (i = PLAYERCOUNT(); i < g_MpNumChrs; i++) {
if (self->team == g_MpAllChrPtrs[i]->team) {
if (includeself || self != g_MpAllChrPtrs[i]) {
if (command == g_MpAllChrPtrs[i]->aibot->command) {
count++;
}
}
}
}
return count;
}
s32 botIsChrsCtcTokenHeld(struct chrdata *chr)
{
struct mpchrconfig *mpchr = g_MpAllChrConfigPtrs[mpPlayerGetIndex(chr)];
struct prop *prop = g_ScenarioData.ctc.tokens[mpchr->team];
return prop && (prop->type & (PROPTYPE_CHR | PROPTYPE_PLAYER));
}
/**
* If chr doesn't have the case, return false.
* If chr has the case:
* If chr is on a team by themself and their token is stolen, return false
* Otherwise, return true
*/
bool botShouldReturnCtcToken(struct chrdata *chr)
{
if (chr->aibot->hascase) {
if (!chr->aibot->teamisonlyai || botGetTeamSize(chr) >= 2 || !botIsChrsCtcTokenHeld(chr)) {
return true;
}
}
return false;
}
s32 botGetNumTeammatesDefendingHill(struct chrdata *bot)
{
s32 count = 0;
s32 i;
for (i = 0; i < g_MpNumChrs; i++) {
if (bot->team == g_MpAllChrPtrs[i]->team
&& g_MpAllChrPtrs[i]->prop->rooms[0] == g_ScenarioData.koh.hillrooms[0]) {
if (g_MpAllChrPtrs[i]->aibot->command == AIBOTCMD_DEFHILL
|| g_MpAllChrPtrs[i]->aibot->command == AIBOTCMD_HOLDHILL) {
count++;
}
}
}
return count;
}
/**
* Find the opposing team who has the most players in the hill and return the
* number of their players who are in the hill.
*
* This function is slightly misnamed.
*/
s32 botGetNumOpponentsInHill(struct chrdata *chr)
{
struct mpchrconfig *mpchr = g_MpAllChrConfigPtrs[mpPlayerGetIndex(chr)];
struct mpchrconfig *loopmpchr;
s32 countsperteam[8] = {0};
s32 max = 0;
s32 i;
for (i = 0; i < g_MpNumChrs; i++) {
if (g_MpAllChrPtrs[i]->prop->rooms[0] == g_ScenarioData.koh.hillrooms[0]) {
s32 mpindex = func0f18d074(i);
loopmpchr = MPCHR(mpindex);
if (loopmpchr->team != mpchr->team) {
countsperteam[loopmpchr->team]++;
}
}
}
for (i = 0; i < 8; i++) {
if (countsperteam[i] > max) {
max = countsperteam[i];
}
}
return max;
}
/**
* The tick function for a multiplayer simulant while the game is running.
*
* The function implements per-scenario decision logic as well as shooting and
* reloading.
*
* General function overview:
* - Some minor things at the start
* - If team is only AI, choose scenario commands as if player had assigned them
* - If the bot is in its main loop:
* - Choose a new myaction value based on the command.
* - Apply the new myaction if any
* - If the existing myaction is no longer valid, force a return to the main
* loop on the next tick
* - Decide whether to switch weapons (via botinvTick)
* - Decide whether to discharge a shot on this tick
*/
void botTickUnpaused(struct chrdata *chr)
{
s32 newaction = -1;
if (!chrIsDead(chr)) {
struct aibot *aibot = chr->aibot;
s32 i;
// Consider updating random values
aibot->random2ttl60 -= g_Vars.lvupdate60;
if (aibot->random2ttl60 < 0) {
aibot->random2ttl60 = TICKS(1800) + random() % TICKS(60 * 240);
aibot->random2 = random();
aibot->randomfrac = RANDOMFRAC();
}
// Consider reloading
for (i = 0; i != 2; i++) {
// Reload if timer has reached 0
if (aibot->timeuntilreload60[i] > 0) {
aibot->timeuntilreload60[i] -= g_Vars.lvupdate60;
if (aibot->timeuntilreload60[i] <= 0) {
botactReload(chr, i, true);
}
} else if (!botactIsWeaponThrowable(aibot->weaponnum, aibot->gunfunc)) {
// If the weapon is reloadable, schedule a reload if bot is out
// of ammo or has less than half a clip and last saw their
// target 2 seconds ago
s32 loadedammo = aibot->loadedammo[i];
s32 clipsize = botactGetClipCapacityByFunction(aibot->weaponnum, aibot->gunfunc);
if (loadedammo <= 0 && clipsize > 0) {
botScheduleReload(chr, i);
} else if (loadedammo < clipsize / 2 && aibot->lastseenanytarget60 < g_Vars.lvframe60 - TICKS(120)) {
botScheduleReload(chr, i);
}
}
}
// Handle switching weapons
if (aibot->changeguntimer60 > 0) {
aibot->changeguntimer60 -= g_Vars.lvupdate60;
if (aibot->changeguntimer60 <= 0) {
struct invitem *item = botinvGetItem(chr, aibot->weaponnum);
s32 modelnum = playermgrGetModelOfWeapon(aibot->weaponnum);
s32 i;
if (item && modelnum >= 0) {
chrGiveWeapon(chr, modelnum, aibot->weaponnum, 0);
botactReload(chr, HAND_RIGHT, false);
if (item->type == INVITEMTYPE_DUAL) {
chrGiveWeapon(chr, modelnum, aibot->weaponnum, OBJFLAG_WEAPON_LEFTHANDED);
botactReload(chr, HAND_LEFT, false);
}
} else {
// Bot doesn't have the weapon it was told to switch to
chr->aibot->weaponnum = WEAPON_UNARMED;
chr->aibot->gunfunc = FUNC_PRIMARY;
chr->aibot->iscloserangeweapon = 1;
}
aibot->throwtimer60 = 0;
for (i = 0; i < 2; i++) {
aibot->punchtimer60[i] = 0;
if (chr->weapons_held[i]) {
chr->weapons_held[i]->weapon->gunfunc = chr->aibot->gunfunc;
}
}
}
}
// The laser has unlimited ammo
if (aibot->weaponnum == WEAPON_LASER) {
chr->aibot->loadedammo[HAND_RIGHT] = 999;
}
// Consider starting or stopping cloak
if (aibot->ammoheld[AMMOTYPE_CLOAK] > 0
&& (botIsAboutToAttack(chr, true) || chr->myaction == MA_AIBOTDOWNLOAD)) {
aibot->cloakdeviceenabled = true;
} else {
if (aibot->ammoheld[AMMOTYPE_CLOAK] > 1200 + (aibot->random1 >> 5) % 1200) {
aibot->cloakdeviceenabled = true;
} else if (aibot->ammoheld[AMMOTYPE_CLOAK] <= (aibot->random1 >> 17) % 1200) {
aibot->cloakdeviceenabled = false;
}
}
// Consider starting or stopping RC-P120 cloak
if (!aibot->cloakdeviceenabled && aibot->weaponnum == WEAPON_RCP120) {
s32 qty = botactGetAmmoQuantityByWeapon(aibot, WEAPON_RCP120, FUNC_PRIMARY, true);
if (botIsAboutToAttack(chr, true)) {
if (qty > 200 + (aibot->random1 >> 6) % 200) {
aibot->rcp120cloakenabled = true;
} else if (qty <= 30 + (aibot->random1 >> 16) % 70) {
aibot->rcp120cloakenabled = false;
}
} else {
if (qty > 300 + (aibot->random1 >> 12) % 500) {
aibot->rcp120cloakenabled = true;
} else {
aibot->rcp120cloakenabled = false;
}
}
} else {
aibot->rcp120cloakenabled = false;
}
// KazeSims will attack on sight
if (aibot->config->type == BOTTYPE_KAZE
&& chr->target != -1
&& aibot->targetinsight
&& chr->myaction != MA_AIBOTATTACK) {
aibot->forcemainloop = true;
}
// If there's no humans on the bot's team to give it commands, figure
// out which commands to apply automatically based on the scenario.
if (aibot->teamisonlyai) {
if (aibot->commandtimer60 > 0) {
aibot->commandtimer60 -= g_Vars.lvupdate60;
}
if (aibot->commandtimer60 <= 0) {
s32 teamsize = botGetTeamSize(chr);
if (g_MpSetup.scenario == MPSCENARIO_HOLDTHEBRIEFCASE) {
s32 numgetting = botGetCountInTeamDoingCommand(chr, AIBOTCMD_GETCASE2, false);
if (numgetting <= 0 || (numgetting < (teamsize + 1) / 2 || random() % 100 < 66)) {
botApplyScenarioCommand(chr, AIBOTCMD_GETCASE2);
} else {
botApplyScenarioCommand(chr, AIBOTCMD_NORMAL);
}
} else if (g_MpSetup.scenario == MPSCENARIO_HACKERCENTRAL) {
s32 numbots = botGetCountInTeamDoingCommand(chr, AIBOTCMD_DOWNLOAD, false);
if (aibot->hasuplink || numbots <= 0 || (numbots < (teamsize + 1) / 2 || random() % 100 < 50)) {
botApplyScenarioCommand(chr, AIBOTCMD_DOWNLOAD);
} else {
botApplyScenarioCommand(chr, AIBOTCMD_NORMAL);
}
} else if (g_MpSetup.scenario == MPSCENARIO_POPACAP) {
s32 numchasing = botGetCountInTeamDoingCommand(chr, AIBOTCMD_POPCAP, false);
if (numchasing <= 0 || numchasing < (teamsize + 1) / 2 || random() % 100 < 50) {
botApplyScenarioCommand(chr, AIBOTCMD_POPCAP);
} else {
botApplyScenarioCommand(chr, AIBOTCMD_NORMAL);
}
} else if (g_MpSetup.scenario == MPSCENARIO_KINGOFTHEHILL) {
s32 numinhill = botGetNumTeammatesDefendingHill(chr);
// Don't count ourselves
if (chr->prop->rooms[0] == g_ScenarioData.koh.hillrooms[0]) {
numinhill--;
}
if (numinhill <= 0 || numinhill < teamsize / 2) {
botApplyScenarioCommand(chr, AIBOTCMD_HOLDHILL);
} else if (numinhill > botGetNumOpponentsInHill(chr)) {
if (random() % 100 < 50) {
botApplyScenarioCommand(chr, AIBOTCMD_DEFHILL);
} else {
botApplyScenarioCommand(chr, AIBOTCMD_NORMAL);
}
} else {
botApplyScenarioCommand(chr, AIBOTCMD_HOLDHILL);
}
} else if (g_MpSetup.scenario == MPSCENARIO_CAPTURETHECASE) {
if (teamsize == 1) {
// One man team
s32 numgetting = botGetCountInTeamDoingCommand(chr, AIBOTCMD_GETCASE, true);
if (botShouldReturnCtcToken(chr)) {
botApplyScenarioCommand(chr, AIBOTCMD_GETCASE);
} else if (botIsChrsCtcTokenHeld(chr)) {
if (random() % 100 < 30) {
botApplyScenarioCommand(chr, AIBOTCMD_GETCASE);
} else {
botApplyScenarioCommand(chr, AIBOTCMD_SAVECASE);
}
} else {
if (random() % 100 < 70 || numgetting <= 0) {
botApplyScenarioCommand(chr, AIBOTCMD_GETCASE);
} else {
botApplyScenarioCommand(chr, AIBOTCMD_SAVECASE);
}
}
} else {
// Not a one man team
s32 numgetting = botGetCountInTeamDoingCommand(chr, AIBOTCMD_GETCASE, false);
s32 numsaving = botGetCountInTeamDoingCommand(chr, AIBOTCMD_SAVECASE, false);
if (botShouldReturnCtcToken(chr)) {
botApplyScenarioCommand(chr, AIBOTCMD_GETCASE);
} else if (botIsChrsCtcTokenHeld(chr)) {
if (numsaving <= 0 || random() % 100 < 70) {
botApplyScenarioCommand(chr, AIBOTCMD_SAVECASE);
} else {
botApplyScenarioCommand(chr, AIBOTCMD_GETCASE);
}
} else if (numgetting <= 0 || numgetting < teamsize / 3) {
botApplyScenarioCommand(chr, AIBOTCMD_GETCASE);
} else if (numsaving <= 0 || numsaving < teamsize / 4) {
botApplyScenarioCommand(chr, AIBOTCMD_SAVECASE);
} else if (random() % 100 < 30) {
botApplyScenarioCommand(chr, AIBOTCMD_GETCASE);
} else if (random() % 100 < 30) {
botApplyScenarioCommand(chr, AIBOTCMD_SAVECASE);
} else {
botApplyScenarioCommand(chr, AIBOTCMD_NORMAL);
}
}
}
// Consider changing command in 20 to 60 seconds
aibot->commandtimer60 = TICKS(1200) + random() % TICKS(2400);
}
}
// The main loop is entered whenever the bot needs something to do
// or when a player gives it a command. It calculates a new myaction
// value and associated arguments based on its assigned command.
if (chr->myaction == MA_AIBOTMAINLOOP || aibot->forcemainloop) {
aibot->forcemainloop = false;
aibot->attackingplayernum = -1;
// KazeSim will attack people on sight regardless of command
if (aibot->config->type == BOTTYPE_KAZE && chr->target != -1 && aibot->targetinsight) {
newaction = MA_AIBOTATTACK;
aibot->attackingplayernum = mpPlayerGetIndex(chrGetTargetProp(chr)->chr);
aibot->abortattacktimer60 = -1;
}
// Check if the bot needs to fetch some weapons or ammo
if (newaction < 0) {
aibot->gotoprop = botFindDefaultPickup(chr);
if (aibot->gotoprop) {
newaction = MA_AIBOTGETITEM;
}
}
// Bot is good to implement the assigned command
if (newaction < 0) {
if (aibot->command == AIBOTCMD_ATTACK) {
// Attack the prop (player) given in attackpropnum
// This is a human command only
struct chrdata *targetchr = (g_Vars.props + aibot->attackpropnum)->chr;
if (!chrIsDead(targetchr)
&& !botIsTargetInvisible(chr, targetchr)
&& botPassesCowardCheck(chr, targetchr)) {
newaction = MA_AIBOTATTACK;
aibot->attackingplayernum = mpPlayerGetIndex(targetchr);
aibot->abortattacktimer60 = -1;
}
} else if (aibot->command == AIBOTCMD_FOLLOW) {
// Follow the prop (player) given in followprotectpropnum
// This is a human command only
newaction = MA_AIBOTFOLLOW;
aibot->canbreakfollow = true;
aibot->followingplayernum = mpPlayerGetIndex((g_Vars.props + aibot->followprotectpropnum)->chr);
} else if (aibot->command == AIBOTCMD_PROTECT) {
// Protect the prop (player) given in followprotectpropnum
// This is a human command only
newaction = MA_AIBOTFOLLOW;
aibot->canbreakfollow = false;
aibot->followingplayernum = mpPlayerGetIndex((g_Vars.props + aibot->followprotectpropnum)->chr);
} else if (aibot->command == AIBOTCMD_DEFEND) {
// Defend the position given in defendholdpos
// This is a human command only
newaction = MA_AIBOTDEFEND;
aibot->canbreakdefend = true;
} else if (aibot->command == AIBOTCMD_HOLD) {
// Hold the position given in defendholdpos
// This is a human command only
newaction = MA_AIBOTDEFEND;
aibot->canbreakdefend = false;
} else if (aibot->command == AIBOTCMD_GETCASE) {
// Capture the case - fetch and return the opponent's token
if (g_MpSetup.scenario == MPSCENARIO_CAPTURETHECASE && !aibot->hascase) {
// Make an array of pointers to other teams' tokens
// but ignore enemy tokens held by other teams
s32 botteamindex = radarGetTeamIndex(chr->team);
s32 i;
struct prop *tokens[4];
s32 numtokens = 0;
for (i = 0; i != 4; i++) {
if (i != botteamindex && g_ScenarioData.ctc.playercountsperteam[i]) {
if (g_ScenarioData.ctc.tokens[i]->type & (PROPTYPE_WEAPON | PROPTYPE_OBJ)) {
// Token is not held
tokens[numtokens++] = g_ScenarioData.ctc.tokens[i];
} else if (g_ScenarioData.ctc.tokens[i]->type & (PROPTYPE_CHR | PROPTYPE_PLAYER)) {
// Token is held
struct chrdata *tokenchr = g_ScenarioData.ctc.tokens[i]->chr;
if (tokenchr->team == chr->team) {
// Token is held by teammate
tokens[numtokens++] = g_ScenarioData.ctc.tokens[i];
}
}
}
}
// Prefer a token within 10 metres, otherwise pick any
if (numtokens > 0) {
s32 index;
s32 i;
index = random() % numtokens;
i = (index + 1) % numtokens;
while (true) {
f32 sqdist = chrGetSquaredDistanceToCoord(chr, &tokens[i]->pos);
if (sqdist < 1000 * 1000) {
index = i;
break;
}
if (i == index) {
break;
}
i = (i + 1) % numtokens;
}
if (index);
// If the chosen token is not held then collect it,
// otherwise it's held by a teammate so go protect them
if (tokens[index]->type & (PROPTYPE_WEAPON | PROPTYPE_OBJ)) {
newaction = MA_AIBOTGETITEM;
aibot->gotoprop = tokens[index];
} else if (botCanFollow(chr, tokens[index]->chr)) {
newaction = MA_AIBOTFOLLOW;
aibot->canbreakfollow = random() % 4 == 0;
aibot->followingplayernum = mpPlayerGetIndex(tokens[index]->chr);
}
}
}
} else if (aibot->command == AIBOTCMD_SAVECASE) {
// Capture the case - recover/protect bot's own token
if (g_MpSetup.scenario == MPSCENARIO_CAPTURETHECASE) {
// Find out where the bot's token is
struct prop *token = g_ScenarioData.ctc.tokens[radarGetTeamIndex(chr->team)];
if (token->type & (PROPTYPE_CHR | PROPTYPE_PLAYER)) {
struct chrdata *tokenchr = token->chr;
if (tokenchr->team == chr->team) {
// Held by a teammate - follow/protect them
if (botCanFollow(chr, tokenchr)) {
newaction = MA_AIBOTFOLLOW;
aibot->canbreakfollow = random() % 4 == 0;
aibot->followingplayernum = mpPlayerGetIndex(tokenchr);
}
} else {
// Held by an opponent - attack them
if (!chrIsDead(tokenchr)
&& !botIsTargetInvisible(chr, tokenchr)
&& botPassesCowardCheck(chr, tokenchr)) {
newaction = MA_AIBOTATTACK;
aibot->attackingplayernum = mpPlayerGetIndex(tokenchr);
aibot->abortattacktimer60 = -1;
}
}
} else {
// Token is not held - go to the pos to defend it
newaction = MA_AIBOTGOTOPOS;
aibot->gotopos = token->pos;
roomsCopy(token->rooms, aibot->gotorooms);
aibot->unk04c_00 = false;
}
}
} else if (aibot->command == AIBOTCMD_DEFHILL) {
// King of the hill - defend the hill (allow wandering out)
if (g_MpSetup.scenario == MPSCENARIO_KINGOFTHEHILL) {
if (chr->prop->rooms[0] == g_ScenarioData.koh.hillrooms[0]
&& chr->target != -1
&& aibot->targetinsight
&& botPassesCowardCheck(chr, chrGetTargetProp(chr)->chr)) {
// Bot is in the hill and sees target - attack them
newaction = MA_AIBOTATTACK;
aibot->attackingplayernum = mpPlayerGetIndex(chrGetTargetProp(chr)->chr);
aibot->abortattacktimer60 = TICKS(300);
} else {
// Go to the hill if not there already
u32 stack;
struct coord posinhill;
f32 angle;
s32 padnuminhill;
s32 covernuminhill;
if (botroomFindPos(g_ScenarioData.koh.hillrooms[0], &posinhill, &angle, &padnuminhill, &covernuminhill)) {
newaction = MA_AIBOTGOTOPOS;
aibot->gotopos = posinhill;
roomsCopy(g_ScenarioData.koh.hillrooms, aibot->gotorooms);
aibot->unk04c_00 = (chr->prop->rooms[0] == g_ScenarioData.koh.hillrooms[0]) != 0;
aibot->hillpadnum = padnuminhill;
aibot->hillcovernum = covernuminhill;
aibot->lastknownhill = g_ScenarioData.koh.hillrooms[0];
}
}
}
} else if (aibot->command == AIBOTCMD_HOLDHILL) {
// King of the hill - hold the hill (don't wander out)
if (g_MpSetup.scenario == MPSCENARIO_KINGOFTHEHILL) {
struct coord posinhill;
f32 angle;
s32 padnuminhill;
s32 covernuminhill;
// Go to the hill if not there already
if (botroomFindPos(g_ScenarioData.koh.hillrooms[0], &posinhill, &angle, &padnuminhill, &covernuminhill)) {
newaction = MA_AIBOTGOTOPOS;
aibot->gotopos = posinhill;
roomsCopy(g_ScenarioData.koh.hillrooms, aibot->gotorooms);
aibot->unk04c_00 = (chr->prop->rooms[0] == g_ScenarioData.koh.hillrooms[0]) != 0;
aibot->hillpadnum = padnuminhill;
aibot->hillcovernum = covernuminhill;
aibot->lastknownhill = g_ScenarioData.koh.hillrooms[0];
}
}
} else if (aibot->command == AIBOTCMD_DOWNLOAD) {
// Hacker Central - fetch uplink
if (g_MpSetup.scenario == MPSCENARIO_HACKERCENTRAL
&& g_ScenarioData.htm.uplink
&& g_ScenarioData.htm.uplink != chr->prop) {
// Uplink is not held by current bot
if (g_ScenarioData.htm.uplink->type & (PROPTYPE_CHR | PROPTYPE_PLAYER)) {
struct chrdata *uplinkchr = g_ScenarioData.htm.uplink->chr;
if ((g_MpSetup.options & MPOPTION_TEAMSENABLED) && uplinkchr->team == chr->team) {
// Uplink is held by teammate - protect them
if (botCanFollow(chr, uplinkchr)) {
newaction = MA_AIBOTFOLLOW;
aibot->canbreakfollow = random() % 4 == 0;
aibot->followingplayernum = mpPlayerGetIndex(uplinkchr);
}
} else {
// Uplink is held by opponent - attack them
if (!botIsTargetInvisible(chr, uplinkchr) && botPassesCowardCheck(chr, uplinkchr)) {
newaction = MA_AIBOTATTACK;
aibot->attackingplayernum = mpPlayerGetIndex(uplinkchr);
aibot->abortattacktimer60 = -1;
}
}
} else {
// Uplink is not held by anyone - fetch it
newaction = MA_AIBOTGOTOPROP;
aibot->gotoprop = g_ScenarioData.htm.uplink;
}
}
} else if (aibot->command == AIBOTCMD_GETCASE2) {
// Hold the briefcase - fetch and hold the case
if (g_MpSetup.scenario == MPSCENARIO_HOLDTHEBRIEFCASE
&& g_ScenarioData.htb.token
&& g_ScenarioData.htb.token != chr->prop) {
// Briefcase is not held by current bot
if (g_ScenarioData.htb.token->type & (PROPTYPE_CHR | PROPTYPE_PLAYER)) {
struct chrdata *tokenchr = g_ScenarioData.htb.token->chr;
if ((g_MpSetup.options & MPOPTION_TEAMSENABLED) && tokenchr->team == chr->team) {
// Briefcase is held by teammate - protect them
if (botCanFollow(chr, tokenchr)) {
newaction = MA_AIBOTFOLLOW;
aibot->canbreakfollow = random() % 4 == 0;
aibot->followingplayernum = mpPlayerGetIndex(tokenchr);
}
} else if (!botIsTargetInvisible(chr, tokenchr) && botPassesCowardCheck(chr, tokenchr)) {
// Briefcase is held by opponent - attack them
newaction = MA_AIBOTATTACK;
aibot->attackingplayernum = mpPlayerGetIndex(tokenchr);
aibot->abortattacktimer60 = -1;
}
} else {
// Briefcase is not held by anyone - fetch it
newaction = MA_AIBOTGOTOPROP;
aibot->gotoprop = g_ScenarioData.htb.token;
}
}
} else if (aibot->command == AIBOTCMD_POPCAP) {
// Pop a cap - attack the target
if (g_MpSetup.scenario == MPSCENARIO_POPACAP && g_ScenarioData.pac.victimindex >= 0) {
struct prop *victimprop = g_MpAllChrPtrs[g_ScenarioData.pac.victims[g_ScenarioData.pac.victimindex]]->prop;
if (victimprop != chr->prop) {
// Victim is not the current bot
struct chrdata *victimchr = victimprop->chr;
if ((g_MpSetup.options & MPOPTION_TEAMSENABLED) && victimchr->team == chr->team) {
// Victim is a teammate - protect them
if (botCanFollow(chr, victimchr)) {
newaction = MA_AIBOTFOLLOW;
aibot->canbreakfollow = random() % 4 == 0;
aibot->followingplayernum = mpPlayerGetIndex(victimchr);
}
} else {
// Victim is an opponent - attack them
if (!botIsTargetInvisible(chr, victimchr) && botPassesCowardCheck(chr, victimchr)) {
newaction = MA_AIBOTATTACK;
aibot->attackingplayernum = mpPlayerGetIndex(victimchr);
aibot->abortattacktimer60 = -1;
}
}
}
}
}
}
// The bot can still not have an action if it's general combat, or
// in some situations like having the uplink in Hacker Central or
// holding the briefcase in Hold the Briefcase.
if (newaction < 0) {
if (g_MpSetup.scenario == MPSCENARIO_HOLDTHEBRIEFCASE) {
if (aibot->hasbriefcase) {
// Current bot has the briefcase - follow a teammate for protection
s32 playernum = -1;
if (random() % 100 < 66) {
playernum = botFindTeammateToFollow(chr, 100000);
}
if (playernum >= 0) {
newaction = MA_AIBOTFOLLOW;
aibot->canbreakfollow = random() % 4 == 0;
aibot->followingplayernum = playernum;
}
}
} else if (g_MpSetup.scenario == MPSCENARIO_POPACAP) {
if (g_ScenarioData.pac.victimindex >= 0) {
struct prop *victimprop = g_MpAllChrPtrs[g_ScenarioData.pac.victims[g_ScenarioData.pac.victimindex]]->prop;
if (victimprop == chr->prop) {
// Current bot is the victim - follow a teammate for protection
s32 playernum = -1;
if (random() % 100 < 66) {
playernum = botFindTeammateToFollow(chr, 100000);
}
if (playernum >= 0) {
newaction = MA_AIBOTFOLLOW;
aibot->canbreakfollow = random() % 4 == 0;
aibot->followingplayernum = playernum;
}
}
}
} else if (g_MpSetup.scenario == MPSCENARIO_CAPTURETHECASE) {
// If the bot is holding an opponent's token, take it home
if (botShouldReturnCtcToken(chr)) {
struct pad *pad;
s32 teamindex = g_ScenarioData.ctc.teamindexes[radarGetTeamIndex(chr->team)];
newaction = MA_AIBOTGOTOPOS;
pad = &g_Pads[g_ScenarioData.ctc.spawnpadsperteam[teamindex].homepad];
aibot->gotopos = pad->pos;
aibot->gotorooms[0] = pad->room;
aibot->gotorooms[1] = -1;
aibot->unk04c_00 = false;
}
} else if (g_MpSetup.scenario == MPSCENARIO_HACKERCENTRAL) {
// If the bot has the uplink, go to the terminal
if (g_ScenarioData.htm.uplink == chr->prop) {
if (g_ScenarioData.htm.playernuminrange != mpPlayerGetIndex(chr)) {
newaction = MA_AIBOTGOTOPROP;
aibot->gotoprop = g_ScenarioData.htm.terminals[0].prop;
} else {
newaction = MA_AIBOTDOWNLOAD;
}
}
}
}
// If there's nothing to do for scenarios then go find some people to kill
if (newaction < 0) {
if (aibot->config->type == BOTTYPE_VENGE) {
// Attack the last player who killed the bot
if (aibot->lastkilledbyplayernum >= 0
&& !botIsTargetInvisible(chr, g_MpAllChrPtrs[aibot->lastkilledbyplayernum])) {
newaction = MA_AIBOTATTACK;
aibot->attackingplayernum = aibot->lastkilledbyplayernum;
aibot->abortattacktimer60 = -1;
}
} else if (aibot->config->type == BOTTYPE_FEUD) {
// Attack a single player the whole match
if (aibot->feudplayernum < 0
&& aibot->lastkilledbyplayernum >= 0
&& !chrCompareTeams(chr, g_MpAllChrPtrs[aibot->lastkilledbyplayernum], COMPARE_FRIENDS)) {
aibot->feudplayernum = aibot->lastkilledbyplayernum;
}
if (aibot->feudplayernum >= 0 && !botIsTargetInvisible(chr, g_MpAllChrPtrs[aibot->feudplayernum])) {
newaction = MA_AIBOTATTACK;
aibot->abortattacktimer60 = -1;
aibot->attackingplayernum = aibot->feudplayernum;
}
} else if (aibot->config->type == BOTTYPE_JUDGE) {
// Attack the winning player
struct ranking rankings[MAX_MPCHRS];
s32 count = mpGetPlayerRankings(rankings);
s32 i;
for (i = 0; i < count; i++) {
s32 playernum = func0f18d0e8(rankings[i].chrnum);
struct chrdata *otherchr = mpGetChrFromPlayerIndex(playernum);
if (otherchr != chr && !chrIsDead(otherchr)) {
#if PAL
if (1);
#endif
if (chrCompareTeams(chr, otherchr, COMPARE_ENEMIES)
&& !botIsTargetInvisible(chr, otherchr)) {
newaction = MA_AIBOTATTACK;
aibot->attackingplayernum = playernum;
aibot->abortattacktimer60 = -1;
}
}
}
} else if (aibot->config->type == BOTTYPE_PREY) {
// Attack the weakest player
f32 minhealth = 0;
s32 weakestplayernum = -1;
f32 health;
s32 i;
for (i = 0; i < g_MpNumChrs; i++) {
struct chrdata *otherchr = mpGetChrFromPlayerIndex(i);
if (otherchr != chr
&& !chrIsDead(otherchr)
&& chrCompareTeams(chr, otherchr, COMPARE_ENEMIES)
&& !botIsTargetInvisible(chr, otherchr)) {
if (otherchr->aibot) {
health = otherchr->maxdamage - otherchr->damage;
} else {
health = g_Vars.players[playermgrGetPlayerNumByProp(otherchr->prop)]->bondhealth * 8;
}
if (weakestplayernum < 0 || health < minhealth) {
weakestplayernum = i;
minhealth = health;
}
}
}
if (weakestplayernum >= 0) {
newaction = MA_AIBOTATTACK;
aibot->attackingplayernum = weakestplayernum;
aibot->abortattacktimer60 = -1;
}
}
}
// If the bot didn't set an action above,
// try attacking the existing target
if (newaction < 0) {
if (chr->target != -1 && botPassesCowardCheck(chr, chrGetTargetProp(chr)->chr)) {
newaction = MA_AIBOTATTACK;
aibot->abortattacktimer60 = -1;
}
}
// If there's no existing target, just follow a teammate
if (newaction < 0) {
s32 playernum = botFindTeammateToFollow(chr, 300);
if (playernum >= 0) {
newaction = MA_AIBOTFOLLOW;
aibot->canbreakfollow = random() % 4 == 0;
aibot->followingplayernum = playernum;
}
}
// If there's no teammate to follow, stock up on weapons and ammo
if (newaction < 0) {
aibot->gotoprop = botFindAnyPickup(chr);
if (aibot->gotoprop) {
newaction = MA_AIBOTGETITEM;
}
}
}
// Implement the new action
if (newaction >= 0) {
if (newaction == MA_AIBOTGETITEM) {
if (aibot->gotoprop) {
chrGoToProp(chr, aibot->gotoprop, GOPOSFLAG_RUN);
chr->myaction = newaction;
}
} else if (newaction == MA_AIBOTATTACK) {
if (chr->myaction != MA_AIBOTATTACK) {
chr->myaction = newaction;
aibot->distmode = -1;
}
} else if (newaction == MA_AIBOTFOLLOW) {
if (chr->myaction != MA_AIBOTFOLLOW) {
chr->myaction = newaction;
aibot->distmode = -1;
if (aibot->canbreakfollow) {
botSetTarget(chr, -1);
}
}
} else if (newaction == MA_AIBOTDEFEND) {
chr->myaction = newaction;
if (aibot->canbreakdefend) {
botSetTarget(chr, -1);
}
chrGoToRoomPos(chr, &aibot->defendholdpos, aibot->defendholdrooms, GOPOSFLAG_RUN);
} else if (newaction == MA_AIBOTGOTOPOS) {
f32 xdist = chr->prop->pos.x - aibot->gotopos.x;
f32 ydist = chr->prop->pos.y - aibot->gotopos.y;
f32 zdist = chr->prop->pos.z - aibot->gotopos.z;
if (xdist < 0) {
xdist = -xdist;
}
if (ydist < 0) {
ydist = -ydist;
}
if (zdist < 0) {
zdist = -zdist;
}
if (xdist > 20 || zdist > 20 || (ydist > 200 && chr->inlift == 0)) {
chr->myaction = newaction;
chrGoToRoomPos(chr, &aibot->gotopos, aibot->gotorooms, GOPOSFLAG_RUN);
} else {
chrStand(chr);
}
} else if (newaction == MA_AIBOTGOTOPROP) {
if (aibot->gotoprop) {
chr->myaction = newaction;
chrGoToProp(chr, aibot->gotoprop, GOPOSFLAG_RUN);
}
} else if (newaction == MA_AIBOTDOWNLOAD) {
chr->myaction = newaction;
chrStand(chr);
}
}
// If the action is no longer valid, go back to the main loop
// so another action can be chosen on the next tick
if (chr->myaction == MA_AIBOTGETITEM) {
if (chr->actiontype != ACT_GOPOS
|| aibot->gotoprop == NULL
|| aibot->gotoprop->parent
|| aibot->gotoprop->timetoregen != 0) {
chr->myaction = MA_AIBOTMAINLOOP;
}
} else if (chr->myaction == MA_AIBOTATTACK) {
if (aibot->attackingplayernum >= 0
&& (chrIsDead(g_MpAllChrPtrs[aibot->attackingplayernum]) || !botPassesCowardCheck(chr, g_MpAllChrPtrs[aibot->attackingplayernum]))) {
chr->myaction = MA_AIBOTMAINLOOP;
} else if (aibot->attackingplayernum < 0
&& (chr->target == -1
|| chrIsDead(chrGetTargetProp(chr)->chr)
|| !botPassesCowardCheck(chr, chrGetTargetProp(chr)->chr))) {
chr->myaction = MA_AIBOTMAINLOOP;
} else {
botcmdTickDistMode(chr);
if (botCanDoCriticalPickup(chr)) {
chr->myaction = MA_AIBOTMAINLOOP;
} else if (aibot->abortattacktimer60 >= 0 && aibot->targetlastseen60 < g_Vars.lvframe60 - aibot->abortattacktimer60) {
chr->myaction = MA_AIBOTMAINLOOP;
}
}
} else if (chr->myaction == MA_AIBOTFOLLOW) {
if (aibot->followingplayernum < 0
|| chrIsDead(g_MpAllChrPtrs[aibot->followingplayernum])) {
chr->myaction = MA_AIBOTMAINLOOP;
} else {
botcmdTickDistMode(chr);
if (aibot->canbreakfollow
&& chr->target != -1
&& aibot->targetinsight
&& botPassesCowardCheck(chr, chrGetTargetProp(chr)->chr)) {
f32 xdist = chr->prop->pos.x - g_MpAllChrPtrs[aibot->followingplayernum]->prop->pos.x;
f32 zdist = chr->prop->pos.z - g_MpAllChrPtrs[aibot->followingplayernum]->prop->pos.z;
if (xdist < 0) {
xdist = -xdist;
}
if (zdist < 0) {
zdist = -zdist;
}
// No y check?
if (xdist < 500 && zdist < 500) {
chr->myaction = MA_AIBOTATTACK;
aibot->attackingplayernum = mpPlayerGetIndex(chrGetTargetProp(chr)->chr);
aibot->abortattacktimer60 = TICKS(300);
aibot->distmode = -1;
}
}
if (botCanDoCriticalPickup(chr)) {
chr->myaction = MA_AIBOTMAINLOOP;
}
}
} else if (chr->myaction == MA_AIBOTDEFEND) {
if (chr->actiontype != ACT_GOPOS) {
f32 xdist = chr->prop->pos.x - aibot->defendholdpos.x;
f32 ydist = chr->prop->pos.y - aibot->defendholdpos.y;
f32 zdist = chr->prop->pos.z - aibot->defendholdpos.z;
if (xdist < 0) {
xdist = -xdist;
}
if (ydist < 0) {
ydist = -ydist;
}
if (zdist < 0) {
zdist = -zdist;
}
if (aibot->returntodefendtimer60 > 0) {
aibot->returntodefendtimer60 -= g_Vars.lvupdate60;
}
if (xdist > 40 || zdist > 40 || (ydist > 200 && !chr->inlift)) {
if (aibot->returntodefendtimer60 <= 0) {
chrGoToRoomPos(chr, &aibot->defendholdpos, aibot->defendholdrooms, GOPOSFLAG_RUN);
}
} else if (aibot->canbreakdefend
&& chr->target != -1
&& aibot->targetinsight
&& botPassesCowardCheck(chr, chrGetTargetProp(chr)->chr)) {
chr->myaction = MA_AIBOTATTACK;
aibot->attackingplayernum = mpPlayerGetIndex(chrGetTargetProp(chr)->chr);
aibot->abortattacktimer60 = TICKS(300);
aibot->distmode = -1;
}
if (aibot->returntodefendtimer60 <= 0) {
aibot->returntodefendtimer60 = TICKS(60);
}
}
if (botCanDoCriticalPickup(chr)) {
chr->myaction = MA_AIBOTMAINLOOP;
}
} else if (chr->myaction == MA_AIBOTGOTOPOS) {
if (g_MpSetup.scenario == MPSCENARIO_KINGOFTHEHILL && aibot->unk04c_00) {
if (aibot->lastknownhill != g_ScenarioData.koh.hillrooms[0]) {
// Someone scored the hill
aibot->unk04c_00 = false;
} else if (chr->prop->rooms[0] == g_ScenarioData.koh.hillrooms[0]) {
// empty
} else if (aibot->hillpadnum >= 0) {
padSetFlag(aibot->hillpadnum, PADFLAG_AIBOTINUSE);
} else if (aibot->hillcovernum >= 0) {
coverSetFlag(aibot->hillcovernum, COVERFLAG_AIBOTINUSE);
}
}
if (chr->actiontype != ACT_GOPOS) {
chr->myaction = MA_AIBOTMAINLOOP;
} else if (botCanDoCriticalPickup(chr)) {
chr->myaction = MA_AIBOTMAINLOOP;
}
} else if (chr->myaction == MA_AIBOTGOTOPROP) {
if (botCanDoCriticalPickup(chr)) {
chr->myaction = MA_AIBOTMAINLOOP;
} else if (chr->actiontype != ACT_GOPOS || aibot->gotoprop == NULL || aibot->gotoprop->parent) {
chr->myaction = MA_AIBOTMAINLOOP;
} else if (g_MpSetup.scenario == MPSCENARIO_HOLDTHEBRIEFCASE) {
// empty
} else if (g_MpSetup.scenario == MPSCENARIO_HACKERCENTRAL
&& g_ScenarioData.htm.uplink == chr->prop
&& g_ScenarioData.htm.playernuminrange == mpPlayerGetIndex(chr)) {
chr->myaction = MA_AIBOTMAINLOOP;
}
} else if (chr->myaction == MA_AIBOTDOWNLOAD) {
if (botCanDoCriticalPickup(chr)) {
chr->myaction = MA_AIBOTMAINLOOP;
} else if (g_ScenarioData.htm.playernuminrange != mpPlayerGetIndex(chr)) {
chr->myaction = MA_AIBOTMAINLOOP;
}
}
// Regardless of the action, choose a general target and maintain a
// route to them, even if it won't be followed
botChooseGeneralTarget(chr);
if (mpPlayerGetIndex(chr) == (g_Vars.lvframenum % g_MpNumChrs) && chr->target != -1) {
struct prop *targetprop = chrGetTargetProp(chr);
struct waypoint *first = waypointFindClosestToPos(&chr->prop->pos, chr->prop->rooms);
struct waypoint *last = waypointFindClosestToPos(&targetprop->pos, targetprop->rooms);
if (first && last) {
s32 hash = (g_Vars.lvframe60 >> 9) * 128 + chr->chrnum * 8;
waypointSetHashThing(hash, hash);
aibot->unk208 = waypointFindRoute(last, first, aibot->waypoints, ARRAYCOUNT(aibot->waypoints));
waypointSetHashThing(0, 0);
}
}
// Tick the bot's inventory. They may decide to switch weapons.
botinvTick(chr);
// Iterate both hands and handle shooting
{
bool firingright = false;
s32 i;
for (i = 0; i < 2; i++) {
bool firing = false;
// nextbullettimer60 is positive if the chr is firing
// It's used to implement the weapon's fire rate correctly
if (aibot->nextbullettimer60[i] > 0) {
aibot->nextbullettimer60[i] -= g_Vars.lvupdate60;
}
// Don't shoot the left hand on the same frame as the right
// (note that right is iterated first)
if (i == HAND_LEFT && firingright) {
aibot->nextbullettimer60[i] = 1;
}
if (aibot->skrocket == NULL && aibot->changeguntimer60 <= 0) {
if (aibot->iscloserangeweapon) {
// Consider punching, pistol whipping etc
// Despite the name, punchtimer60 is used for all close
// range attacks. Its value is 0 if not currently
// punching, positive (and ticking down) when cooling
// down from a previous punch, and negative when
// starting the punch.
if (aibot->punchtimer60[i] >= 0 && aibot->timeuntilreload60[i] <= 0) {
if (aibot->weaponnum == WEAPON_TRANQUILIZER
&& aibot->loadedammo[i] < bgunGetMinClipQty(WEAPON_TRANQUILIZER, FUNC_SECONDARY)) {
aibot->punchtimer60[i] = 0;
botScheduleReload(chr, i);
} else {
f32 range = 210;
// Decide whether to actually punch or not.
// This seems a bit backwards in that the timer
// is set to negative (ie. punch) then the check
// below has to cancel it by setting it back to 0.
aibot->punchtimer60[i] -= g_Vars.lvupdate60;
if (chr->target != -1
&& aibot->targetinsight
&& aibot->shootdelaytimer60 >= g_BotDifficulties[aibot->config->difficulty].shootdelay) {
if (!botIsDizzy(chr)) {
if (aibot->weaponnum == WEAPON_TRANQUILIZER) {
if (!chrIsTargetInFov(chr, 30, 0) || chrGetDistanceToTarget(chr) > range) {
aibot->punchtimer60[i] = 0;
}
} else {
if (!chrIsTargetInFov(chr, 40, 0) || chrGetDistanceToTarget(chr) > range + 150) {
aibot->punchtimer60[i] = 0;
}
}
}
} else {
aibot->punchtimer60[i] = 0;
}
// If the punch was not cancelled, execute it
if (aibot->punchtimer60[i] < 0) {
chrUncloakTemporarily(chr);
chrPunchInflictDamage(chr, 2, range, false);
if (i == HAND_RIGHT) {
// Set the punch cooldown timer
switch (aibot->weaponnum) {
case WEAPON_UNARMED:
case WEAPON_MAGSEC4:
case WEAPON_MAULER:
case WEAPON_PHOENIX:
case WEAPON_CMP150:
case WEAPON_CYCLONE:
case WEAPON_CALLISTO:
case WEAPON_RCP120:
case WEAPON_LAPTOPGUN:
case WEAPON_DRAGON:
case WEAPON_K7AVENGER:
case WEAPON_AR34:
case WEAPON_SUPERDRAGON:
case WEAPON_SHOTGUN:
case WEAPON_SNIPERRIFLE:
case WEAPON_FARSIGHT:
case WEAPON_DEVASTATOR:
case WEAPON_ROCKETLAUNCHER:
case WEAPON_SLAYER:
case WEAPON_CROSSBOW:
default:
if (chr->aibot->config->difficulty == BOTDIFF_MEAT) {
aibot->punchtimer60[0] = TICKS(120);
} else if (chr->aibot->config->difficulty == BOTDIFF_EASY) {
aibot->punchtimer60[0] = TICKS(60);
} else {
aibot->punchtimer60[0] = TICKS(30);
}
if (random() % 3 == 0) {
aibot->punchtimer60[1] = aibot->punchtimer60[0] - TICKS(20);
}
break;
case WEAPON_FALCON2:
case WEAPON_FALCON2_SILENCER:
case WEAPON_FALCON2_SCOPE:
case WEAPON_DY357MAGNUM:
case WEAPON_DY357LX:
case WEAPON_COMBATKNIFE:
if (chr->aibot->config->difficulty == BOTDIFF_MEAT) {
aibot->punchtimer60[0] = TICKS(120);
} else if (chr->aibot->config->difficulty == BOTDIFF_EASY) {
aibot->punchtimer60[0] = TICKS(90);
} else {
aibot->punchtimer60[0] = TICKS(60);
}
if (chr->weapons_held[HAND_LEFT]) {
aibot->punchtimer60[1] = aibot->punchtimer60[0] - TICKS(40);
}
break;
case WEAPON_TRANQUILIZER:
aibot->punchtimer60[0] = TICKS(60);
aibot->loadedammo[0] -= bgunGetMinClipQty(WEAPON_TRANQUILIZER, FUNC_SECONDARY);
break;
case WEAPON_REAPER:
aibot->punchtimer60[0] = 0;
break;
}
}
}
}
}
} else if (aibot->weaponnum == WEAPON_SLAYER && aibot->gunfunc != FUNC_PRIMARY && chr->target != -1) {
// Bots fire Slayer rockets regardless of where they are
// on the map provided they have ammo
if (aibot->loadedammo[0] > 0) {
chrUncloakTemporarily(chr);
botactCreateSlayerRocket(chr);
aibot->loadedammo[0]--;
}
} else if (botactIsWeaponThrowable(aibot->weaponnum, aibot->gunfunc)) {
// Hand throwing a weapon
if (i == HAND_RIGHT) {
if (aibot->throwtimer60 > 0) {
aibot->throwtimer60 -= g_Vars.lvupdate60;
}
if (chr->aibot->throwtimer60 <= 0) {
if (botactGetAmmoQuantityByWeapon(aibot, aibot->weaponnum, aibot->gunfunc, false) > 0
|| aibot->weaponnum == WEAPON_LAPTOPGUN
|| aibot->weaponnum == WEAPON_DRAGON) {
bool throw = false;
if (chr->target != -1
&& aibot->targetinsight
&& aibot->shootdelaytimer60 >= g_BotDifficulties[aibot->config->difficulty].shootdelay
&& (botIsDizzy(chr) || chrIsTargetInFov(chr, 45, false))) {
throw = true;
}
if (throw) {
struct weaponfunc *func;
chrUncloakTemporarily(chr);
botactTryRemoveAmmoFromReserve(aibot, aibot->weaponnum, aibot->gunfunc, 1);
botact0f19a37c(chr);
func = weaponGetFunctionById(aibot->weaponnum, aibot->gunfunc);
if (func && (func->flags & FUNCFLAG_DISCARDWEAPON)) {
botinvRemoveItem(chr, aibot->weaponnum);
botinvSwitchToWeapon(chr, WEAPON_UNARMED, FUNC_PRIMARY);
}
aibot->throwtimer60 = botactGetProjectileThrowInterval(chr->aibot->weaponnum);
}
}
}
}
} else if (chr->weapons_held[i] && aibot->loadedammo[i] > 0) {
// Handle firing a regular weapon
bool canshoot = false;
if (weaponGetNumTicksPerShot(aibot->weaponnum, aibot->gunfunc) <= 0) {
// Increment the mauler charge and deplete ammo as
// the charge amount crosses each whole number.
// Yes, this is actually implemented for bots.
if (aibot->weaponnum == WEAPON_MAULER
&& aibot->gunfunc == FUNC_SECONDARY
&& aibot->loadedammo[i] >= 2) {
s32 newchargei;
s32 oldchargei = aibot->maulercharge[i];
aibot->maulercharge[i] += g_Vars.lvupdate60freal * 0.05f;
if (aibot->maulercharge[i] > 5) {
aibot->maulercharge[i] = 5;
}
newchargei = aibot->maulercharge[i];
if (newchargei != oldchargei) {
aibot->loadedammo[i]--;
}
}
if (aibot->nextbullettimer60[i] <= 0) {
canshoot = true;
}
} else {
canshoot = true;
}
if (canshoot) {
if (aibot->cyclonedischarging[i] || aibot->burstsdone[i] > 0) {
firing = true;
} else if (chr->target != -1
&& aibot->targetinsight
&& aibot->shootdelaytimer60 >= g_BotDifficulties[aibot->config->difficulty].shootdelay
&& (botIsDizzy(chr) || chrIsTargetInFov(chr, 45, false))
&& !chrIsDead(chrGetTargetProp(chr)->chr)) {
firing = true;
if (aibot->weaponnum == WEAPON_CYCLONE && aibot->gunfunc == FUNC_SECONDARY) {
aibot->cyclonedischarging[i] = true;
} else if (aibot->weaponnum == WEAPON_REAPER) {
aibot->reaperspeed[i] += g_Vars.lvupdate60;
if (aibot->reaperspeed[i] > TICKS(90)) {
aibot->reaperspeed[i] = TICKS(90);
}
}
}
// The Reaper continues shooting momentarily when
// the trigger is released
if (!firing && aibot->reaperspeed[i] > 0) {
firing = true;
aibot->reaperspeed[i] -= g_Vars.lvupdate60;
if (aibot->reaperspeed[i] < 0) {
aibot->reaperspeed[i] = 0;
}
}
}
if (weaponGetNumTicksPerShot(aibot->weaponnum, aibot->gunfunc) <= 0 && firing) {
struct weaponfunc *func;
aibot->nextbullettimer60[i] = botactGetShootInterval60(aibot->weaponnum, aibot->gunfunc);
#if PAL
if (aibot->nextbullettimer60[i] >= 6) {
aibot->nextbullettimer60[i] = TICKS(aibot->nextbullettimer60[i]);
}
#endif
func = weaponGetFunctionById(aibot->weaponnum, aibot->gunfunc);
if (func
&& (func->flags & (FUNCFLAG_BURST3 | FUNCFLAG_BURST2))
&& aibot->loadedammo[i] >= 2) {
s32 burstqty = (func->flags & FUNCFLAG_BURST2) ? 2 : 3;
chr->aibot->burstsdone[i]++;
chr->aibot->burstsdone[i] %= burstqty;
if (chr->aibot->burstsdone[i]) {
chr->aibot->nextbullettimer60[i] = 5;
}
}
}
} else {
aibot->cyclonedischarging[i] = false;
aibot->burstsdone[i] = 0;
aibot->reaperspeed[i] = 0;
}
}
if (firing) {
chrUncloakTemporarily(chr);
if (i == HAND_RIGHT) {
firingright = true;
}
}
chrSetHandFiring(chr, i, firing);
}
}
}
}
void botCheckFetch(struct chrdata *chr)
{
bool alreadyfetching = false;
struct aibot *aibot = chr->aibot;
if (chr->myaction == MA_AIBOTGETITEM) {
if (chr->act_gopos.waypoints[chr->act_gopos.curindex] == 0) {
struct prop *prop = aibot->gotoprop;
if (prop && !prop->parent && prop->timetoregen == 0) {
if (prop->type & (PROPTYPE_WEAPON | PROPTYPE_OBJ)) {
prop->obj->flags3 |= OBJFLAG3_ISFETCHTARGET;
}
}
}
aibot->forcemainloop = true;
alreadyfetching = true;
}
if (!alreadyfetching) {
chrGoToRoomPos(chr, &chr->act_gopos.endpos, chr->act_gopos.endrooms, chr->act_gopos.flags);
}
}