5641 lines
No EOL
166 KiB
C++
5641 lines
No EOL
166 KiB
C++
// m_heliai.cpp
|
|
#include "g_local.h"
|
|
#include "ai_private.h"
|
|
#include "m_generic.h"
|
|
#include "m_heliai.h"
|
|
#include "g_obj.h" // for aiming the gun and the heads
|
|
#include "callback.h"
|
|
#include "..\qcommon\ef_flags.h"
|
|
|
|
//#include "m_heli.h"
|
|
|
|
//#include <windows.h> // temp, purely for OutputDebugString(). Will be removed.
|
|
|
|
#ifndef max
|
|
#define max(a,b) (((a) > (b)) ? (a) : (b))
|
|
#endif
|
|
|
|
#ifndef min
|
|
#define min(a,b) (((a) < (b)) ? (a) : (b))
|
|
#endif
|
|
|
|
#define HELI_DIFFICULTY 3
|
|
|
|
typedef enum
|
|
{
|
|
HELIOBJ_HELI = 0,
|
|
HELIOBJ_BARREL,
|
|
HELIOBJ_BARREL_NULL,
|
|
HELIOBJ_HEAD,
|
|
HELIOBJ_ROTOR,
|
|
HELIOBJ_NAV_LITE1,
|
|
HELIOBJ_NAV_LITE2,
|
|
HELIOBJ_NAV_LITE3,
|
|
HELIOBJ_TAIL_LITE,
|
|
HELIOBJ_CANOPY,
|
|
HELIOBJ_HIND,
|
|
HELIOBJ_HIND_CANOPY,
|
|
HELIOBJ_MAXOBJS
|
|
};
|
|
|
|
modelSpawnData_t heliModelData[MD_HELI_SIZE] =
|
|
{
|
|
// dir file surfaceType material health solid material file cnt scale
|
|
"enemy/chopper", "cobrass", SURF_METAL, MAT_METAL_DGREY, 100, SOLID_BBOX, "cobra", 0, DEBRIS_SM, NULL, // HELIOBJ_HELI = 0,
|
|
"enemy/chopper", "barrel", SURF_METAL, MAT_METAL_DGREY, 100, SOLID_BBOX, "cobra", 0, DEBRIS_SM, NULL, // HELIOBJ_BARREL,
|
|
"objects/generic/gun_auto", "null", SURF_METAL, MAT_METAL_DGREY, 100, SOLID_BBOX, NULL, 0, DEBRIS_SM, NULL, // HELIOBJ_BARREL_NULL
|
|
"enemy/chopper", "head", SURF_METAL, MAT_METAL_DGREY, 100, SOLID_BBOX, "cobra", 0, DEBRIS_SM, NULL, // HELIOBJ_HEAD,
|
|
"enemy/chopper", "rotor", SURF_METAL, MAT_METAL_DGREY, 100, SOLID_BBOX, "cobra", 0, DEBRIS_SM, NULL, // HELIOBJ_ROTOR,
|
|
"enemy/chopper", "nav_lite", SURF_METAL, MAT_METAL_DGREY, 100, SOLID_BBOX, "cobra", 0, DEBRIS_SM, NULL, // HELIOBJ_NAV_LITE1,
|
|
"enemy/chopper", "nav_lite2", SURF_METAL, MAT_METAL_DGREY, 100, SOLID_BBOX, "cobra", 0, DEBRIS_SM, NULL, // HELIOBJ_NAV_LITE2,
|
|
"enemy/chopper", "nav_lite3", SURF_METAL, MAT_METAL_DGREY, 100, SOLID_BBOX, "cobra", 0, DEBRIS_SM, NULL, // HELIOBJ_NAV_LITE3,
|
|
"enemy/chopper", "tail_lite", SURF_METAL, MAT_METAL_DGREY, 100, SOLID_BBOX, "cobra", 0, DEBRIS_SM, NULL, // HELIOBJ_TAIL_LITE,
|
|
"enemy/chopper", "canopy", SURF_METAL, MAT_METAL_DGREY, 100, SOLID_BBOX, "cobra", 0, DEBRIS_SM, NULL, // HELIOBJ_CANOPY,
|
|
"enemy/hind", "hind", SURF_METAL, MAT_METAL_DGREY, 100, SOLID_BBOX, "hind", 0, DEBRIS_SM, NULL, // HELIOBJ_HIND,
|
|
"enemy/hind", "canopy", SURF_METAL, MAT_METAL_DGREY, 100, SOLID_BBOX, "hind", 0, DEBRIS_SM, NULL, // HELIOBJ_HIND_CANOPY,
|
|
};
|
|
|
|
// kef -- copy of FX_ fns of the same name on the client
|
|
int GetGhoulChildPosDir2(vec3_t sourcePos, vec3_t sourceAng, IGhoulInst *inst, GhoulID partID,
|
|
IGhoulInst *childInst, GhoulID childPartID, char *name, char *name2,
|
|
vec3_t pos, vec3_t dir, vec3_t right, vec3_t up);
|
|
int GetGhoulPosDir2(vec3_t sourcePos, vec3_t sourceAng, IGhoulInst *inst,
|
|
GhoulID partID, char *name, vec3_t pos, vec3_t dir, vec3_t right,
|
|
vec3_t up);
|
|
|
|
/// Someone please put this in the right header
|
|
void EntToWorldMatrix(vec3_t org, vec3_t angles, Matrix4 &m);
|
|
|
|
|
|
|
|
MainRotor2Callback theMainRotor2Callback;
|
|
MainHeliCallback2 theMainHeliCallback2;
|
|
|
|
bool MainRotor2Callback::Execute(IGhoulInst *me,void *ent,float time,const void *matrix)
|
|
{
|
|
edict_t* self = (edict_t*)ent;
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = NULL;
|
|
|
|
if (ai)
|
|
{
|
|
body = ai->GetHeliBody();
|
|
}
|
|
if (!body)
|
|
{
|
|
return false;
|
|
}
|
|
body->SetRotorEOS(true);
|
|
return true;
|
|
}
|
|
|
|
bool MainHeliCallback2::Execute(IGhoulInst *me,void *ent,float time,const void *matrix)
|
|
{
|
|
edict_t* self = (edict_t*)ent;
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = NULL;
|
|
|
|
if (ai)
|
|
{
|
|
body = ai->GetHeliBody();
|
|
}
|
|
if (!body)
|
|
{
|
|
return false;
|
|
}
|
|
body->SetFuselageEOS(true);
|
|
return true;
|
|
}
|
|
|
|
// ensure angle is in range -179..0..180
|
|
//
|
|
_inline float angle2posneg2 (float _angle)
|
|
{
|
|
_angle = anglemod(_angle); // ensure known-range
|
|
if (_angle>180)
|
|
_angle-=360;
|
|
|
|
return _angle;
|
|
}
|
|
|
|
|
|
float generic_ghoul_heli_ai::fHeliXWorldMin2 = -10000;
|
|
float generic_ghoul_heli_ai::fHeliXWorldMax2 = 10000;
|
|
float generic_ghoul_heli_ai::fHeliYWorldMin2 = -10000;
|
|
float generic_ghoul_heli_ai::fHeliYWorldMax2 = 10000;
|
|
float generic_ghoul_heli_ai::fHeliZWorldMin2 = -10000;
|
|
float generic_ghoul_heli_ai::fHeliZWorldMax2 = 10000;
|
|
|
|
|
|
void debug_drawbox(edict_t* self,vec3_t vOrigin, vec3_t vMins, vec3_t vMaxs, int nColor)
|
|
{
|
|
vec3_t vO;
|
|
vec3_t mns, mxs;
|
|
if (self)
|
|
{
|
|
VectorCopy(self->mins, mns);
|
|
VectorCopy(self->maxs, mxs);
|
|
}
|
|
else
|
|
{
|
|
VectorCopy(vMins, mns);
|
|
VectorCopy(vMaxs, mxs);
|
|
}
|
|
// corpse stuff override
|
|
// mns[0] = -8;
|
|
// mns[1] = -8;
|
|
// mxs[0] = 8;
|
|
// mxs[1] = 8;
|
|
// mxs[2] = 8;
|
|
|
|
vec3_t corners[] = {
|
|
{mns[0], mns[1], mns[2]},
|
|
{mns[0], mns[1], mxs[2]},
|
|
{mns[0], mxs[1], mns[2]},
|
|
{mns[0], mxs[1], mxs[2]},
|
|
{mxs[0], mns[1], mns[2]},
|
|
{mxs[0], mns[1], mxs[2]},
|
|
{mxs[0], mxs[1], mns[2]},
|
|
{mxs[0], mxs[1], mxs[2]}
|
|
};
|
|
|
|
if (self)
|
|
{
|
|
VectorCopy(self->s.origin, vO);
|
|
}
|
|
else
|
|
{
|
|
VectorCopy(vOrigin, vO);
|
|
}
|
|
paletteRGBA_t p;
|
|
float colors[] = {
|
|
0, 0, 64,
|
|
0, 0, 128,
|
|
0, 0, 192,
|
|
0, 0, 255,
|
|
0, 64, 0,
|
|
0, 128,0,
|
|
0, 192,0,
|
|
0, 255,0,
|
|
64, 0, 0,
|
|
128,0, 0,
|
|
192,0, 0,
|
|
255,0, 0
|
|
};
|
|
int lines[] = {
|
|
0, 1,
|
|
0, 2,
|
|
0, 4,
|
|
7, 6,
|
|
7, 5,
|
|
7, 3,
|
|
2, 3,
|
|
2, 6,
|
|
6, 4,
|
|
4, 5,
|
|
5, 1,
|
|
1, 3
|
|
};
|
|
vec3_t c1, c2;
|
|
int i = 0;
|
|
p.a = 250;
|
|
p.r = p.g = p.b = 0;
|
|
|
|
switch (nColor)
|
|
{
|
|
case 1:
|
|
p.r = 255;
|
|
break;
|
|
case 2:
|
|
p.g = 255;
|
|
break;
|
|
default:
|
|
if (nColor)
|
|
{
|
|
p.b = 255;
|
|
}
|
|
break;
|
|
}
|
|
for (i = 0; i < 12; i++)
|
|
{
|
|
if (0 == nColor)
|
|
{
|
|
p.r = colors[i*3];
|
|
p.g = colors[i*3+1];
|
|
p.b = colors[i*3+2];
|
|
}
|
|
VectorAdd(vO, corners[lines[i*2]], c1);
|
|
VectorAdd(vO, corners[lines[i*2+1]], c2);
|
|
FX_MakeLine(c1, c2, p); // need a better one than this
|
|
}
|
|
}
|
|
|
|
void generic_ghoul_heli_ai::Activate(edict_t &monster)
|
|
{
|
|
isActive=true;
|
|
}
|
|
|
|
void generic_ghoul_heli_ai::BuildHeliModel(edict_t *self, char *ghoulname, char *subclass)
|
|
{
|
|
// fuselage
|
|
ggObjC *cheli=NULL;
|
|
// bolt located on the fuselage (aka "bolter")
|
|
GhoulID cBolterBolt=0;
|
|
// bolt located on the bolted-on item (aka "boltee")
|
|
GhoulID cBolteeBolt=0;
|
|
ggOinstC *t=NULL;
|
|
ggBinstC *cBolteeBolted=NULL, *bInstBarrelNULL = NULL;
|
|
IGhoulInst *pBoltInst=NULL;
|
|
Matrix4 mat,mat1,mat2;
|
|
GhoulID tempNote=0;
|
|
bool bHind = (0 == strcmp("hind", subclass)); // heh heh. bHind. heh heh.
|
|
float fTailRotorScale = bHind?0.375:0.25f,
|
|
fMainRotorScale = bHind?1.875:1.25f;
|
|
|
|
|
|
// create the helicopter fuselage, init ghoul stuff, sequence
|
|
|
|
// gotta do this to make setsimpleghoulmodel happy
|
|
ent->s.renderfx &= ~RF_GHOUL;
|
|
|
|
if (bHind)
|
|
{
|
|
SimpleModelInit2(self, &heliModelData[HELIOBJ_HIND], NULL, NULL);
|
|
}
|
|
else
|
|
{
|
|
SimpleModelInit2(self, &heliModelData[HELIOBJ_HELI], NULL, NULL);
|
|
}
|
|
if (self->ghoulInst)
|
|
{
|
|
self->ghoulInst->SetUserData(self);
|
|
|
|
if (GetHeliBody())
|
|
{
|
|
//GetHeliBody()->SetFuselageObj(cheli);
|
|
//let body know about my inst, too
|
|
GetHeliBody()->SetHeliEdict(self);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
gi.dprintf("generic_ghoul_heli_ai::Init failed to create heli fuselage\n");
|
|
return;
|
|
}
|
|
// create some navigation lights
|
|
if (bHind)
|
|
{
|
|
}
|
|
else
|
|
{
|
|
cBolteeBolted = SimpleModelAddBolt(self, heliModelData[HELIOBJ_HELI], "nav_lite",
|
|
heliModelData[HELIOBJ_NAV_LITE1], "to_nav_lite", NULL);
|
|
if (cBolteeBolted)
|
|
{
|
|
SimpleModelSetSequence2(cBolteeBolted->GetInstPtr(),"nav_lite", SMSEQ_LOOP);
|
|
}
|
|
else
|
|
{
|
|
gi.dprintf("generic_ghoul_heli_ai::Init failed to create nav_lite bolton\n");
|
|
}
|
|
}
|
|
|
|
if (bHind)
|
|
{
|
|
}
|
|
else
|
|
{
|
|
cBolteeBolted = SimpleModelAddBolt(self, heliModelData[HELIOBJ_HELI], "nav_lite2",
|
|
heliModelData[HELIOBJ_NAV_LITE2], "to_nav_lite2", NULL);
|
|
if (cBolteeBolted)
|
|
{
|
|
SimpleModelSetSequence2(cBolteeBolted->GetInstPtr(),"nav_lite2", SMSEQ_LOOP);
|
|
}
|
|
else
|
|
{
|
|
gi.dprintf("generic_ghoul_heli_ai::Init failed to create nav_lite2 bolton\n");
|
|
}
|
|
}
|
|
|
|
if (bHind)
|
|
{
|
|
}
|
|
else
|
|
{
|
|
cBolteeBolted = SimpleModelAddBolt(self, heliModelData[HELIOBJ_HELI], "nav_lite3",
|
|
heliModelData[HELIOBJ_NAV_LITE3], "to_nav_lite3", NULL);
|
|
if (cBolteeBolted)
|
|
{
|
|
SimpleModelSetSequence2(cBolteeBolted->GetInstPtr(),"nav_lite3", SMSEQ_LOOP);
|
|
}
|
|
else
|
|
{
|
|
gi.dprintf("generic_ghoul_heli_ai::Init failed to create nav_lite3 bolton\n");
|
|
}
|
|
}
|
|
|
|
// create a tail light
|
|
if (bHind)
|
|
{
|
|
}
|
|
else
|
|
{
|
|
cBolteeBolted = SimpleModelAddBolt(self, heliModelData[HELIOBJ_HELI], "tail_lite",
|
|
heliModelData[HELIOBJ_TAIL_LITE], "to_tail_lite", NULL);
|
|
if (NULL == cBolteeBolted)
|
|
{
|
|
gi.dprintf("generic_ghoul_heli_ai::Init failed to create tail_lite bolton\n");
|
|
}
|
|
}
|
|
|
|
// create the gun
|
|
//
|
|
// step 1 -- create a NULL, attach it to the heli. this will allow yaw.
|
|
if (bHind)
|
|
{
|
|
cBolteeBolted = SimpleModelAddBolt(self, heliModelData[HELIOBJ_HIND], "barrel_base",
|
|
heliModelData[HELIOBJ_BARREL_NULL], "DUMMY01", NULL);
|
|
}
|
|
else
|
|
{
|
|
cBolteeBolted = SimpleModelAddBolt(self, heliModelData[HELIOBJ_HELI], "barrel_base",
|
|
heliModelData[HELIOBJ_BARREL_NULL], "DUMMY01", NULL);
|
|
}
|
|
if (cBolteeBolted)
|
|
{
|
|
bInstBarrelNULL = cBolteeBolted;
|
|
GetHeliBody()->SetBarrelNULLInst(cBolteeBolted);
|
|
if ((pBoltInst = cBolteeBolted->GetInstPtr()) && pBoltInst->GetGhoulObject())
|
|
{
|
|
GetHeliBody()->SetBarrelNULLBolt(pBoltInst->GetGhoulObject()->FindPart("DUMMY01"));
|
|
// the NULL actually has some geometry in it (ghoul made me do it :< ) so turn it off
|
|
SimpleModelTurnOnOff(pBoltInst, false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
gi.dprintf("generic_ghoul_heli_ai::Init failed to create NULL (for barrel) bolton\n");
|
|
}
|
|
|
|
// step 2 -- create the barrel, attach it to the NULL. this will allow pitch.
|
|
cBolteeBolted = ComplexModelAddBolt(bInstBarrelNULL, heliModelData[HELIOBJ_BARREL_NULL], "DUMMY01",
|
|
heliModelData[HELIOBJ_BARREL], "to_barrel_base", NULL);
|
|
if (cBolteeBolted)
|
|
{
|
|
GetHeliBody()->SetBarrelInst(cBolteeBolted);
|
|
if ((pBoltInst = cBolteeBolted->GetInstPtr()) && pBoltInst->GetGhoulObject())
|
|
{
|
|
GetHeliBody()->SetBarrelBolt(pBoltInst->GetGhoulObject()->FindPart("to_barrel_base"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
gi.dprintf("generic_ghoul_heli_ai::Init failed to create barrel bolton\n");
|
|
}
|
|
|
|
// create the gunner's head
|
|
if (bHind)
|
|
{
|
|
cBolteeBolted = SimpleModelAddBolt(self, heliModelData[HELIOBJ_HIND], "body1",
|
|
heliModelData[HELIOBJ_HEAD], "to_body", NULL);
|
|
}
|
|
else
|
|
{
|
|
cBolteeBolted = SimpleModelAddBolt(self, heliModelData[HELIOBJ_HELI], "body1",
|
|
heliModelData[HELIOBJ_HEAD], "to_body", NULL);
|
|
}
|
|
if (cBolteeBolted)
|
|
{
|
|
GetHeliBody()->SetGunnerInst(cBolteeBolted);
|
|
if ((pBoltInst = cBolteeBolted->GetInstPtr()) && pBoltInst->GetGhoulObject())
|
|
{
|
|
GetHeliBody()->SetGunnerBolt(pBoltInst->GetGhoulObject()->FindPart("to_body"));
|
|
GetHeliBody()->SetGunBoltOffset( 0/*84.795f*/, 0.0f, 6.411f);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
gi.dprintf("generic_ghoul_heli_ai::Init failed to create gunner's head bolton\n");
|
|
}
|
|
|
|
// create the pilot's head
|
|
if (bHind)
|
|
{
|
|
cBolteeBolted = SimpleModelAddBolt(self, heliModelData[HELIOBJ_HIND], "body2",
|
|
heliModelData[HELIOBJ_HEAD], "to_body", NULL);
|
|
}
|
|
else
|
|
{
|
|
cBolteeBolted = SimpleModelAddBolt(self, heliModelData[HELIOBJ_HELI], "body2",
|
|
heliModelData[HELIOBJ_HEAD], "to_body", NULL);
|
|
}
|
|
if (cBolteeBolted)
|
|
{
|
|
GetHeliBody()->SetPilotInst(cBolteeBolted);
|
|
if ((pBoltInst = cBolteeBolted->GetInstPtr()) && pBoltInst->GetGhoulObject())
|
|
{
|
|
GetHeliBody()->SetPilotBolt(pBoltInst->GetGhoulObject()->FindPart("to_body"));
|
|
GetHeliBody()->SetPilotBoltOffset(22/*44.215f*/, 0.0f, 12.935f);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
gi.dprintf("generic_ghoul_heli_ai::Init failed to create pilot's head bolton\n");
|
|
}
|
|
|
|
// create the tail rotor
|
|
if (bHind)
|
|
{
|
|
cBolteeBolted = SimpleModelAddBolt(self, heliModelData[HELIOBJ_HIND], "rotor_tail",
|
|
heliModelData[HELIOBJ_ROTOR], "to_rotor", NULL);
|
|
}
|
|
else
|
|
{
|
|
cBolteeBolted = SimpleModelAddBolt(self, heliModelData[HELIOBJ_HELI], "rotor_tail",
|
|
heliModelData[HELIOBJ_ROTOR], "to_rotor", NULL);
|
|
}
|
|
if (cBolteeBolted)
|
|
{
|
|
GetHeliBody()->SetTailRotorBoltInst(cBolteeBolted);
|
|
GetHeliBody()->SetTailRotorInst(cBolteeBolted);
|
|
if (pBoltInst = cBolteeBolted->GetInstPtr())
|
|
{
|
|
pBoltInst->GetXForm(mat);
|
|
mat1=mat2=mat;
|
|
mat2.Scale(fTailRotorScale);
|
|
mat.Concat(mat1, mat2);
|
|
pBoltInst->SetXForm(mat);
|
|
}
|
|
if (self->spawnflags & IN_AIR)
|
|
SimpleModelSetSequence2(pBoltInst,"rotor_anim", SMSEQ_LOOP);
|
|
else
|
|
SimpleModelSetSequence2(pBoltInst,"rotor", SMSEQ_LOOP);
|
|
}
|
|
else
|
|
{
|
|
gi.dprintf("generic_ghoul_heli_ai::Init failed to create tail rotor bolton\n");
|
|
}
|
|
|
|
// create the main rotor
|
|
if (bHind)
|
|
{
|
|
cBolteeBolted = SimpleModelAddBolt(self, heliModelData[HELIOBJ_HIND], "rotor_main",
|
|
heliModelData[HELIOBJ_ROTOR], "to_rotor", NULL);
|
|
}
|
|
else
|
|
{
|
|
cBolteeBolted = SimpleModelAddBolt(self, heliModelData[HELIOBJ_HELI], "rotor_main",
|
|
heliModelData[HELIOBJ_ROTOR], "to_rotor", NULL);
|
|
}
|
|
if (cBolteeBolted)
|
|
{
|
|
GetHeliBody()->SetMainRotorBoltInst(cBolteeBolted);
|
|
GetHeliBody()->SetMainRotorInst(cBolteeBolted);
|
|
if (pBoltInst = cBolteeBolted->GetInstPtr())
|
|
{
|
|
pBoltInst->GetXForm(mat);
|
|
mat1=mat2=mat;
|
|
mat2.Scale(fMainRotorScale);
|
|
mat.Concat(mat1, mat2);
|
|
pBoltInst->SetXForm(mat);
|
|
}
|
|
GetHeliBody()->SetRotorEOS(false);
|
|
if (self->spawnflags & IN_AIR)
|
|
SimpleModelSetSequence2(pBoltInst,"rotor_anim", SMSEQ_LOOP);
|
|
else
|
|
SimpleModelSetSequence2(pBoltInst,"rotor", SMSEQ_LOOP);
|
|
// register a callback for the main rotor's sequence
|
|
tempNote=pBoltInst->GetGhoulObject()->FindNoteToken("EOS");
|
|
if (tempNote && pBoltInst)
|
|
{
|
|
pBoltInst->AddNoteCallBack(&theMainRotor2Callback,tempNote);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
gi.dprintf("generic_ghoul_heli_ai::Init failed to create main rotor bolton\n");
|
|
}
|
|
|
|
// create the canopy for the cockpit
|
|
if (bHind)
|
|
{
|
|
cBolteeBolted = SimpleModelAddBolt(self, heliModelData[HELIOBJ_HIND], "canopy",
|
|
heliModelData[HELIOBJ_HIND_CANOPY], "to_canopy", NULL);
|
|
}
|
|
else
|
|
{
|
|
cBolteeBolted = SimpleModelAddBolt(self, heliModelData[HELIOBJ_HELI], "canopy",
|
|
heliModelData[HELIOBJ_CANOPY], "to_canopy", NULL);
|
|
}
|
|
if (cBolteeBolted)
|
|
{
|
|
GetHeliBody()->SetCockpitInst(cBolteeBolted);
|
|
if (pBoltInst = cBolteeBolted->GetInstPtr())
|
|
{
|
|
SimpleModelSetSequence2(pBoltInst,"canopy", SMSEQ_HOLD);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
gi.dprintf("generic_ghoul_heli_ai::Init failed to create canopy bolton\n");
|
|
}
|
|
|
|
// create our rotor sound
|
|
self->s.sound = gi.soundindex("Enemy/Helicopter/Blade.wav");
|
|
self->s.sound_data = (255 & ENT_VOL_MASK) | SND_FARATTN;
|
|
}
|
|
|
|
void generic_ghoul_heli_ai::Init(edict_t *self, char *ghoulname, char* subclass)
|
|
{
|
|
m_bActionDone = false;
|
|
m_bTimeToDie = false;
|
|
m_bPilotDead = false;
|
|
m_bGunnerDead = false;
|
|
m_attacker = NULL;
|
|
m_nActionSize = 0;
|
|
m_ScriptActionCounter = m_LastThinkingActionID = 0;
|
|
|
|
m_waypoints = NULL;
|
|
m_nNumWaypoints = m_nNumWaypointPts = m_nNumWaypointVols = 0;
|
|
|
|
BuildHeliModel(self, ghoulname, subclass);
|
|
}
|
|
|
|
void generic_ghoul_heli_ai::ChangeBody(edict_t *self, char *ghoulname, char* subclass)
|
|
{
|
|
float fMin = min(self->mins[0],self->mins[1]);
|
|
float fMax = max(self->maxs[0],self->maxs[1]);
|
|
float fXYRadius = max(fabs(fMin),fMax); // max radius dist from centre
|
|
float fZRadius = (max(fabs(self->mins[2]),self->maxs[2]))*2; // max radius dist from centre
|
|
float fAggressiveness = 0;
|
|
vec3_t vPad = {0,0,0}, vDeathDest = {0,0,0};
|
|
|
|
body_heli* oldbody = GetHeliBody(), *newbody = NULL;
|
|
|
|
if (oldbody)
|
|
{
|
|
fAggressiveness = oldbody->m_fAIAggressiveness;
|
|
VectorCopy(oldbody->m_v3HeliPadRestPos, vPad);
|
|
VectorCopy(oldbody->m_v3HeliDeathDest, vDeathDest);
|
|
}
|
|
body.Destroy();
|
|
game_ghoul.RemoveObjectInstances(self);
|
|
MyGhoulObj = NULL;
|
|
AddBody(self);
|
|
BuildHeliModel(self, ghoulname, subclass);
|
|
|
|
newbody = GetHeliBody();
|
|
|
|
if (newbody)
|
|
{
|
|
VectorSet(newbody->m_v3HeliTraceMins,-fXYRadius,-fXYRadius,-fZRadius);
|
|
VectorSet(newbody->m_v3HeliTraceMaxs, fXYRadius, fXYRadius, fZRadius);
|
|
VectorCopy(vPad, newbody->m_v3HeliPadRestPos);
|
|
VectorCopy(vDeathDest, newbody->m_v3HeliDeathDest);
|
|
newbody->m_fAIAggressiveness = fAggressiveness;
|
|
}
|
|
self->flags |= FL_NO_KNOCKBACK;
|
|
self->movetype = MOVETYPE_FLY;
|
|
self->solid = SOLID_BBOX;
|
|
self->takedamage= DAMAGE_YES;
|
|
self->pain = NULL;//CobraThink_Pain;
|
|
self->die = NULL;//CobraThink_Die;
|
|
}
|
|
|
|
|
|
generic_ghoul_heli_ai::~generic_ghoul_heli_ai()
|
|
{
|
|
heliWaypoint* temp = NULL;
|
|
while (m_waypoints)
|
|
{
|
|
temp = m_waypoints;
|
|
m_waypoints = m_waypoints->m_next;
|
|
m_nNumWaypoints--;
|
|
delete temp;
|
|
}
|
|
}
|
|
|
|
void generic_ghoul_heli_ai::AddBody(edict_t *monster)
|
|
{
|
|
if (!monster)
|
|
{
|
|
return;
|
|
}
|
|
if (!body)
|
|
{
|
|
body = new body_heli(monster);
|
|
}
|
|
}
|
|
|
|
action_c *generic_ghoul_heli_ai::MoveAction(decision_c *od, action_c *oa,
|
|
ai_c* ai, mmove_t *newanim, int nCommand, vec3_t vPos, edict_t* target, float fArg)
|
|
{
|
|
// uniquely identify each action generated by the script so we can poll it later in
|
|
//HelicopterDoneEvent::Process() to find out when it's done
|
|
m_ScriptActionCounter++;
|
|
return new helimove_action(od, oa, ai, newanim, nCommand, vPos, target, fArg, m_ScriptActionCounter);
|
|
}
|
|
|
|
action_c *generic_ghoul_heli_ai::FaceAction(decision_c *od, action_c *oa,
|
|
ai_c* ai, mmove_t *newanim, int nCommand, vec3_t vPos, edict_t* target, float fArg)
|
|
{
|
|
// uniquely identify each action generated by the script so we can poll it later in
|
|
//HelicopterDoneEvent::Process() to find out when it's done
|
|
m_ScriptActionCounter++;
|
|
return new heliface_action(od, oa, ai, newanim, nCommand, vPos, target, fArg, m_ScriptActionCounter);
|
|
}
|
|
|
|
action_c *generic_ghoul_heli_ai::AttackAction(decision_c *od, action_c *oa,
|
|
ai_c* ai, mmove_t *newanim, int nCommand, vec3_t vPos, edict_t* target, float fArg)
|
|
{
|
|
// uniquely identify each action generated by the script so we can poll it later in
|
|
//HelicopterDoneEvent::Process() to find out when it's done
|
|
m_ScriptActionCounter++;
|
|
return new heliattack_action(od, oa, ai, newanim, nCommand, vPos, target, fArg, m_ScriptActionCounter);
|
|
}
|
|
|
|
void generic_ghoul_heli_ai::SetCurrentActionThink(void (*think)(edict_t *self))
|
|
{
|
|
if (current_action)
|
|
{
|
|
((heli_action*)(action_c*)current_action)->SetThink(think);
|
|
}
|
|
}
|
|
|
|
void generic_ghoul_heli_ai::SetCurrentActionNextThink(void (*think)(edict_t *self))
|
|
{
|
|
if (current_action)
|
|
{
|
|
((heli_action*)(action_c*)current_action)->SetNextThink(think);
|
|
}
|
|
}
|
|
void EntToWorldMatrix(vec3_t org, vec3_t angles, Matrix4 &m);
|
|
|
|
static float s_fNextThink = 0; // only used when the heli dies in jpn3
|
|
|
|
void exploding_heli_final(edict_t *self)
|
|
{
|
|
G_FreeEdict(self);
|
|
}
|
|
|
|
void exploding_heli_remove(edict_t *self)
|
|
{
|
|
vec3_t vPos, vF;
|
|
generic_ghoul_heli_ai *ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli *body = ai->GetHeliBody();
|
|
|
|
// big explosion inside the heli fuselage
|
|
GetGhoulPosDir2(self->s.origin, self->s.angles, self->ghoulInst, 0, "ROTOR_MAIN1", vPos, vF, NULL, NULL);
|
|
fxRunner.exec("heli_expl", vPos, MULTICAST_ALL);
|
|
self->s.effects |= EF_EXPLODING;
|
|
// make the fuselage disappear
|
|
//SimpleModelTurnOnOff(self->ghoulInst, false);
|
|
|
|
// make the rotor smoke go away
|
|
body->UpdateSmoke(self, -1);
|
|
|
|
// in a few seconds make the whole deal go away
|
|
s_fNextThink = level.time + 5;
|
|
self->nextthink = level.time + FRAMETIME;
|
|
self->think = exploding_heli_final;
|
|
}
|
|
|
|
void exploding_heli_think2(edict_t *self)
|
|
{
|
|
IGhoulObj *obj = NULL;
|
|
GhoulID idBase = 0;
|
|
float xattenuation = ATTN_NORM, xvolume = 1.0;
|
|
|
|
// explosion at base of rotor
|
|
if(obj = self->ghoulInst->GetGhoulObject())
|
|
{
|
|
idBase = obj->FindPart("ROTOR_MAIN");
|
|
fxRunner.execWithInst("weapons/world/autogundeath", self,
|
|
self->ghoulInst, idBase);
|
|
gi.positioned_sound (self->s.origin, g_edicts, 0, gi.soundindex("impact/explosion/exp1.wav"), xvolume, xattenuation, 0);
|
|
}
|
|
|
|
s_fNextThink = level.time + 1;
|
|
self->nextthink = level.time + FRAMETIME;
|
|
self->think = exploding_heli_remove;
|
|
}
|
|
|
|
void exploding_heli_think(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai *ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli *body = ai->GetHeliBody();
|
|
IGhoulObj *obj = NULL;
|
|
GhoulID idCockpit = 0;
|
|
float xattenuation = ATTN_NORM, xvolume = 1.0;
|
|
|
|
if (body)
|
|
{
|
|
// remove pilot and gunner
|
|
if (body->GetGunnerInst())
|
|
{
|
|
body->GetGunnerInst()->SetOnOff(false,level.time);
|
|
}
|
|
if (body->GetPilotInst())
|
|
{
|
|
body->GetPilotInst()->SetOnOff(false,level.time);
|
|
}
|
|
}
|
|
|
|
// explosion inside cockpit
|
|
if(obj = self->ghoulInst->GetGhoulObject())
|
|
{
|
|
idCockpit = obj->FindPart("BODY1");
|
|
fxRunner.execWithInst("weapons/world/autogundeath", self,
|
|
self->ghoulInst, idCockpit);
|
|
gi.positioned_sound (self->s.origin, g_edicts, 0, gi.soundindex("impact/explosion/med.wav"), xvolume, xattenuation, 0);
|
|
}
|
|
|
|
s_fNextThink = level.time + .2;
|
|
self->nextthink = level.time + FRAMETIME;
|
|
self->think = exploding_heli_think2;
|
|
}
|
|
|
|
void generic_ghoul_heli_ai::Think(edict_t &monster)
|
|
{
|
|
int i = 0, nDifficulty = HELI_DIFFICULTY; // right now this is the number of shots fired per attack
|
|
|
|
// if we're dead, do some special stuff
|
|
monster.nextthink = level.time + FRAMETIME;
|
|
if (monster.deadflag == DEAD_DEAD)
|
|
{
|
|
monster.s.effects &= ~EF_EXPLODING;
|
|
monster.movetype = MOVETYPE_FLY;
|
|
monster.gravity = 0;
|
|
monster.velocity[2] = 0;
|
|
if (level.time >= s_fNextThink)
|
|
{
|
|
monster.think(&monster);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (ai_freeze->value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// debug_drawbox(&monster, NULL, NULL, NULL, 0);
|
|
// Com_Printf("%3.2f%%, %d hit points\n", (float)monster.health/(float)monster.max_health, monster.health);
|
|
// that Greg...
|
|
if (GetHeliBody() && GetHeliBody()->IsFiring())
|
|
{
|
|
IGhoulInst *instMuzzle = GetHeliBody()->GetBarrelInst();
|
|
IGhoulInst *instHeli = GetHeliBody()->GetHeliInst();
|
|
IGhoulObj *obj = NULL;
|
|
GhoulID idEndOfMuzzle = 0, idBoltHeliToMuzzle = 0;
|
|
if(instHeli && instMuzzle && (obj = instMuzzle->GetGhoulObject()) )
|
|
{
|
|
idEndOfMuzzle = obj->FindPart("flash_barrel");
|
|
fxRunner.execWithInst("weapons/othermz/heli", &monster,
|
|
instMuzzle, idEndOfMuzzle);
|
|
// actually attack our current target
|
|
vec3_t vWaver, vFirePoint, vFireDir, vFireDir2;
|
|
obj = instHeli->GetGhoulObject();
|
|
if(obj)
|
|
{
|
|
idBoltHeliToMuzzle = obj->FindPart("barrel_base");
|
|
|
|
GetGhoulChildPosDir2(monster.s.origin, monster.s.angles,
|
|
instHeli,
|
|
idBoltHeliToMuzzle,
|
|
instMuzzle,
|
|
idEndOfMuzzle,
|
|
NULL, NULL, vFirePoint, vFireDir, NULL, NULL); // this is way too long ;(
|
|
VectorScale(vFireDir, 100, vFireDir);
|
|
// for (i = 0; i < nDifficulty; i++)
|
|
{
|
|
VectorSet(vWaver, gi.irand(-10,10), gi.irand(-10,10), gi.irand(-10,10));
|
|
VectorAdd(vFireDir, vWaver, vFireDir2);
|
|
weapons.attack(ATK_HELIGUN, &monster, vFirePoint, vFireDir2);//same damage, but bigger impact sound
|
|
}
|
|
//gi.sound (self, CHAN_WEAPON, gi.soundindex("weapons/assault/fire.wav"), .9, .5, 0);//tihs wasn't here before - do we want it here?
|
|
}
|
|
}
|
|
}
|
|
EvaluateSenses(monster);
|
|
#if 0
|
|
//#ifdef _DEBUG
|
|
if (monster.spawnflags & DEBUG_WAYPOINTS)
|
|
{
|
|
// loop-limit number is how many to draw per cycle...
|
|
for (int iPlexPass=0; iPlexPass<1; iPlexPass++)
|
|
{
|
|
vec3_t vOrigin = {0,0,0},
|
|
vMins = {fHeliXWorldMin2,fHeliYWorldMin2,fHeliZWorldMin2},
|
|
vMaxs = {fHeliXWorldMax2,fHeliYWorldMax2,fHeliZWorldMax2};
|
|
|
|
|
|
static int iMasterPlex=0;
|
|
int iThisPlex=-1;
|
|
|
|
// draw sky box
|
|
if (++iThisPlex==iMasterPlex)
|
|
debug_drawbox(NULL, vOrigin, vMins, vMaxs, 1);
|
|
|
|
// draw heli box
|
|
if (++iThisPlex==iMasterPlex)
|
|
debug_drawbox(&monster, vOrigin, vMins, vMaxs, 0);
|
|
|
|
// draw volumes
|
|
int i = 0, n = GetNumWaypointVols();
|
|
heliWaypoint* waypt = NULL;
|
|
|
|
for (i = 0; i < n; i++)
|
|
{
|
|
if (++iThisPlex==iMasterPlex)
|
|
{
|
|
waypt = GetWaypointVol(i);
|
|
if (waypt)
|
|
{
|
|
waypt->GetVolumeMins(vMins);
|
|
waypt->GetVolumeMaxs(vMaxs);
|
|
debug_drawbox(NULL, vOrigin, vMins, vMaxs, 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
n = GetNumWaypointPts();
|
|
for (i = 0; i < n; i++)
|
|
{
|
|
if (++iThisPlex==iMasterPlex)
|
|
{
|
|
waypt = GetWaypointPt(i);
|
|
if (waypt)
|
|
{
|
|
VectorSet(vMins,-10,-10,-10);
|
|
VectorSet(vMaxs, 10, 10, 10);
|
|
debug_drawbox(NULL, waypt->m_v, vMins, vMaxs, 3);
|
|
}
|
|
}
|
|
}
|
|
if (++iMasterPlex == 2 + GetNumWaypointVols() + GetNumWaypointPts())
|
|
iMasterPlex = 0;
|
|
}// for (int iPlexPass=0; iPlexPass<5; iPlexPass++)
|
|
}// if (monster.spawnflags & DEBUG_WAYPOINTS)
|
|
#endif
|
|
// if someone just capped the pilot or gunner, we want the heli
|
|
//to go out of control and crash regardless of the current action.
|
|
if (GetHeliBody())
|
|
{
|
|
if (GetHeliBody()->IsPilotDead())
|
|
{
|
|
if (GetHeliBody()->IsGunnerDead())
|
|
{
|
|
CrewMemberKilled(&monster, HC_SKIN_BOTH);
|
|
}
|
|
else
|
|
{
|
|
CrewMemberKilled(&monster, HC_SKIN_PILOT);
|
|
}
|
|
}
|
|
else if (GetHeliBody()->IsGunnerDead())
|
|
{
|
|
CrewMemberKilled(&monster, HC_SKIN_GUNNER);
|
|
}
|
|
}
|
|
if (!current_action)
|
|
{ // do some thinking - this is not correct
|
|
if (actions.size())
|
|
{
|
|
NextAction(monster);
|
|
}
|
|
}
|
|
m_nActionSize = actions.size();
|
|
#if 0
|
|
#else
|
|
if (current_action)
|
|
{
|
|
m_LastThinkingActionID = ((heli_action*)(action_c*)current_action)->GetID();
|
|
if (current_action->Think(*this, monster))
|
|
{
|
|
NextAction(monster);
|
|
if (GetHeliBody())
|
|
{
|
|
GetHeliBody()->ResetScriptParams();
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
gi.linkentity(&monster);
|
|
|
|
if (monster.linkcount != linkcount)
|
|
{
|
|
linkcount = monster.linkcount;
|
|
gmonster.CheckGround (&monster);
|
|
}
|
|
gmonster.CatagorizePosition (&monster);
|
|
gmonster.WorldEffects (&monster);//leaving this in for drowning, lava damage, etc., but it should prolly be handled in ai class somewhere
|
|
|
|
//if monster is dead, consider removing it
|
|
if (m_bTimeToDie)
|
|
{
|
|
//get ai to poll actions, decisions, & senses to check if removal is ok
|
|
if (SafeToRemove(monster))
|
|
{
|
|
Die(monster, m_attacker, m_attacker, monster.max_health, vec3_origin);
|
|
if (monster.killtarget && (strcmp("musicscript", monster.killtarget)))
|
|
{ // hacked in ending for jpn3
|
|
vec3_t pos = {-1000, 744, 986};
|
|
VectorSet(monster.velocity, -10, 40, 0);
|
|
VectorCopy(pos,monster.s.origin);
|
|
monster.s.angles[ROLL] = -10;
|
|
s_fNextThink = level.time + 1;
|
|
monster.think = exploding_heli_think;
|
|
monster.nextthink = level.time + FRAMETIME;
|
|
}
|
|
else
|
|
{
|
|
G_FreeEdict (&monster);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void generic_ghoul_heli_ai::CrewMemberKilled(edict_t* self, int nSkin)
|
|
{
|
|
// make sure we're solid so we can hit something and explode now that we're out of control
|
|
self->movetype = MOVETYPE_FLY;
|
|
if ( !(GetHeliBody()->m_flags & HELIFLAG_ISDEADACKNOWLEDGED) )
|
|
{
|
|
// get rid of all actions currently available
|
|
list<action_c_ptr>::iterator ia;
|
|
vec3_t vPos = {0,0,0};
|
|
for (ia=actions.begin();ia!=actions.end();ia++)
|
|
{
|
|
(*ia).Destroy();
|
|
}
|
|
while (actions.size())
|
|
{
|
|
actions.pop_front();
|
|
}
|
|
|
|
//this may be unnecessary, but it makes me nervous seeing current_action just set to NULL--ss
|
|
if (current_action)
|
|
{
|
|
current_action.Destroy();
|
|
}
|
|
current_action = NULL;
|
|
|
|
// only send the heli out of control if it's not landed
|
|
if (self->s.origin[0] != GetHeliBody()->m_v3HeliPadRestPos[0] ||
|
|
self->s.origin[1] != GetHeliBody()->m_v3HeliPadRestPos[1] ||
|
|
self->s.origin[2] != GetHeliBody()->m_v3HeliPadRestPos[2])
|
|
{
|
|
// add a new action for going out of control
|
|
decision_c* DecisionPointer;
|
|
if (!(DecisionPointer = ((ai_c*)(this))->FindScriptedDecision(self)))
|
|
{
|
|
NewDecision(DecisionPointer = new scripted_decision(NULL,SCRIPTED_PRIORITY_ROOT,0,self),self);
|
|
}
|
|
NewAction( MoveAction(DecisionPointer,
|
|
NULL, (ai_c*)this, &MMoves[15],
|
|
generic_ghoul_heli_ai::hse_OUT_OF_CONTROL,
|
|
vPos, NULL, 0),self, true);
|
|
|
|
// alert our body to its imminent demise
|
|
GetHeliBody()->m_flags |= HELIFLAG_ISDEADACKNOWLEDGED;
|
|
}
|
|
if (lock_blood)
|
|
{
|
|
// no exploding heads for you
|
|
return;
|
|
}
|
|
// change skin on the cockpit
|
|
GetHeliBody()->ChangeCockpitSkin(nSkin);
|
|
// make the afflicted head disappear
|
|
if (nSkin == HC_SKIN_PILOT &&
|
|
GetHeliBody()->GetPilotInst())
|
|
{
|
|
GetHeliBody()->GetPilotInst()->SetOnOff(false,level.time);
|
|
}
|
|
else if (nSkin == HC_SKIN_GUNNER &&
|
|
GetHeliBody()->GetGunnerInst())
|
|
{
|
|
GetHeliBody()->GetGunnerInst()->SetOnOff(false,level.time);
|
|
}
|
|
else if (nSkin == HC_SKIN_BOTH &&
|
|
GetHeliBody()->GetGunnerInst() &&
|
|
GetHeliBody()->GetPilotInst())
|
|
{
|
|
GetHeliBody()->GetGunnerInst()->SetOnOff(false,level.time);
|
|
GetHeliBody()->GetPilotInst()->SetOnOff(false,level.time);
|
|
}
|
|
}
|
|
}
|
|
|
|
qboolean generic_ghoul_heli_ai::Damage (edict_t &monster, edict_t *inflictor,
|
|
edict_t *attacker, vec3_t dir, vec3_t point,
|
|
vec3_t origin, int damage, int knockback,
|
|
int dflags, int mod, float penetrate, float absorb)
|
|
{
|
|
// fixme: centralize skill level stuff
|
|
int take;
|
|
|
|
VectorNormalize(dir);
|
|
|
|
take = damage;
|
|
|
|
if (mod == MOD_C4)
|
|
{
|
|
return false;
|
|
}
|
|
if (body && take)
|
|
{
|
|
if(dflags&(DT_PROJECTILE|DT_EXPLODE))
|
|
{
|
|
take=damage=body->ShowDamage(monster, inflictor, attacker, dir, point, origin, damage, knockback, dflags, mod, penetrate, absorb);
|
|
}
|
|
}
|
|
|
|
// do the damage
|
|
if (take)
|
|
{
|
|
monster.health = monster.health - take;
|
|
|
|
if (monster.health <= 0)
|
|
{
|
|
m_attacker = attacker;
|
|
monster.flags |= FL_NO_KNOCKBACK;
|
|
if (GetHeliBody())
|
|
{
|
|
GetHeliBody()->m_flags |= HELIFLAG_ISDEAD;
|
|
}
|
|
monster.takedamage = DAMAGE_NO;
|
|
monster.nextthink = level.time + FRAMETIME; // should break heli out of PAUSE command if in one, but only private heli pause, not script main pause (unless that breaks out when you get shot anyway)
|
|
return true;
|
|
}
|
|
|
|
if(dflags&(DT_PROJECTILE|DT_EXPLODE))
|
|
{
|
|
Pain (monster, attacker, knockback, take);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;//no hit!
|
|
}
|
|
|
|
void generic_ghoul_heli_ai::Pain(edict_t &monster, edict_t *other, float kick, int damage)
|
|
{
|
|
float fHealthPercentage = (float)monster.health/(float)monster.max_health;
|
|
|
|
if (GetHeliBody())
|
|
{
|
|
GetHeliBody()->UpdateSmoke(&monster, fHealthPercentage);
|
|
}
|
|
}
|
|
|
|
void generic_ghoul_heli_ai::Die(edict_t &monster, edict_t *inflictor, edict_t *attacker,
|
|
int damage, vec3_t point)
|
|
{
|
|
vec3_t forward,to_dam;
|
|
edict_t *killTarget = NULL;
|
|
bool bSpecialDeath = false;
|
|
|
|
if (monster.health < -999)
|
|
{
|
|
monster.health = -999;
|
|
}
|
|
|
|
if (monster.killtarget)
|
|
{
|
|
bSpecialDeath = true;
|
|
while ((killTarget = G_Find (killTarget, FOFS(targetname), ent->killtarget)))
|
|
{
|
|
killTarget->use(killTarget, &monster, &monster);
|
|
}
|
|
}
|
|
|
|
monster.enemy = attacker;//for awarding credit?
|
|
// CHECKME possible issues:
|
|
// If monster is killed by barrel, should we chain to find who was the barrel's killer?
|
|
// what about being clever and using architecture to kill monsters (like 16 ton weights)
|
|
|
|
if (monster.deadflag != DEAD_DEAD)
|
|
{
|
|
vec3_t facedir;
|
|
list<action_c_ptr>::iterator ia;
|
|
list<sense_c_ptr>::iterator is;
|
|
list<decision_c_ptr>::iterator id;
|
|
|
|
|
|
/* if (attacker && game.GameStats->CanBuy(attacker))
|
|
{
|
|
//changed this to be conditional so it wouldn't crash --ss
|
|
if(CMonsterStats *MonsterStats = game.GameStats->GetMonsterStats(&monster))
|
|
{
|
|
GetPlayerStats(attacker)->AdjustCashToBeAwarded(MonsterStats->GetKilledValue());
|
|
}
|
|
else
|
|
{
|
|
gi.dprintf("Error: no stats for monster!\n");
|
|
}
|
|
// this stuff gets put into bank account at "end" of mission
|
|
}
|
|
*/
|
|
//fixme: do need the npc check, but do it a good way
|
|
// if (!(monster.monsterinfo.aiflags & AI_GOOD_GUY))
|
|
// {
|
|
level.killed_monsters++;
|
|
/* if (coop->value && attacker && attacker->client)
|
|
{
|
|
attacker->client->resp.score++;
|
|
}*/
|
|
// }
|
|
|
|
// huh? should have list-eating separated out?
|
|
|
|
for (is=senses.begin();is != senses.end();is++)
|
|
{
|
|
(*is).Destroy();
|
|
}
|
|
for (ia=actions.begin();ia!=actions.end();ia++)
|
|
{
|
|
(*ia).Destroy();
|
|
}
|
|
for (id=decisions.begin();id!=decisions.end();id++)
|
|
{
|
|
(*id).Destroy();
|
|
}
|
|
|
|
//now ditch nodes
|
|
|
|
while (senses.size())
|
|
{
|
|
senses.pop_front();
|
|
}
|
|
|
|
while (actions.size())
|
|
{
|
|
actions.pop_front();
|
|
}
|
|
while (decisions.size())
|
|
{
|
|
decisions.pop_front();
|
|
}
|
|
|
|
if (current_action)
|
|
{
|
|
current_action.Destroy();
|
|
}
|
|
|
|
if (recycle_action)
|
|
{
|
|
recycle_action.Destroy();
|
|
}
|
|
|
|
AngleVectors(monster.s.angles, forward, NULL, NULL);
|
|
VectorSubtract(point, monster.s.origin, to_dam);
|
|
VectorNormalize(to_dam);
|
|
|
|
monster.touch = NULL;//do we need this here?
|
|
monster.deadflag = DEAD_DEAD;
|
|
|
|
VectorSubtract(point,monster.s.origin,facedir);
|
|
VectorCopy(monster.s.angles, ideal_angles);
|
|
gi.linkentity (&monster);
|
|
}
|
|
|
|
if (bSpecialDeath && (strcmp("musicscript", monster.killtarget)))
|
|
{ // this means we had a kill target (like, say, in jpn3) and fired a script or something,
|
|
//so don't do the regular explosion
|
|
}
|
|
else
|
|
{
|
|
monster.s.effects |= EF_EXPLODING;
|
|
//FX_VehicleExplosion(monster.s.origin, 255);
|
|
fxRunner.exec("heli_expl", monster.s.origin);
|
|
//BlindingLight(monster.s.origin, 5000, 0.9, 0.5);
|
|
}
|
|
}
|
|
|
|
heliWaypoint* generic_ghoul_heli_ai::GetWaypointPt(int nWay)
|
|
{
|
|
if (nWay < 0 || nWay > m_nNumWaypointPts || 0 == m_nNumWaypoints)
|
|
{
|
|
return NULL;
|
|
}
|
|
int nPts = 0;
|
|
heliWaypoint* temp = m_waypoints;
|
|
while (temp)
|
|
{
|
|
if (temp->IsPoint())
|
|
{
|
|
if (nPts == nWay)
|
|
{
|
|
return temp;
|
|
}
|
|
nPts++;
|
|
}
|
|
temp = temp->m_next;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
heliWaypoint* generic_ghoul_heli_ai::GetWaypointVol(int nWay)
|
|
{
|
|
if (nWay < 0 || nWay > m_nNumWaypointVols || 0 == m_nNumWaypoints)
|
|
{
|
|
return NULL;
|
|
}
|
|
int nVols = 0;
|
|
heliWaypoint* temp = m_waypoints;
|
|
while (temp)
|
|
{
|
|
// if the heliWaypoint is, in fact, a volume and not just a point
|
|
if (!temp->IsPoint())
|
|
{
|
|
if (nVols == nWay)
|
|
{
|
|
return temp;
|
|
}
|
|
nVols++;
|
|
}
|
|
temp = temp->m_next;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
int generic_ghoul_heli_ai::AddWaypoint(vec3_t v)
|
|
{
|
|
heliWaypoint* newPt = new heliWaypoint(v), *temp = m_waypoints;
|
|
if (0 == m_nNumWaypoints)
|
|
{
|
|
m_waypoints = newPt;
|
|
}
|
|
else
|
|
{
|
|
while (temp->m_next)
|
|
{
|
|
temp = temp->m_next;
|
|
}
|
|
temp->m_next = newPt;
|
|
}
|
|
m_nNumWaypointPts++;
|
|
return ++m_nNumWaypoints;
|
|
}
|
|
|
|
qboolean generic_ghoul_heli_ai::SetWaypointPtVolume(int nWay, vec3_t v)
|
|
{
|
|
heliWaypoint* pt = GetWaypointPt(nWay);
|
|
vec3_t vClipMins, vClipMaxs;
|
|
|
|
if (pt && ent)
|
|
{
|
|
if (pt->IsPoint())
|
|
{
|
|
// turning a point into a volume
|
|
m_nNumWaypointPts--;
|
|
}
|
|
// perform any necessary clipping to reduce volume to a space
|
|
//in which we can place our body's origin, not just our bbox
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
if (v[i]*2 >= (ent->maxs[i] - ent->mins[i]))
|
|
{
|
|
vClipMins[i] = -v[i] - ent->mins[i];
|
|
vClipMaxs[i] = v[i] - ent->maxs[i];
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
m_nNumWaypointVols++;
|
|
VectorAdd(pt->m_v, vClipMins, vClipMins);
|
|
VectorAdd(pt->m_v, vClipMaxs, vClipMaxs);
|
|
pt->SetVolume(vClipMins, vClipMaxs);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
int generic_ghoul_heli_ai::GetCurrentActionID()
|
|
{
|
|
if (current_action)
|
|
{
|
|
return ((heli_action*)(action_c*)current_action)->GetID();
|
|
}
|
|
else
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
int generic_ghoul_heli_ai::GetMostRecentlyAddedActionID()
|
|
{
|
|
return m_ScriptActionCounter;
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
// begin: stuff copied directly from m_heli.cpp
|
|
////////////////////////////////////////////////////////////////////////
|
|
/*
|
|
#define HELIFLAG_DOINGSCRIPTEDLANDING 0x00000001 // #
|
|
#define HELIFLAG_ROTORSCHANGINGSPEED 0x00000002
|
|
#define HELIFLAG_SIDESTRAFEISTOLEFT 0x00000004
|
|
#define HELIFLAG_ROCKETSENABLED 0x00000008
|
|
#define HELIFLAG_CHAINGUNENABLED 0x00000010
|
|
#define HELIFLAG_MGUN_GHOULSEQ_RUNNING 0x00000020 // special use thingy
|
|
#define HELIFLAG_AUTOFIRE 0x00000040
|
|
#define HELIFLAG_DOINGSCRIPTEDHOVER 0x00000080 // #
|
|
#define HELIFLAG_HOVERISAGGRESIVE 0x00000100
|
|
#define HELIFLAG_DOINGSCRIPTEDFIREAT 0x00000200 // #
|
|
*/
|
|
|
|
void Cobra2Think_TakeOff(edict_t *self)
|
|
{
|
|
ggBinstC* boltMain=NULL, *boltTail=NULL;
|
|
ggObjC* objMain=NULL, *objTail=NULL;
|
|
IGhoulInst* instMainRotor = NULL, *instTailRotor = NULL;
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
// were we waiting for rotors to windup to speed?
|
|
//
|
|
if (self->spawnflags & IN_AIR)
|
|
{
|
|
#ifdef _DEBUG
|
|
#ifdef SHOW_DESIGNER_ERRORS
|
|
gi.dprintf("### HELISCRIPT: Can't takeoff when spawned in air!\n");
|
|
#endif
|
|
#endif
|
|
ai->ActionIsDone(true);
|
|
return;
|
|
}
|
|
instMainRotor = body->GetMainRotorInst();
|
|
instTailRotor = body->GetTailRotorInst();
|
|
if (body->m_flags & HELIFLAG_ROTORSCHANGINGSPEED)
|
|
{
|
|
if (!body->GetRotorEOS())
|
|
return;
|
|
|
|
// rotors at full speed, switch to normal looping anim...
|
|
//
|
|
//if (objMain && boltMain && objTail && boltTail)
|
|
if (instMainRotor && instTailRotor)
|
|
{
|
|
SimpleModelSetSequence2(instMainRotor, "rotor_anim", SMSEQ_LOOP);
|
|
SimpleModelSetSequence2(instTailRotor, "rotor_anim", SMSEQ_LOOP);
|
|
}
|
|
body->m_flags&= ~HELIFLAG_ROTORSCHANGINGSPEED;
|
|
}
|
|
else
|
|
{
|
|
// time to start rotor windup...
|
|
//
|
|
if (instMainRotor && instTailRotor)
|
|
{
|
|
SimpleModelSetSequence2(instMainRotor, "rotor_start", SMSEQ_HOLD);
|
|
SimpleModelSetSequence2(instTailRotor, "rotor_start", SMSEQ_HOLD);
|
|
body->SetRotorEOS(false);
|
|
body->m_flags |= HELIFLAG_ROTORSCHANGINGSPEED;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// set dest as being a pos above us...
|
|
//
|
|
vec3_t vDest;
|
|
VectorCopy(self->s.origin,vDest);
|
|
vDest[2] += 56*4; // climb to 4 man-heights above pad initially
|
|
Cobra2Think_SetDumbFlyDest(self, vDest);
|
|
}// void Cobra2Think_TakeOff(edict_t *self)
|
|
|
|
void Cobra2_InitTurnStuff(edict_t *self, vec3_t v3)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
body->SetPosStop(v3);
|
|
body->SetStartYaw(self->s.angles[HELI_YAW]);
|
|
}
|
|
|
|
void Cobra2Think_HeadToDest(edict_t *self)
|
|
{
|
|
// interesting bugette here, if I set the pilot's head to look at the dest target every cycle then when
|
|
// we get to the end of a move he suddenly tries to look backwards over his right shoulder because he himself
|
|
// is beyond the dest point (I think that's why, anyway), so to compensate we should project a vector further
|
|
// along (past the end point) for him to look at...(the vector being at least the size of the heli)
|
|
//
|
|
// note that the value worked out is constant, ie could be determined at move start rather than every cycle, but
|
|
// apparently field-space within entities is at a premium so trade it off for CPU work instead...
|
|
//
|
|
vec3_t v3Gradient;
|
|
vec3_t v3HeliDims;
|
|
vec3_t v3LookDest;
|
|
float fHeliLength;
|
|
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
Vector_GetHalfWayPoint(body->m_v3HeliDesiredPos_Start, body->m_v3HeliDesiredPos_Stop, body->m_v3HeliDesiredPos_Halfway);
|
|
VectorSubtract (body->m_v3HeliDesiredPos_Stop,body->m_v3HeliDesiredPos_Start,v3Gradient);
|
|
VectorNormalize(v3Gradient);
|
|
VectorSubtract (self->maxs, self->mins, v3HeliDims);
|
|
fHeliLength = VectorLength(v3HeliDims);
|
|
VectorMA(body->m_v3HeliDesiredPos_Stop, fHeliLength, v3Gradient, v3LookDest);
|
|
Cobra2AimPilotAtPos(self, v3LookDest);
|
|
// aim the gunner/gun as well, but only if we're going to fire at something on arrival... :-)
|
|
//
|
|
if (body->m_flags & HELIFLAG_DOINGSCRIPTEDFIREAT)
|
|
Cobra2AimGunAtPos(self, body->m_v3HeliFireAtCoords, true); // qboolean bSynchGunner
|
|
else
|
|
{
|
|
if ( (body->m_flags & HELIFLAG_AUTOFIRE) && (body->m_flags & HELIFLAG_CHAINGUNENABLED) ) // do not OR these 2 into one "(flag & val)" check
|
|
{
|
|
// keith's seek-to-45-degree-pitch code...
|
|
//
|
|
vec3_t vF;
|
|
|
|
AngleVectors(self->s.angles, vF, NULL, NULL);
|
|
vF[2]=0;
|
|
VectorNormalize(vF);
|
|
vec3_t v3;
|
|
VectorSet(v3,0,0,-1);
|
|
VectorAdd(vF,v3,v3);
|
|
Cobra2AimGunInDirection(self, v3, true); // qboolean bSynchGunner
|
|
}
|
|
else
|
|
Cobra2ResetGunner(self);
|
|
}
|
|
|
|
// Helicopters can (and do) head towards things then start turning to face them once they're on the way, so
|
|
// head in that dir regardless, do the turn-to-face stuff as you go...
|
|
//
|
|
|
|
// have we reached our dest?...
|
|
//
|
|
if (fabs (self->s.origin[0] - body->m_v3HeliDesiredPos_Stop[0]) <= body->m_fHeliMoveSpeed &&
|
|
fabs (self->s.origin[1] - body->m_v3HeliDesiredPos_Stop[1]) <= body->m_fHeliMoveSpeed &&
|
|
fabs (self->s.origin[2] - body->m_v3HeliDesiredPos_Stop[2]) <= body->m_fHeliMoveSpeed
|
|
)
|
|
{
|
|
// we're reached it (or close enough), so do final position adjustement to make it exact...
|
|
//
|
|
VectorCopy(body->m_v3HeliDesiredPos_Stop,self->s.origin);
|
|
body->m_fHeliMoveSpeed = 0;
|
|
|
|
// now detonate if this is the right time...
|
|
//
|
|
if (body->m_flags & HELIFLAG_ISDEADFINALGOTO)
|
|
{
|
|
ai->SetCurrentActionNextThink(Cobra2_Explode);
|
|
return;
|
|
}
|
|
|
|
ai->SetCurrentActionNextThink(Cobra2Think_MovementFinish);
|
|
}
|
|
else
|
|
{// not reached dest...
|
|
|
|
//
|
|
// acceleration/pitch logic here...
|
|
//
|
|
qboolean qbBeyondHalfWay =
|
|
WithinOrBeyondVectors2(self->s.origin, body->m_v3HeliDesiredPos_Halfway,
|
|
body->m_v3HeliDesiredPos_Stop);
|
|
if (!qbBeyondHalfWay || (body->m_flags & HELIFLAG_ISDEADFINALGOTO)) // keep accelerating during death plunge
|
|
{
|
|
// accel...
|
|
//
|
|
body->m_fHeliMoveSpeed += HELI_COBRA_ACCEL;
|
|
|
|
// pitch. First, how far are we along the first half of the journey as a %age?...
|
|
//
|
|
vec3_t v3TotLengthVect,v3LengthVect;
|
|
VectorSubtract(body->m_v3HeliDesiredPos_Halfway, body->m_v3HeliDesiredPos_Start, v3TotLengthVect); // journey dist
|
|
VectorSubtract(body->m_v3HeliDesiredPos_Halfway, self->s.origin, v3LengthVect); // dist so far
|
|
v3TotLengthVect[2] = v3LengthVect[2] = 0; // lose the Z component
|
|
|
|
// get the inverse percentage...
|
|
// (broken into if-checked lines because of sqrt() returning float-errors occasionally when zeros occur)
|
|
float fTotLen = VectorLength(v3TotLengthVect);
|
|
if (fTotLen>0)
|
|
{
|
|
float fSoFarLen = VectorLength(v3LengthVect);
|
|
if (fSoFarLen>0)
|
|
{
|
|
float fDistPercent = 100-((100/fTotLen) * fSoFarLen); // 100%= start, 0 = reached halfway (end of accel)
|
|
float fIdealPitch = -(HELI_COBRA_MAXPITCH-((fDistPercent * HELI_COBRA_MAXPITCH)/100)); // HELI_COBRA_MAXPITCH..0
|
|
if (qbBeyondHalfWay)
|
|
fIdealPitch = 0;
|
|
fIdealPitch *= -1; // kef
|
|
|
|
// now just adjust our pitch to suit...
|
|
//
|
|
if (self->s.angles[HELI_PITCH] + HELI_COBRA_PITCHADJSPEED <= fIdealPitch) // kef
|
|
self->s.angles[HELI_PITCH] +=HELI_COBRA_PITCHADJSPEED;
|
|
else
|
|
if (self->s.angles[HELI_PITCH] - HELI_COBRA_PITCHADJSPEED >= fIdealPitch) // kef
|
|
self->s.angles[HELI_PITCH] -=HELI_COBRA_PITCHADJSPEED;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// decel...
|
|
//
|
|
body->m_fHeliMoveSpeed -= HELI_COBRA_ACCEL;
|
|
if (body->m_fHeliMoveSpeed < HELI_COBRA_MIN_HEAD2DESTSPEED) // arb, should be z, but this ensure we keep moving if we fall a little short
|
|
body->m_fHeliMoveSpeed = HELI_COBRA_MIN_HEAD2DESTSPEED;
|
|
|
|
// pitch. First, how far are we along the 2nd half of the journey as a %age?...
|
|
//
|
|
vec3_t v3TotLengthVect,v3LengthVect;
|
|
VectorSubtract(body->m_v3HeliDesiredPos_Stop, body->m_v3HeliDesiredPos_Halfway, v3TotLengthVect); // journey dist
|
|
VectorSubtract(body->m_v3HeliDesiredPos_Stop, self->s.origin, v3LengthVect); // dist so far
|
|
v3TotLengthVect[2] = v3LengthVect[2] = 0; // lose the Z component
|
|
|
|
// get the percentage...
|
|
// (broken into if-checked lines because of sqrt() returning float-errors occasionally when zeros occur)
|
|
float fTotLen = VectorLength(v3TotLengthVect);
|
|
if (fTotLen>0)
|
|
{
|
|
float fSoFarLen = VectorLength(v3LengthVect);
|
|
if (fSoFarLen>0)
|
|
{
|
|
float fDistPercent = (100/fTotLen) * fSoFarLen;
|
|
float fIdealPitch = HELI_COBRA_MAXPITCH_DECEL-((fDistPercent * HELI_COBRA_MAXPITCH_DECEL)/100); // 0..HELI_COBRA_MAXPITCH_DECEL
|
|
if (body->m_fHeliMoveSpeed < 4)
|
|
{
|
|
fIdealPitch = 0;
|
|
}
|
|
fIdealPitch *= -1; // kef
|
|
|
|
// now just adjust our pitch to suit...
|
|
//
|
|
self->s.angles[HELI_PITCH] = angle2posneg (self->s.angles[HELI_PITCH]);
|
|
if (self->s.angles[HELI_PITCH] + HELI_COBRA_PITCHADJSPEED_DECEL <= fIdealPitch) // kef
|
|
self->s.angles[HELI_PITCH] +=HELI_COBRA_PITCHADJSPEED_DECEL;
|
|
else
|
|
if (self->s.angles[HELI_PITCH] - HELI_COBRA_PITCHADJSPEED_DECEL >= fIdealPitch) // kef
|
|
self->s.angles[HELI_PITCH] -=HELI_COBRA_PITCHADJSPEED_DECEL;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// actual move...
|
|
//
|
|
vec3_t v;
|
|
|
|
VectorSubtract(body->m_v3HeliDesiredPos_Stop,self->s.origin,v);
|
|
VectorNormalize(v);
|
|
VectorMA(self->s.origin, (body->m_fHeliMoveSpeed<HELI_COBRA_MAXSPEED || (body->m_flags & HELIFLAG_ISDEADFINALGOTO))?
|
|
body->m_fHeliMoveSpeed:HELI_COBRA_MAXSPEED, v, self->s.origin);
|
|
|
|
// swing round to face it as we move... (and record for firing decision later)
|
|
//
|
|
float fDegreesToGo;
|
|
Cobra2_TurnToFace(self, body->m_v3HeliDesiredPos_Stop,
|
|
false, // qbRoll
|
|
false, // qbYAWDictatesFiring
|
|
&fDegreesToGo // float *pfDegreesToGo
|
|
);
|
|
|
|
// should we be firing?...
|
|
//
|
|
if (body->GetFlags() & HELIFLAG_AUTOFIRE)
|
|
{
|
|
// check YAW is within (say) 20 degrees either side of desired final ,
|
|
// and that heli is facing downwards (not up into air on decel phase)......
|
|
//
|
|
// (update, greg wants it firing almost all the time, so check based on speed instead)
|
|
//
|
|
if (fDegreesToGo<20 && //self->s.angles[HELI_PITCH]<0
|
|
body->m_fHeliMoveSpeed>(HELI_COBRA_MAXSPEED/10)
|
|
)
|
|
{
|
|
// (-ve pitch is nose-down)
|
|
Cobra2_Fire(self, (self->s.angles[HELI_PITCH]<=0)? // +ve pitch is nose-up
|
|
true:false // qboolean qbInihibitRockets
|
|
);
|
|
}
|
|
else
|
|
Cobra2_Fire_OFF(self);
|
|
|
|
}// if (dwHeliFlags & HELIFLAG_AUTOFIRE)
|
|
|
|
}// not reached dest...
|
|
}// void Cobra2Think_HeadToDest(edict_t *self)
|
|
|
|
int Cobra2AimHeadAtPos(edict_t *self, vec3_t vTarget, IGhoulInst* head, GhoulID bolt,
|
|
vec3_t vOffset)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
int bRet = AIM_TRUE;
|
|
boltonOrientation_c boltonInfo;
|
|
|
|
if (!head || !bolt)
|
|
return AIM_ERROR;
|
|
|
|
boltonInfo.root = self;
|
|
boltonInfo.boltonInst = head;
|
|
boltonInfo.boltonID = bolt;
|
|
boltonInfo.parentInst = self->ghoulInst;
|
|
VectorCopy(vTarget, boltonInfo.vTarget);
|
|
boltonInfo.fMinPitch = -GUN_PITCH_UP;
|
|
boltonInfo.fMaxPitch = GUN_PITCH_DOWN;
|
|
boltonInfo.fMinYaw = GUN_YAW_CCW;
|
|
boltonInfo.fMaxYaw = GUN_YAW_CW;
|
|
boltonInfo.fMaxTurnSpeed = AIM_MAX_TURN_DELTA;
|
|
boltonInfo.bUsePitch = false;
|
|
boltonInfo.bUseYaw = true;
|
|
boltonInfo.bToRoot = true;
|
|
|
|
return boltonInfo.OrientBolton();
|
|
|
|
}
|
|
|
|
int Cobra2AimGunnerAtPos(edict_t *self, vec3_t vTarget)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
int bRet = AIM_TRUE;
|
|
|
|
vec3_t vTargetCopy, vOffset;
|
|
|
|
VectorCopy(vTarget, vTargetCopy);
|
|
body->GetGunnerBoltOffset(vOffset);
|
|
bRet = Cobra2AimHeadAtPos(self, vTargetCopy, body->GetGunnerInst(),
|
|
body->GetGunnerBolt(), vOffset);
|
|
return bRet;
|
|
}
|
|
|
|
int Cobra2AimPilotAtPos(edict_t *self, vec3_t vTarget)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
int bRet = AIM_TRUE;
|
|
|
|
vec3_t vTargetCopy, vOffset;
|
|
|
|
VectorCopy(vTarget, vTargetCopy);
|
|
body->GetPilotBoltOffset(vOffset);
|
|
bRet = Cobra2AimHeadAtPos(self, vTargetCopy, body->GetPilotInst(),
|
|
body->GetPilotBolt(), vOffset);
|
|
return bRet;
|
|
}
|
|
|
|
// set self->think to this funct when a movement such as turntoface, strafetodest, headtodest etc has finished
|
|
// and this'll take care of ensuring pitch/roll/pilot head levelled out...
|
|
//
|
|
void Cobra2Think_MovementFinish(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
int bRet = AIM_TRUE;
|
|
|
|
if (body->GetFlags() & HELIFLAG_AUTOFIRE)
|
|
Cobra2_Fire_OFF(self);
|
|
|
|
if (Cobra2MovementFinished(self))
|
|
{
|
|
ai->ActionIsDone(true);
|
|
}
|
|
}// void Cobra2Think_MovementFinish(edict_t *self)
|
|
|
|
|
|
// called at the end of a firing sequence, basically just used to clean up some ghoul anim stuff...
|
|
//
|
|
void Cobra2_Fire_OFF(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
if (body->GetFlags() & HELIFLAG_CHAINGUNENABLED)
|
|
Cobra2FireGun_Off(self);
|
|
}
|
|
|
|
// Do not try and use the helicopter after you've called this!!!!
|
|
void Cobra2_Explode(edict_t *self)
|
|
{
|
|
// let the ai know it needs to explode and go away now
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
ai->TimeToDie(true);
|
|
ai->ActionIsDone(true);
|
|
}
|
|
|
|
// called by above, kept seperate so it can also be called by landing code...
|
|
//
|
|
qboolean Cobra2MovementFinished(edict_t *self)
|
|
{
|
|
// we must do these calls even if we early-ret later...
|
|
//
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
qboolean qbRollFinished = Cobra2_EnsureRollFinished(self);
|
|
int iPilotStatus = Cobra2ResetPilot(self);
|
|
int iGunnerStatus = ( (body->m_flags & HELIFLAG_AUTOFIRE) && // do not OR these 2 into one "(flag & val)" check
|
|
(body->m_flags & HELIFLAG_CHAINGUNENABLED)
|
|
)
|
|
?Cobra2ResetGunner(self):AIM_TRUE;
|
|
//
|
|
// level off before going into hover...
|
|
//
|
|
if (self->s.angles[HELI_PITCH] > 180)
|
|
{
|
|
self->s.angles[HELI_PITCH] = 360 - self->s.angles[HELI_PITCH];
|
|
}
|
|
else if (self->s.angles[HELI_PITCH] < -180)
|
|
{
|
|
self->s.angles[HELI_PITCH] = self->s.angles[HELI_PITCH] + 360;
|
|
}
|
|
if ( (self->s.angles[HELI_PITCH] >= 0 && self->s.angles[HELI_PITCH] <= HELI_COBRA_PITCHADJSPEED) ||
|
|
(self->s.angles[HELI_PITCH] <= 0 && self->s.angles[HELI_PITCH] >= -HELI_COBRA_PITCHADJSPEED)
|
|
)
|
|
self->s.angles[HELI_PITCH] = 0;
|
|
else
|
|
{
|
|
// pitch needs leveling...
|
|
//
|
|
if (self->s.angles[HELI_PITCH] < -HELI_COBRA_PITCHADJSPEED)
|
|
{
|
|
self->s.angles[HELI_PITCH] += HELI_COBRA_PITCHADJSPEED; // kef
|
|
}
|
|
else
|
|
{
|
|
self->s.angles[HELI_PITCH] -= HELI_COBRA_PITCHADJSPEED; // kef
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!qbRollFinished)// || iPilotStatus == AIM_FALSE || iGunnerStatus == AIM_FALSE)
|
|
return false;
|
|
|
|
// levelled off pitch and roll, and pilot yaw'd back to default...
|
|
//
|
|
return true;
|
|
|
|
}// qboolean Cobra2MovementFinished(edict_t *self)
|
|
|
|
// adjusts heli YAW by a certain amount, returns TRUE if now facing the desired direction...
|
|
//
|
|
qboolean Cobra2_TurnToFace(edict_t *self, vec3_t v3WorldPos, qboolean qbRoll, qboolean qbYAWDictatesFiring, float *pfDegreesToGo)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
qboolean qbTurningLeft;
|
|
vec3_t v;
|
|
|
|
// new bit: special check, if target dir differs only in Z then just return, saves the effect of zero-diff always causing
|
|
// it to face east when changing Z position only during HeadToDest...
|
|
//
|
|
if (self->s.origin[0] == v3WorldPos[0] && self->s.origin[1] == v3WorldPos[1])
|
|
{
|
|
if (pfDegreesToGo)
|
|
*pfDegreesToGo = 0;
|
|
return true;
|
|
}
|
|
|
|
// gradually alter our facing angle (YAW) to suit travel dir...
|
|
//
|
|
VectorSubtract(v3WorldPos,self->s.origin,v);
|
|
|
|
vec3_t v3DirToFace;
|
|
vectoangles(v,v3DirToFace);
|
|
|
|
// v3DirToFace[HELI_YAW] -= 90;
|
|
// v3DirToFace[HELI_YAW] = anglemod(v3DirToFace[HELI_YAW]);
|
|
|
|
// this line is needed to avoid tail-skittering when on autofire in HeadToDest (which converts the YAW to -180..0..180)
|
|
//
|
|
// self->s.angles[HELI_YAW] = anglemod(self->s.angles[HELI_YAW]);
|
|
if (self->s.angles[HELI_YAW] > 180)
|
|
{
|
|
self->s.angles[HELI_YAW] -= 360;
|
|
}
|
|
|
|
float fMove = v3DirToFace[HELI_YAW] - self->s.angles[HELI_YAW];
|
|
|
|
if (fabs(fMove) <= HELI_YAWADJ_SPEED)
|
|
{
|
|
self->s.angles[HELI_YAW] = v3DirToFace[HELI_YAW];
|
|
if (pfDegreesToGo)
|
|
*pfDegreesToGo = 0;
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
if (pfDegreesToGo)
|
|
*pfDegreesToGo = fabs(fMove);
|
|
|
|
fMove = anglemod(fMove);
|
|
|
|
if (fMove<180)
|
|
{
|
|
// turn to left...
|
|
//
|
|
self->s.angles[HELI_YAW] += HELI_YAWADJ_SPEED;
|
|
qbTurningLeft = true;
|
|
}
|
|
else
|
|
{
|
|
// turn to right...
|
|
//
|
|
self->s.angles[HELI_YAW] -= HELI_YAWADJ_SPEED;
|
|
qbTurningLeft = false;
|
|
}
|
|
|
|
self->s.angles[HELI_YAW] = anglemod(self->s.angles[HELI_YAW]);
|
|
|
|
if (qbRoll)
|
|
{
|
|
// now handle ROLL...
|
|
// (note that this can be much simpler if accel/decel max roll angles are forced to be the same, but they're not, so...
|
|
//
|
|
// work out how far we are along the turn, and therefore our ideal ROLL angle...
|
|
//
|
|
float fDegreesInTurn = v3DirToFace[HELI_YAW] - body->m_fHeliTurnStartYaw;
|
|
fDegreesInTurn = anglemod(fDegreesInTurn);
|
|
if (fDegreesInTurn > 180)
|
|
fDegreesInTurn = fabs(360-fDegreesInTurn);
|
|
float fHalfWay = fDegreesInTurn/2;
|
|
float fCurrentTurnDegs= self->s.angles[HELI_YAW] - body->m_fHeliTurnStartYaw;
|
|
fCurrentTurnDegs= anglemod(fCurrentTurnDegs);
|
|
if (fCurrentTurnDegs > 180)
|
|
fCurrentTurnDegs = fabs(360-fCurrentTurnDegs);
|
|
|
|
// how far are we from midpoint in the turn as a percentage?... (with 0% at centre, 100% at edges)
|
|
//
|
|
float fPercent = (100/fHalfWay) * (fabs(fCurrentTurnDegs-fHalfWay));
|
|
|
|
// (should we be firing, by the way?)
|
|
//
|
|
if (qbYAWDictatesFiring && (body->m_flags & HELIFLAG_AUTOFIRE))
|
|
{
|
|
if (fPercent<90)
|
|
Cobra2_Fire(self,false); // qboolean qbInihibitRockets);
|
|
else
|
|
Cobra2_Fire_OFF(self);
|
|
}
|
|
|
|
// so are we on the accel or decel phase?...
|
|
//
|
|
if (fCurrentTurnDegs<fHalfWay)
|
|
{
|
|
// accel phase...
|
|
//
|
|
float fIdealRoll = (fPercent * HELI_COBRA_MAXROLL)/100; // 0..HELI_COBRA_MAXROLL
|
|
|
|
if (qbTurningLeft)
|
|
fIdealRoll = -fIdealRoll;
|
|
|
|
// make sure 0..360 angle is -180..0..180...
|
|
//
|
|
if (self->s.angles[HELI_ROLL]>180)
|
|
self->s.angles[HELI_ROLL]-= 360;
|
|
|
|
if (fabs(self->s.angles[HELI_ROLL] - fIdealRoll) <= HELI_ROLLADJ_SPEED)
|
|
self->s.angles[HELI_ROLL] = fIdealRoll;
|
|
else
|
|
{
|
|
if (self->s.angles[HELI_ROLL] < fIdealRoll)
|
|
self->s.angles[HELI_ROLL] += HELI_ROLLADJ_SPEED;
|
|
else
|
|
self->s.angles[HELI_ROLL] -= HELI_ROLLADJ_SPEED;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// decel phase...
|
|
//
|
|
float fIdealRoll = (fPercent * HELI_COBRA_MAXROLL_DECEL)/100; // 0..HELI_COBRA_MAXROLL_DECEL
|
|
|
|
if (!qbTurningLeft)
|
|
fIdealRoll = -fIdealRoll;
|
|
|
|
// make sure 0..360 angle is -180..0..180...
|
|
//
|
|
if (self->s.angles[HELI_ROLL]>180)
|
|
self->s.angles[HELI_ROLL]-= 360;
|
|
|
|
if (fabs(self->s.angles[HELI_ROLL] - fIdealRoll) <= HELI_ROLLADJ_SPEED)
|
|
self->s.angles[HELI_ROLL] = fIdealRoll;
|
|
else
|
|
{
|
|
if (self->s.angles[HELI_ROLL] < fIdealRoll)
|
|
self->s.angles[HELI_ROLL] += HELI_ROLLADJ_SPEED;
|
|
else
|
|
self->s.angles[HELI_ROLL] -= HELI_ROLLADJ_SPEED;
|
|
}
|
|
}
|
|
}// if (qbRoll)
|
|
}
|
|
|
|
return false;
|
|
|
|
}// qboolean Cobra2_TurnToFace(edict_t *self, vec3_t v3WorldPos, qboolean qbRoll, qboolean qbYAWDictatesFiring, float *pfDegreesToGo)
|
|
|
|
// feed this our current pos, the halfway point along our root, and the end point
|
|
// returns FALSE if not yet halfway, else TRUE for anything else.
|
|
//
|
|
qboolean WithinOrBeyondVectors2(vec3_t v3Me, vec3_t v3Halfway, vec3_t v3End)
|
|
{
|
|
// sigh, we need to cope with stupid rounding errors, because of when the debugger shows (eg) 452.50 & 452.50 and yet
|
|
// says that one is bigger than the other because of some tiny rounding errors that it doesn't display.
|
|
|
|
// X...
|
|
//
|
|
if (fabs(v3Halfway[0]-v3End[0]) > 1.0f)
|
|
{
|
|
if (v3Halfway[0] < v3End[0])
|
|
{
|
|
if (v3Me[0]<v3Halfway[0])
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (v3Me[0]>v3Halfway[0])
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Y...
|
|
//
|
|
if (fabs(v3Halfway[1]-v3End[1]) > 1.0f)
|
|
{
|
|
if (v3Halfway[1] < v3End[1])
|
|
{
|
|
if (v3Me[1]<v3Halfway[1])
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (v3Me[1]>v3Halfway[1])
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Z...
|
|
//
|
|
if (fabs(v3Halfway[2]-v3End[2]) > 1.0f)
|
|
{
|
|
if (v3Halfway[2] < v3End[2])
|
|
{
|
|
if (v3Me[2]<v3Halfway[2])
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (v3Me[2]>v3Halfway[2])
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
|
|
}// qboolean WithinOrBeyondVectors(vec3_t v3Me, vec3_t v3Halfway, vec3_t v3End)
|
|
|
|
void Cobra2FireRockets(edict_t *self, int nLeftRockets, int nRightRockets)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
IGhoulInst *instHeli = NULL;
|
|
GhoulID idBoltLeftRocketPod, idBoltRightRocketPod;
|
|
IGhoulObj *obj = NULL;
|
|
//vec3_t vWaver;
|
|
vec3_t vTemp, vF, vR, vU, vFirePoint, vFireDir;
|
|
int i = 0;
|
|
|
|
instHeli = self->ghoulInst;
|
|
if (NULL == instHeli)
|
|
{
|
|
return;
|
|
}
|
|
|
|
obj = instHeli->GetGhoulObject();
|
|
if(NULL == obj)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AngleVectors(self->s.angles, vF, vR, vU);
|
|
VectorScale(vF, 5.0, vF);
|
|
VectorScale(vR, 5.0, vR);
|
|
VectorScale(vU, 5.0, vU);
|
|
|
|
// firing rocket from left pod
|
|
if (nLeftRockets > 0)
|
|
{
|
|
// get id of bolt on the front of left rocket pod
|
|
idBoltLeftRocketPod = obj->FindPart("FLASH_ROCKET1");
|
|
|
|
|
|
// actually attack our current target
|
|
|
|
GetGhoulPosDir2(self->s.origin, self->s.angles,
|
|
instHeli,
|
|
idBoltLeftRocketPod,
|
|
NULL, vTemp, vFireDir, NULL, NULL);
|
|
{
|
|
switch(nLeftRockets)
|
|
{
|
|
case 1:
|
|
VectorAdd(vTemp, vR, vFirePoint);
|
|
VectorAdd(vFirePoint, vU, vFirePoint);
|
|
break;
|
|
case 2:
|
|
VectorAdd(vTemp, vR, vFirePoint);
|
|
VectorSubtract(vFirePoint, vU, vFirePoint);
|
|
break;
|
|
case 3:
|
|
VectorSubtract(vTemp, vR, vFirePoint);
|
|
VectorSubtract(vFirePoint, vU, vFirePoint);
|
|
break;
|
|
case 4:
|
|
VectorSubtract(vTemp, vR, vFirePoint);
|
|
VectorAdd(vFirePoint, vU, vFirePoint);
|
|
break;
|
|
}
|
|
|
|
// launch the rocket
|
|
|
|
// instead of pitching the heli, we'll just fire the rocket at the target
|
|
//regardless of z difference
|
|
edict_t *targ = body->m_AITarget?body->m_AITarget:Cobra2_GetEntToTarget(self);
|
|
VectorSubtract(targ->s.origin, self->s.origin, vFireDir);
|
|
VectorNormalize(vFireDir);
|
|
fireHeliRocket2(self, vFirePoint, vFireDir);
|
|
}
|
|
}
|
|
// firing rocket from right pod
|
|
if (nRightRockets > 0)
|
|
{
|
|
// get id of bolt on the front of right rocket pod
|
|
idBoltRightRocketPod = obj->FindPart("FLASH_ROCKET2");
|
|
|
|
// actually attack our current target
|
|
GetGhoulPosDir2(self->s.origin, self->s.angles,
|
|
instHeli,
|
|
idBoltRightRocketPod,
|
|
NULL, vTemp, vFireDir, NULL, NULL);
|
|
{
|
|
switch(nRightRockets)
|
|
{
|
|
case 1:
|
|
VectorAdd(vTemp, vR, vFirePoint);
|
|
VectorAdd(vFirePoint, vU, vFirePoint);
|
|
break;
|
|
case 2:
|
|
VectorAdd(vTemp, vR, vFirePoint);
|
|
VectorSubtract(vFirePoint, vU, vFirePoint);
|
|
break;
|
|
case 3:
|
|
VectorSubtract(vTemp, vR, vFirePoint);
|
|
VectorSubtract(vFirePoint, vU, vFirePoint);
|
|
break;
|
|
case 4:
|
|
VectorSubtract(vTemp, vR, vFirePoint);
|
|
VectorAdd(vFirePoint, vU, vFirePoint);
|
|
break;
|
|
}
|
|
|
|
// launch the rocket
|
|
|
|
// instead of pitching the heli, we'll just fire the rocket at the target
|
|
//regardless of z difference
|
|
edict_t *targ = body->m_AITarget?body->m_AITarget:Cobra2_GetEntToTarget(self);
|
|
VectorSubtract(targ->s.origin, self->s.origin, vFireDir);
|
|
VectorNormalize(vFireDir);
|
|
fireHeliRocket2(self, vFirePoint, vFireDir);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Cobra2FireGun(edict_t *self, qboolean bSporadic)
|
|
{
|
|
IGhoulInst *instHeli = NULL, *instMuzzle = NULL;
|
|
GhoulID idBoltHeliToMuzzle, idEndOfMuzzle;
|
|
IGhoulObj *obj = NULL;
|
|
GhoulID seq = 0;
|
|
vec3_t vFirePoint, vFireDir, vFireDir2;
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
int i = 0, nDifficulty =HELI_DIFFICULTY;
|
|
|
|
if (body->m_nRandGunWait || !bSporadic)
|
|
{
|
|
if (body->m_bGunFiring || !bSporadic)
|
|
{
|
|
instHeli = self->ghoulInst;
|
|
if (NULL == instHeli)
|
|
{
|
|
return;
|
|
}
|
|
instMuzzle = body->GetBarrelInst();
|
|
if (NULL == instMuzzle)
|
|
{
|
|
return;
|
|
}
|
|
if (!(body->m_flags & HELIFLAG_MGUN_GHOULSEQ_RUNNING))
|
|
{
|
|
body->m_flags |= HELIFLAG_MGUN_GHOULSEQ_RUNNING;
|
|
|
|
// play the barrel sequence with attached flickering light
|
|
if (body->GetBarrelInst())
|
|
{
|
|
SimpleModelSetSequence2(body->GetBarrelInst(), "barrel", SMSEQ_LOOP);
|
|
}
|
|
}
|
|
// get id of bolt that connects the gun barrel to the heli
|
|
obj = instHeli->GetGhoulObject();
|
|
if(NULL == obj)
|
|
{
|
|
return;
|
|
}
|
|
idBoltHeliToMuzzle = obj->FindPart("barrel_base");
|
|
|
|
// get id of the muzzle of the gun barrel
|
|
obj = instMuzzle->GetGhoulObject();
|
|
if(NULL == obj)
|
|
{
|
|
return;
|
|
}
|
|
idEndOfMuzzle = obj->FindPart("flash_barrel");
|
|
|
|
// instantaneous muzzle flash
|
|
// FX_HeliGun(self, instHeli, instMuzzle, idBoltHeliToMuzzle, idEndOfMuzzle);
|
|
fxRunner.execWithInst("weapons/othermz/heli", self, instMuzzle, idEndOfMuzzle);
|
|
|
|
// actually attack our current target
|
|
vec3_t vWaver;
|
|
|
|
GetGhoulChildPosDir2(self->s.origin, self->s.angles,
|
|
instHeli,
|
|
idBoltHeliToMuzzle,
|
|
instMuzzle,
|
|
idEndOfMuzzle,
|
|
NULL, NULL, vFirePoint, vFireDir, NULL, NULL); // this is way too long ;(
|
|
VectorScale(vFireDir, 100, vFireDir);
|
|
// for (i = 0; i < nDifficulty; i++)
|
|
if (body->m_fArg != 1337)
|
|
{ // introduce some waver to our accuracy
|
|
VectorSet(vWaver, gi.irand(-10,10), gi.irand(-10,10), gi.irand(-10,10));
|
|
VectorAdd(vFireDir, vWaver, vFireDir2);
|
|
}
|
|
else
|
|
{ // the heck with waver, we want to hit a particular molecule
|
|
VectorCopy(vFireDir, vFireDir2);
|
|
}
|
|
weapons.attack(ATK_HELIGUN, self, vFirePoint, vFireDir2);//same damage, but bigger impact sound
|
|
gi.sound (self, CHAN_WEAPON, gi.soundindex("weapons/assault/fire.wav"), .9, .5, 0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
body->m_nRandGunWait = gi.irand(2, 10);
|
|
body->m_bGunFiring = !body->m_bGunFiring;
|
|
}
|
|
body->m_nRandGunWait--;
|
|
|
|
}
|
|
|
|
// stop the ghoul mgun barrel sequence when I stop firing...
|
|
//
|
|
void Cobra2FireGun_Off(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
if (body->m_flags & HELIFLAG_MGUN_GHOULSEQ_RUNNING)
|
|
{
|
|
body->m_flags &= ~HELIFLAG_MGUN_GHOULSEQ_RUNNING;
|
|
|
|
if (body->GetBarrelInst())
|
|
{
|
|
SimpleModelSetSequence2(body->GetBarrelInst(), "barrel_still", SMSEQ_HOLD);
|
|
}
|
|
}
|
|
}
|
|
|
|
// returns TRUE when ROLL has leveled off ok, called by both the turn and move code
|
|
//
|
|
qboolean Cobra2_EnsureRollFinished(edict_t *self)
|
|
{
|
|
#define HELI_COBRA_ROLLADJSPEED_LEVELOUT 4
|
|
|
|
self->s.angles[HELI_ROLL] = anglemod(self->s.angles[HELI_ROLL]);
|
|
|
|
if ( (self->s.angles[HELI_ROLL] == 0) ||
|
|
(self->s.angles[HELI_ROLL] + HELI_COBRA_ROLLADJSPEED_LEVELOUT > 360) ||
|
|
(self->s.angles[HELI_ROLL] - HELI_COBRA_ROLLADJSPEED_LEVELOUT < 0)
|
|
)
|
|
{
|
|
self->s.angles[HELI_ROLL] = 0;
|
|
return true;
|
|
}
|
|
|
|
if (self->s.angles[HELI_ROLL] > 180)
|
|
self->s.angles[HELI_ROLL] += HELI_COBRA_ROLLADJSPEED_LEVELOUT;
|
|
else
|
|
self->s.angles[HELI_ROLL] -= HELI_COBRA_ROLLADJSPEED_LEVELOUT;
|
|
|
|
return false;
|
|
}
|
|
|
|
int Cobra2ResetHead(edict_t *self, IGhoulInst* head, GhoulID bolt, vec3_t vOffset)
|
|
{
|
|
vec3_t vF, vR, vPos;
|
|
|
|
AngleVectors(self->s.angles, vF, vR, NULL);
|
|
VectorMA(self->s.origin, 100, vF, vPos);
|
|
return Cobra2AimHeadAtPos(self, vPos, head, bolt, vOffset);
|
|
}
|
|
|
|
int Cobra2ResetPilot(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
vec3_t vOffset;
|
|
|
|
body->GetPilotBoltOffset(vOffset);
|
|
return Cobra2ResetHead(self, body->GetPilotInst(), body->GetPilotBolt(), vOffset);
|
|
}
|
|
|
|
|
|
int GetGhoulChildPosDir2(vec3_t sourcePos, vec3_t sourceAng, IGhoulInst *inst, GhoulID partID,
|
|
IGhoulInst *childInst, GhoulID childPartID, char *name, char *name2, vec3_t pos, vec3_t dir, vec3_t right, vec3_t up)
|
|
{
|
|
IGhoulObj *obj = inst->GetGhoulObject();
|
|
IGhoulObj *obj2 = childInst->GetGhoulObject();
|
|
|
|
if(obj && obj2)
|
|
{
|
|
GhoulID part = (partID) ? partID:obj->FindPart(name);
|
|
GhoulID part2 = (childPartID) ? childPartID:obj2->FindPart(name2);
|
|
|
|
if(part && part2)
|
|
{
|
|
Matrix4 EntityToWorld;
|
|
EntToWorldMatrix(sourcePos,sourceAng,EntityToWorld);
|
|
|
|
Matrix4 childToEntity;
|
|
childInst->GetBoltMatrix(level.time, childToEntity, part2, IGhoulInst::MatrixType::Entity);
|
|
|
|
Matrix4 BoltToWorld;
|
|
BoltToWorld.Concat(childToEntity, EntityToWorld);
|
|
|
|
|
|
Vect3 ChunkLoc;
|
|
BoltToWorld.GetRow(3,ChunkLoc); //inital pos of chunk, will be unchanged.
|
|
// this works too BoltToWorld.XFormPoint(ChunkLoc,Vect3(0.0f,0.0f,0.0f));
|
|
|
|
VectorCopy((float *)&ChunkLoc, pos);
|
|
|
|
if (dir)
|
|
{
|
|
Vect3 zaxisV;
|
|
BoltToWorld.GetRow(2,zaxisV);
|
|
VectorCopy((float *)&zaxisV, dir);
|
|
}
|
|
|
|
if(right)
|
|
{
|
|
Vect3 yaxisV;
|
|
BoltToWorld.GetRow(1,yaxisV);
|
|
VectorCopy((float *)&yaxisV, right);
|
|
}
|
|
if(up)
|
|
{
|
|
Vect3 xaxisV;
|
|
BoltToWorld.GetRow(0,xaxisV);
|
|
VectorCopy((float *)&xaxisV, up);
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
VectorClear(pos);
|
|
VectorClear(dir);
|
|
return 0;
|
|
}
|
|
|
|
int GetGhoulPosDir2(vec3_t sourcePos, vec3_t sourceAng, IGhoulInst *inst,
|
|
GhoulID partID, char *name, vec3_t pos, vec3_t dir, vec3_t right, vec3_t up)
|
|
{
|
|
IGhoulObj *obj = inst->GetGhoulObject();
|
|
|
|
if(obj)
|
|
{
|
|
GhoulID part = (partID) ? partID:obj->FindPart(name);
|
|
|
|
if(part)
|
|
{
|
|
Matrix4 EntityToWorld;
|
|
EntToWorldMatrix(sourcePos,sourceAng,EntityToWorld);
|
|
|
|
Matrix4 BoltToWorld;
|
|
Matrix4 BoltToEntity;
|
|
|
|
inst->GetBoltMatrix(level.time, BoltToEntity, part, IGhoulInst::MatrixType::Entity);
|
|
BoltToWorld.Concat(BoltToEntity,EntityToWorld);
|
|
|
|
Vect3 ChunkLoc;
|
|
BoltToWorld.GetRow(3,ChunkLoc);
|
|
|
|
VectorCopy((float *)&ChunkLoc, pos);
|
|
|
|
Vect3 zaxisV;
|
|
BoltToWorld.GetRow(2,zaxisV);
|
|
VectorCopy((float *)&zaxisV, dir);
|
|
|
|
if(right)
|
|
{
|
|
Vect3 yaxisV;
|
|
BoltToWorld.GetRow(1,yaxisV);
|
|
VectorCopy((float *)&yaxisV, right);
|
|
}
|
|
if(up)
|
|
{
|
|
Vect3 xaxisV;
|
|
BoltToWorld.GetRow(0,xaxisV);
|
|
VectorCopy((float *)&xaxisV, up);
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
VectorClear(pos);
|
|
VectorClear(dir);
|
|
return 0;
|
|
}
|
|
|
|
qboolean Cobra2TargetWithinArc(vec3_t vTargetPos, vec3_t vHeliAngles, vec3_t vHeliPos)
|
|
{
|
|
// check the angle between heli's forward vector and the
|
|
//target direction vector
|
|
|
|
// default is a 160deg firing arc (80deg to either side of forward vector)
|
|
float fArcCos = 0.173f; // cos(80degrees)
|
|
|
|
float fDot = fArcCos;
|
|
vec3_t vForward;
|
|
vec3_t vTargetDir;
|
|
int i = 0;
|
|
|
|
// make sure angles are in the range [0, 360)
|
|
for (i=0;i<3;i++)
|
|
{
|
|
vHeliAngles[i]=anglemod(vHeliAngles[i]);
|
|
}
|
|
|
|
VectorSubtract(vTargetPos, vHeliPos, vTargetDir);
|
|
VectorNormalize(vTargetDir);
|
|
AngleVectors(vHeliAngles, vForward, NULL, NULL);
|
|
fDot = DotProduct(vForward, vTargetDir);
|
|
if (fDot >= fArcCos && fDot <= 1.0f)
|
|
{
|
|
// current target is within our firing arc
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// we already know when this is called that the target is valid, ie a live player or merc...
|
|
//
|
|
qboolean Cobra2HasTargetLOS(edict_t *self, edict_t *target, qboolean qbAccountForArc)
|
|
{
|
|
trace_t tr;
|
|
|
|
vec3_t v3TargetEyeLevel;
|
|
VectorCopy(target->s.origin, v3TargetEyeLevel);
|
|
v3TargetEyeLevel[2] += target->viewheight;
|
|
|
|
gi.trace(self->s.origin, NULL, NULL, v3TargetEyeLevel, self, MASK_OPAQUE, &tr);
|
|
if (tr.fraction == 1.0 && (!qbAccountForArc || Cobra2TargetWithinArc(tr.endpos, self->s.angles, self->s.origin)))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
int Cobra2AimGunAtPos(edict_t *self, vec3_t vTarget, qboolean bSynchGunner)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
int bRet = AIM_TRUE;
|
|
IGhoulInst* barrelInst = body->GetBarrelInst();
|
|
GhoulID barrelBolt = body->GetBarrelBolt();
|
|
boltonOrientation_c boltonInfo;
|
|
|
|
if (!barrelInst || !barrelBolt)
|
|
return AIM_ERROR;
|
|
if (bSynchGunner)
|
|
{
|
|
Cobra2AimGunnerAtPos(self, vTarget);
|
|
}
|
|
|
|
// first, yaw the NULL to which the barrel is attached
|
|
boltonInfo.root = self;
|
|
boltonInfo.boltonInst = body->GetBarrelNULLInst();
|
|
boltonInfo.boltonID = body->GetBarrelNULLBolt();
|
|
boltonInfo.parentInst = self->ghoulInst;
|
|
VectorCopy(vTarget, boltonInfo.vTarget);
|
|
boltonInfo.fMinPitch = 0;
|
|
boltonInfo.fMaxPitch = 0;
|
|
boltonInfo.fMinYaw = GUN_YAW_CCW;
|
|
boltonInfo.fMaxYaw = GUN_YAW_CW;
|
|
boltonInfo.fMaxTurnSpeed = AIM_MAX_TURN_DELTA;
|
|
boltonInfo.bUsePitch = false;
|
|
boltonInfo.bUseYaw = true;
|
|
boltonInfo.bToRoot = true;
|
|
boltonInfo.OrientBolton();
|
|
|
|
// now pitch the barrel itself
|
|
boltonInfo.parentInst = boltonInfo.boltonInst;
|
|
boltonInfo.parentID = boltonInfo.boltonID;
|
|
boltonInfo.boltonInst = body->GetBarrelInst();
|
|
boltonInfo.boltonID = body->GetBarrelBolt();
|
|
boltonInfo.fMinPitch = -GUN_PITCH_UP;
|
|
boltonInfo.fMaxPitch = GUN_PITCH_DOWN;
|
|
boltonInfo.fMinYaw = 0;
|
|
boltonInfo.fMaxYaw = 0;
|
|
boltonInfo.fMaxTurnSpeed = AIM_MAX_TURN_DELTA;
|
|
boltonInfo.bUsePitch = true;
|
|
boltonInfo.bUseYaw = false;
|
|
boltonInfo.bToRoot = true;
|
|
|
|
return boltonInfo.OrientBolton();
|
|
}
|
|
|
|
int Cobra2AimGunInDirection(edict_t *self, vec3_t vTargetDir, qboolean bSynchGunner)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
int bRet = AIM_TRUE;
|
|
IGhoulInst* barrelInst = body->GetBarrelInst();
|
|
GhoulID barrelBolt = body->GetBarrelBolt();
|
|
boltonOrientation_c boltonInfo;
|
|
vec3_t vPos, vF;
|
|
|
|
if (!barrelInst || !barrelBolt)
|
|
return AIM_ERROR;
|
|
if (bSynchGunner)
|
|
{
|
|
GetGhoulPosDir2(self->s.origin, self->s.angles, body->GetGunnerInst(), 0, "to_body", vPos, vF, NULL, NULL);
|
|
VectorMA(vPos, 1000, vTargetDir, vPos);
|
|
Cobra2AimGunnerAtPos(self, vPos);
|
|
}
|
|
|
|
GetGhoulPosDir2(self->s.origin, self->s.angles, body->GetBarrelNULLInst(), 0, "DUMMY01", vPos, vF, NULL, NULL);
|
|
VectorMA(vPos, 1000, vTargetDir, vPos);
|
|
return Cobra2AimGunAtPos(self, vPos, bSynchGunner);
|
|
}
|
|
|
|
void HeliMissileRemove2(edict_t *self)
|
|
{
|
|
G_FreeEdict(self);
|
|
}
|
|
|
|
void HeliMissileCollide2(edict_t *self, edict_t *other, cplane_t *plane, mtexinfo_t *surf)
|
|
{
|
|
if(surf)
|
|
{
|
|
if(surf->flags & SURF_SKY)
|
|
{
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
}
|
|
T_RadiusDamage (self, self->owner, 130, self, 150, 0);
|
|
gmonster.RadiusDeafen(self, 150, 250);
|
|
fxRunner.exec("weapons/world/rocketexplode", self->s.origin);
|
|
ShakeCameras (self->s.origin, 100, 300, DEFAULT_JITTER_DELTA);
|
|
G_FreeEdict(self);
|
|
}
|
|
|
|
void fireHeliRocket2(edict_t *self, vec3_t v3FirePoint, vec3_t v3FireDir)
|
|
{
|
|
edict_t *missile;
|
|
|
|
missile = G_Spawn();
|
|
missile->movetype = MOVETYPE_DAN;
|
|
missile->solid = SOLID_BBOX;
|
|
missile->clipmask = MASK_SHOT;
|
|
VectorCopy(v3FirePoint, missile->s.origin);
|
|
VectorScale(v3FireDir, 600.0, missile->velocity);
|
|
VectorSet (missile->mins, -4, -2, -4);
|
|
VectorSet (missile->maxs, 3, 2, 4);
|
|
missile->health = 100;
|
|
missile->think = HeliMissileRemove2;
|
|
missile->nextthink = level.time + 10.0;
|
|
missile->touch = HeliMissileCollide2;
|
|
missile->gravity = .02;
|
|
vectoangles(v3FireDir, missile->s.angles);
|
|
gi.linkentity(missile);
|
|
missile->owner = self;
|
|
|
|
game_ghoul.SetSimpleGhoulModel(missile, "Weapon/Projectiles/Rocket", "Rocket");
|
|
|
|
fxRunner.execContinualEffect("weapons/world/rockettrail2", missile);
|
|
}
|
|
|
|
int Cobra2AimGunnerInDirection(edict_t *self, vec3_t vTargetDir)
|
|
{
|
|
int bRet = AIM_TRUE;
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
IGhoulInst* gunnerInst = body->GetGunnerInst();
|
|
GhoulID gunnerBolt = body->GetGunnerBolt();
|
|
|
|
if (!gunnerInst || !gunnerBolt)
|
|
return AIM_ERROR;
|
|
|
|
vec3_t vTargetDirCopy, vOffset, vPos;
|
|
|
|
VectorCopy(vTargetDir, vTargetDirCopy);
|
|
body->GetGunnerBoltOffset(vOffset);
|
|
VectorMA(self->s.origin, 10, vTargetDirCopy, vPos);
|
|
bRet = Cobra2AimHeadAtPos(self, vPos, gunnerInst, gunnerBolt, vOffset);
|
|
return bRet;
|
|
}
|
|
|
|
int Cobra2AimPilotInDirection(edict_t *self, vec3_t vTargetDir)
|
|
{
|
|
int bRet = AIM_TRUE;
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
IGhoulInst* pilotInst = body->GetPilotInst();
|
|
GhoulID pilotBolt = body->GetPilotBolt();
|
|
|
|
if (!pilotInst || !pilotBolt)
|
|
return AIM_ERROR;
|
|
|
|
vec3_t vTargetDirCopy, vOffset, vPos;
|
|
|
|
VectorCopy(vTargetDir, vTargetDirCopy);
|
|
body->GetPilotBoltOffset(vOffset);
|
|
VectorMA(self->s.origin, 10, vTargetDirCopy, vPos);
|
|
bRet = Cobra2AimHeadAtPos(self, vPos, pilotInst, pilotBolt, vOffset);
|
|
return bRet;
|
|
}
|
|
|
|
void Cobra2InitHead(edict_t *self, vec3_t vTargetDir, IGhoulInst* head,
|
|
GhoulID bolt, vec3_t vOffset)
|
|
{
|
|
vec3_t vOrigin, vF, vR, vU;
|
|
|
|
if (!head || !bolt)
|
|
{
|
|
return;
|
|
}
|
|
// the origin we use is the NULL at the base of the gunner's head, which
|
|
//is calc'd (heli's origin + offset)
|
|
AngleVectors(self->s.angles, vF, vR, vU);
|
|
VectorScale(vR, vOffset[1], vR);
|
|
VectorScale(vF, vOffset[0], vF);
|
|
VectorScale(vU, vOffset[2], vU);
|
|
VectorAdd(self->s.origin, vF, vOrigin);
|
|
VectorAdd(vOrigin, vR, vOrigin);
|
|
VectorAdd(vOrigin, vU, vOrigin);
|
|
|
|
Matrix4 EntityToWorld;
|
|
EntToWorldMatrix(vOrigin,self->s.angles,EntityToWorld);
|
|
|
|
Matrix4 BoltToEntity;
|
|
self->ghoulInst->GetBoltMatrix(level.time,BoltToEntity,bolt,IGhoulInst::MatrixType::Entity);
|
|
|
|
Matrix4 BoltToWorld;
|
|
BoltToWorld.Concat(BoltToEntity,EntityToWorld);
|
|
|
|
Matrix4 WorldToBolt;
|
|
WorldToBolt.Inverse(BoltToWorld);
|
|
|
|
Vect3 TargetDir=*(Vect3 *)vTargetDir;
|
|
|
|
Vect3 TargetDirInBoltSpace;
|
|
WorldToBolt.XFormVect(TargetDirInBoltSpace,TargetDir);
|
|
|
|
Matrix4 matR1, matR2, matNew;
|
|
float angle1=-atan2(TargetDirInBoltSpace.y(),sqrt(TargetDirInBoltSpace.z()*TargetDirInBoltSpace.z()+TargetDirInBoltSpace.x()*TargetDirInBoltSpace.x()));
|
|
float angle2=atan2(TargetDirInBoltSpace.z(),TargetDirInBoltSpace.x());
|
|
float fMin1 = -HEAD_PITCH_UP, fMax1 = HEAD_PITCH_DOWN,
|
|
fMin2 = -HEAD_YAW_CW, fMax2 = HEAD_YAW_CCW;
|
|
float fDelta1 = 0.0f, fDelta2 = 0.0f;
|
|
|
|
//limit the angles here to a reasonble range
|
|
|
|
if (angle1 < fMin1 - FLOAT_ZERO_EPSILON)
|
|
{
|
|
angle1 = fMin1;
|
|
}
|
|
else if (angle1 > fMax1 + FLOAT_ZERO_EPSILON)
|
|
{
|
|
angle1 = fMax1;
|
|
}
|
|
if (angle2 < fMin2 - FLOAT_ZERO_EPSILON || (angle2 >= M_PI/2.0 && angle2 <= M_PI))
|
|
{
|
|
// trying to look at quadrant2 or quadrant3 (looking straight down at the head)
|
|
angle2 = fMin2;
|
|
}
|
|
else if (angle2 > fMax2 + FLOAT_ZERO_EPSILON && angle2 <= M_PI/2.0)
|
|
{
|
|
// trying to look at quadrant1 or quadrant4 (looking straight down at the head)
|
|
angle2 = fMax2;
|
|
}
|
|
|
|
matR1.Rotate(2, angle1);
|
|
matR2.Rotate(1, angle2);
|
|
matNew.Concat(matR1,matR2);
|
|
head->SetXForm(matNew);
|
|
}
|
|
|
|
int Cobra2ResetGunner(edict_t *self)
|
|
{
|
|
vec3_t vF, vR;
|
|
|
|
AngleVectors(self->s.angles, vF, vR, NULL);
|
|
VectorMA(self->s.origin, 500, vF, vF);
|
|
return Cobra2AimGunAtPos(self, vF, true);
|
|
}
|
|
|
|
// called once per script fireat command...
|
|
//
|
|
void Cobra2Think_SetFireAt(edict_t *self, vec3_t v3FireDest, float fTime)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
VectorCopy(v3FireDest,body->m_v3HeliFireAtCoords);
|
|
VectorCopy(v3FireDest,body->m_v3HeliFireAtCoords_Original);
|
|
body->m_fHeliFireAtTimer = fTime; // store timer for now, we're not necessarily in a position to fire
|
|
body->m_flags |= HELIFLAG_DOINGSCRIPTEDFIREAT;
|
|
#ifdef _DEBUG
|
|
#ifdef SHOW_DESIGNER_ERRORS
|
|
if (body->m_fHeliFireAtTimer<0 && !(body->m_flags & HELIFLAG_ROCKETSENABLED))
|
|
{
|
|
gi.dprintf("\n###### HELI SCRIPT ERROR:\n");
|
|
gi.dprintf("Rocket fire wanted, but rockets disabled!\n\n");
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
// this'll look nicer, if doing fire from an aggressive hover, and rockets enabled, then change hover param
|
|
// so it'll do a pitch-fire, rather than Z match...
|
|
//
|
|
// (also done now from scripted AI, so it doesn't lose carefully set up Z points by doing a Z match for >2 rockets firing)
|
|
//
|
|
if ( (body->m_flags & HELIFLAG_ROCKETSENABLED) && body->m_iHeliRocketsLeft)
|
|
{
|
|
// we have rockets available for firing...
|
|
//
|
|
if (body->m_flags & (HELIFLAG_DOINGSCRIPTEDHOVER|HELIFLAG_DOINGSCRIPTEDAI))
|
|
body->m_fHeliFireAtTimer = (gi.irand(0,10)>=5)?-2:-1; // fire 1 or 2 rockets at player, dipping to fire, not hori-z-match
|
|
}
|
|
|
|
ai->SetCurrentActionNextThink(Cobra2Think_GotoFireAt);
|
|
self->nextthink = level.time + FRAMETIME;
|
|
}
|
|
|
|
// called repeatedly until the chopper is in a position to fire at it's assigned target...
|
|
//
|
|
// (after consulting with Greg, it's been decided that the heli will still goto a suitable firing position first,
|
|
// even if it was able to work out that it wouldn't be able to fire even before it set off (eg no rockets for a
|
|
// rocket attack).
|
|
//
|
|
void Cobra2Think_GotoFireAt(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
trace_t tr;
|
|
|
|
self->nextthink = level.time + FRAMETIME;
|
|
|
|
// ok, there's a clear line of sight to the target, but can we bring our weapons to bear at this angle?...
|
|
//
|
|
// (this means checking if missiles are to be used, if so, the entire heli body needs to be brought to bear,
|
|
// if not, then we can just swivel the minigun round to the target)
|
|
//
|
|
qboolean qbWholeBodyMustMatch = ( body->m_fHeliFireAtTimer<0 || (body->m_flags & HELIFLAG_ROCKETSENABLED && body->m_iHeliRocketsLeft));
|
|
|
|
if (qbWholeBodyMustMatch)
|
|
{
|
|
// YAW the heli round to face the target if nec...
|
|
//
|
|
float fDegreesToGo;
|
|
Cobra2_TurnToFace(self, body->m_v3HeliFireAtCoords, // vec3_t v3WorldPos
|
|
false, // qboolean qbRoll
|
|
false, // qboolean qbYAWDictatesFiring
|
|
&fDegreesToGo // float *pfDegreesToGo
|
|
);
|
|
|
|
if (fDegreesToGo)
|
|
{
|
|
// we need to swing the heli round...
|
|
//
|
|
Cobra2Think_SetTurnDest(self, body->m_v3HeliFireAtCoords);
|
|
return;
|
|
}
|
|
|
|
// Missile yaw ok, checking Z...
|
|
|
|
|
|
// if we've specifically asked for either one or 2 rockets to be fired, then we'll do the pitch adjust,
|
|
// any other number gets done via a Z match and a long debounce-timed-stream of them...
|
|
//
|
|
if (body->m_fHeliFireAtTimer == -1 || body->m_fHeliFireAtTimer == -2)
|
|
{
|
|
// Ideally the chopper should pitch forward/back enough to fire missiles at target, but with deadlines
|
|
// pressing I'll do a Z match instead. But for now, the basic algorithm is as follows...
|
|
//
|
|
float fYlen = fabs(self->s.origin[2] - body->m_v3HeliFireAtCoords[2]);
|
|
vec3_t v3DiffVect;
|
|
VectorSubtract(body->m_v3HeliFireAtCoords,self->s.origin,v3DiffVect);
|
|
v3DiffVect[2]=0;
|
|
float fXlen = VectorLength(v3DiffVect);
|
|
float fFudgeFactor = 1.0;
|
|
float fPitch = atan2(fYlen, fXlen) * RADTODEG * fFudgeFactor;
|
|
if (body->m_v3HeliFireAtCoords[2] > self->s.origin[2])
|
|
fPitch = -fPitch;
|
|
|
|
if (fabs(fPitch) < HELI_MAX_FIRE_PITCH)
|
|
{
|
|
// we can hit this target with a pitch change...
|
|
//
|
|
ai->SetCurrentActionNextThink(Cobra2Think_DipFireAt);
|
|
|
|
self->nextthink = level.time + FRAMETIME;
|
|
body->m_flags &= ~HELIFLAG_DOINGSCRIPTEDFIREAT; // important!
|
|
body->m_flags &= ~(HELIFLAG_FIREDIP1DONE | HELIFLAG_FIREDIP2DONE);
|
|
body->m_fHeliFirePitch = fPitch*.1;
|
|
|
|
return;
|
|
}
|
|
// target beyond pitch-adjust, so do normal Z-match version...
|
|
}
|
|
|
|
// code to match heli Z height to ideal flat-launch pos...
|
|
//
|
|
if (self->s.origin[2] != body->m_v3HeliFireAtCoords[2])
|
|
{
|
|
// height needs adjusting...
|
|
//
|
|
vec3_t v3;
|
|
|
|
VectorCopy(self->s.origin,v3);
|
|
v3[2] = body->m_v3HeliFireAtCoords[2];
|
|
|
|
// note, technically the heli should use its real mins/maxs box for this bit, but if I do, it might not be able
|
|
// to rise up again from the firing point if it was juat about level with the ground (eg if it was shooting at
|
|
// th player), so to avoid getting stuck when lifting up again and using the standard size collision box, I
|
|
// have to use the extended one all thtime, even if it means that occasionally it misses a more obvious shot.
|
|
//
|
|
// (this shouldn't matter too much in the final game since on either the train or tokyo level you never actually
|
|
// walk around on the very bottom level of the map, it's only in the choppertest map that it stops some shots)
|
|
//
|
|
|
|
if (gi.trace(self->s.origin, _HELI_TRACEPARAM_MIN2,
|
|
_HELI_TRACEPARAM_MAX2, v3, self,MASK_SOLID, &tr))
|
|
{
|
|
// it's clear, so head for it...
|
|
//
|
|
Cobra2Think_SetDumbFlyDest(self, v3);
|
|
return;
|
|
}
|
|
|
|
// path blocked, so let's just go as far as we can and launch anyway...
|
|
//
|
|
// (lift up a bit from whatever we trace-hit so we don't (eg) touch the floor)...
|
|
//
|
|
VectorCopy(tr.endpos,v3);
|
|
v3[2] += 40; // arb., roughly 2/3 of a man height
|
|
//
|
|
// limit-check this new dest, just in case we're in a really strange map area...
|
|
//
|
|
// (see reasoning on trace call above)
|
|
//
|
|
gi.trace(self->s.origin, _HELI_TRACEPARAM_MIN2, _HELI_TRACEPARAM_MAX2,
|
|
v3, self,MASK_SOLID, &tr);
|
|
if (tr.fraction < 1.0)
|
|
VectorCopy(tr.endpos,v3);
|
|
|
|
body->m_v3HeliFireAtCoords[2] = v3[2]; // so code will see it as a match next time and not do this again
|
|
Cobra2Think_SetDumbFlyDest(self, v3);
|
|
return;
|
|
}
|
|
|
|
// Z coords match!
|
|
|
|
// success (ready to fire), so fall through...
|
|
//
|
|
}// if (qbWholeBodyMustMatch)
|
|
else
|
|
{
|
|
// just try aiming the chaingun at it..
|
|
//
|
|
int iGunAim = Cobra2AimGunAtPos(self, body->m_v3HeliFireAtCoords, true); // qboolean bSynchGunner
|
|
|
|
switch (iGunAim)
|
|
{
|
|
case AIM_FALSE:
|
|
|
|
return; // gun still moving, let's wait and see
|
|
|
|
case AIM_ERROR:
|
|
{
|
|
// bugger, gun has hit edge-stop and can't bear on target, so move heli body...
|
|
//
|
|
// slightly weird usage this, but it does the job... (will cause YAW movement towards target if not already there)
|
|
//
|
|
float fDegreesToGo;
|
|
Cobra2_TurnToFace(self, body->m_v3HeliFireAtCoords, // vec3_t v3WorldPos
|
|
false, // qboolean qbRoll
|
|
false, // qboolean qbYAWDictatesFiring
|
|
&fDegreesToGo // float *pfDegreesToGo
|
|
);
|
|
|
|
if (fDegreesToGo)
|
|
{
|
|
// we need to swing the heli round...
|
|
//
|
|
Cobra2Think_SetTurnDest(self, body->m_v3HeliFireAtCoords);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
// forget it, although we've got an LOS to the target, we can't get the mgun to bear on it from this
|
|
// position even though the YAW is correct, ergo it must be too far underneath us.
|
|
//
|
|
// this is really more of a scripting error (ie heli not put in suitable place), so for now I'm just
|
|
// going to fail this command, though later it could be improved to change position...
|
|
//
|
|
ai->ActionIsDone(true);
|
|
body->m_flags &= ~HELIFLAG_DOINGSCRIPTEDFIREAT;
|
|
return;
|
|
}
|
|
}
|
|
|
|
}// switch (iGunAim)
|
|
}
|
|
|
|
// we're in a suitable firing position here, so...
|
|
//
|
|
ai->SetCurrentActionNextThink(Cobra2Think_FireAt);
|
|
self->nextthink = level.time + FRAMETIME;
|
|
body->m_flags &= ~HELIFLAG_DOINGSCRIPTEDFIREAT; // important!
|
|
|
|
// duration arg is either a time (if +ve), else a -ve rocket count...
|
|
//
|
|
if (body->m_fHeliFireAtTimer>0)
|
|
body->m_fHeliFireAtTimer += level.time;
|
|
|
|
}// void CobraThink_GotoFireAt(edict *self)
|
|
|
|
|
|
|
|
// we're trying to fire at something that involves pitching up/down to aim at it (because of missiles)...
|
|
//
|
|
void Cobra2Think_DipFireAt(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
self->nextthink = level.time + FRAMETIME;
|
|
|
|
Cobra2AimPilotAtPos(self, body->m_v3HeliFireAtCoords);
|
|
Cobra2AimGunAtPos(self, body->m_v3HeliFireAtCoords, true); // qboolean bSynchGunner
|
|
|
|
if ( (body->m_flags & (HELIFLAG_FIREDIP1DONE|HELIFLAG_FIREDIP2DONE)) != (HELIFLAG_FIREDIP1DONE|HELIFLAG_FIREDIP2DONE) )
|
|
_Cobra2Fire_Chaingun(self);
|
|
else
|
|
Cobra2_Fire_OFF(self);
|
|
|
|
if (!(body->m_flags & HELIFLAG_FIREDIP1DONE))
|
|
{
|
|
// still heading towards ideal pitch pos...
|
|
//
|
|
self->s.angles[HELI_PITCH] = angle2posneg2(self->s.angles[HELI_PITCH]);
|
|
|
|
if (fabs(self->s.angles[HELI_PITCH] - body->m_fHeliFirePitch) <= HELI_COBRA_DIPFIRE_PITCHADJSPEED)
|
|
{
|
|
self->s.angles[HELI_PITCH] = body->m_fHeliFirePitch;
|
|
body->m_flags |= HELIFLAG_FIREDIP1DONE;
|
|
|
|
// fire missiles...(fHeliFireAtTimer == -1 or -2 here)
|
|
//
|
|
_Cobra2Fire_Rockets(self, -body->m_fHeliFireAtTimer); // iRocketsToForceFire
|
|
body->m_fHeliFirePitch = -(body->m_fHeliFirePitch/2); // so we pitch back to the other extent afterwards (looks nicer)
|
|
}
|
|
else
|
|
{
|
|
// still heading there...
|
|
//
|
|
if (self->s.angles[HELI_PITCH] > body->m_fHeliFirePitch) // kef
|
|
self->s.angles[HELI_PITCH] -= HELI_COBRA_DIPFIRE_PITCHADJSPEED;
|
|
else
|
|
self->s.angles[HELI_PITCH] += HELI_COBRA_DIPFIRE_PITCHADJSPEED;
|
|
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!(body->m_flags & HELIFLAG_FIREDIP2DONE))
|
|
{
|
|
// heading back towards over-compensate backpitch pos...
|
|
//
|
|
self->s.angles[HELI_PITCH] = angle2posneg2(self->s.angles[HELI_PITCH]);
|
|
|
|
if (fabs(self->s.angles[HELI_PITCH] - body->m_fHeliFirePitch) <= HELI_COBRA_DIPFIRE_PITCHADJSPEED)
|
|
{
|
|
self->s.angles[HELI_PITCH] = body->m_fHeliFirePitch;
|
|
body->m_flags |= HELIFLAG_FIREDIP2DONE;
|
|
}
|
|
else
|
|
{
|
|
// still heading there...
|
|
//
|
|
if (self->s.angles[HELI_PITCH] > body->m_fHeliFirePitch) // kef
|
|
self->s.angles[HELI_PITCH] -= HELI_COBRA_DIPFIRE_PITCHADJSPEED;
|
|
else
|
|
self->s.angles[HELI_PITCH] += HELI_COBRA_DIPFIRE_PITCHADJSPEED;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// all pitch stuff done (though we still need to level out again, so...
|
|
//
|
|
ai->SetCurrentActionNextThink(Cobra2Think_MovementFinish);
|
|
}// void CobraThink_DipFireAt(edict_t *self)
|
|
|
|
|
|
|
|
|
|
|
|
// called repeatedly while actually firing at something...
|
|
//
|
|
void Cobra2Think_FireAt(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
qboolean qbFireDone = false;
|
|
|
|
self->nextthink = level.time + FRAMETIME;
|
|
|
|
// do minor YAW adj here as well for when gun is aimed at target ok, but body still has a bit to go...
|
|
//
|
|
Cobra2_TurnToFace(self, body->m_v3HeliFireAtCoords, // vec3_t v3WorldPos
|
|
false, // qboolean qbRoll
|
|
false, // qboolean qbYAWDictatesFiring
|
|
NULL // float *pfDegreesToGo
|
|
);
|
|
Cobra2AimPilotAtPos(self, body->m_v3HeliFireAtCoords); // same reasoning.
|
|
//
|
|
// if heli YAW'd, gun will be off-target...
|
|
//
|
|
Cobra2AimGunAtPos(self, body->m_v3HeliFireAtCoords_Original, true); // qboolean bSynchGunner
|
|
|
|
int iRocketsFired = Cobra2_Fire(self, false); // qboolean qbInihibitRockets
|
|
|
|
// check whether our time is a duration, or a rocket count...
|
|
//
|
|
if (body->m_fHeliFireAtTimer<0) // if -ve, it's a rocket count...
|
|
{
|
|
#ifdef _DEBUG
|
|
#ifdef SHOW_DESIGNER_ERRORS
|
|
if (!(body->m_flags & HELIFLAG_ROCKETSENABLED))
|
|
{
|
|
gi.dprintf("### HELI SCRIPT: aborting attack, rockets disabled!\n");
|
|
Cobra2_Fire_OFF(self);
|
|
ai->ActionIsDone(true);
|
|
return;
|
|
}
|
|
#endif
|
|
#endif
|
|
body->m_fHeliFireAtTimer += iRocketsFired; // dec rockets-to-fire count (-ve)
|
|
|
|
// this is a specific rocket attack, so cease firing if the required number have been fired
|
|
// or if we've run out...
|
|
//
|
|
if (body->m_iHeliRocketsLeft==0 || body->m_fHeliFireAtTimer==0.0f)
|
|
qbFireDone = true;
|
|
}
|
|
else
|
|
{// end timer must be a real timer then, so...
|
|
|
|
if (level.time > body->m_fHeliFireAtTimer)
|
|
qbFireDone = true;
|
|
}
|
|
|
|
if (qbFireDone || (body->m_flags & HELIFLAG_ISDEAD))
|
|
{
|
|
Cobra2_Fire_OFF(self);
|
|
ai->ActionIsDone(true);
|
|
}
|
|
|
|
}// void CobraThink_FireAt(edict_t *self)
|
|
|
|
// set journey dest but don't worry about any obstructions, just do straight line...
|
|
//
|
|
void Cobra2Think_SetDumbFlyDest(edict_t *self, vec3_t v3)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
Cobra2_InitTurnStuff(self,v3);
|
|
|
|
VectorCopy(self->s.origin, body->m_v3HeliDesiredPos_Start);
|
|
|
|
ai->SetCurrentActionNextThink(Cobra2Think_HeadToDest);
|
|
self->nextthink = level.time + FRAMETIME;
|
|
}
|
|
|
|
|
|
// Set journey dest, and ensure it gets there safely even if you have to do several calls to the dumb one to work
|
|
// your way round obstacles. If you set a dest that's inside a solid area then it sends out a ring of check positions
|
|
// further and further out until one of them is in a clear spot, then changes the dest to that.
|
|
//
|
|
#define _RING_ENTRIES 18
|
|
const vec3_t fRingTable[_RING_ENTRIES]=
|
|
{
|
|
// for speed reasons, this entry is tried first, not in it's logical place below
|
|
{ 0, 0, 1}, // special entry, searches upwards (copes with common hover-point mistakes by designers)
|
|
|
|
{ -1, 1, 0},
|
|
{ 0, 1, 0},
|
|
{ 1, 1, 0},
|
|
|
|
{ -1, 0, 0},
|
|
{ 1, 0, 0},
|
|
|
|
{ -1,-1, 0},
|
|
{ 0,-1, 0},
|
|
{ 1,-1, 0},
|
|
|
|
// unlikely this'll be needed, but just in case we're told to fly too high...
|
|
{ 0, 0,-1},
|
|
|
|
// extra search ring, the surrounding 8 compass points but moving upwards at the same time
|
|
|
|
{ -1, 1, 1},
|
|
{ 0, 1, 1},
|
|
{ 1, 1, 1},
|
|
|
|
{ -1, 0, 1},
|
|
{ 1, 0, 1},
|
|
|
|
{ -1,-1, 1},
|
|
{ 0,-1, 1},
|
|
{ 1,-1, 1},
|
|
|
|
};
|
|
|
|
qboolean Cobra2_VecWithinWorld(edict_t *self, vec3_t v3)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
if (ai)
|
|
{
|
|
return (
|
|
v3[0] < ai->GetHeliXWorldMin2() ||
|
|
v3[0] > ai->GetHeliXWorldMax2() ||
|
|
v3[1] < ai->GetHeliYWorldMin2() ||
|
|
v3[1] > ai->GetHeliYWorldMax2() ||
|
|
v3[2] < ai->GetHeliZWorldMin2() ||
|
|
v3[2] > ai->GetHeliZWorldMax2()
|
|
)?false:true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// ensure that a vector isn't in a wall or off the edge of the map, adjust until legal, then return it...
|
|
//
|
|
void Cobra2_LegaliseVector(edict_t *self, vec3_t v3in, vec3_t v3Out, qboolean qbUseWorldClip)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
trace_t tr;
|
|
|
|
vec3_t v3Dest;
|
|
int iSafetyLoopCount=0;
|
|
|
|
VectorCopy(v3in,v3Dest);
|
|
// Cobra2_ClipVecToWorld(self, v3Dest);
|
|
// new bit, let's see if it's impossible to get to this area because the dest is inside a solid
|
|
// (this can happen with both hover-twitching and entity-relative goto commands)
|
|
//
|
|
// This is achieved by tracing back from the dest and seeing if the starting point was solid...
|
|
//
|
|
gi.trace(v3Dest, _HELI_TRACEPARAM_MIN2, _HELI_TRACEPARAM_MAX2, self->s.origin,
|
|
self, MASK_SOLID, &tr);
|
|
|
|
// if smart dest pos is inside a wall or off the edge of the map, then we need to change it!...
|
|
//
|
|
if (tr.startsolid)
|
|
{
|
|
// dest point is unreachable by any means because it's inside a solid, so we've got to find a new dest pos.
|
|
//
|
|
// Unfortunately the trace command doesn't work how I'd like (ie. if you start off solid then the endpos is the first
|
|
// non-solid point) so I'm going to put 8 points around the dest, then keep expanding them in a ring until I get a
|
|
// valid destination...(aaarrggh!!!!)
|
|
//
|
|
#define HELI_WORLDSPACE_STEPUNITS 100 // step this many units per scan until we find a blank
|
|
|
|
int iRingNumber=-1;
|
|
int iPassMult=1;
|
|
while (1)
|
|
{
|
|
if (++iSafetyLoopCount>100)
|
|
{
|
|
// sod it, something's seriously shagged, probably heli is in a highly-cluttered world (unlikely), or
|
|
// some dozy twat of a designer has given it a dest coord miles off beyond the map edge. So all we can do is
|
|
// forget this goto, since the server's going to take too long to resolve it...
|
|
//
|
|
// (once the game is at release, this part of the code should never be hit, but it's here JIC)
|
|
//
|
|
VectorCopy(self->s.origin, v3Dest);
|
|
break; // out of while loop
|
|
}
|
|
|
|
if (++iRingNumber==_RING_ENTRIES)
|
|
{
|
|
iRingNumber=0;
|
|
iPassMult++;
|
|
}
|
|
|
|
vec3_t v3;
|
|
|
|
VectorSet(v3, fRingTable[iRingNumber][0] * (iPassMult * HELI_WORLDSPACE_STEPUNITS),
|
|
fRingTable[iRingNumber][1] * (iPassMult * HELI_WORLDSPACE_STEPUNITS),
|
|
fRingTable[iRingNumber][2] * (iPassMult * HELI_WORLDSPACE_STEPUNITS)
|
|
);
|
|
Vec3AddAssign(v3Dest, v3);
|
|
|
|
// update, as a speed issue, I can skip known world edges to avoid spurious traces...
|
|
//
|
|
if (Cobra2_VecWithinWorld(self, v3))
|
|
{// within designer-specified world limits...
|
|
gi.trace(v3, _HELI_TRACEPARAM_MIN2, _HELI_TRACEPARAM_MAX2,
|
|
self->s.origin,self,MASK_SOLID, &tr);
|
|
|
|
if (!tr.startsolid)
|
|
{
|
|
// we've maybe found a clear bit, but we have to make sure it isn't off the edge of the map as well, so...
|
|
// (reverse src/dest params and try again)
|
|
gi.trace(self->s.origin, _HELI_TRACEPARAM_MIN2, _HELI_TRACEPARAM_MAX2,
|
|
v3,self,MASK_SOLID, &tr);
|
|
|
|
if (tr.fraction != 0.0f) // gotta have dest that allows *some* movement
|
|
{
|
|
// at last, we've found a clear bit, so let's use this as the final destination...
|
|
//
|
|
VectorCopy(v3,v3Dest);
|
|
break; // out of while loop
|
|
}
|
|
}
|
|
}// within designer-specified world limits...
|
|
}// while (1)
|
|
}// if smart dest is in a wall
|
|
#ifdef _DEBUG
|
|
#ifdef SHOW_DESIGNER_ERRORS
|
|
// doesn't actually appear on screen, but wtf?
|
|
if (!VectorCompare(v3Dest,v3in))
|
|
gi.dprintf("Vector Legalised (%d retries needed)\n",iSafetyLoopCount);
|
|
#endif
|
|
#endif
|
|
VectorCopy(v3Dest,v3Out);
|
|
|
|
}// void Cobra_LegaliseVector(edict_t *self, vec3_t v3in, vec3_t v3Out, qboolean qbUseWorldClip)
|
|
|
|
void Cobra2_ClipVecToWorld(edict_t* self, vec3_t v3)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
if (ai)
|
|
{
|
|
if (v3[0] < ai->GetHeliXWorldMin2())
|
|
v3[0] = ai->GetHeliXWorldMin2();
|
|
else if (v3[0] > ai->GetHeliXWorldMax2())
|
|
v3[0] = ai->GetHeliXWorldMax2();
|
|
|
|
if (v3[1] < ai->GetHeliYWorldMin2())
|
|
v3[1] = ai->GetHeliYWorldMin2();
|
|
else if (v3[1] > ai->GetHeliYWorldMax2())
|
|
v3[1] = ai->GetHeliYWorldMax2();
|
|
|
|
if (v3[2] < ai->GetHeliZWorldMin2())
|
|
v3[2] = ai->GetHeliZWorldMin2();
|
|
else if (v3[2] > ai->GetHeliZWorldMax2())
|
|
v3[2] = ai->GetHeliZWorldMax2();
|
|
}
|
|
}
|
|
|
|
|
|
void Cobra2Think_SetSmartFlyDest(edict_t *self, vec3_t v3)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
VectorCopy(v3,body->m_v3HeliDesiredPos_Smart);
|
|
body->m_flags |= HELIFLAG_DOINGSMARTGOTO;
|
|
|
|
Cobra2_LegaliseVector(self, body->m_v3HeliDesiredPos_Smart,
|
|
body->m_v3HeliDesiredPos_Smart, true);
|
|
|
|
ai->SetCurrentActionNextThink(Cobra2Think_SmartHeadToDest);
|
|
self->nextthink = level.time + FRAMETIME;
|
|
|
|
}// void CobraThink_SetSmartFlyDest(edict_t *self, vec3_t v3)
|
|
|
|
// every time we get here we should either stop the smart-goto, or set another dumb-goto target...
|
|
//
|
|
void Cobra2Think_SmartHeadToDest(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
vec3_t v3Dest; // so we can modify it to work our way round things
|
|
|
|
VectorCopy(body->m_v3HeliDesiredPos_Smart,v3Dest);
|
|
|
|
// are we actually there?...
|
|
//
|
|
if (VectorCompare(self->s.origin,v3Dest))
|
|
{
|
|
ai->ActionIsDone(true);
|
|
self->nextthink = level.time + FRAMETIME;
|
|
body->m_flags &= ~HELIFLAG_DOINGSMARTGOTO; // amazingly important to do this
|
|
return;
|
|
}
|
|
|
|
// we're not there yet, but can we see it from here?...
|
|
trace_t tr;
|
|
|
|
{
|
|
gi.trace(self->s.origin, _HELI_TRACEPARAM_MIN2, _HELI_TRACEPARAM_MAX2, v3Dest, self,MASK_SOLID, &tr);
|
|
}
|
|
|
|
if (tr.fraction == 1.0)
|
|
{
|
|
// yep, so head for it...
|
|
//
|
|
Cobra2Think_SetDumbFlyDest(self, v3Dest);
|
|
if (VectorLength(body->m_v3HeliDeathDest) &&
|
|
VectorCompare(body->m_v3HeliDesiredPos_Smart,body->m_v3HeliDeathDest))
|
|
{
|
|
body->m_flags |= HELIFLAG_ISDEADFINALGOTO;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// ------------------
|
|
// quick test, try tracing from the target back to us (which will be blocked), then set a dest position 3/4 up in the sky
|
|
// above the block point, and see if we can see that...
|
|
//
|
|
vec3_t v3InitialBlockage;
|
|
VectorCopy(tr.endpos,v3InitialBlockage); // preserve this collision point
|
|
gi.trace(v3Dest, _HELI_TRACEPARAM_MIN2, _HELI_TRACEPARAM_MAX2, self->s.origin,
|
|
self,MASK_SOLID, &tr);
|
|
if (tr.fraction != 1.0) // sanity check, should never happen because of collision in code above
|
|
{
|
|
VectorCopy(tr.endpos,v3Dest); // preserve this collision point
|
|
v3Dest[2] = ai->GetHeliZWorldMin2() + (((ai->GetHeliZWorldMax2() - ai->GetHeliZWorldMin2())/4)*3);
|
|
//
|
|
// ok, can we see this sugested point from our current pos...
|
|
//
|
|
|
|
if (gi.trace(self->s.origin, _HELI_TRACEPARAM_MIN2, _HELI_TRACEPARAM_MAX2,
|
|
v3Dest, self,MASK_SOLID, &tr))
|
|
{
|
|
// yep, so head for it...
|
|
//
|
|
Cobra2Think_SetDumbFlyDest(self, v3Dest);
|
|
return;
|
|
}
|
|
// oh well, worth a try...
|
|
}
|
|
VectorCopy(v3InitialBlockage,tr.endpos); // restore old collision point
|
|
// ------------------
|
|
|
|
// we can't see the smart-dest from here, so for now let's try and aim at the blockage point x,y, but set the Z to be
|
|
// 3/4 up in the sky...
|
|
//
|
|
VectorCopy(tr.endpos,v3Dest);
|
|
v3Dest[2] = ai->GetHeliZWorldMin2() + ((ai->GetHeliZWorldMax2() - ai->GetHeliZWorldMin2())*0.75);
|
|
//
|
|
// can we see this point?
|
|
//
|
|
|
|
if (gi.trace(self->s.origin, _HELI_TRACEPARAM_MIN2, _HELI_TRACEPARAM_MAX2, v3Dest,
|
|
self,MASK_SOLID, &tr))
|
|
{
|
|
// yes, so let's go with this...
|
|
//
|
|
// (1st, a fix for things we can't reach by flying over the top of stuff...)
|
|
//
|
|
if (VectorCompare(self->s.origin, v3Dest))
|
|
{
|
|
// let's do a 50-50 thing...
|
|
//
|
|
if (gi.flrand(0,1)>0.5)
|
|
{
|
|
Cobra2Think_SetDumbFlyDest(self, *Cobra2Think_GetRandomPatrolDestPos(self));
|
|
return;
|
|
}
|
|
#ifdef _DEBUG
|
|
#ifdef SHOW_DESIGNER_ERRORS
|
|
gi.dprintf("### HELISCRIPT ERROR: Can't reach dest by climbing over! Aborting...\n");
|
|
#endif
|
|
#endif
|
|
ai->ActionIsDone(true);
|
|
self->nextthink = level.time + FRAMETIME;
|
|
body->m_flags &= ~HELIFLAG_DOINGSMARTGOTO; // amazingly important to do this
|
|
return;
|
|
}
|
|
|
|
Cobra2Think_SetDumbFlyDest(self, v3Dest);
|
|
return;
|
|
}
|
|
|
|
// damn, we must be in a really blocked area, how about straight up?...
|
|
//
|
|
VectorCopy(self->s.origin,v3Dest);
|
|
v3Dest[2] = ai->GetHeliZWorldMin2() + ((ai->GetHeliZWorldMax2() - ai->GetHeliZWorldMin2())*0.75);
|
|
|
|
if (gi.trace(self->s.origin, _HELI_TRACEPARAM_MIN2, _HELI_TRACEPARAM_MAX2, v3Dest,
|
|
self,MASK_SOLID, &tr))
|
|
{
|
|
// yes, so let's go with this...
|
|
//
|
|
Cobra2Think_SetDumbFlyDest(self, v3Dest);
|
|
return;
|
|
}
|
|
|
|
// hmmm, wherever we are we must be *really* blocked in, so fall out and allow normal code to pick a new random pos...
|
|
//
|
|
Cobra2Think_SetDumbFlyDest(self, *Cobra2Think_GetRandomPatrolDestPos(self));
|
|
|
|
}// void CobraThink_SmartHeadToDest(edict_t *self)
|
|
|
|
void Cobra2Think_SetTurnDest(edict_t *self, vec3_t v3)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
Cobra2_InitTurnStuff(self,v3);
|
|
|
|
ai->SetCurrentActionNextThink(Cobra2Think_TurnToFace);
|
|
self->nextthink = level.time + FRAMETIME;
|
|
}
|
|
|
|
// fXStrafeDist is of course +ve to our RT, -ve to LT...
|
|
//
|
|
void Cobra2Think_SetSidestrafeDest(edict_t *self, float fXStrafeDist)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
trace_t tr;
|
|
|
|
vec3_t v3Offset,v3NewRelativeDest, vF, vR;
|
|
|
|
AngleVectors(self->s.angles, vF, vR, NULL);
|
|
VectorScale(vR, fXStrafeDist, v3NewRelativeDest);
|
|
if (fXStrafeDist>=0)
|
|
{ // strafing right
|
|
body->m_flags &= ~HELIFLAG_SIDESTRAFEISTOLEFT;
|
|
}
|
|
else
|
|
{
|
|
body->m_flags |= HELIFLAG_SIDESTRAFEISTOLEFT;
|
|
}
|
|
VectorSet(v3Offset,fXStrafeDist,0,0);
|
|
|
|
VectorAdd(self->s.origin,v3NewRelativeDest,body->m_v3HeliDesiredPos_Stop);
|
|
VectorCopy(self->s.origin, body->m_v3HeliDesiredPos_Start);
|
|
|
|
// we need to know if the strafe will send us off the entire world...
|
|
//
|
|
if (!Cobra2_VecWithinWorld(self, body->m_v3HeliDesiredPos_Stop))
|
|
{
|
|
// hmmm, let's have try re-calcing the dest stop pos...
|
|
//
|
|
// notice the use of the Vector_GetHalfWayPoint() call rather than just world-clipping the dest,
|
|
// this keeps the vector correct...
|
|
//
|
|
for (int iTries=0; iTries<10; iTries++) // 30 = arb.
|
|
{
|
|
Vector_GetHalfWayPoint(self->s.origin, body->m_v3HeliDesiredPos_Stop,
|
|
body->m_v3HeliDesiredPos_Stop);
|
|
|
|
if (Cobra2_VecWithinWorld(self, body->m_v3HeliDesiredPos_Stop))
|
|
break;
|
|
}
|
|
|
|
if (iTries==10)
|
|
{
|
|
// this is daft, it's well off the edge...
|
|
//
|
|
#ifdef _DEBUG
|
|
#ifdef SHOW_DESIGNER_ERRORS
|
|
gi.dprintf("### HELISCRIPT: Sidestrafe would go off world edge!\n");
|
|
#endif
|
|
#endif
|
|
|
|
self->nextthink = level.time + FRAMETIME;
|
|
ai->ActionIsDone(true);
|
|
return;
|
|
}
|
|
// new pos now back within world...
|
|
}
|
|
|
|
// vector is within the world, so let's check if it's clear...
|
|
//
|
|
gi.trace(self->s.origin, _HELI_TRACEPARAM_MIN2, _HELI_TRACEPARAM_MAX2,
|
|
body->m_v3HeliDesiredPos_Stop, self,MASK_SOLID, &tr);
|
|
if (tr.fraction < 1.0) // did the trace hit something?
|
|
VectorCopy(tr.endpos,body->m_v3HeliDesiredPos_Stop);
|
|
|
|
ai->SetCurrentActionNextThink(Cobra2Think_SidestrafeToDest);
|
|
self->nextthink = level.time + FRAMETIME;
|
|
}
|
|
|
|
void Cobra2Think_Hover(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
self->nextthink = level.time + FRAMETIME;
|
|
|
|
// hover anim still playing? (approx 3 seconds)
|
|
//
|
|
if ((body->m_flags & HELIFLAG_PLAYINGHOVERANIM) && !body->GetFuselageEOS())
|
|
return;
|
|
|
|
// Hover anim finished. Switch to first frame (normal static position) and hold...
|
|
//
|
|
body->m_flags &= ~HELIFLAG_PLAYINGHOVERANIM;
|
|
SimpleModelSetSequence(self, "cobrass", SMSEQ_HOLD);
|
|
//
|
|
// Now, either finish hover think,
|
|
// or do something nasty (if aggressive)
|
|
// or start anim off again for another hover cycle.
|
|
|
|
// finish hover?...
|
|
//
|
|
if (level.time > body->m_fHeliHoverTimer || (body->m_flags & (HELIFLAG_ISDEAD|HELIFLAG_SCRIPTEDAIJUSTFINISHED)))
|
|
{
|
|
// yep...
|
|
//
|
|
body->m_flags &= ~HELIFLAG_DOINGSCRIPTEDHOVER;
|
|
ai->SetCurrentActionNextThink(Cobra2Think_MovementFinish);
|
|
return;
|
|
}
|
|
|
|
|
|
// do something nasty?
|
|
//
|
|
if (body->m_flags & HELIFLAG_HOVERISAGGRESIVE)
|
|
{
|
|
// Shoot at people from time to time...
|
|
//
|
|
// (originally, it only did this on a 20% chance, which was fine when this was called every .1 seconds, but
|
|
// since it only gets here after a 3 second hover anim has finished it should do it all the time now)
|
|
if (
|
|
(body->m_flags & (HELIFLAG_ROCKETSENABLED|HELIFLAG_CHAINGUNENABLED)) // any weapon enabled
|
|
)
|
|
{
|
|
edict_t *target = Cobra2_GetEntToTarget(self);
|
|
|
|
// if doing hover as a result of scripted AI then ignore visibility arc (because it's more aggressive and probably went into hover at a known good waypoint)...
|
|
//
|
|
if (target && Cobra2HasTargetLOS(self, target, (body->m_flags & HELIFLAG_DOINGSCRIPTEDAI)?false:true)) // qboolean qbAccountForArc
|
|
{
|
|
Cobra2Think_SetFireAt(self, target->s.origin, 1.0f); // 1 second burst
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Turn occasionally, just to look more belligerent/alert...
|
|
//
|
|
// (originally, it only did this on a 20% chance, which was fine when this was called every .1 seconds, but
|
|
// since it only gets here after a 3 second hover anim has finished it should do it all the time now)
|
|
//
|
|
{
|
|
// set random turn dir...
|
|
//
|
|
|
|
vec3_t v3;
|
|
VectorSet(v3, gi.flrand(-1000,1000), gi.flrand(-1000,1000), 0);
|
|
Vec3AddAssign(self->s.origin,v3);
|
|
Cobra2Think_SetTurnDest(self, v3);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// start hover anim off again...
|
|
//
|
|
SimpleModelSetSequence(self, "cobrass", SMSEQ_HOLD);
|
|
body->m_flags |= HELIFLAG_PLAYINGHOVERANIM;
|
|
body->SetFuselageEOS(false);
|
|
|
|
}// void CobraThink_Hover(edict_t *self)
|
|
|
|
void Cobra2_SetHoverParams(edict_t *self, float fTime, qboolean qbAggressive)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
// set flags...
|
|
//
|
|
body->m_flags |= HELIFLAG_DOINGSCRIPTEDHOVER;
|
|
|
|
if (qbAggressive)
|
|
body->m_flags |= HELIFLAG_HOVERISAGGRESIVE;
|
|
else
|
|
body->m_flags &=~HELIFLAG_HOVERISAGGRESIVE;
|
|
//
|
|
// set duration...
|
|
//
|
|
body->m_fHeliHoverTimer = level.time + fTime;
|
|
|
|
// go...
|
|
//
|
|
self->nextthink = level.time + FRAMETIME;
|
|
ai->SetCurrentActionNextThink(Cobra2Think_Hover);
|
|
}
|
|
|
|
void Cobra2Think_SidestrafeToDest(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
vec3_t v3HeliDesiredPos_Halfway;
|
|
Vector_GetHalfWayPoint(body->m_v3HeliDesiredPos_Start, body->m_v3HeliDesiredPos_Stop, v3HeliDesiredPos_Halfway);
|
|
|
|
self->nextthink = level.time + FRAMETIME;
|
|
|
|
Cobra2AimPilotAtPos(self, body->m_v3HeliDesiredPos_Stop); // should look ok I think
|
|
Cobra2ResetGunner(self); // jic.
|
|
|
|
|
|
// have we reached our dest?...
|
|
//
|
|
if (fabs (self->s.origin[0] - body->m_v3HeliDesiredPos_Stop[0]) <= body->m_fHeliMoveSpeed &&
|
|
fabs (self->s.origin[1] - body->m_v3HeliDesiredPos_Stop[1]) <= body->m_fHeliMoveSpeed &&
|
|
fabs (self->s.origin[2] - body->m_v3HeliDesiredPos_Stop[2]) <= body->m_fHeliMoveSpeed
|
|
)
|
|
{
|
|
// we've reached it (or close enough), so do final position adjustement to make it exact...
|
|
//
|
|
//VectorCopy(body->m_v3HeliDesiredPos_Stop,self->s.origin);
|
|
body->m_fHeliMoveSpeed = 0;
|
|
|
|
// make sure we've turned off the gun barrel sequence
|
|
SimpleModelSetSequence2(body->GetBarrelInst(), "barrel_still", SMSEQ_HOLD);
|
|
ai->SetCurrentActionNextThink(Cobra2Think_MovementFinish);
|
|
}
|
|
else
|
|
{// not reached dest...
|
|
//
|
|
// acceleration/roll logic here...
|
|
//
|
|
if (!WithinOrBeyondVectors2(self->s.origin, v3HeliDesiredPos_Halfway, body->m_v3HeliDesiredPos_Stop))
|
|
{
|
|
// accel...
|
|
//
|
|
body->m_fHeliMoveSpeed += (HELI_COBRA_ACCEL*2.0);
|
|
|
|
// First, how far are we along the first half of the journey as a %age?...
|
|
//
|
|
vec3_t v3TotLengthVect,v3LengthVect;
|
|
VectorSubtract(v3HeliDesiredPos_Halfway, body->m_v3HeliDesiredPos_Start, v3TotLengthVect); // journey dist
|
|
VectorSubtract(v3HeliDesiredPos_Halfway, self->s.origin, v3LengthVect); // dist so far
|
|
v3TotLengthVect[2] = v3LengthVect[2] = 0; // lose the Z component
|
|
|
|
// get the inverse percentage...
|
|
// (broken into if-checked lines because of sqrt() returning float-errors occasionally when zeros occur)
|
|
float fTotLen = VectorLength(v3TotLengthVect);
|
|
if (fTotLen>0)
|
|
{
|
|
float fSoFarLen = VectorLength(v3LengthVect);
|
|
if (fSoFarLen>0)
|
|
{
|
|
float fDistPercent = 100-((100/fTotLen) * fSoFarLen);
|
|
float fIdealRoll = -(HELI_COBRA_MAXROLL-((fDistPercent * HELI_COBRA_MAXROLL)/100)); // HELI_COBRA_MAXROLL..0
|
|
|
|
if (!(body->m_flags & HELIFLAG_SIDESTRAFEISTOLEFT))
|
|
fIdealRoll = -fIdealRoll;
|
|
|
|
// make sure 0..360 angle is -180..0..180...
|
|
//
|
|
self->s.angles[HELI_ROLL] = angle2posneg (self->s.angles[HELI_ROLL]);
|
|
|
|
if (fabs(self->s.angles[HELI_ROLL] - fIdealRoll) <= HELISTRAFE_ROLLADJ_SPEED)
|
|
self->s.angles[HELI_ROLL] = fIdealRoll;
|
|
else
|
|
{
|
|
if (self->s.angles[HELI_ROLL] < fIdealRoll)
|
|
self->s.angles[HELI_ROLL] += HELISTRAFE_ROLLADJ_SPEED;
|
|
else
|
|
self->s.angles[HELI_ROLL] -= HELISTRAFE_ROLLADJ_SPEED;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{// decel...
|
|
|
|
body->m_fHeliMoveSpeed -= (HELI_COBRA_ACCEL*1.5);
|
|
if (body->m_fHeliMoveSpeed < 2) // arb, should be z, but this ensure we keep moving if we fall a little short
|
|
body->m_fHeliMoveSpeed = 2;
|
|
|
|
// pitch. First, how far are we along the 2nd half of the journey as a %age?...
|
|
//
|
|
vec3_t v3TotLengthVect,v3LengthVect;
|
|
VectorSubtract(body->m_v3HeliDesiredPos_Stop, v3HeliDesiredPos_Halfway, v3TotLengthVect); // journey dist
|
|
VectorSubtract(body->m_v3HeliDesiredPos_Stop, self->s.origin, v3LengthVect); // dist so far
|
|
v3TotLengthVect[2] = v3LengthVect[2] = 0; // lose the Z component
|
|
|
|
// get the percentage...
|
|
// (broken into if-checked lines because of sqrt() returning float-errors occasionally when zeros occur)
|
|
float fTotLen = VectorLength(v3TotLengthVect);
|
|
if (fTotLen>0)
|
|
{
|
|
float fSoFarLen = VectorLength(v3LengthVect);
|
|
if (fSoFarLen>0)
|
|
{
|
|
float fDistPercent = (100/fTotLen) * fSoFarLen;
|
|
float fIdealRoll = HELI_COBRA_MAXROLL_DECEL-((fDistPercent * HELI_COBRA_MAXROLL_DECEL)/100); // 0..HELI_COBRA_MAXROLL_DECEL
|
|
if (body->m_fHeliMoveSpeed < 2)
|
|
fIdealRoll = 0;
|
|
|
|
// now just adjust our ROLL to suit...
|
|
//
|
|
if (!(body->m_flags & HELIFLAG_SIDESTRAFEISTOLEFT))
|
|
fIdealRoll = -fIdealRoll;
|
|
|
|
// make sure 0..360 angle is -180..0..180...
|
|
//
|
|
self->s.angles[HELI_ROLL] = angle2posneg (self->s.angles[HELI_ROLL]);
|
|
|
|
if (fabs(self->s.angles[HELI_ROLL] - fIdealRoll) <= HELISTRAFE_ROLLADJ_SPEED)
|
|
self->s.angles[HELI_ROLL] = fIdealRoll;
|
|
else
|
|
{
|
|
if (self->s.angles[HELI_ROLL] < fIdealRoll)
|
|
self->s.angles[HELI_ROLL] += HELISTRAFE_ROLLADJ_SPEED;
|
|
else
|
|
self->s.angles[HELI_ROLL] -= HELISTRAFE_ROLLADJ_SPEED;
|
|
}
|
|
}
|
|
}
|
|
}// decel
|
|
|
|
// actual move...
|
|
//
|
|
vec3_t v;
|
|
|
|
VectorSubtract(body->m_v3HeliDesiredPos_Stop,self->s.origin,v);
|
|
VectorNormalize(v);
|
|
VectorMA(self->s.origin, (body->m_fHeliMoveSpeed<HELI_COBRA_MAXSPEED)?body->m_fHeliMoveSpeed:HELI_COBRA_MAXSPEED, v, self->s.origin);
|
|
|
|
// fire!!...
|
|
//
|
|
if (body->m_fHeliMoveSpeed > HELI_COBRA_MAXSPEED/4) // looks more natural
|
|
Cobra2_Fire(self,false); // qboolean qbInihibitRockets
|
|
else
|
|
Cobra2_Fire_OFF(self);
|
|
|
|
}// not reached dest...
|
|
|
|
}// void CobraThink_SidestrafeToDest(edict_t *self)
|
|
|
|
// called for final vertical descent when over pad...(and rotor winddown)
|
|
//
|
|
void Cobra2Think_Land(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
IGhoulInst* instMainRotor = NULL, *instTailRotor = NULL;
|
|
|
|
self->nextthink = level.time + FRAMETIME;
|
|
instMainRotor = body->GetMainRotorInst();
|
|
instTailRotor = body->GetTailRotorInst();
|
|
|
|
// heli pitch and roll will already be zero here, but straighten out heads of pilot and gunner...
|
|
//
|
|
if (!Cobra2MovementFinished(self))
|
|
return;
|
|
|
|
// set speed as a percentage of the landing height...
|
|
//
|
|
float fPercent = ((100.0f/(float)(HELI_COBRA_IDEAL_LANDING_HEIGHT)) * (self->s.origin[2] - body->m_v3HeliPadRestPos[2]));
|
|
float fSpeed = (fPercent * HELI_MAX_LANDING_SPEED)/100; // HELI_MAX_LANDING_SPEED..0
|
|
if (fSpeed<2)
|
|
fSpeed=2; // arb., maintain a certain minimum speed
|
|
|
|
body->m_fHeliMoveSpeed = fSpeed;
|
|
|
|
// since this is only called when X&Y are ok, we'll just check the Z...
|
|
//
|
|
if (fabs (self->s.origin[2] - body->m_v3HeliPadRestPos[2]) < body->m_fHeliMoveSpeed)
|
|
{
|
|
// close enough, we've either just landed or are winding down the rotors...
|
|
//
|
|
VectorCopy(body->m_v3HeliPadRestPos,self->s.origin); // force right coords anyway
|
|
|
|
// wind down the rotor anim...
|
|
//
|
|
if (body->m_flags & HELIFLAG_ROTORSCHANGINGSPEED)
|
|
{
|
|
if (!body->m_bRotorEOS)
|
|
return;
|
|
|
|
// rotors wound down to stop, switch to static blade anim...
|
|
//
|
|
if (instMainRotor && instTailRotor)
|
|
{
|
|
SimpleModelSetSequence2(instMainRotor, "rotor", SMSEQ_HOLD);
|
|
SimpleModelSetSequence2(instTailRotor, "rotor", SMSEQ_HOLD);
|
|
body->m_flags&= ~HELIFLAG_ROTORSCHANGINGSPEED;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// time to start rotor winddown...
|
|
//
|
|
// (this is still far faster than a real chopper wind-down, but short of having a ghoul anim 2 zillion frames long
|
|
// we'll probably have to live with it.
|
|
// Let's just pretend that combat choppers have rotor-brakes for fast-shutdown... <g>)
|
|
//
|
|
if (instMainRotor && instTailRotor)
|
|
{
|
|
SimpleModelSetSequence2(instMainRotor, "rotor_end", SMSEQ_HOLD);
|
|
SimpleModelSetSequence2(instTailRotor, "rotor_end", SMSEQ_HOLD);
|
|
body->m_flags&= ~HELIFLAG_ROTORSCHANGINGSPEED;
|
|
body->m_flags |= HELIFLAG_ROTORSCHANGINGSPEED;
|
|
body->m_bRotorEOS = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// assuming the rotor is now stopped...
|
|
//
|
|
ai->SetCurrentActionNextThink(Cobra2Think_OnPad);
|
|
body->m_fHeliMoveSpeed = 0;
|
|
return;
|
|
}
|
|
|
|
// still descending...
|
|
//
|
|
self->s.origin[2] -= body->m_fHeliMoveSpeed;
|
|
|
|
}// void CobraThink_Land(edict_t *self)
|
|
|
|
// AI can 'rest' inside this code if no script events pending...
|
|
//
|
|
void Cobra2Think_OnPad(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
if (body->m_flags & HELIFLAG_ISDEAD)
|
|
{
|
|
ai->SetCurrentActionNextThink(Cobra2_Explode);
|
|
}
|
|
else
|
|
{
|
|
ai->ActionIsDone(true);
|
|
}
|
|
}
|
|
|
|
// should only be called when you know it's on the pad...
|
|
//
|
|
void Cobra2Think_Repair(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
self->nextthink = level.time + FRAMETIME;
|
|
|
|
if (self->health < self->max_health)
|
|
{
|
|
// repairing...
|
|
//
|
|
#define HELI_SECONDS_FOR_FULLREPAIR 5
|
|
self->health += (self->max_health / (HELI_SECONDS_FOR_FULLREPAIR*10));
|
|
if (self->health <= self->max_health)
|
|
return;
|
|
|
|
// healed...
|
|
self->health = self->max_health;
|
|
}
|
|
|
|
// done with healing...
|
|
//
|
|
ai->SetCurrentActionNextThink(Cobra2Think_OnPad);
|
|
}
|
|
|
|
vec3_t *Cobra2Think_GetRandomPatrolDestPos(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
static vec3_t v3 = {0,0,0};
|
|
trace_t tr;
|
|
|
|
// come up with a random position relative to ourselves...
|
|
//
|
|
v3[0] = self->s.origin[0] + ((gi.flrand(0, 5000))-2500); // 5000 = roughly 535 feet (assuming 6' man is 56 units high)
|
|
v3[1] = self->s.origin[1] + ((gi.flrand(0, 5000))-2500); //
|
|
v3[2] = self->s.origin[2] + ((gi.flrand(0, 500 ))- 250); // less vert movement
|
|
|
|
Cobra2_ClipVecToWorld(self, v3);
|
|
|
|
// for aesthetics, don't go too near the ground when picking random heli dest positions...
|
|
//
|
|
if (v3[2] < ai->GetHeliZWorldMin2()+56*2) // no lower than 2 man heights above the pad
|
|
v3[2] = ai->GetHeliZWorldMin2()+56*2;
|
|
|
|
// now trace between current pos and dest pos to check flightpath is clear...
|
|
//
|
|
gi.trace(self->s.origin, _HELI_TRACEPARAM_MIN2, _HELI_TRACEPARAM_MAX2, v3,
|
|
self,MASK_SOLID, &tr);
|
|
|
|
// (as it happens, this also copes with map edges by returning current position as the new dest (when tr.fraction=0.0f),
|
|
// which would cause a new random dir to be generated next cycle since we'd 'complete' this one instantly)
|
|
//
|
|
if (tr.fraction < 1.0)
|
|
VectorCopy(tr.endpos,v3); // route blocked, so adopt collision point as dest pos...
|
|
|
|
return &v3;
|
|
}
|
|
|
|
void Cobra2Think_SetPilotlookDest(edict_t *self, vec3_t v3)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
VectorCopy(v3,body->m_v3HeliHeadStop);
|
|
ai->SetCurrentActionNextThink(Cobra2Think_PilotLookat);
|
|
self->nextthink = level.time + FRAMETIME;
|
|
}
|
|
|
|
void Cobra2Think_SetGunnerlookDest(edict_t *self, vec3_t v3)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
VectorCopy(v3,body->m_v3HeliHeadStop);
|
|
ai->SetCurrentActionNextThink(Cobra2Think_GunnerLookat);
|
|
self->nextthink = level.time + FRAMETIME;
|
|
}
|
|
|
|
void Cobra2Think_PilotLookat(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
self->nextthink = level.time + FRAMETIME;
|
|
|
|
if (Cobra2AimPilotAtPos(self, body->m_v3HeliHeadStop) != AIM_FALSE)
|
|
{
|
|
ai->ActionIsDone(true);
|
|
}
|
|
}
|
|
|
|
void Cobra2Think_GunnerLookat(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
self->nextthink = level.time + FRAMETIME;
|
|
|
|
if (Cobra2AimGunAtPos(self, body->m_v3HeliHeadStop, true) != AIM_FALSE) // qboolean bSynchGunner);
|
|
{
|
|
ai->ActionIsDone(true);
|
|
}
|
|
}
|
|
|
|
|
|
// called from the strafeing code... (ie probably every 10th of a second)
|
|
//
|
|
// (if you call this, you must call the OFF version below when finished with it)
|
|
//
|
|
// (return val is a # rockets actually fired)
|
|
//
|
|
int _Cobra2Fire_Rockets(edict_t *self, int iRocketsToForceFire)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
int iRocketsFired = 0;
|
|
|
|
if ((body->m_flags & HELIFLAG_ROCKETSENABLED) && body->m_iHeliRocketsLeft)
|
|
{
|
|
if (iRocketsToForceFire ||
|
|
level.time > body->m_fHeliRocketFireDebounceTime) // takes care of both debounce time and initial zero value
|
|
{
|
|
do
|
|
{
|
|
int iTubeNumber = ((body->m_iHeliRocketsLeft+1)&~1)/2; // 1/2/3/4
|
|
|
|
Cobra2FireRockets(self, (body->m_iHeliRocketsLeft&1)?iTubeNumber:0, // int nLeftRockets
|
|
(body->m_iHeliRocketsLeft&1)?0:iTubeNumber // int nRightRockets
|
|
);
|
|
body->m_iHeliRocketsLeft--;
|
|
// re-arm heli when run out, if Greg etc wants it to. Note that I do this instead of removing the -- operator
|
|
// above because this way the code for which tube to fire out of still works ok
|
|
//
|
|
if (!body->m_iHeliRocketsLeft && (self->spawnflags & INFINITE_ROCKETS))
|
|
body->m_iHeliRocketsLeft = HELI_MAX_ROCKETS;
|
|
|
|
body->m_fHeliRocketFireDebounceTime = level.time + 1.0f; // only fires rockets once a second max
|
|
iRocketsFired++;
|
|
}
|
|
while ((--iRocketsToForceFire)>0);
|
|
}
|
|
}
|
|
|
|
return iRocketsFired;
|
|
}
|
|
void _Cobra2Fire_Chaingun(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
if (body->m_flags & HELIFLAG_CHAINGUNENABLED)
|
|
Cobra2FireGun(self, false); // qboolean bSporadic
|
|
}
|
|
|
|
|
|
int Cobra2_Fire(edict_t *self, qboolean qbInihibitRockets)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
#ifdef _DEBUG
|
|
#ifdef SHOW_DESIGNER_ERRORS
|
|
if (!(body->m_flags & (HELIFLAG_ROCKETSENABLED | HELIFLAG_CHAINGUNENABLED)))
|
|
gi.dprintf("### HELISCRIPT: FIRE called when all weapons disabled!\n");
|
|
#endif
|
|
#endif
|
|
|
|
_Cobra2Fire_Chaingun(self);
|
|
return (qbInihibitRockets)?0: _Cobra2Fire_Rockets (self, 0); // int iRocketsToForceFire)
|
|
|
|
}// int Cobra_Fire(edict_t *self, qboolean qbInihibitRockets)
|
|
|
|
|
|
void Cobra2Think_TurnToFace(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
self->nextthink = level.time + FRAMETIME;
|
|
|
|
// aim the pilot at the turn dest, but only aim the gunner/gun there if we're going to fire at the dest on arrival... :-)
|
|
//
|
|
if (body->m_flags & HELIFLAG_DOINGSCRIPTEDFIREAT)
|
|
Cobra2AimGunAtPos(self, body->m_v3HeliFireAtCoords, true); // qboolean bSynchGunner
|
|
else
|
|
Cobra2ResetGunner(self);
|
|
|
|
Cobra2AimPilotAtPos(self, body->m_v3HeliDesiredPos_Stop);
|
|
|
|
if (!Cobra2_TurnToFace(self, body->m_v3HeliDesiredPos_Stop, true, // qbRoll
|
|
true, // qbYAWDictatesFiring
|
|
NULL)) // float *pfDegreesToGo
|
|
{
|
|
return;
|
|
}
|
|
|
|
ai->SetCurrentActionNextThink(Cobra2Think_MovementFinish);
|
|
}
|
|
|
|
|
|
|
|
void Cobra2_SetAIParams(edict_t *self, float fTime)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
body->m_flags |= HELIFLAG_DOINGSCRIPTEDAI;
|
|
|
|
body->m_fHeliAITimer = level.time + fTime;
|
|
body->m_iHeliAITable = -1;
|
|
|
|
self->nextthink = level.time + FRAMETIME;
|
|
ai->SetCurrentActionNextThink(Cobra2Think_AI);
|
|
}
|
|
|
|
|
|
/*
|
|
findme:mystuff
|
|
qboolean Cobra2TargetEntValid(edict_t *self, edict_t *target)
|
|
edict_t *Cobra2_GetEntToTarget(edict_t *self)
|
|
|
|
edict_t* m_AITarget;
|
|
|
|
|
|
ai logic flow
|
|
=============
|
|
|
|
sublogic code:
|
|
{
|
|
before switch():
|
|
|
|
if (current target ent no longer valid)
|
|
cease this ai sublogic
|
|
else
|
|
{
|
|
if (can see target ent)
|
|
{
|
|
switch (rand)
|
|
{
|
|
case:
|
|
|
|
if (rand())
|
|
{
|
|
if (missiles enabled)
|
|
fire 1..2 missile(s) at target ent
|
|
break;
|
|
}
|
|
|
|
fire for rand() time, qb_rocketinhibit = rand()
|
|
break;
|
|
|
|
case:
|
|
|
|
if (rand)
|
|
{
|
|
find waypoint nearest to target ent
|
|
if (waypoint nearer than present origin)
|
|
{
|
|
if !already there,
|
|
goto it
|
|
// else
|
|
// aggressive hover
|
|
}
|
|
// else
|
|
// aggressive hover
|
|
}
|
|
else
|
|
aggressive hover;
|
|
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
do normal switch-case code
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
sublogic caller:
|
|
|
|
(after sublogic called, before new table chooser)
|
|
|
|
if (current target ent no longer valid)
|
|
if (no new target ent found)
|
|
set random hover
|
|
|
|
|
|
|
|
|
|
// rand ai's...
|
|
//
|
|
type 1: (volume sidestrafe) find volume centre nearest current target, if can Z match him (and X or Y volume bounds exceed player's coord), process for strafe
|
|
type 2: (waypoint hover) goto random waypoint, pilot lookat, hover-aggressive for short rand time
|
|
type 3: (volume wander) pick random points within volume space, hover-aggressive, pick random within another volume (do as clipped moverel), goto.
|
|
type 4: (volume zoom) pick random point within a volume, goto, pick any other point within volume, headtodest
|
|
|
|
*/
|
|
|
|
// return values:-
|
|
//
|
|
// 0 = cease the calling AI code function and pick another (probably because target is dead)
|
|
// 1 = carry on with normal AI code
|
|
// 2 = new logic has been setup in here, so just return (hmmm, in the end, this turns out to need the same action upon return as retval 0. Oh well)
|
|
//
|
|
int Cobra2_AISublogic(edict_t *self, generic_ghoul_heli_ai* ai, body_heli* body)
|
|
{
|
|
|
|
// if current target ent no longer valid then cease this sublogic...
|
|
//
|
|
if (!Cobra2TargetEntValid(self, body->m_AITarget))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// if not a very aggressive heli, then don't bother trying to fire at target...
|
|
if (!(body->m_fAIAggressiveness > gi.irand(0,10)))
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
// if we can't see the target, return and carry on as normal...
|
|
//
|
|
// (check heli aggressiveness to decide whether it can see behind it (actually, this is intelligence really, but
|
|
// there isn't a seperate variable for that)...
|
|
//
|
|
if (!(body->m_fAIAggressiveness > gi.irand(0,10)))
|
|
{
|
|
// not-so-aggressive helis check line-of-sight...
|
|
//
|
|
if (!Cobra2HasTargetLOS(self, body->m_AITarget, true)) // qboolean qbAccountForArc
|
|
{
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// now either fire at the target, or move nearer to him to fire later...
|
|
//
|
|
switch (gi.irand(0,1))
|
|
{
|
|
case 0:
|
|
{
|
|
vec3_t v3;
|
|
|
|
// if there are any rockets available, then do rocket-specific attack (if you don't check for this then it might
|
|
// do rocket-firing during a timed attack, which'd cause it to Z-match and change heights (potentially going behind
|
|
// obstructions etc and losing LOS)
|
|
//
|
|
if ( (body->m_flags & HELIFLAG_ROCKETSENABLED) && body->m_iHeliRocketsLeft)
|
|
{
|
|
if (body->m_flags & HELIFLAG_ROCKETSENABLED)
|
|
{
|
|
VectorSet(v3,gi.flrand(-10,10),gi.flrand(-10,10),0); // at least have a chance of missing
|
|
VectorAdd(body->m_AITarget->s.origin, v3, v3);
|
|
v3[2] += body->m_AITarget->viewheight;
|
|
Cobra2Think_SetFireAt(self, v3, (gi.irand(0,10)>=5)?-1:-2);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// if we got here then no rocket attack happened, so do a timed attack... (which because of above check will be chaingun only)
|
|
//
|
|
VectorSet(v3,0,0,0); // flrand(-50,50),flrand(-50,50),0);
|
|
v3[2] += body->m_AITarget->viewheight;
|
|
VectorAdd(body->m_AITarget->s.origin, v3, v3);
|
|
Cobra2Think_SetFireAt(self, v3, gi.flrand(2,4));
|
|
break;
|
|
}
|
|
|
|
case 1:
|
|
{
|
|
// 50-50 either do an aggressive hover, or goto nearest waypoint from which he can see the player...
|
|
//
|
|
int iWayPoints = ai->GetNumWaypointPts();
|
|
if (iWayPoints && (body->m_fAIAggressiveness > gi.irand(0,10)))
|
|
{
|
|
// find nearest waypoint to target ent...
|
|
//
|
|
vec3_t v3;
|
|
float fBestDist = 1000000; // some suitably huge number bigger than any map (there is some sort of float_max defined, but only for windows, and this is server-side code)
|
|
vec3_t v3BestWayPoint;
|
|
|
|
float fBestDist_Visible = 1000000; // some suitably huge number bigger than any map (there is some sort of float_max defined, but only for windows, and this is server-side code)
|
|
vec3_t v3BestWayPoint_Visible;
|
|
qboolean qbVisibleWayPointFound = false;
|
|
|
|
trace_t tr;
|
|
|
|
// this will be used several times, so work it out here...
|
|
//
|
|
vec3_t v3TargetEyeLevel;
|
|
VectorCopy (body->m_AITarget->s.origin, v3TargetEyeLevel);
|
|
v3TargetEyeLevel[2] += body->m_AITarget->viewheight;
|
|
|
|
float fClosestSeePosDist = 1000000; // some suitably huge number bigger than any map (there is some sort of float_max defined, but only for windows, and this is server-side code)
|
|
vec3_t v3ClosestSeePosToCurrent;
|
|
VectorClear(v3ClosestSeePosToCurrent); // tested for Z later
|
|
|
|
for (int i=0; i<iWayPoints; i++)
|
|
{
|
|
heliWaypoint* pWayPoint = ai->GetWaypointPt(i);
|
|
|
|
if (NULL == pWayPoint)
|
|
{
|
|
continue;
|
|
}
|
|
VectorSubtract(body->m_AITarget->s.origin,pWayPoint->m_v,v3);
|
|
float fThisDist = VectorLength(v3);
|
|
|
|
|
|
// Can we see the target from here?...
|
|
//
|
|
if (gi.trace(pWayPoint->m_v, NULL, NULL, v3TargetEyeLevel, self, MASK_OPAQUE, &tr))
|
|
{
|
|
// yes, so keep the best can-see waypoint...
|
|
//
|
|
if (fThisDist<fBestDist_Visible)
|
|
{
|
|
fBestDist_Visible = fThisDist;
|
|
VectorCopy(pWayPoint->m_v,v3BestWayPoint_Visible);
|
|
qbVisibleWayPointFound = true;
|
|
}
|
|
|
|
// is this visible waypoint the closest to our current position?...(used later on for logic tweak)
|
|
//
|
|
VectorSubtract(self->s.origin,pWayPoint->m_v,v3);
|
|
if (!Vec3IsZero(v3)) // don't bother if we're already at this position
|
|
{
|
|
fThisDist = VectorLength(v3);
|
|
if (fThisDist<fClosestSeePosDist)
|
|
{
|
|
fClosestSeePosDist = fThisDist;
|
|
VectorCopy(pWayPoint->m_v,v3ClosestSeePosToCurrent);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// no, but record it anyway in case none of the waypoint positions can see the target...
|
|
//
|
|
if (fThisDist<fBestDist)
|
|
{
|
|
fBestDist = fThisDist;
|
|
VectorCopy(pWayPoint->m_v,v3BestWayPoint);
|
|
}
|
|
}
|
|
}
|
|
|
|
// We'll always have a valid best waypoint here, no need to verify.
|
|
// Now, if we can already see the target from here, then only adopt the new position if it can also
|
|
// see the player and is closer, but if we can't see it (and the new position can) then go there even if
|
|
// it's further away (this will look really intelligent)...
|
|
//
|
|
VectorSubtract(body->m_AITarget->s.origin,self->s.origin,v3);
|
|
float fThisDist = VectorLength(v3);
|
|
|
|
// can we see the target already?...
|
|
//
|
|
if (gi.trace(self->s.origin, NULL, NULL, v3TargetEyeLevel, self, MASK_OPAQUE, &tr))
|
|
{
|
|
// yes, did we find another waypoint that could also see the target?...
|
|
//
|
|
if (qbVisibleWayPointFound)
|
|
{
|
|
// is the new waypoint at least 800 (arb.) world units closer?...
|
|
//
|
|
if (fBestDist_Visible+800<fThisDist)
|
|
{
|
|
Cobra2Think_SetSmartFlyDest(self, v3BestWayPoint_Visible);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// we can't see the target from here, so should we move to the best waypoint so far?...
|
|
//
|
|
if (qbVisibleWayPointFound) // if we can see it from somewhere else go to it regardless of distance
|
|
{
|
|
// ok, we can either go to the closest visible waypoint to the target, or to us...
|
|
//
|
|
if (!Vec3IsZero(v3))
|
|
{
|
|
// there is a position closer to us than the closest waypoint (so we'd have to move less), so use it...
|
|
//
|
|
Cobra2Think_SetSmartFlyDest(self, v3ClosestSeePosToCurrent);
|
|
break;
|
|
}
|
|
|
|
// goto the waypoint closest the target...
|
|
//
|
|
Cobra2Think_SetSmartFlyDest(self, v3BestWayPoint_Visible);
|
|
break;
|
|
}
|
|
|
|
// no visible point found, so only goto this waypoint if it's significantly closer...
|
|
//
|
|
if (fBestDist+800<fThisDist) // 800 = arb. dist
|
|
{
|
|
Cobra2Think_SetSmartFlyDest(self, v3BestWayPoint);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// either there are no waypoints, or we already appear to be closer to the target than any waypoint alternative, so...
|
|
//
|
|
Cobra2_SetHoverParams(self,gi.flrand(1,3),true); // qboolean qbAggressive
|
|
break;
|
|
}
|
|
}// switch (irand(0,100)%2)
|
|
|
|
return 2;
|
|
|
|
}// int Cobra2_AISublogic(edict_t *self, generic_ghoul_heli_ai* ai, body_heli* body)
|
|
|
|
|
|
|
|
// In these AI routines, simply set iHeliAITable to -1 to signal that the AI table is done
|
|
//
|
|
extern vec3_t TestWayPoints[];
|
|
|
|
|
|
/*
|
|
// rand ai's...
|
|
//
|
|
type 1: (volume sidestrafe) find volume centre nearest current target, if can Z match him (and X or Y volume bounds exceed player's coord), process for strafe
|
|
type 2: (waypoint hover) goto random waypoint, pilot lookat, hover-aggressive for short rand time
|
|
type 3: (volume wander) pick random points within volume space, hover-aggressive, pick random within another volume (do as clipped moverel), goto.
|
|
type 4: (volume zoom) pick random point within a volume, goto, pick any other point within volume, headtodest
|
|
*/
|
|
|
|
|
|
// type 1: (volume sidestrafe) find volume centre nearest current target, if can Z match him (and X or Y volume bounds exceed player's coord), process for strafe
|
|
//
|
|
void Cobra2AI_0(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
int iSublogicRet = Cobra2_AISublogic(self, ai, body);
|
|
|
|
switch (iSublogicRet)
|
|
{
|
|
case 0: body->m_iHeliAITable = -1; // (and fall through)
|
|
case 2: return;
|
|
}
|
|
|
|
switch (body->m_iHeliAIIndex++)
|
|
{
|
|
case 0:
|
|
{
|
|
|
|
// is there a suitable waypoint volume we can go to for a strafe?...
|
|
//
|
|
int iWayPointVolumes = ai->GetNumWaypointVols();
|
|
if (iWayPointVolumes)
|
|
{
|
|
//
|
|
// find nearest volume to target ent...
|
|
//
|
|
vec3_t v3;
|
|
float fBestDist = 1000000; // some suitably huge number bigger than any map (there is some sort of float_max defined, but only for windows, and this is server-side code)
|
|
heliWaypoint* pBestWayPoint = NULL;
|
|
|
|
for (int i=0; i<iWayPointVolumes; i++)
|
|
{
|
|
heliWaypoint* pWayPoint = ai->GetWaypointVol(i);
|
|
|
|
VectorSubtract(body->m_AITarget->s.origin,pWayPoint->m_v,v3);
|
|
float fThisDist = VectorLength(v3);
|
|
|
|
if (fThisDist<fBestDist)
|
|
{
|
|
fBestDist = fThisDist;
|
|
pBestWayPoint = pWayPoint;
|
|
}
|
|
}
|
|
|
|
if (pBestWayPoint) // should always be true, but wtf?
|
|
{
|
|
// Now, can we match target Z inside this volume?...
|
|
//
|
|
vec3_t vMins,vMaxs;
|
|
|
|
pBestWayPoint->GetVolumeMins(vMins);
|
|
pBestWayPoint->GetVolumeMaxs(vMaxs);
|
|
|
|
if (body->m_AITarget->s.origin[2] >= vMins[2] &&
|
|
body->m_AITarget->s.origin[2] <= vMaxs[2]
|
|
)
|
|
{// volume encompasses target Z
|
|
//
|
|
// now check either Y or X first...
|
|
//
|
|
int iAxisIndex = gi.irand(0,1); // 0 or 1
|
|
int iGoodAxis = -1;
|
|
|
|
if (vMins[iAxisIndex] < body->m_AITarget->s.origin[iAxisIndex] &&
|
|
vMaxs[iAxisIndex] > body->m_AITarget->s.origin[iAxisIndex]
|
|
)
|
|
iGoodAxis = iAxisIndex;
|
|
else
|
|
{
|
|
iAxisIndex ^=1; // toggle 0..1 (ie between x and y axis)
|
|
if (vMins[iAxisIndex] < body->m_AITarget->s.origin[iAxisIndex] &&
|
|
vMaxs[iAxisIndex] > body->m_AITarget->s.origin[iAxisIndex]
|
|
)
|
|
iGoodAxis = iAxisIndex;
|
|
}
|
|
|
|
if (iGoodAxis != -1)
|
|
{
|
|
// excellent! we've found a suitable strafeing volume...
|
|
//
|
|
// now work out a suitable sidestrafe distance...
|
|
//
|
|
body->m_fSideStrafeDist = min( fabs(vMins[iGoodAxis] - body->m_AITarget->s.origin[iGoodAxis]),
|
|
fabs(vMaxs[iGoodAxis] - body->m_AITarget->s.origin[iGoodAxis])
|
|
);
|
|
body->m_fSideStrafeDist = min(body->m_fSideStrafeDist,800); // limit it
|
|
|
|
// don't do small strafes, by the time it's ready to fire the run has ended...
|
|
if (body->m_fSideStrafeDist > 100)
|
|
{
|
|
// make up a dest coord within the volume at nearest point to target...(strafe centre)
|
|
//
|
|
vec3_t v3;
|
|
VectorCopy(body->m_AITarget->s.origin,v3);
|
|
|
|
if (vMins[iGoodAxis^1]>body->m_AITarget->s.origin[iGoodAxis^1])
|
|
{
|
|
v3[iGoodAxis^1] = vMins[iGoodAxis^1];
|
|
if (iGoodAxis==1)
|
|
body->m_iSideStrafeDir = 3;
|
|
else
|
|
body->m_iSideStrafeDir = 2;
|
|
}
|
|
else
|
|
{
|
|
v3[iGoodAxis^1] = vMaxs[iGoodAxis^1];
|
|
if (iGoodAxis==1)
|
|
body->m_iSideStrafeDir = 1;
|
|
else
|
|
body->m_iSideStrafeDir = 0;
|
|
}
|
|
|
|
// now move to one side...
|
|
//
|
|
if (gi.irand(0,10)>=5)
|
|
body->m_fSideStrafeDist = -body->m_fSideStrafeDist; // random left or right
|
|
|
|
const vec3_t v3StrafeDirMults[4] = {
|
|
{1,0,0}, // facing north, strafeing RT
|
|
{0,-1,0}, // facing east, strafeing RT
|
|
{-1,0,0}, // facing south, strafeing RT
|
|
{0,1,0} // facing west, strafeing RT
|
|
};
|
|
|
|
for (int i=0; i<3; i++)
|
|
v3[i] += (v3StrafeDirMults[body->m_iSideStrafeDir][i]) * -body->m_fSideStrafeDist;
|
|
|
|
Cobra2Think_SetSmartFlyDest(self, v3);
|
|
return;
|
|
}// if (body->m_fSideStrafeDist > 100)
|
|
}// Cobra2Think_SetSmartFlyDest(self, v3BestWayPoint_Visible);
|
|
}// volume encompasses target Z
|
|
}// if (pBestWayPoint)
|
|
}// if (iWayPointVolumes)
|
|
break; // no suitable volume found, so break out and invalidate table at func bot.
|
|
}// case 0:
|
|
|
|
case 1:
|
|
{
|
|
// we've arrived at one end of the strafe run, so turn to face the proper direction...
|
|
//
|
|
const vec3_t v3StrafeDirAdds[4] = {
|
|
{0,1,0}, // facing north
|
|
{1,0,0}, // facing east
|
|
{0,-1,0}, // facing south
|
|
{-1,0,0} // facing west
|
|
};
|
|
vec3_t v3;
|
|
VectorCopy(self->s.origin,v3);
|
|
for (int i=0; i<3; i++)
|
|
v3[i] += v3StrafeDirAdds[body->m_iSideStrafeDir][i] * 1000;
|
|
|
|
Cobra2Think_SetTurnDest(self, v3);
|
|
return;
|
|
}
|
|
|
|
case 2:
|
|
{
|
|
// we're now at the correct position, facing the correct dir, so start the strafe...
|
|
//
|
|
Cobra2Think_SetSidestrafeDest(self, body->m_fSideStrafeDist*2); // *2 because prev value was effectively the 'radius' (ie half)
|
|
return;
|
|
}
|
|
}// switch (body->m_iHeliAIIndex++)
|
|
|
|
body->m_iHeliAITable = -1;
|
|
}
|
|
|
|
|
|
|
|
|
|
// type 2: (waypoint hover) goto random waypoint, pilot lookat, hover-aggressive for short rand time
|
|
void Cobra2AI_1(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
int iSublogicRet = Cobra2_AISublogic(self, ai, body);
|
|
|
|
switch (iSublogicRet)
|
|
{
|
|
case 0: body->m_iHeliAITable = -1; // (and fall through)
|
|
case 2: return;
|
|
}
|
|
|
|
switch (body->m_iHeliAIIndex++)
|
|
{
|
|
case 0:
|
|
{
|
|
// pick random waypoint to head to...
|
|
//
|
|
int iWayPoints = ai->GetNumWaypointPts();
|
|
if (iWayPoints)
|
|
{
|
|
heliWaypoint* pWayPoint = ai->GetWaypointPt(gi.irand(0,100)%iWayPoints);
|
|
|
|
Cobra2Think_SetSmartFlyDest(self, pWayPoint->m_v);
|
|
return;
|
|
}
|
|
break; // no waypoints, so forget this logic code
|
|
}
|
|
|
|
case 1:
|
|
{
|
|
// I'll not bother doing a pilot lookat, it'd get overridden on the first hover-turn anyway,
|
|
// so just swing round to face the player..
|
|
//
|
|
Cobra2Think_SetTurnDest(self, body->m_AITarget->s.origin);
|
|
return;
|
|
}
|
|
|
|
case 2:
|
|
{
|
|
// hover for a short time...
|
|
//
|
|
Cobra2_SetHoverParams(self,gi.flrand(1,3), true
|
|
//(body->m_fAIAggressiveness > irand(0,10))?true:false); // qboolean qbAggressive
|
|
);
|
|
return;
|
|
}
|
|
}// switch (body->m_iHeliAIIndex++)
|
|
|
|
body->m_iHeliAITable = -1;
|
|
}
|
|
|
|
|
|
// type 3: (volume wander) pick random points within volume space, hover-aggressive, repeat sequence, exit
|
|
void Cobra2AI_2(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
int iSublogicRet = Cobra2_AISublogic(self, ai, body);
|
|
|
|
switch (iSublogicRet)
|
|
{
|
|
case 0: body->m_iHeliAITable = -1; // (and fall through)
|
|
case 2: return;
|
|
}
|
|
|
|
switch (body->m_iHeliAIIndex++)
|
|
{
|
|
case 0:
|
|
case 2:
|
|
{
|
|
|
|
int iWayPointVolumes = ai->GetNumWaypointVols();
|
|
if (iWayPointVolumes)
|
|
{
|
|
heliWaypoint* pVolume = ai->GetWaypointVol(gi.irand(0,100)%iWayPointVolumes);
|
|
|
|
// now goto a random position within that volume...
|
|
//
|
|
vec3_t v3,vMins,vMaxs;
|
|
pVolume->GetVolumeMins(vMins);
|
|
pVolume->GetVolumeMaxs(vMaxs);
|
|
|
|
VectorSet(v3,gi.flrand(vMins[0],vMaxs[0]),gi.flrand(vMins[1],vMaxs[1]),gi.flrand(vMins[2],vMaxs[2]));
|
|
Cobra2Think_SetSmartFlyDest(self, v3);
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 1:
|
|
case 3:
|
|
{
|
|
// hover aggressive for a short time...
|
|
//
|
|
Cobra2_SetHoverParams(self,gi.flrand(1,3), true); // qboolean qbAggressive
|
|
return;
|
|
}
|
|
}// switch (body->m_iHeliAIIndex++)
|
|
|
|
body->m_iHeliAITable = -1;
|
|
}
|
|
|
|
|
|
// type 4: (volume zoom) pick random point within a volume, goto, pick any other point within same volume, headtodest
|
|
void Cobra2AI_3(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
edict_t* nextTarget = Cobra2_GetEntToTarget(self);
|
|
int iSublogicRet = Cobra2_AISublogic(self, ai, body);
|
|
|
|
switch (iSublogicRet)
|
|
{
|
|
case 0: body->m_iHeliAITable = -1; // (and fall through)
|
|
case 2: return;
|
|
}
|
|
|
|
switch (body->m_iHeliAIIndex++)
|
|
{
|
|
case 0:
|
|
{
|
|
int iWayPointVolumes = ai->GetNumWaypointVols();
|
|
if (iWayPointVolumes)
|
|
{
|
|
body->m_iWhichRandVolume = gi.irand(0,100)%iWayPointVolumes;
|
|
heliWaypoint* pVolume = ai->GetWaypointVol(body->m_iWhichRandVolume);
|
|
|
|
// now goto a random position within that volume...
|
|
//
|
|
vec3_t v3,vMins,vMaxs;
|
|
pVolume->GetVolumeMins(vMins);
|
|
pVolume->GetVolumeMaxs(vMaxs);
|
|
|
|
VectorSet(v3,gi.flrand(vMins[0],vMaxs[0]),gi.flrand(vMins[1],vMaxs[1]),gi.flrand(vMins[2],vMaxs[2]));
|
|
Cobra2Think_SetSmartFlyDest(self, v3);
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 1:
|
|
case 2:
|
|
{
|
|
// goto another point within the same volume...
|
|
//
|
|
heliWaypoint* pVolume = ai->GetWaypointVol(body->m_iWhichRandVolume);
|
|
|
|
// now goto a random position within that volume...
|
|
//
|
|
vec3_t v3,vMins,vMaxs;
|
|
pVolume->GetVolumeMins(vMins);
|
|
pVolume->GetVolumeMaxs(vMaxs);
|
|
|
|
VectorSet(v3,gi.flrand(vMins[0],vMaxs[0]),gi.flrand(vMins[1],vMaxs[1]),gi.flrand(vMins[2],vMaxs[2]));
|
|
Cobra2Think_SetSmartFlyDest(self, v3);
|
|
return;
|
|
}
|
|
|
|
case 3:
|
|
{
|
|
// hover aggressive for a short time...
|
|
//
|
|
Cobra2_SetHoverParams(self,2, true); // qboolean qbAggressive
|
|
return;
|
|
}
|
|
}
|
|
|
|
body->m_iHeliAITable = -1;
|
|
}
|
|
|
|
#define NUM_COBRA2_AI_ROUTINES 4 // update this as needed
|
|
|
|
|
|
void Cobra2Think_AI(edict_t *self)
|
|
{
|
|
self->nextthink = level.time + FRAMETIME;
|
|
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
int iHeliAITable_Last = body->m_iHeliAITable;
|
|
|
|
if (body->m_iHeliAITable != -1)
|
|
{
|
|
switch (body->m_iHeliAITable)
|
|
{
|
|
case 0: Cobra2AI_0(self); break;
|
|
case 1: Cobra2AI_1(self); break;
|
|
case 2: Cobra2AI_2(self); break;
|
|
case 3: Cobra2AI_3(self); break;
|
|
}
|
|
}
|
|
|
|
if (body->m_iHeliAITable == -1)
|
|
{
|
|
// pick a rand AI table (and check it's not the one we've just done) and a new target...
|
|
//
|
|
body->m_AITarget = Cobra2_GetEntToTarget(self);
|
|
body->m_iHeliAITable = gi.irand(0,NUM_COBRA2_AI_ROUTINES)%NUM_COBRA2_AI_ROUTINES; // because irand() *almost* never returns the max arg
|
|
if (body->m_iHeliAITable == iHeliAITable_Last)
|
|
body->m_iHeliAITable = (body->m_iHeliAITable+1)%NUM_COBRA2_AI_ROUTINES;
|
|
body->m_iHeliAIIndex = 0;
|
|
ai->ActionIsDone(true);
|
|
|
|
}
|
|
|
|
}// void CobraThink_AI(edict_t *self)
|
|
|
|
|
|
// this should be return true if the target is either a live client or merc...
|
|
//
|
|
qboolean Cobra2TargetEntValid(edict_t *self, edict_t *target)
|
|
{
|
|
// fixme: update this when we decide how to tell a merc from a monster...
|
|
//
|
|
if (!target ||
|
|
!target->inuse || // dead merc
|
|
(target->inuse && target->takedamage==DAMAGE_NO) // dead client
|
|
)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// called at every new internal-AI action to decide who to shoot at, currently this will cycle just
|
|
// between player(s), but later will have to be rewritten in order to shoot at non-client teammates (ie mercs in SoF)
|
|
//
|
|
// (returns NULL if no suitable target found)
|
|
//
|
|
edict_t *Cobra2_GetEntToTarget(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
edict_t *target;
|
|
edict_t *target_firstfound=NULL;
|
|
int iTargetFirstFoundIndex;
|
|
|
|
// time to select a new target?...
|
|
//
|
|
if (level.time < body->m_fNewTargetTimer && Cobra2TargetEntValid(self, body->m_AITarget))
|
|
return body->m_AITarget;
|
|
|
|
body->m_fNewTargetTimer = level.time + 30; // new target picked every 30 seconds
|
|
body->m_AITarget = NULL;
|
|
|
|
// fixme: this loop will probably need expanding when it's finally decided how mercs are implemented...
|
|
for (int i=1; i<MAX_CLIENTS+1; i++)
|
|
{
|
|
target = &g_edicts[i];
|
|
|
|
// (fixme:update this check later to account for hired (non-client) mercs)
|
|
if (!target->client)
|
|
break; // apparently there's no gap in the client list, so this point is EOL
|
|
|
|
if (!Cobra2TargetEntValid(self, target))
|
|
continue;
|
|
|
|
// this entity is alive and a viable heli target...
|
|
//
|
|
if (!target_firstfound)
|
|
{
|
|
target_firstfound = target;
|
|
iTargetFirstFoundIndex = i;
|
|
}
|
|
|
|
// should we use this as the return value?
|
|
//
|
|
if (i>body->m_iLastEntFoundIndex)
|
|
{
|
|
body->m_iLastEntFoundIndex=i;
|
|
body->m_AITarget = target; // found a target further along ent list than last time, so use it
|
|
break;
|
|
}
|
|
}
|
|
|
|
// list exhausted, if there was anyone at all then target them...
|
|
//
|
|
if (target_firstfound)
|
|
{
|
|
body->m_iLastEntFoundIndex=iTargetFirstFoundIndex;
|
|
body->m_AITarget = target_firstfound;
|
|
}
|
|
|
|
return body->m_AITarget;
|
|
|
|
}// edict_t *Cobra2_GetEntToTarget(edict_t *self)
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
// end: stuff copied directly from m_heli.cpp
|
|
////////////////////////////////////////////////////////////////////////
|
|
|
|
void Cobra2_OutOfControl(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
vec3_t vGravity = {0, 0, 0};
|
|
vec3_t vF, vR, vU;
|
|
float fFinalPitch = 20; // kef
|
|
AngleVectors(self->s.angles, vF, vR, vU);
|
|
{
|
|
vF[2] = 0;
|
|
if (self->s.angles[HELI_PITCH] < fFinalPitch)
|
|
{
|
|
self->s.angles[HELI_PITCH] += 2;
|
|
}
|
|
else
|
|
{
|
|
self->s.angles[HELI_PITCH] -= 2;
|
|
}
|
|
|
|
if (-fFinalPitch - fabs(self->s.angles[HELI_PITCH]) < 2)
|
|
{
|
|
vGravity[2] = max(-10, -4*(level.time - body->m_fHeliHoverTimer));
|
|
}
|
|
VectorAdd(self->velocity, vGravity, self->velocity);
|
|
VectorMA(self->velocity, self->s.angles[HELI_PITCH]*-2.5f, vF, self->velocity);
|
|
|
|
self->avelocity[YAW] = min(self->avelocity[YAW]+45,180);
|
|
}
|
|
ai->SetCurrentActionNextThink(Cobra2_OutOfControl);
|
|
}
|
|
|
|
void Cobra2Touch(edict_t *self, edict_t *other, cplane_t *plane, mtexinfo_t *surf)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
if (body->m_flags & HELIFLAG_ISDEADACKNOWLEDGED)
|
|
{
|
|
ai->TimeToDie(true);
|
|
}
|
|
}
|
|
|
|
void Cobra2LandHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
if (self->spawnflags & IN_AIR)
|
|
{
|
|
#ifdef _DEBUG
|
|
#ifdef SHOW_DESIGNER_ERRORS
|
|
gi.dprintf("### HELISCRIPT: Can't land when spawned in air! (no pad position)\n");
|
|
#endif
|
|
#endif
|
|
ai->ActionIsDone(true);
|
|
return;
|
|
}
|
|
|
|
// may as well check if we're already at the right XY position... (saves climbing up to v3IdealLandingPos then dropping again)
|
|
//
|
|
if (self->s.origin[0] != body->m_v3HeliPadRestPos[0] ||
|
|
self->s.origin[1] != body->m_v3HeliPadRestPos[1] ||
|
|
self->s.origin[2] > body->m_v3HeliPadRestPos[2]+HELI_COBRA_IDEAL_LANDING_HEIGHT
|
|
)
|
|
{
|
|
// xy mismatch, or too high, so head to a suitable place above the pad...
|
|
//
|
|
vec3_t v3IdealLandingPos;
|
|
|
|
VectorCopy(body->m_v3HeliPadRestPos,v3IdealLandingPos);
|
|
v3IdealLandingPos[2]+=HELI_COBRA_IDEAL_LANDING_HEIGHT;
|
|
|
|
Cobra2Think_SetSmartFlyDest(self, v3IdealLandingPos);
|
|
}
|
|
else
|
|
{
|
|
ai->ActionIsDone(true);
|
|
}
|
|
body->m_flags |= HELIFLAG_DOINGSCRIPTEDLANDING; // the clever bit (well, there has to be one...)
|
|
}
|
|
|
|
void Cobra2GotoCoordsHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
vec3_t vPos;
|
|
VectorCopy(body->m_vPos,vPos);
|
|
|
|
Cobra2Think_SetSmartFlyDest(self, vPos);
|
|
}
|
|
|
|
void Cobra2GotoRelEntHelper(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
vec3_t vPos;
|
|
VectorCopy(body->m_target->s.origin, body->m_v3HeliDesiredPos_Smart);
|
|
if (body->m_vPos[0])
|
|
{
|
|
body->m_v3HeliDesiredPos_Smart[0] = body->m_target->s.origin[0] + body->m_vPos[0];
|
|
}
|
|
if (body->m_vPos[1])
|
|
{
|
|
body->m_v3HeliDesiredPos_Smart[1] = body->m_target->s.origin[1] + body->m_vPos[1];
|
|
}
|
|
if (body->m_vPos[2])
|
|
{
|
|
body->m_v3HeliDesiredPos_Smart[2] = body->m_target->s.origin[2] + body->m_vPos[2];
|
|
}
|
|
VectorCopy(body->m_v3HeliDesiredPos_Smart,vPos);
|
|
|
|
Cobra2Think_SetSmartFlyDest(self, vPos);
|
|
}
|
|
|
|
void Cobra2GotoRelHelper(edict_t *self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
vec3_t vPos;
|
|
VectorAdd(body->m_vPos, self->s.origin, vPos);
|
|
|
|
Cobra2Think_SetSmartFlyDest(self, vPos);
|
|
}
|
|
|
|
void Cobra2StrafeHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
Cobra2Think_SetSidestrafeDest(self, body->m_fArg);
|
|
}
|
|
|
|
void Cobra2HoverHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
// kinda silly, but I'm using body->m_vPos[0] as a boolean cuz I need
|
|
//a parameter other than body->m_fArg
|
|
Cobra2_SetHoverParams(self, body->m_fArg, body->m_vPos[0]);
|
|
}
|
|
|
|
void Cobra2HamburgerHelper(edict_t* self)
|
|
{
|
|
// I couldn't resist...
|
|
}
|
|
|
|
void Cobra2FaceRelEntHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
vec3_t v3;
|
|
|
|
VectorAdd(body->m_target->s.origin, body->m_vPos, v3);
|
|
Cobra2Think_SetTurnDest(self, v3);
|
|
}
|
|
|
|
void Cobra2FaceAbsHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
vec3_t v3;
|
|
|
|
VectorCopy(body->m_vPos, v3);
|
|
Cobra2Think_SetTurnDest(self, v3);
|
|
}
|
|
|
|
void Cobra2FaceAbsDirHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
vec3_t v3;
|
|
|
|
VectorCopy(body->m_vPos, v3);
|
|
VectorNormalize(v3);
|
|
VectorMA(self->s.origin, 1000, v3, v3);
|
|
Cobra2Think_SetTurnDest(self, v3);
|
|
}
|
|
|
|
void Cobra2FaceRelCoordsHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
vec3_t v3;
|
|
|
|
VectorAdd(self->s.origin, body->m_vPos, v3);
|
|
Cobra2Think_SetTurnDest(self, v3);
|
|
}
|
|
|
|
void Cobra2HeadFaceRelEntHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
vec3_t v3;
|
|
|
|
VectorAdd(body->m_target->s.origin, body->m_vPos, v3);
|
|
if (body->m_fArg)
|
|
{
|
|
Cobra2Think_SetPilotlookDest(self, v3);
|
|
}
|
|
else
|
|
{
|
|
Cobra2Think_SetGunnerlookDest(self, v3);
|
|
}
|
|
}
|
|
|
|
void Cobra2HeadFaceRelCoordsHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
vec3_t v3;
|
|
|
|
VectorAdd(self->s.origin, body->m_vPos, v3);
|
|
if (body->m_fArg)
|
|
{
|
|
Cobra2Think_SetPilotlookDest(self, v3);
|
|
}
|
|
else
|
|
{
|
|
Cobra2Think_SetGunnerlookDest(self, v3);
|
|
}
|
|
}
|
|
|
|
void Cobra2HeadFaceAbsCoordsHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
vec3_t v3;
|
|
|
|
VectorCopy(body->m_vPos, v3);
|
|
if (body->m_fArg)
|
|
{
|
|
Cobra2Think_SetPilotlookDest(self, v3);
|
|
}
|
|
else
|
|
{
|
|
Cobra2Think_SetGunnerlookDest(self, v3);
|
|
}
|
|
}
|
|
|
|
void Cobra2FireAtRelEntHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
// if told to fire at someone who's dead, then don't bother...
|
|
edict_t* target = body->m_target;
|
|
|
|
if (!Cobra2TargetEntValid(self,target))
|
|
{
|
|
ai->ActionIsDone(true);
|
|
return;
|
|
}
|
|
|
|
vec3_t v3;
|
|
VectorAdd(target->s.origin, body->m_vPos, v3);
|
|
Cobra2Think_SetFireAt(self, v3, body->m_fArg);
|
|
}
|
|
|
|
void Cobra2FireAtAbsHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
vec3_t v3;
|
|
|
|
VectorCopy(body->m_vPos, v3);
|
|
Cobra2Think_SetFireAt(self, v3, body->m_fArg);
|
|
}
|
|
|
|
void Cobra2FireAtRelHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
vec3_t v3;
|
|
|
|
VectorAdd(self->s.origin, body->m_vPos, v3);
|
|
Cobra2Think_SetFireAt(self, v3, body->m_fArg);
|
|
}
|
|
|
|
void Cobra2RocketsEnableHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
if (body->m_fArg)
|
|
{
|
|
body->m_flags |= HELIFLAG_ROCKETSENABLED;
|
|
}
|
|
else
|
|
{
|
|
body->m_flags &= ~HELIFLAG_ROCKETSENABLED;
|
|
}
|
|
ai->ActionIsDone(true);
|
|
}
|
|
|
|
void Cobra2ChainGunEnableHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
if (body->m_fArg)
|
|
{
|
|
body->m_flags |= HELIFLAG_CHAINGUNENABLED;
|
|
}
|
|
else
|
|
{
|
|
body->m_flags &= ~HELIFLAG_CHAINGUNENABLED;
|
|
Cobra2_Fire_OFF(self);
|
|
}
|
|
ai->ActionIsDone(true);
|
|
}
|
|
|
|
void Cobra2AutoFireHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
if (body->m_fArg)
|
|
{
|
|
body->m_flags |= HELIFLAG_AUTOFIRE;
|
|
}
|
|
else
|
|
{
|
|
body->m_flags &= ~HELIFLAG_AUTOFIRE;
|
|
}
|
|
ai->ActionIsDone(true);
|
|
}
|
|
|
|
void Cobra2WorldHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
if (body->m_fArg) // 1 == maxs
|
|
{
|
|
ai->SetHeliXWorldMax2(body->m_vPos[0]);
|
|
ai->SetHeliYWorldMax2(body->m_vPos[1]);
|
|
ai->SetHeliZWorldMax2(body->m_vPos[2]);
|
|
}
|
|
else // 0 == mins
|
|
{
|
|
ai->SetHeliXWorldMin2(body->m_vPos[0]);
|
|
ai->SetHeliYWorldMin2(body->m_vPos[1]);
|
|
ai->SetHeliZWorldMin2(body->m_vPos[2]);
|
|
}
|
|
ai->ActionIsDone(true);
|
|
}
|
|
|
|
void Cobra2RearmHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
body->m_iHeliRocketsLeft = HELI_MAX_ROCKETS;
|
|
|
|
ai->ActionIsDone(true);
|
|
}
|
|
|
|
void Cobra2DeathDestHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
VectorCopy(body->m_vPos, body->m_v3HeliDeathDest);
|
|
ai->ActionIsDone(true);
|
|
}
|
|
|
|
void Cobra2HealthHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
float fSkillMultiplier = 1; // 1 is the default for skill level 3 -- "challenging"
|
|
|
|
//fSkillMultiplier = (.25 * (1 + level.skillLevel));
|
|
fSkillMultiplier = ( 0.33 * (1 + game.playerSkills.getEnemyValue()) );
|
|
if (0 == body->m_vPos[0]) // 0 == health
|
|
{
|
|
if (body->m_fArg < 1)
|
|
{
|
|
self->health = 0;
|
|
}
|
|
else
|
|
{
|
|
self->health = 1000 + (int)(fSkillMultiplier * body->m_fArg);
|
|
}
|
|
}
|
|
else if (1 == body->m_vPos[0]) // 1 == maxhealth
|
|
{
|
|
if (self->max_health < 1)
|
|
{
|
|
self->max_health = 0;
|
|
}
|
|
else
|
|
{
|
|
self->max_health = 1000 + (int)(fSkillMultiplier * body->m_fArg);
|
|
}
|
|
}
|
|
ai->ActionIsDone(true);
|
|
}
|
|
|
|
void Cobra2TraceDimsHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
if (body->m_fArg)
|
|
{
|
|
body->m_flags &= ~HELIFLAG_NOTRACEDIMS;
|
|
}
|
|
else
|
|
{
|
|
body->m_flags |= HELIFLAG_NOTRACEDIMS;
|
|
}
|
|
ai->ActionIsDone(true);
|
|
}
|
|
|
|
void Cobra2AIHelper(edict_t* self)
|
|
{
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
Cobra2_SetAIParams(self, body->m_fArg);
|
|
ai->ActionIsDone(true);
|
|
}
|
|
|
|
void Cobra2_ToggleGun(edict_t *self, edict_t *other, edict_t *activator)
|
|
{
|
|
// Greg is so demanding. I swear.
|
|
generic_ghoul_heli_ai* ai = (generic_ghoul_heli_ai*)(ai_public_c*)self->ai;
|
|
body_heli* body = ai->GetHeliBody();
|
|
|
|
if (body)
|
|
{
|
|
body->SetFiring(!body->IsFiring());
|
|
}
|
|
}
|
|
|
|
/*QUAKED m_x_chopper_black (1 .5 0) (-280 -280 -50) (280 280 70) IN_AIR ONDIE_HEAD2SPAWN INFINITE_ROCKETS DEBUG_WAYPOINTS ATTACK_CHOPPER
|
|
|
|
This is the version that uses the class-based AI code.
|
|
|
|
--------SPAWNFLAGS----------
|
|
IN_AIR: already in the air, no pad exists (so don't try landing it!!)
|
|
ONDIE_HEAD2SPAWN: when damaged to death, plummet towards spawn pos before exploding
|
|
INFINITE_ROCKETS: go on, guess.
|
|
DEBUG_WAYPOINTS: enable this flag if you want to see waypoints/volumes/world dims drawn in the air
|
|
ATTACK_CHOPPER: this heli will become a Hind attack chopper with a certain script command
|
|
ALWAYS_SEEN draws no matter what
|
|
|
|
--------KEYS------------
|
|
Volume - aggressiveness. You *must* fill this in (values 0 to 10). 10's good for Tokyo, 3 is ok for Train.
|
|
*/
|
|
|
|
/*QUAKED m_x_chopper_green (1 .5 0) (-280 -280 -50) (280 280 70) IN_AIR ONDIE_HEAD2SPAWN INFINITE_ROCKETS DEBUG_WAYPOINTS ATTACK_CHOPPER ALWAYS_SEEN
|
|
|
|
This is the version that uses the class-based AI code.
|
|
|
|
--------SPAWNFLAGS----------
|
|
IN_AIR: already in the air, no pad exists (so don't try landing it!!)
|
|
ONDIE_HEAD2SPAWN: when damaged to death, plummet towards spawn pos before exploding
|
|
INFINITE_ROCKETS: go on, guess.
|
|
DEBUG_WAYPOINTS: enable this flag if you want to see waypoints/volumes/world dims drawn in the air
|
|
ATTACK_CHOPPER: this heli will become a Hind attack chopper with a certain script command
|
|
ALWAYS_SEEN draws no matter what
|
|
|
|
--------KEYS------------
|
|
Volume - aggressiveness. You *must* fill this in (values 0 to 10). 10's good for Tokyo, 3 is ok for Train.
|
|
*/
|
|
|
|
/*QUAKED m_x_chopper_white (1 .5 0) (-280 -280 -50) (280 280 70) IN_AIR ONDIE_HEAD2SPAWN INFINITE_ROCKETS DEBUG_WAYPOINTS ATTACK_CHOPPER ALWAYS_SEEN
|
|
|
|
This is the version that uses the class-based AI code.
|
|
|
|
--------SPAWNFLAGS----------
|
|
IN_AIR: already in the air, no pad exists (so don't try landing it!!)
|
|
ONDIE_HEAD2SPAWN: when damaged to death, plummet towards spawn pos before exploding
|
|
INFINITE_ROCKETS: go on, guess.
|
|
DEBUG_WAYPOINTS: enable this flag if you want to see waypoints/volumes/world dims drawn in the air
|
|
ATTACK_CHOPPER: this heli will become a Hind attack chopper with a certain script command
|
|
ALWAYS_SEEN draws no matter what
|
|
|
|
--------KEYS------------
|
|
Volume - aggressiveness. You *must* fill this in (values 0 to 10). 10's good for Tokyo, 3 is ok for Train.
|
|
*/
|
|
|
|
/*QUAKED m_x_hind (1 .5 0) (-280 -280 -50) (280 280 70) IN_AIR ONDIE_HEAD2SPAWN INFINITE_ROCKETS DEBUG_WAYPOINTS ATTACK_CHOPPER ALWAYS_SEEN
|
|
|
|
This is the version that uses the class-based AI code.
|
|
|
|
--------SPAWNFLAGS----------
|
|
IN_AIR: already in the air, no pad exists (so don't try landing it!!)
|
|
ONDIE_HEAD2SPAWN: when damaged to death, plummet towards spawn pos before exploding
|
|
INFINITE_ROCKETS: go on, guess.
|
|
DEBUG_WAYPOINTS: enable this flag if you want to see waypoints/volumes/world dims drawn in the air
|
|
ATTACK_CHOPPER: this heli will become a Hind attack chopper with a certain script command
|
|
ALWAYS_SEEN draws no matter what
|
|
|
|
--------KEYS------------
|
|
Volume - aggressiveness. You *must* fill this in (values 0 to 10). 10's good for Tokyo, 3 is ok for Train.
|
|
*/
|
|
|
|
void generic_heli_spawnnow (edict_t *self, char *subclass, char* skinname)
|
|
{
|
|
vec3_t v3DeathDest; // unfortunately we need a temp vector since the body doesn't exist till later
|
|
trace_t tr;
|
|
bool bHind = (0 == strcmp(subclass, "hind"));
|
|
char *modelName = bHind?"enemy/hind":"enemy/chopper",
|
|
*modelSubname = bHind?"hind":"cobrass";
|
|
|
|
// precache sounds and effects
|
|
entSoundsToCache[CLSFX_SHOT_HELI1] = 1;
|
|
entSoundsToCache[CLSFX_SHOT_HELI2] = 1;
|
|
entSoundsToCache[CLSFX_SHOT_HELI3] = 1;
|
|
|
|
gi.effectindex("weapons/othermz/heli");
|
|
CacheAttack(ATK_HELIGUN);
|
|
|
|
|
|
// self->spawnflags |= DEBUG_WAYPOINTS; ///////////'''''''''''''''''''''''''''''''
|
|
// self->spawnflags |= INFINITE_ROCKETS; ///////////'''''''''''''''''''''''''''''''
|
|
// self->volume = 0; ///////////////////////////////'''''''''''''''''''''''''''''''
|
|
|
|
self->svflags |= SVF_MONSTER;
|
|
|
|
if (self->spawnflags & HELI_ALWAYS_SEEN)
|
|
{
|
|
self->svflags |= SVF_ALWAYS_SEND; // draw this thing all the dang time. forget the pvs.
|
|
}
|
|
self->s.renderfx = RF_GHOUL;
|
|
self->use = Cobra2_ToggleGun;
|
|
|
|
VectorSet (self->mins, -280,-280,-50);
|
|
VectorSet (self->maxs, 280, 280, 70);
|
|
|
|
VectorCopy(self->s.origin,v3DeathDest); // default to spawn pos
|
|
|
|
if (!(self->spawnflags & IN_AIR))
|
|
{
|
|
// legalise vector BEFORE doing pad Z-match, or it ends up in the air again!
|
|
|
|
// fixme: Cobra2_LegaliseVector() doesn't work in this position now that the heli is ai/class based
|
|
//findme: body->m_flags |= HELIFLAG_NOTRACEDIMS
|
|
if (self->ai)
|
|
{
|
|
Cobra2_LegaliseVector(self, self->s.origin, self->s.origin, false); // qboolean qbUseWorldClip
|
|
}
|
|
//findme: body->m_flags &= ~HELIFLAG_NOTRACEDIMS
|
|
|
|
// Weird bit...because heli doesn't get pulled down with gravity then use a trace to measure ground distance & place properly.
|
|
//
|
|
// (and of course the code I'm calling still doesn't do the right thing - sigh)
|
|
//
|
|
VectorCopy(self->s.origin,v3DeathDest); // default to legalised spawn pos
|
|
self->s.origin[2]+=200;
|
|
vec3_t v3;
|
|
VectorCopy(self->s.origin,v3);
|
|
v3[2] = -(200*3);
|
|
|
|
// first, work out suitable default death dest...
|
|
//
|
|
gi.trace(self->s.origin, self->mins, self->maxs, v3, self,MASK_SOLID, &tr);
|
|
if (tr.fraction < 1.0 && // if no hit, then this is one *deep* world, and probably a mistake...
|
|
tr.fraction != 0.0f // but if 0.0 returned, then we've gone outside the world, ie low roof!
|
|
)
|
|
VectorCopy(tr.endpos,v3DeathDest);
|
|
|
|
// now work out correct landing pos...
|
|
//
|
|
vec3_t v3LandMins;
|
|
VectorSet(v3LandMins,0,0,self->mins[2]);
|
|
gi.trace(self->s.origin, v3LandMins, NULL, v3, self,MASK_SOLID, &tr);
|
|
if (tr.fraction < 1.0 && // if no hit, then this is one *deep* world, and probably a mistake...
|
|
tr.fraction != 0.0f // but if 0.0 returned, then we've gone outside the world, ie low roof!
|
|
)
|
|
VectorCopy(tr.endpos,self->s.origin);
|
|
else
|
|
self->s.origin[2]-=200; // ... so just put it back as it was.
|
|
}
|
|
|
|
// body->m_flags = 0;
|
|
// body->m_fHeliMoveSpeed = 0;
|
|
// fHeliRocketFireDebounceTime = 0;
|
|
// iHeliRocketsLeft = HELI_MAX_ROCKETS;
|
|
|
|
self->health = self->max_health = 1000;
|
|
|
|
gi.linkentity (self);
|
|
|
|
|
|
// Update, we need to get the biggest bounding box possible for trace statements since the heli
|
|
// can turn as it moves. Note that this should only be the X and Y axis (since 99% of the collision is
|
|
// for the rotor, which is round, and the tail is thin enough that even when it swings to 45 degree points
|
|
// it shouldn't stick out beyond the widest X or Y point. As for the Z, handle it by just doing twice the
|
|
// normal height (to cope with pitches etc)
|
|
//
|
|
float fMin = min(self->mins[0],self->mins[1]);
|
|
float fMax = max(self->maxs[0],self->maxs[1]);
|
|
float fXYRadius = max(fabs(fMin),fMax); // max radius dist from centre
|
|
float fZRadius = (max(fabs(self->mins[2]),self->maxs[2]))*2; // max radius dist from centre
|
|
|
|
self->ai = ai_c::Create(AI_HELI, self, modelName, subclass);//new generic_ghoul_heli_ai(self, subclass);
|
|
|
|
if (self->ai)
|
|
{
|
|
body_heli* body = ((generic_ghoul_heli_ai*)(ai_public_c*)self->ai)->GetHeliBody();
|
|
|
|
if (body)
|
|
{
|
|
VectorSet(body->m_v3HeliTraceMins,-fXYRadius,-fXYRadius,-fZRadius);
|
|
VectorSet(body->m_v3HeliTraceMaxs, fXYRadius, fXYRadius, fZRadius);
|
|
VectorCopy(self->s.origin, body->m_v3HeliPadRestPos);
|
|
VectorCopy(v3DeathDest, body->m_v3HeliDeathDest);
|
|
body->m_fAIAggressiveness = self->volume; // 0..10
|
|
// set the skin
|
|
SetSkin(self, modelName, modelSubname, subclass, skinname, false);
|
|
|
|
if ( (self->spawnflags & ATTACK_CHOPPER) && (game.playerSkills.getEnemyValue() > 1) )
|
|
{
|
|
body->SetSnipeability(false);
|
|
}
|
|
else
|
|
{
|
|
body->SetSnipeability(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
self->flags |= FL_NO_KNOCKBACK;
|
|
self->movetype = MOVETYPE_FLY;
|
|
self->solid = SOLID_BBOX;
|
|
self->takedamage= DAMAGE_YES;
|
|
self->pain = NULL;//CobraThink_Pain;
|
|
self->die = NULL;//CobraThink_Die;
|
|
self->think = NULL;//CobraThink_OnPad;
|
|
self->touch = Cobra2Touch;
|
|
self->nextthink = level.time + FRAMETIME;
|
|
|
|
}
|
|
|
|
void generic_heli_init (edict_t *self)
|
|
{
|
|
// this is the last time we have to set the nextthink. from now on the ai
|
|
//will take care of it.
|
|
self->nextthink = level.time + FRAMETIME;
|
|
self->ai->NewDecision(new base_decision(), self);
|
|
/*
|
|
// self->ai->NewDecision(new formation_mob_decision(NULL), self);
|
|
self->ai->NewDecision(new pursue_decision(), self);
|
|
self->ai->NewDecision(new search_decision(), self);
|
|
// self->ai->NewDecision(new dodge_decision(), self);
|
|
// self->ai->NewDecision(new capture_decision(LEADERFLAG_RECRUIT), self);
|
|
self->ai->NewDecision(new leader_sfile_decision(LEADERFLAG_RECRUIT), self);
|
|
self->ai->NewDecision(new retreat_decision(), self);
|
|
|
|
self->ai->SetRank(3);
|
|
*/
|
|
gi.effectindex("environ/helismoke");
|
|
gi.effectindex("heli_expl");
|
|
gi.soundindex("impact/explosion/exp1.wav");
|
|
gi.soundindex("impact/explosion/med.wav");
|
|
gi.effectindex("weapons/world/autogundeath");
|
|
CacheAttack(ATK_ROCKET);
|
|
gi.soundindex("weapons/assault/fire.wav");
|
|
gi.effectindex("weapons/world/rockettrail2");
|
|
|
|
// if the ATTACK_CHOPPER flag is set, we'll precache the Hind body by creating
|
|
//an edict, using it to spawn a Hind, and then free the edict
|
|
if (self->spawnflags & ATTACK_CHOPPER)
|
|
{
|
|
edict_t *ent = G_Spawn();
|
|
SP_m_x_hind(ent);
|
|
G_FreeEdict(ent);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void SP_m_x_chopper_black (edict_t *self)
|
|
{
|
|
|
|
generic_heli_spawnnow(self,"cobra","m_x_chopper_black");
|
|
self->think = generic_heli_init;
|
|
}
|
|
|
|
void SP_m_x_chopper_green (edict_t *self)
|
|
{
|
|
|
|
generic_heli_spawnnow(self,"cobra","m_x_chopper_green");
|
|
self->think = generic_heli_init;
|
|
}
|
|
|
|
void SP_m_x_chopper_white (edict_t *self)
|
|
{
|
|
|
|
generic_heli_spawnnow(self,"cobra","m_x_chopper_white");
|
|
self->think = generic_heli_init;
|
|
}
|
|
|
|
void SP_m_x_hind (edict_t *self)
|
|
{
|
|
|
|
generic_heli_spawnnow(self,"hind","hind");
|
|
self->think = generic_heli_init;
|
|
}
|
|
|
|
heliWaypoint::heliWaypoint(heliWaypoint *orig)
|
|
{
|
|
VectorCopy(orig->m_v, m_v);
|
|
VectorCopy(orig->m_volMins, m_volMins);
|
|
VectorCopy(orig->m_volMaxs, m_volMaxs);
|
|
m_next = NULL;
|
|
}
|
|
|
|
generic_ghoul_heli_ai::generic_ghoul_heli_ai(generic_ghoul_heli_ai *orig)
|
|
{
|
|
m_bActionDone = orig->m_bActionDone;
|
|
m_bTimeToDie = orig->m_bTimeToDie;
|
|
m_bPilotDead = orig->m_bPilotDead;
|
|
m_bGunnerDead = orig->m_bGunnerDead;
|
|
*(int *)&m_attacker = GetEdictNum(orig->m_attacker);
|
|
m_ScriptActionCounter = orig->m_ScriptActionCounter;
|
|
m_LastThinkingActionID = orig->m_LastThinkingActionID;
|
|
|
|
fHeliXWorldMin2 = orig->fHeliXWorldMin2;
|
|
fHeliXWorldMax2 = orig->fHeliXWorldMax2;
|
|
fHeliYWorldMin2 = orig->fHeliYWorldMin2;
|
|
fHeliYWorldMax2 = orig->fHeliYWorldMax2;
|
|
fHeliZWorldMin2 = orig->fHeliZWorldMin2;
|
|
fHeliZWorldMax2 = orig->fHeliZWorldMax2;
|
|
|
|
m_nActionSize = orig->m_nActionSize;
|
|
|
|
m_waypoints = NULL;
|
|
|
|
m_nNumWaypoints = orig->m_nNumWaypoints;
|
|
m_nNumWaypointPts = orig->m_nNumWaypointPts;
|
|
m_nNumWaypointVols = orig->m_nNumWaypointVols;
|
|
}
|
|
|
|
void generic_ghoul_heli_ai::Evaluate(generic_ghoul_heli_ai *orig)
|
|
{
|
|
m_bActionDone = orig->m_bActionDone;
|
|
m_bTimeToDie = orig->m_bTimeToDie;
|
|
m_bPilotDead = orig->m_bPilotDead;
|
|
m_bGunnerDead = orig->m_bGunnerDead;
|
|
m_attacker = GetEdictPtr((int)orig->m_attacker);
|
|
m_ScriptActionCounter = orig->m_ScriptActionCounter;
|
|
m_LastThinkingActionID = orig->m_LastThinkingActionID;
|
|
|
|
fHeliXWorldMin2 = orig->fHeliXWorldMin2;
|
|
fHeliXWorldMax2 = orig->fHeliXWorldMax2;
|
|
fHeliYWorldMin2 = orig->fHeliYWorldMin2;
|
|
fHeliYWorldMax2 = orig->fHeliYWorldMax2;
|
|
fHeliZWorldMin2 = orig->fHeliZWorldMin2;
|
|
fHeliZWorldMax2 = orig->fHeliZWorldMax2;
|
|
|
|
m_nActionSize = orig->m_nActionSize;
|
|
|
|
m_waypoints = NULL;
|
|
|
|
m_nNumWaypoints = orig->m_nNumWaypoints;
|
|
m_nNumWaypointPts = orig->m_nNumWaypointPts;
|
|
m_nNumWaypointVols = orig->m_nNumWaypointVols;
|
|
// Don't go down the Evaluate heirachy as this is a special case
|
|
}
|
|
|
|
void generic_ghoul_heli_ai::Read()
|
|
{
|
|
char loaded[sizeof(generic_ghoul_heli_ai)];
|
|
heliWaypoint *loadable;
|
|
heliWaypoint **llist;
|
|
int i;
|
|
|
|
gi.ReadFromSavegame('AIHA', loaded + GGHA_SAVE_START, GGHA_SAVE_END - GGHA_SAVE_START);
|
|
Evaluate((generic_ghoul_heli_ai *)loaded);
|
|
|
|
ai_c::Read();
|
|
|
|
if(GetNumWaypoints())
|
|
{
|
|
llist = &m_waypoints;
|
|
for(i = 0; i < GetNumWaypoints(); i++)
|
|
{
|
|
loadable = new heliWaypoint();
|
|
gi.ReadFromSavegame('HEWP', loadable, sizeof(heliWaypoint));
|
|
|
|
*llist = loadable;
|
|
llist = &(loadable->m_next);
|
|
}
|
|
}
|
|
}
|
|
|
|
void generic_ghoul_heli_ai::Write()
|
|
{
|
|
byte *save_start;
|
|
generic_ghoul_heli_ai *savable;
|
|
heliWaypoint *wp;
|
|
heliWaypoint *temp;
|
|
|
|
savable = new generic_ghoul_heli_ai(this);
|
|
save_start = (byte *)savable;
|
|
gi.AppendToSavegame('AIHA', save_start + GGHA_SAVE_START, GGHA_SAVE_END - GGHA_SAVE_START);
|
|
delete savable;
|
|
|
|
ai_c::Write();
|
|
|
|
if(GetNumWaypoints())
|
|
{
|
|
for(wp = m_waypoints; wp; wp = wp->m_next)
|
|
{
|
|
temp = new heliWaypoint(wp);
|
|
gi.AppendToSavegame('HEWP', temp, sizeof(heliWaypoint));
|
|
}
|
|
}
|
|
}
|
|
|
|
// end
|