mirror of
https://gitlab.com/ryandwyer/perfect-dark
synced 2026-05-30 00:46:17 -04:00
548 lines
13 KiB
C
548 lines
13 KiB
C
#include <ultra64.h>
|
|
#include "constants.h"
|
|
#include "game/chraction.h"
|
|
#include "game/debug.h"
|
|
#include "game/chr.h"
|
|
#include "game/propsnd.h"
|
|
#include "game/bondgun.h"
|
|
#include "game/game_0b0fd0.h"
|
|
#include "game/playermgr.h"
|
|
#include "game/botact.h"
|
|
#include "game/pad.h"
|
|
#include "game/padhalllv.h"
|
|
#include "game/propobj.h"
|
|
#include "bss.h"
|
|
#include "lib/rng.h"
|
|
#include "lib/mtx.h"
|
|
#include "lib/anim.h"
|
|
#include "lib/collision.h"
|
|
#include "data.h"
|
|
#include "types.h"
|
|
|
|
s32 botactGetAmmoTypeByFunction(s32 weaponnum, s32 funcnum)
|
|
{
|
|
if (weaponnum >= WEAPON_FALCON2 && weaponnum <= WEAPON_SUICIDEPILL) {
|
|
struct inventory_ammo *ammo = weaponGetAmmoByFunction(weaponnum, funcnum);
|
|
|
|
if (ammo) {
|
|
return ammo->type;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
s32 botactGetClipCapacityByFunction(s32 weaponnum, u32 funcnum)
|
|
{
|
|
if (weaponnum >= WEAPON_FALCON2 && weaponnum <= WEAPON_SUICIDEPILL) {
|
|
struct inventory_ammo *ammo = weaponGetAmmoByFunction(weaponnum, funcnum);
|
|
|
|
if (ammo) {
|
|
return ammo->clipsize;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void botactReload(struct chrdata *chr, s32 handnum, bool withsound)
|
|
{
|
|
struct aibot *aibot = chr->aibot;
|
|
|
|
aibot->timeuntilreload60[handnum] = 0;
|
|
aibot->maulercharge[handnum] = 0;
|
|
|
|
if (chr->weapons_held[handnum] && !botactIsWeaponThrowable(aibot->weaponnum, aibot->gunfunc)) {
|
|
s32 capacity = botactGetClipCapacityByFunction(aibot->weaponnum, aibot->gunfunc);
|
|
|
|
if (capacity > 0) {
|
|
s32 tryamount = capacity - aibot->loadedammo[handnum];
|
|
s32 actualamount = botactTryRemoveAmmoFromReserve(aibot, aibot->weaponnum, aibot->gunfunc, tryamount);
|
|
|
|
if (actualamount > 0) {
|
|
aibot->loadedammo[handnum] += actualamount;
|
|
|
|
if (withsound) {
|
|
if (aibot->weaponnum == WEAPON_FARSIGHT) {
|
|
propsnd0f0939f8(NULL, chr->prop, SFX_RELOAD_FARSIGHT, -1,
|
|
-1, 1024, 0, 0, 0, -1, 0, -1, -1, -1, -1);
|
|
} else {
|
|
propsnd0f0939f8(NULL, chr->prop, SFX_RELOAD_DEFAULT, -1,
|
|
-1, 1024, 0, 0, 0, -1, 0, -1, -1, -1, -1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
s32 botactGetAmmoQuantityByWeapon(struct aibot *aibot, s32 weaponnum, s32 funcnum, bool include_equipped)
|
|
{
|
|
s32 qty = 0;
|
|
s32 ammotype;
|
|
s32 equippedammotype;
|
|
|
|
if (aibot) {
|
|
if (aibot->unk064 & 1) {
|
|
ammotype = botactGetAmmoTypeByFunction(weaponnum, funcnum);
|
|
qty = bgunGetCapacityByAmmotype(ammotype);
|
|
} else {
|
|
ammotype = botactGetAmmoTypeByFunction(weaponnum, funcnum);
|
|
qty = aibot->ammoheld[ammotype];
|
|
}
|
|
|
|
if (include_equipped) {
|
|
ammotype = botactGetAmmoTypeByFunction(weaponnum, funcnum);
|
|
equippedammotype = botactGetAmmoTypeByFunction(aibot->weaponnum, aibot->gunfunc);
|
|
|
|
if (equippedammotype == ammotype) {
|
|
qty += aibot->loadedammo[HAND_LEFT] + aibot->loadedammo[HAND_RIGHT];
|
|
}
|
|
}
|
|
}
|
|
|
|
return qty;
|
|
}
|
|
|
|
s32 botactGetAmmoQuantityByType(struct aibot *aibot, s32 ammotype, bool include_equipped)
|
|
{
|
|
s32 qty = 0;
|
|
|
|
if (aibot) {
|
|
if (aibot->unk064 & 1) {
|
|
qty = bgunGetCapacityByAmmotype(ammotype);
|
|
} else {
|
|
qty = aibot->ammoheld[ammotype];
|
|
}
|
|
|
|
if (include_equipped
|
|
&& botactGetAmmoTypeByFunction(aibot->weaponnum, aibot->gunfunc) == ammotype) {
|
|
qty += aibot->loadedammo[HAND_LEFT] + aibot->loadedammo[HAND_RIGHT];
|
|
}
|
|
}
|
|
|
|
return qty;
|
|
}
|
|
|
|
/**
|
|
* Attempt to remove the given quantity of ammo from the aibot's reserve and
|
|
* return the amount actually removed.
|
|
*
|
|
* The amount removed will be less than the attempted amount if the aibot
|
|
* doesn't have enough ammo in reserve.
|
|
*/
|
|
s32 botactTryRemoveAmmoFromReserve(struct aibot *aibot, s32 weaponnum, s32 funcnum, s32 tryqty)
|
|
{
|
|
s32 amountremoved;
|
|
s32 *ammoheld = &aibot->ammoheld[botactGetAmmoTypeByFunction(weaponnum, funcnum)];
|
|
|
|
if (!aibot || *ammoheld <= 0 || tryqty <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (aibot->unk064 & 1) {
|
|
return tryqty;
|
|
}
|
|
|
|
dprint();
|
|
*ammoheld -= tryqty;
|
|
|
|
if (*ammoheld < 0) {
|
|
amountremoved = tryqty + *ammoheld;
|
|
*ammoheld = 0;
|
|
|
|
if (dprint()) {
|
|
return amountremoved;
|
|
}
|
|
} else {
|
|
amountremoved = tryqty;
|
|
dprint();
|
|
}
|
|
|
|
return amountremoved;
|
|
}
|
|
|
|
void botactGiveAmmoByWeapon(struct aibot *aibot, s32 weaponnum, s32 funcnum, s32 qty)
|
|
{
|
|
s32 max;
|
|
s32 *heldquantity = &aibot->ammoheld[botactGetAmmoTypeByFunction(weaponnum, funcnum)];
|
|
|
|
if (aibot && (aibot->unk064 & 1) == 0 && qty > 0) {
|
|
dprint();
|
|
*heldquantity += qty;
|
|
|
|
if (heldquantity);
|
|
|
|
max = bgunGetCapacityByAmmotype(botactGetAmmoTypeByFunction(weaponnum, funcnum));
|
|
|
|
if (*heldquantity > max) {
|
|
*heldquantity = max;
|
|
}
|
|
|
|
dprint();
|
|
}
|
|
}
|
|
|
|
void botactGiveAmmoByType(struct aibot *aibot, u32 ammotype, s32 quantity)
|
|
{
|
|
s32 max;
|
|
s32 *heldquantity = &aibot->ammoheld[ammotype];
|
|
|
|
if (!aibot || (aibot->unk064 & 1) || quantity <= 0) {
|
|
return;
|
|
}
|
|
|
|
dprint();
|
|
|
|
*heldquantity += quantity;
|
|
|
|
if (heldquantity);
|
|
|
|
max = bgunGetCapacityByAmmotype(ammotype);
|
|
|
|
if (*heldquantity > max) {
|
|
*heldquantity = max;
|
|
}
|
|
|
|
dprint();
|
|
}
|
|
|
|
bool botactShootFarsight(struct chrdata *chr, s32 arg1, struct coord *vector, struct coord *arg3)
|
|
{
|
|
struct aibot *aibot;
|
|
struct chrdata *oppchr;
|
|
struct prop *oppprop;
|
|
s32 i;
|
|
s32 rand;
|
|
f32 speed;
|
|
|
|
if (!chr || !chr->aibot) {
|
|
return false;
|
|
}
|
|
|
|
aibot = chr->aibot;
|
|
|
|
if (aibot->weaponnum == WEAPON_FARSIGHT) {
|
|
rand = random() % 100;
|
|
|
|
// 3 in 10 chance of this passing
|
|
if (rand < 30) {
|
|
struct modelnode *node = NULL;
|
|
struct model *model = NULL;
|
|
s32 side = -1;
|
|
s32 hitpart = HITPART_GENERAL;
|
|
struct gset gset = {WEAPON_FARSIGHT, 0, 0, FUNC_PRIMARY};
|
|
f32 damage = gsetGetDamage(&gset);
|
|
s32 fallback = 30;
|
|
s32 value = fallback;
|
|
|
|
for (i = 0; i < g_MpNumChrs; i++) {
|
|
oppchr = g_MpAllChrPtrs[i];
|
|
oppprop = g_MpAllChrPtrs[i]->prop;
|
|
|
|
if (oppprop->type == PROPTYPE_PLAYER) {
|
|
struct player *player = g_Vars.players[playermgrGetPlayerNumByProp(oppprop)];
|
|
speed = player->speedforwards * player->speedforwards
|
|
+ player->speedsideways * player->speedsideways;
|
|
|
|
if (speed > 0) {
|
|
value = fallback * 0.05f;
|
|
}
|
|
} else {
|
|
if (oppchr->actiontype != ACT_STAND) {
|
|
value = fallback * 0.05f;
|
|
}
|
|
}
|
|
|
|
// value is 30 if player was still, or 1.5 if moving. So if 30
|
|
// then this will always pass, or if 1.5 then this has 1 in 15
|
|
// chance of passing.
|
|
if (oppchr != chr
|
|
&& value > rand
|
|
&& func0f06b39c(arg3, vector, &oppprop->pos, chr0f0278a4(oppchr))) {
|
|
bgunPlayPropHitSound(&gset, oppprop, -1);
|
|
|
|
if (oppchr->model && chrGetShield(oppchr) > 0) {
|
|
chrCalculateShieldHit(oppchr, &oppprop->pos, vector, &node, &hitpart, &model, &side);
|
|
}
|
|
|
|
chrEmitSparks(oppchr, oppprop, hitpart, &oppprop->pos, vector, chr);
|
|
func0f0341dc(oppchr, damage, vector, &gset, chr->prop, HITPART_GENERAL, oppprop, node, model, side, 0);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
s32 botactGetWeaponModel(s32 weapon)
|
|
{
|
|
return playermgrGetModelOfWeapon(weapon);
|
|
}
|
|
|
|
bool botactIsWeaponThrowable(s32 weaponnum, bool is_secondary)
|
|
{
|
|
switch (weaponnum) {
|
|
case WEAPON_LAPTOPGUN:
|
|
case WEAPON_DRAGON:
|
|
case WEAPON_COMBATKNIFE:
|
|
return is_secondary;
|
|
case WEAPON_GRENADE:
|
|
case WEAPON_NBOMB:
|
|
case WEAPON_TIMEDMINE:
|
|
case WEAPON_PROXIMITYMINE:
|
|
case WEAPON_REMOTEMINE:
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
u32 botactGetProjectileThrowInterval(u32 weapon)
|
|
{
|
|
switch (weapon) {
|
|
case WEAPON_COMBATKNIFE:
|
|
return TICKS(120);
|
|
case WEAPON_GRENADE:
|
|
case WEAPON_NBOMB:
|
|
return TICKS(90);
|
|
case WEAPON_CROSSBOW:
|
|
case WEAPON_TRANQUILIZER:
|
|
case WEAPON_LASER:
|
|
case WEAPON_TIMEDMINE:
|
|
case WEAPON_PROXIMITYMINE:
|
|
case WEAPON_REMOTEMINE:
|
|
default:
|
|
return TICKS(60);
|
|
}
|
|
}
|
|
|
|
s32 botactGetWeaponByAmmoType(s32 ammotype)
|
|
{
|
|
switch (ammotype) {
|
|
case AMMOTYPE_NBOMB: return WEAPON_NBOMB;
|
|
case AMMOTYPE_GRENADE: return WEAPON_GRENADE;
|
|
case AMMOTYPE_KNIFE: return WEAPON_COMBATKNIFE;
|
|
case AMMOTYPE_REMOTE_MINE: return WEAPON_REMOTEMINE;
|
|
case AMMOTYPE_PROXY_MINE: return WEAPON_PROXIMITYMINE;
|
|
case AMMOTYPE_TIMED_MINE: return WEAPON_TIMEDMINE;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void botact0f19a37c(struct chrdata *chr)
|
|
{
|
|
struct coord sp228 = {0, 0, 0};
|
|
Mtxf sp164;
|
|
struct coord sp152;
|
|
struct prop *prop = chr->prop;
|
|
Mtxf sp84;
|
|
f32 sp80 = chrGetAimAngle(chr);
|
|
u32 stack;
|
|
struct gset gset = {0};
|
|
struct prop *target = chrGetTargetProp(chr);
|
|
struct coord sp56;
|
|
f32 mult;
|
|
|
|
gset.weaponnum = chr->aibot->weaponnum;
|
|
gset.weaponfunc = chr->aibot->gunfunc;
|
|
|
|
if (chrIsTargetInFov(chr, 30, 0)) {
|
|
sp56.x = target->pos.x;
|
|
sp56.z = target->pos.z;
|
|
|
|
if (chr->aibot->weaponnum == WEAPON_GRENADE || chr->aibot->weaponnum == WEAPON_NBOMB) {
|
|
sp56.y = target->chr->manground;
|
|
} else {
|
|
sp56.y = target->pos.y;
|
|
|
|
if (target->type == PROPTYPE_PLAYER) {
|
|
sp56.y -= 25.0f;
|
|
}
|
|
}
|
|
|
|
chrCalculateTrajectory(&prop->pos, 16.666666f, &sp56, &sp152);
|
|
} else {
|
|
// These numbers are about 2 billionths away from BADDEG2RAD(20),
|
|
// but tweaking the multiplier in BADDEG2RAD doesn't make this match
|
|
// without creating mismatches in other places :(
|
|
sp152.x = cosf(0.34901028871536f) * sinf(sp80);
|
|
sp152.y = sinf(0.34901028871536f);
|
|
sp152.z = cosf(0.34901028871536f) * cosf(sp80);
|
|
}
|
|
|
|
mult = 16.666666f;
|
|
|
|
sp228.x = sp152.x * mult;
|
|
sp228.y = sp152.y * mult;
|
|
sp228.z = sp152.z * mult;
|
|
|
|
mtx4LoadIdentity(&sp164);
|
|
|
|
if (chr->aibot->weaponnum == WEAPON_COMBATKNIFE) {
|
|
mtx4LoadZRotation(M_BADPI * 1.5f, &sp164);
|
|
mtx4LoadXRotation(M_BADPI, &sp84);
|
|
mtx4MultMtx4InPlace(&sp84, &sp164);
|
|
}
|
|
|
|
mtx4LoadXRotation(0.34901028871536f, &sp84);
|
|
mtx00015be0(&sp84, &sp164);
|
|
mtx4LoadYRotation(sp80, &sp84);
|
|
mtx00015be0(&sp84, &sp164);
|
|
|
|
bgunCreateThrownProjectile2(chr, &gset, &prop->pos, prop->rooms, &sp164, &sp228);
|
|
|
|
if (gset.weaponnum == WEAPON_REMOTEMINE) {
|
|
chr->aibot->unk064 |= 0x1000;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the shoot interval of the given weapon, in time60.
|
|
*/
|
|
s32 botactGetShootInterval60(s32 weaponnum, s32 funcnum)
|
|
{
|
|
s32 stack[2];
|
|
s32 result = 1;
|
|
struct weapon *weapon = weaponFindById(weaponnum);
|
|
|
|
if (weapon) {
|
|
struct weaponfunc *func = weapon->functions[funcnum];
|
|
|
|
if (func) {
|
|
if (func->type == INVENTORYFUNCTYPE_SHOOT_SINGLE) {
|
|
struct weaponfunc_shoot *func2 = (struct weaponfunc_shoot *)func;
|
|
result = func2->unk24 + func2->unk25;
|
|
} else if (func->type == INVENTORYFUNCTYPE_SHOOT_AUTOMATIC) {
|
|
struct weaponfunc_shoot *func2 = (struct weaponfunc_shoot *)func;
|
|
result = func2->unk24 + func2->unk25;
|
|
} else if (func->type == INVENTORYFUNCTYPE_SHOOT_PROJECTILE) {
|
|
struct weaponfunc_shoot *func2 = (struct weaponfunc_shoot *)func;
|
|
result = func2->unk24 + func2->unk25;
|
|
} else if (func->type == INVENTORYFUNCTYPE_CLOSE && weaponnum != WEAPON_REAPER) {
|
|
result = 60;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Do pathfinding for a bot's Slayer rocket in fly-by-wire mode and populate
|
|
* the projectil's waypads.
|
|
*
|
|
* Return true if a route was found, false if not.
|
|
*/
|
|
bool botactFindRocketRoute(struct chrdata *chr, struct coord *frompos, struct coord *topos, s16 *fromrooms, s16 *torooms, struct projectile *projectile)
|
|
{
|
|
struct waypoint *from = waypointFindClosestToPos(frompos, fromrooms);
|
|
struct waypoint *to = waypointFindClosestToPos(topos, torooms);
|
|
struct waypoint *waypoints[6];
|
|
s32 numwaypoints;
|
|
|
|
if (from && to) {
|
|
s32 hash = (g_Vars.lvframe60 >> 9) * 128 + chr->chrnum * 8;
|
|
|
|
waypointSetHashThing(hash, hash);
|
|
numwaypoints = waypointFindRoute(from, to, waypoints, 6);
|
|
waypointSetHashThing(0, 0);
|
|
|
|
if (numwaypoints > 1) {
|
|
s32 i = 0;
|
|
|
|
while (waypoints[i]) {
|
|
projectile->waypads[i] = waypoints[i]->padnum;
|
|
i++;
|
|
}
|
|
|
|
projectile->step = 0;
|
|
projectile->numwaypads = i;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Populate pos with the position of the given pad
|
|
* for a Slayer rocket in fly-by-wire mode.
|
|
*
|
|
* It's the ground position of the pad plus 1.5 metres.
|
|
*/
|
|
void botactGetRocketNextStepPos(u16 padnum, struct coord *pos)
|
|
{
|
|
struct pad pad;
|
|
s16 rooms[2];
|
|
|
|
padUnpack(padnum, PADFIELD_ROOM | PADFIELD_POS, &pad);
|
|
|
|
rooms[0] = pad.room;
|
|
rooms[1] = -1;
|
|
|
|
pos->x = pad.pos.x;
|
|
pos->y = cd0002a36c(&pad.pos, rooms, 0, 0) + 150;
|
|
pos->z = pad.pos.z;
|
|
}
|
|
|
|
/**
|
|
* Create a Slayer rocket in fly-by-wire mode (ie. remote controlled).
|
|
*/
|
|
void botactCreateSlayerRocket(struct chrdata *chr)
|
|
{
|
|
struct weaponobj *rocket = weaponCreateProjectileFromWeaponNum(MODEL_CHRSKROCKETMIS, WEAPON_SKROCKET, chr);
|
|
|
|
if (rocket) {
|
|
Mtxf sp260;
|
|
Mtxf sp196;
|
|
Mtxf sp132;
|
|
struct coord sp120 = {0, 0, 0};
|
|
f32 yrot;
|
|
f32 xrot;
|
|
struct coord sp100;
|
|
|
|
yrot = chrGetAimAngle(chr);
|
|
xrot = func0f03e754(chr);
|
|
|
|
sp100.x = cosf(xrot) * sinf(yrot);
|
|
sp100.y = sinf(xrot);
|
|
sp100.z = cosf(xrot) * cosf(yrot);
|
|
|
|
mtx4LoadXRotation(xrot, &sp196);
|
|
mtx4LoadYRotation(yrot, &sp132);
|
|
mtx00015be0(&sp132, &sp196);
|
|
mtx4LoadIdentity(&sp260);
|
|
|
|
bgun0f09ebcc(&rocket->base, &chr->prop->pos, chr->prop->rooms, &sp196, &sp100, &sp260, chr->prop, &chr->prop->pos);
|
|
|
|
if (rocket->base.hidden & OBJHFLAG_AIRBORNE) {
|
|
struct prop *target = chrGetTargetProp(chr);
|
|
rocket->timer240 = -1;
|
|
rocket->base.projectile->unk010 = 7.5;
|
|
rocket->base.projectile->unk014 = xrot;
|
|
rocket->base.projectile->unk018 = yrot;
|
|
rocket->base.projectile->smoketimer240 = 0;
|
|
rocket->base.projectile->unk0b4 = 0x20000000;
|
|
|
|
// Fire rocket sound
|
|
propsnd0f0939f8(NULL, rocket->base.prop, SFX_LAUNCH_ROCKET_8053, -1,
|
|
-1, 0, 0, 0, 0, -1, 0, -1, -1, -1, -1);
|
|
|
|
if (!botactFindRocketRoute(chr, &chr->prop->pos, &target->pos, chr->prop->rooms, target->rooms, rocket->base.projectile)) {
|
|
rocket->timer240 = 0; // blow up rocket
|
|
} else {
|
|
botactGetRocketNextStepPos(rocket->base.projectile->waypads[0], &rocket->base.projectile->nextsteppos);
|
|
chr->aibot->skrocket = rocket->base.prop;
|
|
}
|
|
}
|
|
}
|
|
}
|