mirror of
https://gitlab.com/ryandwyer/perfect-dark
synced 2026-05-29 08:42:48 -04:00
3515 lines
102 KiB
C
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);
|
|
}
|
|
}
|