mirror of
https://github.com/ioquake/jedi-academy.git
synced 2024-11-26 14:11:20 +00:00
1297 lines
32 KiB
C
1297 lines
32 KiB
C
#include "g_local.h"
|
|
#include "../ghoul2/G2.h"
|
|
#include "q_shared.h"
|
|
|
|
void G_SetEnemy( gentity_t *self, gentity_t *enemy );
|
|
void finish_spawning_turretG2( gentity_t *base );
|
|
void ObjectDie (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int meansOfDeath );
|
|
void turretG2_base_use( gentity_t *self, gentity_t *other, gentity_t *activator );
|
|
|
|
|
|
#define SPF_TURRETG2_CANRESPAWN 4
|
|
#define SPF_TURRETG2_TURBO 8
|
|
#define SPF_TURRETG2_LEAD_ENEMY 16
|
|
#define SPF_SHOWONRADAR 32
|
|
|
|
#define ARM_ANGLE_RANGE 60
|
|
#define HEAD_ANGLE_RANGE 90
|
|
|
|
#define name "models/map_objects/imp_mine/turret_canon.glm"
|
|
#define name2 "models/map_objects/imp_mine/turret_damage.md3"
|
|
#define name3 "models/map_objects/wedge/laser_cannon_model.glm"
|
|
|
|
//special routine for tracking angles between client and server -rww
|
|
void G2Tur_SetBoneAngles(gentity_t *ent, char *bone, vec3_t angles)
|
|
{
|
|
#ifdef _XBOX
|
|
byte *thebone = &ent->s.boneIndex1;
|
|
byte *firstFree = NULL;
|
|
#else
|
|
int *thebone = &ent->s.boneIndex1;
|
|
int *firstFree = NULL;
|
|
#endif
|
|
int i = 0;
|
|
int boneIndex = G_BoneIndex(bone);
|
|
int flags, up, right, forward;
|
|
vec3_t *boneVector = &ent->s.boneAngles1;
|
|
vec3_t *freeBoneVec = NULL;
|
|
|
|
while (thebone)
|
|
{
|
|
if (!*thebone && !firstFree)
|
|
{ //if the value is 0 then this index is clear, we can use it if we don't find the bone we want already existing.
|
|
firstFree = thebone;
|
|
freeBoneVec = boneVector;
|
|
}
|
|
else if (*thebone)
|
|
{
|
|
if (*thebone == boneIndex)
|
|
{ //this is it
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (i)
|
|
{
|
|
case 0:
|
|
thebone = &ent->s.boneIndex2;
|
|
boneVector = &ent->s.boneAngles2;
|
|
break;
|
|
case 1:
|
|
thebone = &ent->s.boneIndex3;
|
|
boneVector = &ent->s.boneAngles3;
|
|
break;
|
|
case 2:
|
|
thebone = &ent->s.boneIndex4;
|
|
boneVector = &ent->s.boneAngles4;
|
|
break;
|
|
default:
|
|
thebone = NULL;
|
|
boneVector = NULL;
|
|
break;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
if (!thebone)
|
|
{ //didn't find it, create it
|
|
if (!firstFree)
|
|
{ //no free bones.. can't do a thing then.
|
|
Com_Printf("WARNING: NPC has no free bone indexes\n");
|
|
return;
|
|
}
|
|
|
|
thebone = firstFree;
|
|
|
|
*thebone = boneIndex;
|
|
boneVector = freeBoneVec;
|
|
}
|
|
|
|
//If we got here then we have a vector and an index.
|
|
|
|
//Copy the angles over the vector in the entitystate, so we can use the corresponding index
|
|
//to set the bone angles on the client.
|
|
VectorCopy(angles, *boneVector);
|
|
|
|
//Now set the angles on our server instance if we have one.
|
|
|
|
if (!ent->ghoul2)
|
|
{
|
|
return;
|
|
}
|
|
|
|
flags = BONE_ANGLES_POSTMULT;
|
|
up = POSITIVE_Y;
|
|
right = NEGATIVE_Z;
|
|
forward = NEGATIVE_X;
|
|
|
|
//first 3 bits is forward, second 3 bits is right, third 3 bits is up
|
|
ent->s.boneOrient = ((forward)|(right<<3)|(up<<6));
|
|
|
|
trap_G2API_SetBoneAngles( ent->ghoul2,
|
|
0,
|
|
bone,
|
|
angles,
|
|
flags,
|
|
up,
|
|
right,
|
|
forward,
|
|
NULL,
|
|
100,
|
|
level.time );
|
|
}
|
|
|
|
void turretG2_set_models( gentity_t *self, qboolean dying )
|
|
{
|
|
if ( dying )
|
|
{
|
|
if ( !(self->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{
|
|
self->s.modelindex = G_ModelIndex( name2 );
|
|
self->s.modelindex2 = G_ModelIndex( name );
|
|
}
|
|
|
|
trap_G2API_RemoveGhoul2Model( &self->ghoul2, 0 );
|
|
G_KillG2Queue( self->s.number );
|
|
self->s.modelGhoul2 = 0;
|
|
/*
|
|
trap_G2API_InitGhoul2Model( &self->ghoul2,
|
|
name2,
|
|
0, //base->s.modelindex,
|
|
//note, this is not the same kind of index - this one's referring to the actual
|
|
//index of the model in the g2 instance, whereas modelindex is the index of a
|
|
//configstring -rww
|
|
0,
|
|
0,
|
|
0,
|
|
0);
|
|
*/
|
|
}
|
|
else
|
|
{
|
|
if ( !(self->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{
|
|
self->s.modelindex = G_ModelIndex( name );
|
|
self->s.modelindex2 = G_ModelIndex( name2 );
|
|
//set the new onw
|
|
trap_G2API_InitGhoul2Model( &self->ghoul2,
|
|
name,
|
|
0, //base->s.modelindex,
|
|
//note, this is not the same kind of index - this one's referring to the actual
|
|
//index of the model in the g2 instance, whereas modelindex is the index of a
|
|
//configstring -rww
|
|
0,
|
|
0,
|
|
0,
|
|
0);
|
|
}
|
|
else
|
|
{
|
|
self->s.modelindex = G_ModelIndex( name3 );
|
|
//set the new onw
|
|
trap_G2API_InitGhoul2Model( &self->ghoul2,
|
|
name3,
|
|
0, //base->s.modelindex,
|
|
//note, this is not the same kind of index - this one's referring to the actual
|
|
//index of the model in the g2 instance, whereas modelindex is the index of a
|
|
//configstring -rww
|
|
0,
|
|
0,
|
|
0,
|
|
0);
|
|
}
|
|
|
|
self->s.modelGhoul2 = 1;
|
|
if ( (self->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{//larger
|
|
self->s.g2radius = 128;
|
|
}
|
|
else
|
|
{
|
|
self->s.g2radius = 80;
|
|
}
|
|
|
|
if ( (self->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{//different pitch bone and muzzle flash points
|
|
G2Tur_SetBoneAngles(self, "pitch", vec3_origin);
|
|
self->genericValue11 = trap_G2API_AddBolt( self->ghoul2, 0, "*muzzle1" );
|
|
self->genericValue12 = trap_G2API_AddBolt( self->ghoul2, 0, "*muzzle2" );
|
|
}
|
|
else
|
|
{
|
|
G2Tur_SetBoneAngles(self, "Bone_body", vec3_origin);
|
|
self->genericValue11 = trap_G2API_AddBolt( self->ghoul2, 0, "*flash03" );
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------------------------
|
|
void TurretG2Pain( gentity_t *self, gentity_t *attacker, int damage )
|
|
//------------------------------------------------------------------------------------------------------------
|
|
{
|
|
if (self->paintarget && self->paintarget[0])
|
|
{
|
|
if (self->genericValue8 < level.time)
|
|
{
|
|
G_UseTargets2(self, self, self->paintarget);
|
|
self->genericValue8 = level.time + self->genericValue4;
|
|
}
|
|
}
|
|
|
|
if ( attacker->client && attacker->client->ps.weapon == WP_DEMP2 )
|
|
{
|
|
self->attackDebounceTime = level.time + 2000 + random() * 500;
|
|
self->painDebounceTime = self->attackDebounceTime;
|
|
}
|
|
if ( !self->enemy )
|
|
{//react to being hit
|
|
G_SetEnemy( self, attacker );
|
|
}
|
|
//self->s.health = self->health;
|
|
//mmm..yes..bad.
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------------------------
|
|
void turretG2_die ( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int meansOfDeath )
|
|
//------------------------------------------------------------------------------------------------------------
|
|
{
|
|
vec3_t forward = { 0,0,-1 }, pos;
|
|
|
|
// Turn off the thinking of the base & use it's targets
|
|
//self->think = NULL;
|
|
self->use = NULL;
|
|
|
|
// clear my data
|
|
self->die = NULL;
|
|
self->pain = NULL;
|
|
self->takedamage = qfalse;
|
|
self->s.health = self->health = 0;
|
|
self->s.loopSound = 0;
|
|
self->s.shouldtarget = qfalse;
|
|
//self->s.owner = MAX_CLIENTS; //not owned by any client
|
|
|
|
if ( attacker
|
|
&& attacker->s.number < MAX_CLIENTS
|
|
&& !OnSameTeam( attacker, self ) )
|
|
{//give them a point for the kill
|
|
AddScore( attacker, self->r.currentOrigin, 1 );
|
|
//should we send an obit? nah...
|
|
}
|
|
|
|
// hack the effect angle so that explode death can orient the effect properly
|
|
if ( self->spawnflags & 2 )
|
|
{
|
|
VectorSet( forward, 0, 0, 1 );
|
|
}
|
|
|
|
// VectorCopy( self->r.currentOrigin, self->s.pos.trBase );
|
|
|
|
VectorMA( self->r.currentOrigin, 12, forward, pos );
|
|
G_PlayEffect( EFFECT_EXPLOSION_TURRET, pos, forward );
|
|
|
|
if ( self->splashDamage > 0 && self->splashRadius > 0 )
|
|
{
|
|
G_RadiusDamage( self->r.currentOrigin,
|
|
attacker,
|
|
self->splashDamage,
|
|
self->splashRadius,
|
|
attacker,
|
|
NULL,
|
|
MOD_UNKNOWN );
|
|
}
|
|
|
|
if ( self->s.eFlags & EF_SHADER_ANIM )
|
|
{
|
|
self->s.frame = 1; // black
|
|
}
|
|
|
|
self->s.weapon = 0; // crosshair code uses this to mark crosshair red
|
|
|
|
if ( self->s.modelindex2 )
|
|
{
|
|
// switch to damage model if we should
|
|
turretG2_set_models( self, qtrue );
|
|
|
|
VectorCopy( self->r.currentAngles, self->s.apos.trBase );
|
|
VectorClear( self->s.apos.trDelta );
|
|
|
|
if ( self->target )
|
|
{
|
|
G_UseTargets( self, attacker );
|
|
}
|
|
|
|
if (self->spawnflags & SPF_TURRETG2_CANRESPAWN)
|
|
{//respawn
|
|
if (self->health < 1 && !self->genericValue5)
|
|
{ //we are dead, set our respawn delay if we have one
|
|
self->genericValue5 = level.time + self->count;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ObjectDie( self, inflictor, attacker, damage, meansOfDeath );
|
|
}
|
|
}
|
|
|
|
#define START_DIS 15
|
|
|
|
//start an animation on model_root both server side and client side
|
|
void TurboLaser_SetBoneAnim(gentity_t *eweb, int startFrame, int endFrame)
|
|
{
|
|
//set info on the entity so it knows to start the anim on the client next snapshot.
|
|
eweb->s.eFlags |= EF_G2ANIMATING;
|
|
|
|
if (eweb->s.torsoAnim == startFrame && eweb->s.legsAnim == endFrame)
|
|
{ //already playing this anim, let's flag it to restart
|
|
eweb->s.torsoFlip = !eweb->s.torsoFlip;
|
|
}
|
|
else
|
|
{
|
|
eweb->s.torsoAnim = startFrame;
|
|
eweb->s.legsAnim = endFrame;
|
|
}
|
|
|
|
//now set the animation on the server ghoul2 instance.
|
|
assert(eweb->ghoul2);
|
|
trap_G2API_SetBoneAnim(eweb->ghoul2, 0, "model_root", startFrame, endFrame,
|
|
(BONE_ANIM_OVERRIDE_FREEZE|BONE_ANIM_BLEND), 1.0f, level.time, -1, 100);
|
|
}
|
|
|
|
extern void WP_FireTurboLaserMissile( gentity_t *ent, vec3_t start, vec3_t dir );
|
|
//----------------------------------------------------------------
|
|
static void turretG2_fire ( gentity_t *ent, vec3_t start, vec3_t dir )
|
|
//----------------------------------------------------------------
|
|
{
|
|
vec3_t org, ang;
|
|
gentity_t *bolt;
|
|
|
|
if ( (trap_PointContents( start, ent->s.number )&MASK_SHOT) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
VectorMA( start, -START_DIS, dir, org ); // dumb....
|
|
|
|
if ( ent->random )
|
|
{
|
|
vectoangles( dir, ang );
|
|
ang[PITCH] += flrand( -ent->random, ent->random );
|
|
ang[YAW] += flrand( -ent->random, ent->random );
|
|
AngleVectors( ang, dir, NULL, NULL );
|
|
}
|
|
|
|
vectoangles(dir, ang);
|
|
|
|
if ( (ent->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{
|
|
//muzzle flash
|
|
G_PlayEffectID( ent->genericValue13, org, ang );
|
|
WP_FireTurboLaserMissile( ent, start, dir );
|
|
if ( ent->alt_fire )
|
|
{
|
|
TurboLaser_SetBoneAnim( ent, 2, 3 );
|
|
}
|
|
else
|
|
{
|
|
TurboLaser_SetBoneAnim( ent, 0, 1 );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
G_PlayEffectID( G_EffectIndex("blaster/muzzle_flash"), org, ang );
|
|
bolt = G_Spawn();
|
|
|
|
bolt->classname = "turret_proj";
|
|
bolt->nextthink = level.time + 10000;
|
|
bolt->think = G_FreeEntity;
|
|
bolt->s.eType = ET_MISSILE;
|
|
bolt->s.weapon = WP_BLASTER;
|
|
bolt->r.ownerNum = ent->s.number;
|
|
bolt->damage = ent->damage;
|
|
bolt->alliedTeam = ent->alliedTeam;
|
|
bolt->teamnodmg = ent->teamnodmg;
|
|
bolt->dflags = (DAMAGE_NO_KNOCKBACK|DAMAGE_HEAVY_WEAP_CLASS); // Don't push them around, or else we are constantly re-aiming
|
|
bolt->splashDamage = ent->splashDamage;
|
|
bolt->splashRadius = ent->splashDamage;
|
|
bolt->methodOfDeath = MOD_TARGET_LASER;//MOD_ENERGY;
|
|
bolt->splashMethodOfDeath = MOD_TARGET_LASER;//MOD_ENERGY;
|
|
bolt->clipmask = MASK_SHOT | CONTENTS_LIGHTSABER;
|
|
//bolt->trigger_formation = qfalse; // don't draw tail on first frame
|
|
|
|
VectorSet( bolt->r.maxs, 1.5, 1.5, 1.5 );
|
|
VectorScale( bolt->r.maxs, -1, bolt->r.mins );
|
|
bolt->s.pos.trType = TR_LINEAR;
|
|
bolt->s.pos.trTime = level.time;
|
|
VectorCopy( start, bolt->s.pos.trBase );
|
|
VectorScale( dir, ent->mass, bolt->s.pos.trDelta );
|
|
SnapVector( bolt->s.pos.trDelta ); // save net bandwidth
|
|
VectorCopy( start, bolt->r.currentOrigin);
|
|
}
|
|
}
|
|
|
|
void turretG2_respawn( gentity_t *self )
|
|
{
|
|
self->use = turretG2_base_use;
|
|
self->pain = TurretG2Pain;
|
|
self->die = turretG2_die;
|
|
self->takedamage = qtrue;
|
|
self->s.shouldtarget = qtrue;
|
|
//self->s.owner = MAX_CLIENTS; //not owned by any client
|
|
if ( self->s.eFlags & EF_SHADER_ANIM )
|
|
{
|
|
self->s.frame = 0; // normal
|
|
}
|
|
self->s.weapon = WP_TURRET; // crosshair code uses this to mark crosshair red
|
|
|
|
turretG2_set_models( self, qfalse );
|
|
self->s.health = self->health = self->genericValue6;
|
|
if (self->maxHealth) {
|
|
G_ScaleNetHealth(self);
|
|
}
|
|
self->genericValue5 = 0;//clear this now
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
void turretG2_head_think( gentity_t *self )
|
|
//-----------------------------------------------------
|
|
{
|
|
// if it's time to fire and we have an enemy, then gun 'em down! pushDebounce time controls next fire time
|
|
if ( self->enemy
|
|
&& self->setTime < level.time
|
|
&& self->attackDebounceTime < level.time )
|
|
{
|
|
vec3_t fwd, org;
|
|
mdxaBone_t boltMatrix;
|
|
|
|
// set up our next fire time
|
|
self->setTime = level.time + self->wait;
|
|
|
|
// Getting the flash bolt here
|
|
trap_G2API_GetBoltMatrix( self->ghoul2,
|
|
0,
|
|
(self->alt_fire?self->genericValue12:self->genericValue11),
|
|
&boltMatrix,
|
|
self->r.currentAngles,
|
|
self->r.currentOrigin,
|
|
level.time,
|
|
NULL,
|
|
self->modelScale );
|
|
if ( (self->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{
|
|
self->alt_fire = !self->alt_fire;
|
|
}
|
|
|
|
BG_GiveMeVectorFromMatrix( &boltMatrix, ORIGIN, org );
|
|
//BG_GiveMeVectorFromMatrix( &boltMatrix, POSITIVE_Y, fwd );
|
|
if ( (self->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{
|
|
BG_GiveMeVectorFromMatrix( &boltMatrix, POSITIVE_X, fwd );
|
|
}
|
|
else
|
|
{
|
|
BG_GiveMeVectorFromMatrix( &boltMatrix, NEGATIVE_X, fwd );
|
|
}
|
|
|
|
VectorMA( org, START_DIS, fwd, org );
|
|
|
|
turretG2_fire( self, org, fwd );
|
|
self->fly_sound_debounce_time = level.time;//used as lastShotTime
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
static void turretG2_aim( gentity_t *self )
|
|
//-----------------------------------------------------
|
|
{
|
|
vec3_t enemyDir, org, org2;
|
|
vec3_t desiredAngles, setAngle;
|
|
float diffYaw = 0.0f, diffPitch = 0.0f;
|
|
float maxYawSpeed = (self->spawnflags&SPF_TURRETG2_TURBO)?30.0f:14.0f;
|
|
float maxPitchSpeed = (self->spawnflags&SPF_TURRETG2_TURBO)?15.0f:3.0f;
|
|
|
|
// move our gun base yaw to where we should be at this time....
|
|
BG_EvaluateTrajectory( &self->s.apos, level.time, self->r.currentAngles );
|
|
self->r.currentAngles[YAW] = AngleNormalize360( self->r.currentAngles[YAW] );
|
|
self->speed = AngleNormalize360( self->speed );
|
|
|
|
if ( self->enemy )
|
|
{
|
|
mdxaBone_t boltMatrix;
|
|
// ...then we'll calculate what new aim adjustments we should attempt to make this frame
|
|
// Aim at enemy
|
|
if ( self->enemy->client )
|
|
{
|
|
VectorCopy( self->enemy->client->renderInfo.eyePoint, org );
|
|
}
|
|
else
|
|
{
|
|
VectorCopy( self->enemy->r.currentOrigin, org );
|
|
}
|
|
if ( self->spawnflags & 2 )
|
|
{
|
|
org[2] -= 15;
|
|
}
|
|
else
|
|
{
|
|
org[2] -= 5;
|
|
}
|
|
|
|
if ( (self->spawnflags&SPF_TURRETG2_LEAD_ENEMY) )
|
|
{//we want to lead them a bit
|
|
vec3_t diff, velocity;
|
|
float dist;
|
|
VectorSubtract( org, self->s.origin, diff );
|
|
dist = VectorNormalize( diff );
|
|
if ( self->enemy->client )
|
|
{
|
|
VectorCopy( self->enemy->client->ps.velocity, velocity );
|
|
}
|
|
else
|
|
{
|
|
VectorCopy( self->enemy->s.pos.trDelta, velocity );
|
|
}
|
|
VectorMA( org, (dist/self->mass), velocity, org );
|
|
}
|
|
|
|
// Getting the "eye" here
|
|
trap_G2API_GetBoltMatrix( self->ghoul2,
|
|
0,
|
|
(self->alt_fire?self->genericValue12:self->genericValue11),
|
|
&boltMatrix,
|
|
self->r.currentAngles,
|
|
self->s.origin,
|
|
level.time,
|
|
NULL,
|
|
self->modelScale );
|
|
|
|
BG_GiveMeVectorFromMatrix( &boltMatrix, ORIGIN, org2 );
|
|
|
|
VectorSubtract( org, org2, enemyDir );
|
|
vectoangles( enemyDir, desiredAngles );
|
|
|
|
diffYaw = AngleSubtract( self->r.currentAngles[YAW], desiredAngles[YAW] );
|
|
diffPitch = AngleSubtract( self->speed, desiredAngles[PITCH] );
|
|
}
|
|
else
|
|
{
|
|
// no enemy, so make us slowly sweep back and forth as if searching for a new one
|
|
// diffYaw = sin( level.time * 0.0001f + self->count ) * 5.0f; // don't do this for now since it can make it go into walls.
|
|
}
|
|
|
|
if ( diffYaw )
|
|
{
|
|
// cap max speed....
|
|
if ( fabs(diffYaw) > maxYawSpeed )
|
|
{
|
|
diffYaw = ( diffYaw >= 0 ? maxYawSpeed : -maxYawSpeed );
|
|
}
|
|
|
|
// ...then set up our desired yaw
|
|
VectorSet( setAngle, 0.0f, diffYaw, 0.0f );
|
|
|
|
VectorCopy( self->r.currentAngles, self->s.apos.trBase );
|
|
VectorScale( setAngle,- 5, self->s.apos.trDelta );
|
|
self->s.apos.trTime = level.time;
|
|
self->s.apos.trType = TR_LINEAR;
|
|
}
|
|
|
|
if ( diffPitch )
|
|
{
|
|
if ( fabs(diffPitch) > maxPitchSpeed )
|
|
{
|
|
// cap max speed
|
|
self->speed += (diffPitch > 0.0f) ? -maxPitchSpeed : maxPitchSpeed;
|
|
}
|
|
else
|
|
{
|
|
// small enough, so just add half the diff so we smooth out the stopping
|
|
self->speed -= ( diffPitch );//desiredAngles[PITCH];
|
|
}
|
|
|
|
// Note that this is NOT interpolated, so it will be less smooth...On the other hand, it does use Ghoul2 to blend, so it may smooth it out a bit?
|
|
if ( (self->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{
|
|
if ( self->spawnflags & 2 )
|
|
{
|
|
VectorSet( desiredAngles, 0.0f, 0.0f, -self->speed );
|
|
}
|
|
else
|
|
{
|
|
VectorSet( desiredAngles, 0.0f, 0.0f, self->speed );
|
|
}
|
|
G2Tur_SetBoneAngles(self, "pitch", desiredAngles);
|
|
}
|
|
else
|
|
{
|
|
if ( self->spawnflags & 2 )
|
|
{
|
|
VectorSet( desiredAngles, self->speed, 0.0f, 0.0f );
|
|
}
|
|
else
|
|
{
|
|
VectorSet( desiredAngles, -self->speed, 0.0f, 0.0f );
|
|
}
|
|
G2Tur_SetBoneAngles(self, "Bone_body", desiredAngles);
|
|
}
|
|
/*
|
|
trap_G2API_SetBoneAngles( self->ghoul2,
|
|
0,
|
|
"Bone_body",
|
|
desiredAngles,
|
|
BONE_ANGLES_POSTMULT,
|
|
POSITIVE_Y,
|
|
POSITIVE_Z,
|
|
POSITIVE_X,
|
|
NULL,
|
|
100,
|
|
level.time );
|
|
*/
|
|
}
|
|
|
|
if ( diffYaw || diffPitch )
|
|
{//FIXME: turbolaser sounds
|
|
if ( (self->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{
|
|
self->s.loopSound = G_SoundIndex( "sound/vehicles/weapons/turbolaser/turn.wav" );
|
|
}
|
|
else
|
|
{
|
|
self->s.loopSound = G_SoundIndex( "sound/chars/turret/move.wav" );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self->s.loopSound = 0;
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
static void turretG2_turnoff( gentity_t *self )
|
|
//-----------------------------------------------------
|
|
{
|
|
if ( self->enemy == NULL )
|
|
{
|
|
// we don't need to turnoff
|
|
return;
|
|
}
|
|
if ( (self->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{
|
|
TurboLaser_SetBoneAnim( self, 4, 5 );
|
|
}
|
|
// shut-down sound
|
|
if ( !(self->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{
|
|
G_Sound( self, CHAN_BODY, G_SoundIndex( "sound/chars/turret/shutdown.wav" ));
|
|
}
|
|
|
|
// make turret play ping sound for 5 seconds
|
|
self->aimDebounceTime = level.time + 5000;
|
|
|
|
// Clear enemy
|
|
self->enemy = NULL;
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
static qboolean turretG2_find_enemies( gentity_t *self )
|
|
//-----------------------------------------------------
|
|
{
|
|
qboolean found = qfalse;
|
|
int i, count;
|
|
float bestDist = self->radius * self->radius;
|
|
float enemyDist;
|
|
vec3_t enemyDir, org, org2;
|
|
qboolean foundClient = qfalse;
|
|
gentity_t *entity_list[MAX_GENTITIES], *target, *bestTarget = NULL;
|
|
|
|
if ( self->aimDebounceTime > level.time ) // time since we've been shut off
|
|
{
|
|
// We were active and alert, i.e. had an enemy in the last 3 secs
|
|
if ( self->painDebounceTime < level.time )
|
|
{
|
|
if ( !(self->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{
|
|
G_Sound(self, CHAN_BODY, G_SoundIndex( "sound/chars/turret/ping.wav" ));
|
|
}
|
|
self->painDebounceTime = level.time + 1000;
|
|
}
|
|
}
|
|
|
|
VectorCopy( self->r.currentOrigin, org2 );
|
|
if ( self->spawnflags & 2 )
|
|
{
|
|
org2[2] += 20;
|
|
}
|
|
else
|
|
{
|
|
org2[2] -= 20;
|
|
}
|
|
|
|
count = G_RadiusList( org2, self->radius, self, qtrue, entity_list );
|
|
|
|
for ( i = 0; i < count; i++ )
|
|
{
|
|
trace_t tr;
|
|
target = entity_list[i];
|
|
|
|
if ( !target->client )
|
|
{
|
|
// only attack clients
|
|
if ( !(target->flags&FL_BBRUSH)//not a breakable brush
|
|
|| !target->takedamage//is a bbrush, but invincible
|
|
|| (target->NPC_targetname&&self->targetname&&Q_stricmp(target->NPC_targetname,self->targetname)!=0) )//not in invicible bbrush, but can only be broken by an NPC that is not me
|
|
{
|
|
continue;
|
|
}
|
|
//else: we will shoot at bbrushes!
|
|
}
|
|
if ( target == self || !target->takedamage || target->health <= 0 || ( target->flags & FL_NOTARGET ))
|
|
{
|
|
continue;
|
|
}
|
|
if ( target->client && target->client->sess.sessionTeam == TEAM_SPECTATOR )
|
|
{
|
|
continue;
|
|
}
|
|
if ( self->alliedTeam )
|
|
{
|
|
if ( target->client )
|
|
{
|
|
if ( target->client->sess.sessionTeam == self->alliedTeam )
|
|
{
|
|
// A bot/client/NPC we don't want to shoot
|
|
continue;
|
|
}
|
|
}
|
|
else if ( target->teamnodmg == self->alliedTeam )
|
|
{
|
|
// An ent we don't want to shoot
|
|
continue;
|
|
}
|
|
}
|
|
if ( !trap_InPVS( org2, target->r.currentOrigin ))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( target->client )
|
|
{
|
|
VectorCopy( target->client->renderInfo.eyePoint, org );
|
|
}
|
|
else
|
|
{
|
|
VectorCopy( target->r.currentOrigin, org );
|
|
}
|
|
|
|
if ( self->spawnflags & 2 )
|
|
{
|
|
org[2] -= 15;
|
|
}
|
|
else
|
|
{
|
|
org[2] += 5;
|
|
}
|
|
|
|
trap_Trace( &tr, org2, NULL, NULL, org, self->s.number, MASK_SHOT );
|
|
|
|
if ( !tr.allsolid && !tr.startsolid && ( tr.fraction == 1.0 || tr.entityNum == target->s.number ))
|
|
{
|
|
// Only acquire if have a clear shot, Is it in range and closer than our best?
|
|
VectorSubtract( target->r.currentOrigin, self->r.currentOrigin, enemyDir );
|
|
enemyDist = VectorLengthSquared( enemyDir );
|
|
|
|
if ( enemyDist < bestDist || (target->client && !foundClient))// all things equal, keep current
|
|
{
|
|
if ( self->attackDebounceTime < level.time )
|
|
{
|
|
// We haven't fired or acquired an enemy in the last 2 seconds-start-up sound
|
|
if ( !(self->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{
|
|
G_Sound( self, CHAN_BODY, G_SoundIndex( "sound/chars/turret/startup.wav" ));
|
|
}
|
|
|
|
// Wind up turrets for a bit
|
|
self->attackDebounceTime = level.time + 1400;
|
|
}
|
|
|
|
bestTarget = target;
|
|
bestDist = enemyDist;
|
|
found = qtrue;
|
|
if ( target->client )
|
|
{//prefer clients over non-clients
|
|
foundClient = qtrue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( found )
|
|
{
|
|
/*
|
|
if ( !self->enemy )
|
|
{//just aquired one
|
|
AddSoundEvent( bestTarget, self->r.currentOrigin, 256, AEL_DISCOVERED );
|
|
AddSightEvent( bestTarget, self->r.currentOrigin, 512, AEL_DISCOVERED, 20 );
|
|
}
|
|
*/
|
|
G_SetEnemy( self, bestTarget );
|
|
if ( VALIDSTRING( self->target2 ))
|
|
{
|
|
G_UseTargets2( self, self, self->target2 );
|
|
}
|
|
}
|
|
|
|
return found;
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
void turretG2_base_think( gentity_t *self )
|
|
//-----------------------------------------------------
|
|
{
|
|
qboolean turnOff = qtrue;
|
|
float enemyDist;
|
|
vec3_t enemyDir, org, org2;
|
|
|
|
self->nextthink = level.time + FRAMETIME;
|
|
|
|
if ( self->health <= 0 )
|
|
{//dead
|
|
if (self->spawnflags & SPF_TURRETG2_CANRESPAWN)
|
|
{//can respawn
|
|
if ( self->genericValue5 && self->genericValue5 < level.time )
|
|
{ //we are dead, see if it's time to respawn
|
|
turretG2_respawn( self );
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
else if ( self->spawnflags & 1 )
|
|
{// not turned on
|
|
turretG2_turnoff( self );
|
|
turretG2_aim( self );
|
|
|
|
// No target
|
|
self->flags |= FL_NOTARGET;
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
// I'm all hot and bothered
|
|
self->flags &= ~FL_NOTARGET;
|
|
}
|
|
|
|
if ( self->enemy )
|
|
{
|
|
if ( self->enemy->health < 0
|
|
|| !self->enemy->inuse )
|
|
{
|
|
self->enemy = NULL;
|
|
}
|
|
}
|
|
|
|
if ( self->last_move_time < level.time )
|
|
{//MISNOMER: used a enemy recalcing debouncer
|
|
if ( turretG2_find_enemies( self ) )
|
|
{//found one
|
|
turnOff = qfalse;
|
|
if ( self->enemy->client )
|
|
{//hold on to clients for a min of 3 seconds
|
|
self->last_move_time = level.time + 3000;
|
|
}
|
|
else
|
|
{//hold less
|
|
self->last_move_time = level.time + 500;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( self->enemy != NULL )
|
|
{
|
|
if ( self->enemy->client && self->enemy->client->sess.sessionTeam == TEAM_SPECTATOR )
|
|
{//don't keep going after spectators
|
|
self->enemy = NULL;
|
|
}
|
|
else
|
|
{//FIXME: remain single-minded or look for a new enemy every now and then?
|
|
// enemy is alive
|
|
VectorSubtract( self->enemy->r.currentOrigin, self->r.currentOrigin, enemyDir );
|
|
enemyDist = VectorLengthSquared( enemyDir );
|
|
|
|
if ( enemyDist < self->radius * self->radius )
|
|
{
|
|
// was in valid radius
|
|
if ( trap_InPVS( self->r.currentOrigin, self->enemy->r.currentOrigin ) )
|
|
{
|
|
// Every now and again, check to see if we can even trace to the enemy
|
|
trace_t tr;
|
|
|
|
if ( self->enemy->client )
|
|
{
|
|
VectorCopy( self->enemy->client->renderInfo.eyePoint, org );
|
|
}
|
|
else
|
|
{
|
|
VectorCopy( self->enemy->r.currentOrigin, org );
|
|
}
|
|
VectorCopy( self->r.currentOrigin, org2 );
|
|
if ( self->spawnflags & 2 )
|
|
{
|
|
org2[2] += 10;
|
|
}
|
|
else
|
|
{
|
|
org2[2] -= 10;
|
|
}
|
|
trap_Trace( &tr, org2, NULL, NULL, org, self->s.number, MASK_SHOT );
|
|
|
|
if ( !tr.allsolid && !tr.startsolid && tr.entityNum == self->enemy->s.number )
|
|
{
|
|
turnOff = qfalse; // Can see our enemy
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
if ( turnOff )
|
|
{
|
|
if ( self->bounceCount < level.time ) // bounceCount is used to keep the thing from ping-ponging from on to off
|
|
{
|
|
turretG2_turnoff( self );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// keep our enemy for a minimum of 2 seconds from now
|
|
self->bounceCount = level.time + 2000 + random() * 150;
|
|
}
|
|
|
|
turretG2_aim( self );
|
|
if ( !turnOff )
|
|
{
|
|
turretG2_head_think( self );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void turretG2_base_use( gentity_t *self, gentity_t *other, gentity_t *activator )
|
|
//-----------------------------------------------------------------------------
|
|
{
|
|
// Toggle on and off
|
|
self->spawnflags = (self->spawnflags ^ 1);
|
|
|
|
if (( self->s.eFlags & EF_SHADER_ANIM ) && ( self->spawnflags & 1 )) // Start_Off
|
|
{
|
|
self->s.frame = 1; // black
|
|
}
|
|
else
|
|
{
|
|
self->s.frame = 0; // glow
|
|
}
|
|
}
|
|
|
|
|
|
/*QUAKED misc_turretG2 (1 0 0) (-8 -8 -22) (8 8 0) START_OFF UPSIDE_DOWN CANRESPAWN TURBO LEAD SHOWRADAR
|
|
|
|
Turret that hangs from the ceiling, will aim and shoot at enemies
|
|
|
|
START_OFF - Starts off
|
|
UPSIDE_DOWN - make it rest on a surface/floor instead of hanging from the ceiling
|
|
CANRESPAWN - will respawn after being killed (use count)
|
|
TURBO - Big-ass, Boxy Death Star Turbo Laser version
|
|
LEAD - Turret will aim ahead of moving targets ("lead" them)
|
|
SHOWRADAR - show on radar
|
|
|
|
radius - How far away an enemy can be for it to pick it up (default 512)
|
|
wait - Time between shots (default 150 ms)
|
|
dmg - How much damage each shot does (default 5)
|
|
health - How much damage it can take before exploding (default 100)
|
|
count - if CANRESPAWN spawnflag, decides how long it is before gun respawns (in ms) - defaults to 20000 (20 seconds)
|
|
|
|
paintarget - target to fire off upon being hurt
|
|
painwait - ms to wait between firing off pain targets
|
|
|
|
random - random error (in degrees) of projectile direction when it comes out of the muzzle (default is 2)
|
|
|
|
shotspeed - the speed of the missile it fires travels at (default is 1100 for regular turrets, 20000 for TURBOLASERS)
|
|
|
|
splashDamage - How much damage the explosion does
|
|
splashRadius - The radius of the explosion
|
|
|
|
targetname - Toggles it on/off
|
|
target - What to use when destroyed
|
|
target2 - What to use when it decides to start shooting at an enemy
|
|
|
|
showhealth - set to 1 to show health bar on this entity when crosshair is over it
|
|
|
|
teamowner - crosshair shows green for this team, red for opposite team
|
|
0 - none
|
|
1 - red
|
|
2 - blue
|
|
|
|
alliedTeam - team that this turret won't target
|
|
0 - none
|
|
1 - red
|
|
2 - blue
|
|
|
|
teamnodmg - team that turret does not take damage from
|
|
0 - none
|
|
1 - red
|
|
2 - blue
|
|
|
|
customscale - custom scaling size. 100 is normal size, 1024 is the max scaling. this will change the bounding box size, so be careful of starting in solid!
|
|
|
|
"icon" - icon that represents the objective on the radar
|
|
*/
|
|
//-----------------------------------------------------
|
|
void SP_misc_turretG2( gentity_t *base )
|
|
//-----------------------------------------------------
|
|
{
|
|
int customscaleVal;
|
|
char* s;
|
|
|
|
turretG2_set_models( base, qfalse );
|
|
|
|
G_SpawnInt("painwait", "0", &base->genericValue4);
|
|
base->genericValue8 = 0;
|
|
|
|
G_SpawnInt("customscale", "0", &customscaleVal);
|
|
base->s.iModelScale = customscaleVal;
|
|
if (base->s.iModelScale)
|
|
{
|
|
if (base->s.iModelScale > 1023)
|
|
{
|
|
base->s.iModelScale = 1023;
|
|
}
|
|
base->modelScale[0] = base->modelScale[1] = base->modelScale[2] = base->s.iModelScale/100.0f;
|
|
}
|
|
|
|
G_SpawnString( "icon", "", &s );
|
|
if (s && s[0])
|
|
{
|
|
// We have an icon, so index it now. We are reusing the genericenemyindex
|
|
// variable rather than adding a new one to the entity state.
|
|
base->s.genericenemyindex = G_IconIndex(s);
|
|
}
|
|
|
|
finish_spawning_turretG2( base );
|
|
|
|
if (( base->spawnflags & 1 )) // Start_Off
|
|
{
|
|
base->s.frame = 1; // black
|
|
}
|
|
else
|
|
{
|
|
base->s.frame = 0; // glow
|
|
}
|
|
if ( !(base->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{
|
|
base->s.eFlags |= EF_SHADER_ANIM;
|
|
}
|
|
|
|
if (base->spawnflags & SPF_SHOWONRADAR)
|
|
{
|
|
base->s.eFlags |= EF_RADAROBJECT;
|
|
}
|
|
#undef name
|
|
#undef name2
|
|
#undef name3
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
void finish_spawning_turretG2( gentity_t *base )
|
|
{
|
|
vec3_t fwd;
|
|
int t;
|
|
|
|
if ( (base->spawnflags&2) )
|
|
{
|
|
base->s.angles[ROLL] += 180;
|
|
base->s.origin[2] -= 22.0f;
|
|
}
|
|
|
|
G_SetAngles( base, base->s.angles );
|
|
AngleVectors( base->r.currentAngles, fwd, NULL, NULL );
|
|
|
|
G_SetOrigin(base, base->s.origin);
|
|
|
|
base->s.eType = ET_GENERAL;
|
|
|
|
if ( base->team && base->team[0] && //g_gametype.integer == GT_SIEGE &&
|
|
!base->teamnodmg)
|
|
{
|
|
base->teamnodmg = atoi(base->team);
|
|
}
|
|
base->team = NULL;
|
|
|
|
// Set up our explosion effect for the ExplodeDeath code....
|
|
G_EffectIndex( "turret/explode" );
|
|
G_EffectIndex( "sparks/spark_exp_nosnd" );
|
|
|
|
base->use = turretG2_base_use;
|
|
base->pain = TurretG2Pain;
|
|
|
|
// don't start working right away
|
|
base->think = turretG2_base_think;
|
|
base->nextthink = level.time + FRAMETIME * 5;
|
|
|
|
// this is really the pitch angle.....
|
|
base->speed = 0;
|
|
|
|
// respawn time defaults to 20 seconds
|
|
if ( (base->spawnflags&SPF_TURRETG2_CANRESPAWN) && !base->count )
|
|
{
|
|
base->count = 20000;
|
|
}
|
|
|
|
G_SpawnFloat( "shotspeed", "0", &base->mass );
|
|
if ( (base->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{
|
|
if ( !base->random )
|
|
{//error worked into projectile direction
|
|
base->random = 2.0f;
|
|
}
|
|
|
|
if ( !base->mass )
|
|
{//misnomer: speed of projectile
|
|
base->mass = 20000;
|
|
}
|
|
|
|
if ( !base->health )
|
|
{
|
|
base->health = 2000;
|
|
}
|
|
|
|
// search radius
|
|
if ( !base->radius )
|
|
{
|
|
base->radius = 32768;
|
|
}
|
|
|
|
// How quickly to fire
|
|
if ( !base->wait )
|
|
{
|
|
base->wait = 1000;// + random() * 500;
|
|
}
|
|
|
|
if ( !base->splashDamage )
|
|
{
|
|
base->splashDamage = 200;
|
|
}
|
|
|
|
if ( !base->splashRadius )
|
|
{
|
|
base->splashRadius = 500;
|
|
}
|
|
|
|
// how much damage each shot does
|
|
if ( !base->damage )
|
|
{
|
|
base->damage = 500;
|
|
}
|
|
|
|
if ( (base->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{
|
|
VectorSet( base->r.maxs, 64.0f, 64.0f, 30.0f );
|
|
VectorSet( base->r.mins, -64.0f, -64.0f, -30.0f );
|
|
}
|
|
//start in "off" anim
|
|
TurboLaser_SetBoneAnim( base, 4, 5 );
|
|
if ( g_gametype.integer == GT_SIEGE )
|
|
{//FIXME: designer-specified?
|
|
//FIXME: put on other entities, too, particularly siege objectives and bbrushes...
|
|
base->s.eFlags2 |= EF2_BRACKET_ENTITY;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( !base->random )
|
|
{//error worked into projectile direction
|
|
base->random = 2.0f;
|
|
}
|
|
|
|
if ( !base->mass )
|
|
{//misnomer: speed of projectile
|
|
base->mass = 1100;
|
|
}
|
|
|
|
if ( !base->health )
|
|
{
|
|
base->health = 100;
|
|
}
|
|
|
|
// search radius
|
|
if ( !base->radius )
|
|
{
|
|
base->radius = 512;
|
|
}
|
|
|
|
// How quickly to fire
|
|
if ( !base->wait )
|
|
{
|
|
base->wait = 150 + random() * 55;
|
|
}
|
|
|
|
if ( !base->splashDamage )
|
|
{
|
|
base->splashDamage = 10;
|
|
}
|
|
|
|
if ( !base->splashRadius )
|
|
{
|
|
base->splashRadius = 25;
|
|
}
|
|
|
|
// how much damage each shot does
|
|
if ( !base->damage )
|
|
{
|
|
base->damage = 5;
|
|
}
|
|
|
|
if ( base->spawnflags & 2 )
|
|
{//upside-down, invert r.mins and maxe
|
|
VectorSet( base->r.maxs, 10.0f, 10.0f, 30.0f );
|
|
VectorSet( base->r.mins, -10.0f, -10.0f, 0.0f );
|
|
}
|
|
else
|
|
{
|
|
VectorSet( base->r.maxs, 10.0f, 10.0f, 0.0f );
|
|
VectorSet( base->r.mins, -10.0f, -10.0f, -30.0f );
|
|
}
|
|
}
|
|
|
|
//stash health off for respawn. NOTE: cannot use maxhealth because that might not be set if not showing the health bar
|
|
base->genericValue6 = base->health;
|
|
|
|
G_SpawnInt( "showhealth", "0", &t );
|
|
if (t)
|
|
{ //a non-0 maxhealth value will mean we want to show the health on the hud
|
|
base->maxHealth = base->health;
|
|
G_ScaleNetHealth(base);
|
|
base->s.shouldtarget = qtrue;
|
|
//base->s.owner = MAX_CLIENTS; //not owned by any client
|
|
}
|
|
|
|
if (base->s.iModelScale)
|
|
{ //let's scale the bbox too...
|
|
float fScale = base->s.iModelScale/100.0f;
|
|
VectorScale(base->r.mins, fScale, base->r.mins);
|
|
VectorScale(base->r.maxs, fScale, base->r.maxs);
|
|
}
|
|
|
|
// Precache special FX and moving sounds
|
|
if ( (base->spawnflags&SPF_TURRETG2_TURBO) )
|
|
{
|
|
base->genericValue13 = G_EffectIndex( "turret/turb_muzzle_flash" );
|
|
base->genericValue14 = G_EffectIndex( "turret/turb_shot" );
|
|
base->genericValue15 = G_EffectIndex( "turret/turb_impact" );
|
|
//FIXME: Turbo Laser Cannon sounds!
|
|
G_SoundIndex( "sound/vehicles/weapons/turbolaser/turn.wav" );
|
|
}
|
|
else
|
|
{
|
|
G_SoundIndex( "sound/chars/turret/startup.wav" );
|
|
G_SoundIndex( "sound/chars/turret/shutdown.wav" );
|
|
G_SoundIndex( "sound/chars/turret/ping.wav" );
|
|
G_SoundIndex( "sound/chars/turret/move.wav" );
|
|
}
|
|
|
|
base->r.contents = CONTENTS_BODY|CONTENTS_PLAYERCLIP|CONTENTS_MONSTERCLIP|CONTENTS_SHOTCLIP;
|
|
|
|
//base->max_health = base->health;
|
|
base->takedamage = qtrue;
|
|
base->die = turretG2_die;
|
|
|
|
base->material = MAT_METAL;
|
|
//base->r.svFlags |= SVF_NO_TELEPORT|SVF_NONNPC_ENEMY|SVF_SELF_ANIMATING;
|
|
|
|
// Register this so that we can use it for the missile effect
|
|
RegisterItem( BG_FindItemForWeapon( WP_BLASTER ));
|
|
|
|
// But set us as a turret so that we can be identified as a turret
|
|
base->s.weapon = WP_TURRET;
|
|
|
|
trap_LinkEntity( base );
|
|
}
|