summaryrefslogtreecommitdiff
path: root/src/game/g_buildable.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/game/g_buildable.c')
-rw-r--r--src/game/g_buildable.c4548
1 files changed, 4548 insertions, 0 deletions
diff --git a/src/game/g_buildable.c b/src/game/g_buildable.c
new file mode 100644
index 0000000..ae3a97d
--- /dev/null
+++ b/src/game/g_buildable.c
@@ -0,0 +1,4548 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous is free software; you can redistribute it
+and/or modify it under the terms of the GNU General Public License as
+published by the Free Software Foundation; either version 2 of the License,
+or (at your option) any later version.
+
+Tremulous is distributed in the hope that it will be
+useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+===========================================================================
+*/
+
+#include "g_local.h"
+
+/*
+================
+G_SetBuildableAnim
+
+Triggers an animation client side
+================
+*/
+void G_SetBuildableAnim( gentity_t *ent, buildableAnimNumber_t anim, qboolean force )
+{
+ int localAnim = anim | ( ent->s.legsAnim & ANIM_TOGGLEBIT );
+
+ if( force )
+ localAnim |= ANIM_FORCEBIT;
+
+ // don't flip the togglebit more than once per frame
+ if( ent->animTime != level.time )
+ {
+ ent->animTime = level.time;
+ localAnim ^= ANIM_TOGGLEBIT;
+ }
+
+ ent->s.legsAnim = localAnim;
+}
+
+/*
+================
+G_SetIdleBuildableAnim
+
+Set the animation to use whilst no other animations are running
+================
+*/
+void G_SetIdleBuildableAnim( gentity_t *ent, buildableAnimNumber_t anim )
+{
+ ent->s.torsoAnim = anim;
+}
+
+/*
+===============
+G_CheckSpawnPoint
+
+Check if a spawn at a specified point is valid
+===============
+*/
+gentity_t *G_CheckSpawnPoint( int spawnNum, const vec3_t origin,
+ const vec3_t normal, buildable_t spawn, vec3_t spawnOrigin )
+{
+ float displacement;
+ vec3_t mins, maxs;
+ vec3_t cmins, cmaxs;
+ vec3_t localOrigin;
+ trace_t tr;
+
+ BG_BuildableBoundingBox( spawn, mins, maxs );
+
+ if( spawn == BA_A_SPAWN )
+ {
+ VectorSet( cmins, -MAX_ALIEN_BBOX, -MAX_ALIEN_BBOX, -MAX_ALIEN_BBOX );
+ VectorSet( cmaxs, MAX_ALIEN_BBOX, MAX_ALIEN_BBOX, MAX_ALIEN_BBOX );
+
+ displacement = ( maxs[ 2 ] + MAX_ALIEN_BBOX ) * M_ROOT3;
+ VectorMA( origin, displacement, normal, localOrigin );
+ }
+ else if( spawn == BA_H_SPAWN )
+ {
+ BG_ClassBoundingBox( PCL_HUMAN, cmins, cmaxs, NULL, NULL, NULL );
+
+ VectorCopy( origin, localOrigin );
+ localOrigin[ 2 ] += maxs[ 2 ] + fabs( cmins[ 2 ] ) + 1.0f;
+ }
+ else
+ return NULL;
+
+ trap_Trace( &tr, origin, NULL, NULL, localOrigin, spawnNum, MASK_SHOT );
+
+ if( tr.entityNum != ENTITYNUM_NONE )
+ return &g_entities[ tr.entityNum ];
+
+ trap_Trace( &tr, localOrigin, cmins, cmaxs, localOrigin, -1, MASK_PLAYERSOLID );
+
+ if( tr.entityNum != ENTITYNUM_NONE )
+ return &g_entities[ tr.entityNum ];
+
+ if( spawnOrigin != NULL )
+ VectorCopy( localOrigin, spawnOrigin );
+
+ return NULL;
+}
+
+#define POWER_REFRESH_TIME 2000
+
+/*
+================
+G_FindPower
+
+attempt to find power for self, return qtrue if successful
+================
+*/
+qboolean G_FindPower( gentity_t *self, qboolean searchUnspawned )
+{
+ int i, j;
+ gentity_t *ent, *ent2;
+ gentity_t *closestPower = NULL;
+ int distance = 0;
+ int minDistance = REPEATER_BASESIZE + 1;
+ vec3_t temp_v;
+
+ if( self->buildableTeam != TEAM_HUMANS )
+ return qfalse;
+
+ // Reactor is always powered
+ if( self->s.modelindex == BA_H_REACTOR )
+ {
+ self->parentNode = self;
+
+ return qtrue;
+ }
+
+ // Handle repeaters
+ if( self->s.modelindex == BA_H_REPEATER )
+ {
+ self->parentNode = G_Reactor( );
+
+ return self->parentNode != NULL;
+ }
+
+ // Iterate through entities
+ for( i = MAX_CLIENTS, ent = g_entities + i; i < level.num_entities; i++, ent++ )
+ {
+ if( ent->s.eType != ET_BUILDABLE )
+ continue;
+
+ // If entity is a power item calculate the distance to it
+ if( ( ent->s.modelindex == BA_H_REACTOR || ent->s.modelindex == BA_H_REPEATER ) &&
+ ( searchUnspawned || ent->spawned ) && ent->powered && ent->health > 0 )
+ {
+ VectorSubtract( self->s.origin, ent->s.origin, temp_v );
+ distance = VectorLength( temp_v );
+
+ // Always prefer a reactor if there is one in range
+ if( ent->s.modelindex == BA_H_REACTOR && distance <= REACTOR_BASESIZE )
+ {
+ // Only power as much BP as the reactor can hold
+ if( self->s.modelindex != BA_NONE )
+ {
+ int buildPoints = g_humanBuildPoints.integer;
+
+ // Scan the buildables in the reactor zone
+ for( j = MAX_CLIENTS, ent2 = g_entities + j; j < level.num_entities; j++, ent2++ )
+ {
+ gentity_t *powerEntity;
+
+ if( ent2->s.eType != ET_BUILDABLE )
+ continue;
+
+ if( ent2 == self )
+ continue;
+
+ powerEntity = ent2->parentNode;
+
+ if( powerEntity && powerEntity->s.modelindex == BA_H_REACTOR && ( powerEntity == ent ) )
+ {
+ buildPoints -= BG_Buildable( ent2->s.modelindex, ent2->cuboidSize )->buildPoints;
+ }
+ }
+
+ buildPoints -= level.humanBuildPointQueue;
+
+ buildPoints -= BG_Buildable( self->s.modelindex, self->cuboidSize )->buildPoints;
+
+ if( buildPoints >= 0 )
+ {
+ self->parentNode = ent;
+ return qtrue;
+ }
+ else
+ {
+ // a buildable can still be built if it shares BP from two zones
+
+ // TODO: handle combined power zones here
+ }
+ }
+
+ // Dummy buildables don't need to look for zones
+ else
+ {
+ self->parentNode = ent;
+ return qtrue;
+ }
+ }
+ else if( distance < minDistance )
+ {
+ // It's a repeater, so check that enough BP will be available to power
+ // the buildable but only if self is a real buildable
+
+ if( self->s.modelindex != BA_NONE )
+ {
+ int buildPoints = g_humanRepeaterBuildPoints.integer;
+
+ // Scan the buildables in the repeater zone
+ for( j = MAX_CLIENTS, ent2 = g_entities + j; j < level.num_entities; j++, ent2++ )
+ {
+ gentity_t *powerEntity;
+
+ if( ent2->s.eType != ET_BUILDABLE )
+ continue;
+
+ if( ent2 == self )
+ continue;
+
+ powerEntity = ent2->parentNode;
+
+ if( powerEntity && powerEntity->s.modelindex == BA_H_REPEATER && ( powerEntity == ent ) )
+ {
+ buildPoints -= BG_Buildable( ent2->s.modelindex, ent->cuboidSize )->buildPoints;
+ }
+ }
+
+ if( ent->usesBuildPointZone && level.buildPointZones[ ent->buildPointZone ].active )
+ buildPoints -= level.buildPointZones[ ent->buildPointZone ].queuedBuildPoints;
+
+ buildPoints -= BG_Buildable( self->s.modelindex, self->cuboidSize )->buildPoints;
+
+ if( buildPoints >= 0 )
+ {
+ closestPower = ent;
+ minDistance = distance;
+ }
+ else
+ {
+ // a buildable can still be built if it shares BP from two zones
+
+ // TODO: handle combined power zones here
+ }
+ }
+ else
+ {
+ // Dummy buildables don't need to look for zones
+ closestPower = ent;
+ minDistance = distance;
+ }
+ }
+ }
+ }
+
+ self->parentNode = closestPower;
+ return self->parentNode != NULL;
+}
+
+/*
+================
+G_PowerEntityForPoint
+
+Simple wrapper to G_FindPower to find the entity providing
+power for the specified point
+================
+*/
+gentity_t *G_PowerEntityForPoint( const vec3_t origin )
+{
+ gentity_t dummy;
+
+ dummy.parentNode = NULL;
+ dummy.buildableTeam = TEAM_HUMANS;
+ dummy.s.modelindex = BA_NONE;
+ VectorCopy( origin, dummy.s.origin );
+
+ if( G_FindPower( &dummy, qfalse ) )
+ return dummy.parentNode;
+ else
+ return NULL;
+}
+
+/*
+================
+G_PowerEntityForEntity
+
+Simple wrapper to G_FindPower to find the entity providing
+power for the specified entity
+================
+*/
+gentity_t *G_PowerEntityForEntity( gentity_t *ent )
+{
+ if( G_FindPower( ent, qfalse ) )
+ return ent->parentNode;
+ return NULL;
+}
+
+/*
+================
+G_IsPowered
+
+Check if a location has power, returning the entity type
+that is providing it
+================
+*/
+buildable_t G_IsPowered( vec3_t origin )
+{
+ gentity_t *ent = G_PowerEntityForPoint( origin );
+
+ if( ent )
+ return ent->s.modelindex;
+ else
+ return BA_NONE;
+}
+
+
+/*
+==================
+G_GetBuildPoints
+
+Get the number of build points from a position
+==================
+*/
+int G_GetBuildPoints( const vec3_t pos, team_t team )
+{
+ if( G_TimeTilSuddenDeath( ) <= 0 )
+ {
+ return 0;
+ }
+ else if( team == TEAM_ALIENS )
+ {
+ return level.alienBuildPoints;
+ }
+ else if( team == TEAM_HUMANS )
+ {
+ gentity_t *powerPoint = G_PowerEntityForPoint( pos );
+
+ if( powerPoint && powerPoint->s.modelindex == BA_H_REACTOR )
+ return level.humanBuildPoints;
+
+ if( powerPoint && powerPoint->s.modelindex == BA_H_REPEATER &&
+ powerPoint->usesBuildPointZone && level.buildPointZones[ powerPoint->buildPointZone ].active )
+ {
+ return level.buildPointZones[ powerPoint->buildPointZone ].totalBuildPoints -
+ level.buildPointZones[ powerPoint->buildPointZone ].queuedBuildPoints;
+ }
+
+ // Return the BP of the main zone by default
+ return level.humanBuildPoints;
+ }
+
+ return 0;
+}
+
+/*
+==================
+G_GetMarkedBuildPoints
+
+Get the number of marked build points from a position
+==================
+*/
+int G_GetMarkedBuildPoints( const vec3_t pos, team_t team )
+{
+ gentity_t *ent;
+ int i;
+ int sum = 0;
+
+ if( G_TimeTilSuddenDeath( ) <= 0 )
+ return 0;
+
+ if( !g_markDeconstruct.integer )
+ return 0;
+
+ for( i = MAX_CLIENTS, ent = g_entities + i; i < level.num_entities; i++, ent++ )
+ {
+ if( ent->s.eType != ET_BUILDABLE )
+ continue;
+
+ if( team == TEAM_HUMANS &&
+ ent->s.modelindex != BA_H_REACTOR &&
+ ent->s.modelindex != BA_H_REPEATER &&
+ ent->parentNode != G_PowerEntityForPoint( pos ) )
+ continue;
+
+ if( !ent->inuse )
+ continue;
+
+ if( ent->health <= 0 )
+ continue;
+
+ if( ent->buildableTeam != team )
+ continue;
+
+ if( ent->deconstruct )
+ sum += BG_Buildable( ent->s.modelindex, ent->cuboidSize )->buildPoints;
+ }
+
+ return sum;
+}
+
+/*
+==================
+G_InPowerZone
+
+See if a buildable is inside of another power zone.
+Return pointer to provider if so.
+It's different from G_FindPower because FindPower for
+providers will find themselves.
+(This doesn't check if power zones overlap)
+==================
+*/
+gentity_t *G_InPowerZone( gentity_t *self )
+{
+ int i;
+ gentity_t *ent;
+ int distance;
+ vec3_t temp_v;
+
+ for( i = MAX_CLIENTS, ent = g_entities + i; i < level.num_entities; i++, ent++ )
+ {
+ if( ent->s.eType != ET_BUILDABLE )
+ continue;
+
+ if( ent == self )
+ continue;
+
+ if( !ent->spawned )
+ continue;
+
+ if( ent->health <= 0 )
+ continue;
+
+ // if entity is a power item calculate the distance to it
+ if( ( ent->s.modelindex == BA_H_REACTOR || ent->s.modelindex == BA_H_REPEATER ) &&
+ ent->spawned && ent->powered )
+ {
+ VectorSubtract( self->s.origin, ent->s.origin, temp_v );
+ distance = VectorLength( temp_v );
+
+ if( ent->s.modelindex == BA_H_REACTOR && distance <= REACTOR_BASESIZE )
+ return ent;
+ else if( ent->s.modelindex == BA_H_REPEATER && distance <= REPEATER_BASESIZE )
+ return ent;
+ }
+ }
+
+ return NULL;
+}
+
+/*
+================
+G_FindDCC
+
+attempt to find a controlling DCC for self, return number found
+================
+*/
+int G_FindDCC( gentity_t *self )
+{
+ int i;
+ gentity_t *ent;
+ int distance = 0;
+ vec3_t temp_v;
+ int foundDCC = 0;
+
+ if( self->buildableTeam != TEAM_HUMANS )
+ return 0;
+
+ //iterate through entities
+ for( i = MAX_CLIENTS, ent = g_entities + i; i < level.num_entities; i++, ent++ )
+ {
+ if( ent->s.eType != ET_BUILDABLE )
+ continue;
+
+ //if entity is a dcc calculate the distance to it
+ if( ent->s.modelindex == BA_H_DCC && ent->spawned )
+ {
+ VectorSubtract( self->s.origin, ent->s.origin, temp_v );
+ distance = VectorLength( temp_v );
+ if( distance < DC_RANGE && ent->powered )
+ {
+ foundDCC++;
+ }
+ }
+ }
+
+ return foundDCC;
+}
+
+/*
+================
+G_IsDCCBuilt
+
+See if any powered DCC exists
+================
+*/
+qboolean G_IsDCCBuilt( void )
+{
+ int i;
+ gentity_t *ent;
+
+ for( i = MAX_CLIENTS, ent = g_entities + i; i < level.num_entities; i++, ent++ )
+ {
+ if( ent->s.eType != ET_BUILDABLE )
+ continue;
+
+ if( ent->s.modelindex != BA_H_DCC )
+ continue;
+
+ if( !ent->spawned )
+ continue;
+
+ if( ent->health <= 0 )
+ continue;
+
+ return qtrue;
+ }
+
+ return qfalse;
+}
+
+/*
+================
+G_Reactor
+G_Overmind
+
+Since there's only one of these and we quite often want to find them, cache the
+results, but check them for validity each time
+
+The code here will break if more than one reactor or overmind is allowed, even
+if one of them is dead/unspawned
+================
+*/
+static gentity_t *G_FindBuildable( buildable_t buildable );
+
+gentity_t *G_Reactor( void )
+{
+ static gentity_t *rc;
+
+ // If cache becomes invalid renew it
+ if( !rc || rc->s.eType != ET_BUILDABLE || rc->s.modelindex != BA_H_REACTOR )
+ rc = G_FindBuildable( BA_H_REACTOR );
+
+ // If we found it and it's alive, return it
+ if( rc && rc->spawned && rc->health > 0 )
+ return rc;
+
+ return NULL;
+}
+
+gentity_t *G_Overmind( void )
+{
+ static gentity_t *om;
+
+ // If cache becomes invalid renew it
+ if( !om || om->s.eType != ET_BUILDABLE || om->s.modelindex != BA_A_OVERMIND )
+ om = G_FindBuildable( BA_A_OVERMIND );
+
+ // If we found it and it's alive, return it
+ if( om && om->spawned && om->health > 0 )
+ return om;
+
+ return NULL;
+}
+
+/*
+================
+G_FindCreep
+
+attempt to find creep for self, return qtrue if successful
+================
+*/
+qboolean G_FindCreep( gentity_t *self )
+{
+ int i;
+ gentity_t *ent;
+ gentity_t *closestSpawn = NULL;
+ int distance = 0;
+ int minDistance = 10000;
+ vec3_t temp_v;
+
+ //don't check for creep if flying through the air
+ if( self->s.groundEntityNum == -1 )
+ return qtrue;
+
+ //if self does not have a parentNode or it's parentNode is invalid find a new one
+ if( self->client || self->parentNode == NULL || !self->parentNode->inuse ||
+ self->parentNode->health <= 0 )
+ {
+ for( i = MAX_CLIENTS, ent = g_entities + i; i < level.num_entities; i++, ent++ )
+ {
+ if( ent->s.eType != ET_BUILDABLE )
+ continue;
+
+ if( ( ent->s.modelindex == BA_A_SPAWN ||
+ ent->s.modelindex == BA_A_OVERMIND ) &&
+ ent->spawned && ent->health > 0 )
+ {
+ VectorSubtract( self->s.origin, ent->s.origin, temp_v );
+ distance = VectorLength( temp_v );
+ if( distance < minDistance )
+ {
+ closestSpawn = ent;
+ minDistance = distance;
+ }
+ }
+ }
+
+ if( minDistance <= CREEP_BASESIZE )
+ {
+ if( !self->client )
+ self->parentNode = closestSpawn;
+ return qtrue;
+ }
+ else
+ return qfalse;
+ }
+
+ if( self->client )
+ return qfalse;
+
+ //if we haven't returned by now then we must already have a valid parent
+ return qtrue;
+}
+
+/*
+================
+G_IsCreepHere
+
+simple wrapper to G_FindCreep to check if a location has creep
+================
+*/
+static qboolean G_IsCreepHere( vec3_t origin )
+{
+ gentity_t dummy;
+
+ memset( &dummy, 0, sizeof( gentity_t ) );
+
+ dummy.parentNode = NULL;
+ dummy.s.modelindex = BA_NONE;
+ VectorCopy( origin, dummy.s.origin );
+
+ return G_FindCreep( &dummy );
+}
+
+/*
+================
+G_CreepSlow
+
+Set any nearby humans' SS_CREEPSLOWED flag
+================
+*/
+static void G_CreepSlow( gentity_t *self )
+{
+ int entityList[ MAX_GENTITIES ];
+ vec3_t range;
+ vec3_t mins, maxs;
+ int i, num;
+ gentity_t *enemy;
+ buildable_t buildable = self->s.modelindex;
+ float creepSize = (float)BG_Buildable( buildable, self->cuboidSize )->creepSize;
+
+ VectorSet( range, creepSize, creepSize, creepSize );
+
+ VectorAdd( self->s.origin, range, maxs );
+ VectorSubtract( self->s.origin, range, mins );
+
+ //find humans
+ num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
+ for( i = 0; i < num; i++ )
+ {
+ enemy = &g_entities[ entityList[ i ] ];
+
+ if( enemy->flags & FL_NOTARGET )
+ continue;
+
+ if( enemy->client && enemy->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS &&
+ enemy->client->ps.groundEntityNum != ENTITYNUM_NONE )
+ {
+ enemy->client->ps.stats[ STAT_STATE ] |= SS_CREEPSLOWED;
+ enemy->client->lastCreepSlowTime = level.time;
+ }
+ }
+}
+
+/*
+================
+nullDieFunction
+
+hack to prevent compilers complaining about function pointer -> NULL conversion
+================
+*/
+static void nullDieFunction( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod )
+{
+}
+
+//==================================================================================
+
+
+
+/*
+================
+AGeneric_CreepRecede
+
+Called when an alien buildable dies
+================
+*/
+void AGeneric_CreepRecede( gentity_t *self )
+{
+ //if the creep just died begin the recession
+ if( !( self->s.eFlags & EF_DEAD ) )
+ {
+ self->s.eFlags |= EF_DEAD;
+ G_QueueBuildPoints( self );
+
+ G_RewardAttackers( self );
+
+ G_AddEvent( self, EV_BUILD_DESTROY, 0 );
+
+ if( self->spawned )
+ self->s.time = -level.time;
+ else
+ self->s.time = -( level.time -
+ (int)( (float)CREEP_SCALEDOWN_TIME *
+ ( 1.0f - ( (float)( level.time - self->buildTime ) /
+ (float)BG_Buildable( self->s.modelindex, self->cuboidSize )->buildTime ) ) ) );
+ }
+
+ //creep is still receeding
+ if( ( self->timestamp + 10000 ) > level.time )
+ self->nextthink = level.time + 500;
+ else //creep has died
+ G_FreeEntity( self );
+}
+
+/*
+================
+AGeneric_Blast
+
+Called when an Alien buildable explodes after dead state
+================
+*/
+void AGeneric_Blast( gentity_t *self )
+{
+ vec3_t dir;
+
+ VectorCopy( self->s.origin2, dir );
+
+ //do a bit of radius damage
+ G_SelectiveRadiusDamage( self->s.pos.trBase, g_entities + self->killedBy, self->splashDamage,
+ self->splashRadius, self, self->splashMethodOfDeath,
+ TEAM_ALIENS );
+
+ //pretty events and item cleanup
+ self->s.eFlags |= EF_NODRAW; //don't draw the model once it's destroyed
+ G_AddEvent( self, EV_ALIEN_BUILDABLE_EXPLOSION, DirToByte( dir ) );
+ self->timestamp = level.time;
+ self->think = AGeneric_CreepRecede;
+ self->nextthink = level.time + 500;
+
+ self->r.contents = 0; //stop collisions...
+ trap_LinkEntity( self ); //...requires a relink
+}
+
+/*
+================
+AGeneric_Die
+
+Called when an Alien buildable is killed and enters a brief dead state prior to
+exploding.
+================
+*/
+void AGeneric_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod )
+{
+ G_SetBuildableAnim( self, BANIM_DESTROY1, qtrue );
+ G_SetIdleBuildableAnim( self, BANIM_DESTROYED );
+
+ self->die = nullDieFunction;
+ self->killedBy = attacker - g_entities;
+ self->think = AGeneric_Blast;
+ self->s.eFlags &= ~EF_FIRING; //prevent any firing effects
+ self->powered = qfalse;
+
+ if( self->spawned )
+ self->nextthink = level.time + 5000;
+ else
+ self->nextthink = level.time; //blast immediately
+
+ G_LogDestruction( self, attacker, mod );
+}
+
+/*
+================
+AGeneric_CreepCheck
+
+Tests for creep and kills the buildable if there is none
+================
+*/
+void AGeneric_CreepCheck( gentity_t *self )
+{
+ gentity_t *spawn;
+
+ spawn = self->parentNode;
+ if( !G_FindCreep( self ) )
+ {
+ if( spawn )
+ G_Damage( self, NULL, g_entities + spawn->killedBy, NULL, NULL,
+ self->health, 0, MOD_NOCREEP );
+ else
+ G_Damage( self, NULL, NULL, NULL, NULL, self->health, 0, MOD_NOCREEP );
+ return;
+ }
+ G_CreepSlow( self );
+}
+
+/*
+================
+AGeneric_Think
+
+A generic think function for Alien buildables
+================
+*/
+void AGeneric_Think( gentity_t *self )
+{
+ self->powered = G_Overmind( ) != NULL;
+ self->nextthink = level.time + BG_Buildable( self->s.modelindex, NULL )->nextthink;
+ AGeneric_CreepCheck( self );
+}
+
+/*
+================
+AGeneric_Pain
+
+A generic pain function for Alien buildables
+================
+*/
+void AGeneric_Pain( gentity_t *self, gentity_t *attacker, int damage )
+{
+ if( self->health <= 0 )
+ return;
+
+ // Alien buildables only have the first pain animation defined
+ G_SetBuildableAnim( self, BANIM_PAIN1, qfalse );
+}
+
+
+
+
+//==================================================================================
+
+/*
+================
+ASpawn_Think
+
+think function for Alien Spawn
+================
+*/
+void ASpawn_Think( gentity_t *self )
+{
+ gentity_t *ent;
+
+ if( self->spawned )
+ {
+ //only suicide if at rest
+ if( self->s.groundEntityNum )
+ {
+ if( ( ent = G_CheckSpawnPoint( self->s.number, self->s.origin,
+ self->s.origin2, BA_A_SPAWN, NULL ) ) != NULL )
+ {
+ // If the thing blocking the spawn is a buildable, kill it.
+ // If it's part of the map, kill self.
+ if( ent->s.eType == ET_BUILDABLE )
+ {
+ if( ent->builtBy >= 0 ) // don't queue the bp from this
+ G_Damage( ent, NULL, g_entities + ent->builtBy, NULL, NULL, 10000, 0, MOD_SUICIDE );
+ else
+ G_Damage( ent, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE );
+
+ G_SetBuildableAnim( self, BANIM_SPAWN1, qtrue );
+ }
+ else if( ent->s.number == ENTITYNUM_WORLD || ent->s.eType == ET_MOVER )
+ {
+ G_Damage( self, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE );
+ return;
+ }
+
+ if( ent->s.eType == ET_CORPSE )
+ G_FreeEntity( ent ); //quietly remove
+ }
+ }
+ }
+
+ G_CreepSlow( self );
+
+ self->nextthink = level.time + BG_Buildable( self->s.modelindex, NULL )->nextthink;
+}
+
+
+
+
+
+//==================================================================================
+
+
+
+
+
+#define OVERMIND_ATTACK_PERIOD 10000
+#define OVERMIND_DYING_PERIOD 5000
+#define OVERMIND_SPAWNS_PERIOD 30000
+
+/*
+================
+AOvermind_Think
+
+Think function for Alien Overmind
+================
+*/
+void AOvermind_Think( gentity_t *self )
+{
+ vec3_t range = { OVERMIND_ATTACK_RANGE, OVERMIND_ATTACK_RANGE, OVERMIND_ATTACK_RANGE };
+ int i;
+
+ if( self->spawned && ( self->health > 0 ) )
+ {
+ //do some damage
+ if( G_SelectiveRadiusDamage( self->s.pos.trBase, self, self->splashDamage,
+ self->splashRadius, self, MOD_OVERMIND, TEAM_ALIENS ) )
+ {
+ self->timestamp = level.time;
+ G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse );
+ }
+
+ // just in case an egg finishes building after we tell overmind to stfu
+ if( level.numAlienSpawns > 0 )
+ level.overmindMuted = qfalse;
+
+ // shut up during intermission
+ if( level.intermissiontime )
+ level.overmindMuted = qtrue;
+
+ //low on spawns
+ if( !level.overmindMuted && level.numAlienSpawns <= 0 &&
+ level.time > self->overmindSpawnsTimer )
+ {
+ qboolean haveBuilder = qfalse;
+ gentity_t *builder;
+
+ self->overmindSpawnsTimer = level.time + OVERMIND_SPAWNS_PERIOD;
+ G_BroadcastEvent( EV_OVERMIND_SPAWNS, 0 );
+
+ for( i = 0; i < level.numConnectedClients; i++ )
+ {
+ builder = &g_entities[ level.sortedClients[ i ] ];
+ if( builder->health > 0 &&
+ ( builder->client->pers.classSelection == PCL_ALIEN_BUILDER0 ||
+ builder->client->pers.classSelection == PCL_ALIEN_BUILDER0_UPG ) )
+ {
+ haveBuilder = qtrue;
+ break;
+ }
+ }
+ // aliens now know they have no eggs, but they're screwed, so stfu
+ if( !haveBuilder || G_TimeTilSuddenDeath( ) <= 0 )
+ level.overmindMuted = qtrue;
+ }
+
+ //overmind dying
+ if( self->health < ( OVERMIND_HEALTH / 10.0f ) && level.time > self->overmindDyingTimer )
+ {
+ self->overmindDyingTimer = level.time + OVERMIND_DYING_PERIOD;
+ G_BroadcastEvent( EV_OVERMIND_DYING, 0 );
+ }
+
+ //overmind under attack
+ if( self->health < self->lastHealth && level.time > self->overmindAttackTimer )
+ {
+ self->overmindAttackTimer = level.time + OVERMIND_ATTACK_PERIOD;
+ G_BroadcastEvent( EV_OVERMIND_ATTACK, 0 );
+ }
+
+ self->lastHealth = self->health;
+ }
+ else
+ self->overmindSpawnsTimer = level.time + OVERMIND_SPAWNS_PERIOD;
+
+ G_CreepSlow( self );
+
+ self->nextthink = level.time + BG_Buildable( self->s.modelindex, NULL )->nextthink;
+}
+
+
+
+
+
+//==================================================================================
+
+
+
+
+
+/*
+================
+ABarricade_Pain
+
+Barricade pain animation depends on shrunk state
+================
+*/
+void ABarricade_Pain( gentity_t *self, gentity_t *attacker, int damage )
+{
+ if( self->health <= 0 )
+ return;
+
+ if( !self->shrunkTime )
+ G_SetBuildableAnim( self, BANIM_PAIN1, qfalse );
+ else
+ G_SetBuildableAnim( self, BANIM_PAIN2, qfalse );
+}
+
+/*
+================
+ABarricade_Shrink
+
+Set shrink state for a barricade. When unshrinking, checks to make sure there
+is enough room.
+================
+*/
+void ABarricade_Shrink( gentity_t *self, qboolean shrink )
+{
+ if ( !self->spawned || self->health <= 0 )
+ shrink = qtrue;
+ if ( shrink && self->shrunkTime )
+ {
+ int anim;
+
+ // We need to make sure that the animation has been set to shrunk mode
+ // because we start out shrunk but with the construct animation when built
+ self->shrunkTime = level.time;
+ anim = self->s.torsoAnim & ~( ANIM_FORCEBIT | ANIM_TOGGLEBIT );
+ if ( self->spawned && self->health > 0 && anim != BANIM_DESTROYED )
+ {
+ G_SetIdleBuildableAnim( self, BANIM_DESTROYED );
+ G_SetBuildableAnim( self, BANIM_ATTACK1, qtrue );
+ }
+ return;
+ }
+
+ if ( !shrink && ( !self->shrunkTime ||
+ level.time < self->shrunkTime + BARRICADE_SHRINKTIMEOUT ) )
+ return;
+
+ BG_BuildableBoundingBox( BA_A_BARRICADE, self->r.mins, self->r.maxs );
+
+ if ( shrink )
+ {
+ self->r.maxs[ 2 ] = (int)( self->r.maxs[ 2 ] * BARRICADE_SHRINKPROP );
+ self->shrunkTime = level.time;
+
+ // shrink animation, the destroy animation is used
+ if ( self->spawned && self->health > 0 )
+ {
+ G_SetBuildableAnim( self, BANIM_ATTACK1, qtrue );
+ G_SetIdleBuildableAnim( self, BANIM_DESTROYED );
+ }
+ }
+ else
+ {
+ trace_t tr;
+ int anim;
+
+ trap_Trace( &tr, self->s.origin, self->r.mins, self->r.maxs,
+ self->s.origin, self->s.number, MASK_PLAYERSOLID );
+ if ( tr.startsolid || tr.fraction < 1.0f )
+ {
+ self->r.maxs[ 2 ] = (int)( self->r.maxs[ 2 ] * BARRICADE_SHRINKPROP );
+ return;
+ }
+ self->shrunkTime = 0;
+
+ // unshrink animation, IDLE2 has been hijacked for this
+ anim = self->s.legsAnim & ~( ANIM_FORCEBIT | ANIM_TOGGLEBIT );
+ if ( self->spawned && self->health > 0 &&
+ anim != BANIM_CONSTRUCT1 && anim != BANIM_CONSTRUCT2 )
+ {
+ G_SetIdleBuildableAnim( self, BG_Buildable( BA_A_BARRICADE, NULL )->idleAnim );
+ G_SetBuildableAnim( self, BANIM_ATTACK2, qtrue );
+ }
+ }
+
+ // a change in size requires a relink
+ if ( self->spawned )
+ trap_LinkEntity( self );
+}
+
+/*
+================
+ABarricade_Die
+
+Called when an alien barricade dies
+================
+*/
+void ABarricade_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod )
+{
+ AGeneric_Die( self, inflictor, attacker, damage, mod );
+ ABarricade_Shrink( self, qtrue );
+}
+
+/*
+================
+ABarricade_Think
+
+Think function for Alien Barricade
+================
+*/
+void ABarricade_Think( gentity_t *self )
+{
+ AGeneric_Think( self );
+
+ // Shrink if unpowered
+ ABarricade_Shrink( self, !self->powered );
+}
+
+/*
+================
+ABarricade_Touch
+
+Barricades shrink when they are come into contact with an Alien that can
+pass through
+================
+*/
+
+void ABarricade_Touch( gentity_t *self, gentity_t *other, trace_t *trace )
+{
+ gclient_t *client = other->client;
+ int client_z, min_z;
+
+ if( !client || client->pers.teamSelection != TEAM_ALIENS )
+ return;
+
+ // Client must be high enough to pass over. Note that STEPSIZE (18) is
+ // hardcoded here because we don't include bg_local.h!
+ client_z = other->s.origin[ 2 ] + other->r.mins[ 2 ];
+ min_z = self->s.origin[ 2 ] - 18 +
+ (int)( self->r.maxs[ 2 ] * BARRICADE_SHRINKPROP );
+ if( client_z < min_z )
+ return;
+ ABarricade_Shrink( self, qtrue );
+}
+
+//==================================================================================
+
+
+
+
+/*
+================
+AAcidTube_Think
+
+Think function for Alien Acid Tube
+================
+*/
+void AAcidTube_Think( gentity_t *self )
+{
+ int entityList[ MAX_GENTITIES ];
+ vec3_t range = { ACIDTUBE_RANGE, ACIDTUBE_RANGE, ACIDTUBE_RANGE };
+ vec3_t mins, maxs;
+ int i, num;
+ gentity_t *enemy;
+
+ AGeneric_Think( self );
+
+ VectorAdd( self->s.origin, range, maxs );
+ VectorSubtract( self->s.origin, range, mins );
+
+ // attack nearby humans
+ if( self->spawned && self->health > 0 && self->powered )
+ {
+ num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
+ for( i = 0; i < num; i++ )
+ {
+ enemy = &g_entities[ entityList[ i ] ];
+
+ if( enemy->flags & FL_NOTARGET )
+ continue;
+
+ if( !G_Visible( self, enemy, CONTENTS_SOLID ) )
+ continue;
+
+ if( enemy->client && enemy->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS )
+ {
+ // start the attack animation
+ if( level.time >= self->timestamp + ACIDTUBE_REPEAT_ANIM )
+ {
+ self->timestamp = level.time;
+ G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse );
+ G_AddEvent( self, EV_ALIEN_ACIDTUBE, DirToByte( self->s.origin2 ) );
+ }
+
+ G_SelectiveRadiusDamage( self->s.pos.trBase, self, ACIDTUBE_DAMAGE,
+ ACIDTUBE_RANGE, self, MOD_ATUBE, TEAM_ALIENS );
+ self->nextthink = level.time + ACIDTUBE_REPEAT;
+ return;
+ }
+ }
+ }
+}
+
+
+
+
+//==================================================================================
+
+/*
+================
+AHive_CheckTarget
+
+Returns true and fires the hive missile if the target is valid
+================
+*/
+static qboolean AHive_CheckTarget( gentity_t *self, gentity_t *enemy )
+{
+ trace_t trace;
+ vec3_t tip_origin, dirToTarget;
+
+ // Check if this is a valid target
+ if( enemy->health <= 0 || !enemy->client ||
+ enemy->client->ps.stats[ STAT_TEAM ] != TEAM_HUMANS )
+ return qfalse;
+
+ if( enemy->flags & FL_NOTARGET )
+ return qfalse;
+
+ // Check if the tip of the hive can see the target
+ VectorMA( self->s.pos.trBase, self->r.maxs[ 2 ], self->s.origin2,
+ tip_origin );
+ if( Distance( tip_origin, enemy->s.origin ) > HIVE_SENSE_RANGE )
+ return qfalse;
+
+ trap_Trace( &trace, tip_origin, NULL, NULL, enemy->s.pos.trBase,
+ self->s.number, MASK_SHOT );
+ if( trace.fraction < 1.0f && trace.entityNum != enemy->s.number )
+ return qfalse;
+
+ self->active = qtrue;
+ self->target_ent = enemy;
+ self->timestamp = level.time + HIVE_REPEAT;
+
+ VectorSubtract( enemy->s.pos.trBase, self->s.pos.trBase, dirToTarget );
+ VectorNormalize( dirToTarget );
+ vectoangles( dirToTarget, self->turretAim );
+
+ // Fire at target
+ FireWeapon( self );
+ G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse );
+ return qtrue;
+}
+
+/*
+================
+AHive_Think
+
+Think function for Alien Hive
+================
+*/
+void AHive_Think( gentity_t *self )
+{
+ int start;
+
+ AGeneric_Think( self );
+
+ // Hive missile hasn't returned in HIVE_REPEAT seconds, forget about it
+ if( self->timestamp < level.time )
+ self->active = qfalse;
+
+ // Find a target to attack
+ if( self->spawned && !self->active && self->powered )
+ {
+ int i, num, entityList[ MAX_GENTITIES ];
+ vec3_t mins, maxs,
+ range = { HIVE_SENSE_RANGE, HIVE_SENSE_RANGE, HIVE_SENSE_RANGE };
+
+ VectorAdd( self->s.origin, range, maxs );
+ VectorSubtract( self->s.origin, range, mins );
+
+ num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
+
+ if( num == 0 )
+ return;
+
+ start = rand( ) / ( RAND_MAX / num + 1 );
+ for( i = start; i < num + start; i++ )
+ {
+ if( AHive_CheckTarget( self, g_entities + entityList[ i % num ] ) )
+ return;
+ }
+ }
+}
+
+/*
+================
+AHive_Pain
+
+pain function for Alien Hive
+================
+*/
+void AHive_Pain( gentity_t *self, gentity_t *attacker, int damage )
+{
+ if( self->spawned && self->powered && !self->active )
+ AHive_CheckTarget( self, attacker );
+
+ G_SetBuildableAnim( self, BANIM_PAIN1, qfalse );
+}
+
+
+//==================================================================================
+
+
+/*
+================
+ABooster_Touch
+
+Called when an alien touches a booster
+================
+*/
+void ABooster_Touch( gentity_t *self, gentity_t *other, trace_t *trace )
+{
+ gclient_t *client = other->client;
+
+ if( !self->spawned || !self->powered || self->health <= 0 )
+ return;
+
+ if( !client )
+ return;
+
+ if( client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS )
+ return;
+
+ if( other->flags & FL_NOTARGET )
+ return; // notarget cancels even beneficial effects?
+
+ client->ps.stats[ STAT_STATE ] |= SS_BOOSTED;
+ client->boostedTime = level.time;
+
+ // give the rant bomb
+ if( client->lastRantBombTime + LEVEL4_BOMB_REGEN <= level.time )
+ {
+ client->lastRantBombTime = level.time;
+ client->ps.ammo = 1;
+ }
+}
+
+
+
+
+//==================================================================================
+
+#define TRAPPER_ACCURACY 10 // lower is better
+
+/*
+================
+ATrapper_FireOnEnemy
+
+Used by ATrapper_Think to fire at enemy
+================
+*/
+void ATrapper_FireOnEnemy( gentity_t *self, int firespeed, float range )
+{
+ gentity_t *enemy = self->enemy;
+ vec3_t dirToTarget;
+ vec3_t halfAcceleration, thirdJerk;
+ float distanceToTarget = BG_Buildable( self->s.modelindex, NULL )->turretRange;
+ int lowMsec = 0;
+ int highMsec = (int)( (
+ ( ( distanceToTarget * LOCKBLOB_SPEED ) +
+ ( distanceToTarget * BG_Class( enemy->client->ps.stats[ STAT_CLASS ] )->speed ) ) /
+ ( LOCKBLOB_SPEED * LOCKBLOB_SPEED ) ) * 1000.0f );
+
+ VectorScale( enemy->acceleration, 1.0f / 2.0f, halfAcceleration );
+ VectorScale( enemy->jerk, 1.0f / 3.0f, thirdJerk );
+
+ // highMsec and lowMsec can only move toward
+ // one another, so the loop must terminate
+ while( highMsec - lowMsec > TRAPPER_ACCURACY )
+ {
+ int partitionMsec = ( highMsec + lowMsec ) / 2;
+ float time = (float)partitionMsec / 1000.0f;
+ float projectileDistance = LOCKBLOB_SPEED * time;
+
+ VectorMA( enemy->s.pos.trBase, time, enemy->s.pos.trDelta, dirToTarget );
+ VectorMA( dirToTarget, time * time, halfAcceleration, dirToTarget );
+ VectorMA( dirToTarget, time * time * time, thirdJerk, dirToTarget );
+ VectorSubtract( dirToTarget, self->s.pos.trBase, dirToTarget );
+ distanceToTarget = VectorLength( dirToTarget );
+
+ if( projectileDistance < distanceToTarget )
+ lowMsec = partitionMsec;
+ else if( projectileDistance > distanceToTarget )
+ highMsec = partitionMsec;
+ else if( projectileDistance == distanceToTarget )
+ break; // unlikely to happen
+ }
+
+ VectorNormalize( dirToTarget );
+ vectoangles( dirToTarget, self->turretAim );
+
+ //fire at target
+ FireWeapon( self );
+ G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse );
+ self->count = level.time + firespeed;
+}
+
+/*
+================
+ATrapper_CheckTarget
+
+Used by ATrapper_Think to check enemies for validity
+================
+*/
+qboolean ATrapper_CheckTarget( gentity_t *self, gentity_t *target, int range )
+{
+ vec3_t distance;
+ trace_t trace;
+
+ if( !target ) // Do we have a target?
+ return qfalse;
+ if( !target->inuse ) // Does the target still exist?
+ return qfalse;
+ if( target == self ) // is the target us?
+ return qfalse;
+ if( !target->client ) // is the target a bot or player?
+ return qfalse;
+ if( target->flags & FL_NOTARGET ) // is the target cheating?
+ return qfalse;
+ if( target->client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) // one of us?
+ return qfalse;
+ if( target->client->sess.spectatorState != SPECTATOR_NOT ) // is the target alive?
+ return qfalse;
+ if( target->health <= 0 ) // is the target still alive?
+ return qfalse;
+ if( target->client->ps.stats[ STAT_STATE ] & SS_BLOBLOCKED ) // locked?
+ return qfalse;
+
+ VectorSubtract( target->r.currentOrigin, self->r.currentOrigin, distance );
+ if( VectorLength( distance ) > range ) // is the target within range?
+ return qfalse;
+
+ //only allow a narrow field of "vision"
+ VectorNormalize( distance ); //is now direction of target
+ if( DotProduct( distance, self->s.origin2 ) < LOCKBLOB_DOT )
+ return qfalse;
+
+ trap_Trace( &trace, self->s.pos.trBase, NULL, NULL, target->s.pos.trBase, self->s.number, MASK_SHOT );
+ if ( trace.contents & CONTENTS_SOLID ) // can we see the target?
+ return qfalse;
+
+ return qtrue;
+}
+
+/*
+================
+ATrapper_FindEnemy
+
+Used by ATrapper_Think to locate enemy gentities
+================
+*/
+void ATrapper_FindEnemy( gentity_t *ent, int range )
+{
+ gentity_t *target;
+ int i;
+ int start;
+
+ // iterate through entities
+ // note that if we exist then level.num_entities != 0
+ start = rand( ) / ( RAND_MAX / level.num_entities + 1 );
+ for( i = start; i < level.num_entities + start; i++ )
+ {
+ target = g_entities + ( i % level.num_entities );
+ //if target is not valid keep searching
+ if( !ATrapper_CheckTarget( ent, target, range ) )
+ continue;
+
+ //we found a target
+ ent->enemy = target;
+ return;
+ }
+
+ //couldn't find a target
+ ent->enemy = NULL;
+}
+
+/*
+================
+ATrapper_Think
+
+think function for Alien Defense
+================
+*/
+void ATrapper_Think( gentity_t *self )
+{
+ int range = BG_Buildable( self->s.modelindex, NULL )->turretRange;
+ int firespeed = BG_Buildable( self->s.modelindex, NULL )->turretFireSpeed;
+
+ AGeneric_Think( self );
+
+ if( self->spawned && self->powered )
+ {
+ //if the current target is not valid find a new one
+ if( !ATrapper_CheckTarget( self, self->enemy, range ) )
+ ATrapper_FindEnemy( self, range );
+
+ //if a new target cannot be found don't do anything
+ if( !self->enemy )
+ return;
+
+ //if we are pointing at our target and we can fire shoot it
+ if( self->count < level.time )
+ ATrapper_FireOnEnemy( self, firespeed, range );
+ }
+}
+
+
+
+
+//==================================================================================
+
+
+
+
+/*
+================
+G_SuicideIfNoPower
+
+Destroy human structures that have been unpowered too long
+================
+*/
+static qboolean G_SuicideIfNoPower( gentity_t *self )
+{
+ if( self->buildableTeam != TEAM_HUMANS )
+ return qfalse;
+
+ if( !self->powered )
+ {
+ // if the power hasn't reached this buildable for some time, then destroy the buildable
+ if( self->count == 0 )
+ self->count = level.time;
+ else if( ( level.time - self->count ) >= HUMAN_BUILDABLE_INACTIVE_TIME )
+ {
+ if( self->parentNode )
+ G_Damage( self, NULL, g_entities + self->parentNode->killedBy,
+ NULL, NULL, self->health, 0, MOD_NOCREEP );
+ else
+ G_Damage( self, NULL, NULL, NULL, NULL, self->health, 0, MOD_NOCREEP );
+ return qtrue;
+ }
+ }
+ else
+ self->count = 0;
+
+ return qfalse;
+}
+
+/*
+================
+G_IdlePowerState
+
+Set buildable idle animation to match power state
+================
+*/
+static void G_IdlePowerState( gentity_t *self )
+{
+ if( self->powered )
+ {
+ if( self->s.torsoAnim == BANIM_IDLE3 )
+ G_SetIdleBuildableAnim( self, BG_Buildable( self->s.modelindex, NULL )->idleAnim );
+ }
+ else
+ {
+ if( self->s.torsoAnim != BANIM_IDLE3 )
+ G_SetIdleBuildableAnim( self, BANIM_IDLE3 );
+ }
+}
+
+
+
+
+//==================================================================================
+
+
+
+
+/*
+================
+HSpawn_Disappear
+
+Called when a human spawn is destroyed before it is spawned
+think function
+================
+*/
+void HSpawn_Disappear( gentity_t *self )
+{
+ self->s.eFlags |= EF_NODRAW; //don't draw the model once its destroyed
+ self->timestamp = level.time;
+ G_QueueBuildPoints( self );
+ G_RewardAttackers( self );
+
+ G_FreeEntity( self );
+}
+
+
+/*
+================
+HSpawn_blast
+
+Called when a human spawn explodes
+think function
+================
+*/
+void HSpawn_Blast( gentity_t *self )
+{
+ vec3_t dir;
+
+ // we don't have a valid direction, so just point straight up
+ dir[ 0 ] = dir[ 1 ] = 0;
+ dir[ 2 ] = 1;
+
+ self->timestamp = level.time;
+
+ //do some radius damage
+ G_RadiusDamage( self->s.pos.trBase, g_entities + self->killedBy, self->splashDamage,
+ self->splashRadius, self, self->splashMethodOfDeath );
+
+ // begin freeing build points
+ G_QueueBuildPoints( self );
+ G_RewardAttackers( self );
+ // turn into an explosion
+ self->s.eType = ET_EVENTS + EV_HUMAN_BUILDABLE_EXPLOSION;
+ self->freeAfterEvent = qtrue;
+ G_AddEvent( self, EV_HUMAN_BUILDABLE_EXPLOSION, DirToByte( dir ) );
+}
+
+
+/*
+================
+HSpawn_die
+
+Called when a human spawn dies
+================
+*/
+void HSpawn_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod )
+{
+ G_SetBuildableAnim( self, BANIM_DESTROY1, qtrue );
+ G_SetIdleBuildableAnim( self, BANIM_DESTROYED );
+
+ self->die = nullDieFunction;
+ self->killedBy = attacker - g_entities;
+ self->powered = qfalse; //free up power
+ self->s.eFlags &= ~EF_FIRING; //prevent any firing effects
+
+ if( self->spawned )
+ {
+ self->think = HSpawn_Blast;
+ self->nextthink = level.time + HUMAN_DETONATION_DELAY;
+ }
+ else
+ {
+ self->think = HSpawn_Disappear;
+ self->nextthink = level.time; //blast immediately
+ }
+
+ G_LogDestruction( self, attacker, mod );
+}
+
+/*
+================
+HSpawn_Think
+
+Think for human spawn
+================
+*/
+void HSpawn_Think( gentity_t *self )
+{
+ gentity_t *ent;
+
+ // set parentNode
+ self->powered = G_FindPower( self, qfalse );
+
+ if( G_SuicideIfNoPower( self ) )
+ return;
+
+ if( self->spawned )
+ {
+ //only suicide if at rest
+ if( self->s.groundEntityNum )
+ {
+ if( ( ent = G_CheckSpawnPoint( self->s.number, self->s.origin,
+ self->s.origin2, BA_H_SPAWN, NULL ) ) != NULL )
+ {
+ // If the thing blocking the spawn is a buildable, kill it.
+ // If it's part of the map, kill self.
+ if( ent->s.eType == ET_BUILDABLE )
+ {
+ G_Damage( ent, NULL, NULL, NULL, NULL, self->health, 0, MOD_SUICIDE );
+ G_SetBuildableAnim( self, BANIM_SPAWN1, qtrue );
+ }
+ else if( ent->s.number == ENTITYNUM_WORLD || ent->s.eType == ET_MOVER )
+ {
+ G_Damage( self, NULL, NULL, NULL, NULL, self->health, 0, MOD_SUICIDE );
+ return;
+ }
+
+ if( ent->s.eType == ET_CORPSE )
+ G_FreeEntity( ent ); //quietly remove
+ }
+ }
+ }
+
+ self->nextthink = level.time + BG_Buildable( self->s.modelindex, NULL )->nextthink;
+}
+
+
+
+
+//==================================================================================
+
+
+
+
+/*
+================
+HRepeater_Die
+
+Called when a repeater dies
+================
+*/
+static void HRepeater_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod )
+{
+ G_SetBuildableAnim( self, BANIM_DESTROY1, qtrue );
+ G_SetIdleBuildableAnim( self, BANIM_DESTROYED );
+
+ self->die = nullDieFunction;
+ self->killedBy = attacker - g_entities;
+ self->powered = qfalse; //free up power
+ self->s.eFlags &= ~EF_FIRING; //prevent any firing effects
+
+ if( self->spawned )
+ {
+ self->think = HSpawn_Blast;
+ self->nextthink = level.time + HUMAN_DETONATION_DELAY;
+ }
+ else
+ {
+ self->think = HSpawn_Disappear;
+ self->nextthink = level.time; //blast immediately
+ }
+
+ G_LogDestruction( self, attacker, mod );
+
+ if( self->usesBuildPointZone )
+ {
+ buildPointZone_t *zone = &level.buildPointZones[self->buildPointZone];
+
+ zone->active = qfalse;
+ self->usesBuildPointZone = qfalse;
+ }
+}
+
+/*
+================
+HRepeater_Think
+
+Think for human power repeater
+================
+*/
+void HRepeater_Think( gentity_t *self )
+{
+ int i;
+ gentity_t *powerEnt;
+ buildPointZone_t *zone;
+
+ self->powered = G_FindPower( self, qfalse );
+
+ powerEnt = G_InPowerZone( self );
+ if( powerEnt != NULL )
+ {
+ // If the repeater is inside of another power zone then suicide
+ // Attribute death to whoever built the reactor if that's a human,
+ // which will ensure that it does not queue the BP
+ if( powerEnt->builtBy >= 0 )
+ G_Damage( self, NULL, g_entities + powerEnt->builtBy, NULL, NULL, self->health, 0, MOD_SUICIDE );
+ else
+ G_Damage( self, NULL, NULL, NULL, NULL, self->health, 0, MOD_SUICIDE );
+ return;
+ }
+
+ G_IdlePowerState( self );
+
+ // Initialise the zone once the repeater has spawned
+ if( self->spawned && ( !self->usesBuildPointZone || !level.buildPointZones[ self->buildPointZone ].active ) )
+ {
+ // See if a free zone exists
+ for( i = 0; i < g_humanRepeaterMaxZones.integer; i++ )
+ {
+ zone = &level.buildPointZones[ i ];
+
+ if( !zone->active )
+ {
+ // Initialise the BP queue with no BP queued
+ zone->queuedBuildPoints = 0;
+ zone->totalBuildPoints = g_humanRepeaterBuildPoints.integer;
+ zone->nextQueueTime = level.time;
+ zone->active = qtrue;
+
+ self->buildPointZone = zone - level.buildPointZones;
+ self->usesBuildPointZone = qtrue;
+
+ break;
+ }
+ }
+ }
+
+ self->nextthink = level.time + POWER_REFRESH_TIME;
+}
+
+/*
+================
+HRepeater_Use
+
+Use for human power repeater
+================
+*/
+void HRepeater_Use( gentity_t *self, gentity_t *other, gentity_t *activator )
+{
+ if( self->health <= 0 || !self->spawned )
+ return;
+
+ if( other && other->client )
+ G_GiveClientMaxAmmo( other, qtrue );
+}
+
+/*
+================
+HReactor_Think
+
+Think function for Human Reactor
+================
+*/
+void HReactor_Think( gentity_t *self )
+{
+ int entityList[ MAX_GENTITIES ];
+ vec3_t range = { REACTOR_ATTACK_RANGE,
+ REACTOR_ATTACK_RANGE,
+ REACTOR_ATTACK_RANGE };
+ vec3_t dccrange = { REACTOR_ATTACK_DCC_RANGE,
+ REACTOR_ATTACK_DCC_RANGE,
+ REACTOR_ATTACK_DCC_RANGE };
+ vec3_t mins, maxs;
+ int i, num;
+ gentity_t *enemy, *tent;
+
+ if( self->dcc )
+ {
+ VectorAdd( self->s.origin, dccrange, maxs );
+ VectorSubtract( self->s.origin, dccrange, mins );
+ }
+ else
+ {
+ VectorAdd( self->s.origin, range, maxs );
+ VectorSubtract( self->s.origin, range, mins );
+ }
+
+ if( self->spawned && ( self->health > 0 ) )
+ {
+ qboolean fired = qfalse;
+
+ // Creates a tesla trail for every target
+ num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
+ for( i = 0; i < num; i++ )
+ {
+ enemy = &g_entities[ entityList[ i ] ];
+ if( !enemy->client ||
+ enemy->client->ps.stats[ STAT_TEAM ] != TEAM_ALIENS )
+ continue;
+ if( enemy->flags & FL_NOTARGET )
+ continue;
+
+ tent = G_TempEntity( enemy->s.pos.trBase, EV_TESLATRAIL );
+ tent->s.generic1 = self->s.number; //src
+ tent->s.clientNum = enemy->s.number; //dest
+ VectorCopy( self->s.pos.trBase, tent->s.origin2 );
+ fired = qtrue;
+ }
+
+ // Actual damage is done by radius
+ if( fired )
+ {
+ self->timestamp = level.time;
+ if( self->dcc )
+ G_SelectiveRadiusDamage( self->s.pos.trBase, self,
+ REACTOR_ATTACK_DCC_DAMAGE,
+ REACTOR_ATTACK_DCC_RANGE, self,
+ MOD_REACTOR, TEAM_HUMANS );
+ else
+ G_SelectiveRadiusDamage( self->s.pos.trBase, self,
+ REACTOR_ATTACK_DAMAGE,
+ REACTOR_ATTACK_RANGE, self,
+ MOD_REACTOR, TEAM_HUMANS );
+ }
+ }
+
+ if( self->dcc )
+ self->nextthink = level.time + REACTOR_ATTACK_DCC_REPEAT;
+ else
+ self->nextthink = level.time + REACTOR_ATTACK_REPEAT;
+}
+
+//==================================================================================
+
+
+
+/*
+================
+HArmoury_Activate
+
+Called when a human activates an Armoury
+================
+*/
+void HArmoury_Activate( gentity_t *self, gentity_t *other, gentity_t *activator )
+{
+ if( self->spawned )
+ {
+ //only humans can activate this
+ if( activator->client->ps.stats[ STAT_TEAM ] != TEAM_HUMANS )
+ return;
+
+ //if this is powered then call the armoury menu
+ if( self->powered )
+ G_TriggerMenu( activator->client->ps.clientNum, MN_H_ARMOURY );
+ else
+ G_TriggerMenu( activator->client->ps.clientNum, MN_H_NOTPOWERED );
+ }
+}
+
+/*
+================
+HArmoury_Think
+
+Think for armoury
+================
+*/
+void HArmoury_Think( gentity_t *self )
+{
+ //make sure we have power
+ self->nextthink = level.time + POWER_REFRESH_TIME;
+
+ self->powered = G_FindPower( self, qfalse );
+
+ G_SuicideIfNoPower( self );
+}
+
+
+
+
+//==================================================================================
+
+
+
+
+
+/*
+================
+HDCC_Think
+
+Think for dcc
+================
+*/
+void HDCC_Think( gentity_t *self )
+{
+ //make sure we have power
+ self->nextthink = level.time + POWER_REFRESH_TIME;
+
+ self->powered = G_FindPower( self, qfalse );
+
+ G_SuicideIfNoPower( self );
+}
+
+
+
+
+//==================================================================================
+
+
+
+
+/*
+================
+HMedistat_Die
+
+Die function for Human Medistation
+================
+*/
+void HMedistat_Die( gentity_t *self, gentity_t *inflictor,
+ gentity_t *attacker, int damage, int mod )
+{
+ //clear target's healing flag
+ if( self->enemy && self->enemy->client )
+ self->enemy->client->ps.stats[ STAT_STATE ] &= ~SS_HEALING_ACTIVE;
+
+ HSpawn_Die( self, inflictor, attacker, damage, mod );
+}
+
+/*
+================
+HMedistat_Think
+
+think function for Human Medistation
+================
+*/
+void HMedistat_Think( gentity_t *self )
+{
+ int entityList[ MAX_GENTITIES ];
+ vec3_t mins, maxs;
+ int i, num;
+ gentity_t *player;
+ qboolean occupied = qfalse;
+
+ self->nextthink = level.time + BG_Buildable( self->s.modelindex, NULL )->nextthink;
+
+ self->powered = G_FindPower( self, qfalse );
+ if( G_SuicideIfNoPower( self ) )
+ return;
+ G_IdlePowerState( self );
+
+ //clear target's healing flag
+ if( self->enemy && self->enemy->client )
+ self->enemy->client->ps.stats[ STAT_STATE ] &= ~SS_HEALING_ACTIVE;
+
+ //make sure we have power
+ if( !self->powered )
+ {
+ if( self->active )
+ {
+ self->active = qfalse;
+ self->enemy = NULL;
+ }
+
+ self->nextthink = level.time + POWER_REFRESH_TIME;
+ return;
+ }
+
+ if( self->spawned )
+ {
+ VectorAdd( self->s.origin, self->r.maxs, maxs );
+ VectorAdd( self->s.origin, self->r.mins, mins );
+
+ mins[ 2 ] += fabs( self->r.mins[ 2 ] ) + self->r.maxs[ 2 ];
+ maxs[ 2 ] += 60; //player height
+
+ //if active use the healing idle
+ if( self->active )
+ G_SetIdleBuildableAnim( self, BANIM_IDLE2 );
+
+ //check if a previous occupier is still here
+ num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
+ for( i = 0; i < num; i++ )
+ {
+ player = &g_entities[ entityList[ i ] ];
+
+ if( player->flags & FL_NOTARGET )
+ continue; // notarget cancels even beneficial effects?
+
+ //remove poison from everyone, not just the healed player
+ if( player->client && player->client->ps.stats[ STAT_STATE ] & SS_POISONED )
+ player->client->ps.stats[ STAT_STATE ] &= ~SS_POISONED;
+
+ if( self->enemy == player && player->client &&
+ ( (player->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) && // DIFF NOTE: remove this change from diffs ASAP
+ player->health < player->client->ps.stats[ STAT_MAX_HEALTH ] &&
+ PM_Live( player->client->ps.pm_type ) ) )
+ {
+ occupied = qtrue;
+ player->client->ps.stats[ STAT_STATE ] |= SS_HEALING_ACTIVE;
+ }
+ }
+
+ if( !occupied )
+ {
+ self->enemy = NULL;
+
+ //look for something to heal
+ for( i = 0; i < num; i++ )
+ {
+ player = &g_entities[ entityList[ i ] ];
+
+ if( player->flags & FL_NOTARGET )
+ continue; // notarget cancels even beneficial effects?
+
+ if( player->client && player->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS )
+ {
+ if( ( player->health < player->client->ps.stats[ STAT_MAX_HEALTH ] ||
+ player->client->ps.stats[ STAT_STAMINA ] < STAMINA_MAX ) &&
+ PM_Live( player->client->ps.pm_type ) )
+ {
+ self->enemy = player;
+
+ //start the heal anim
+ if( !self->active )
+ {
+ G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse );
+ self->active = qtrue;
+ player->client->ps.stats[ STAT_STATE ] |= SS_HEALING_ACTIVE;
+ }
+ }
+ else if( !BG_InventoryContainsUpgrade( UP_MEDKIT, player->client->ps.stats ) )
+ BG_AddUpgradeToInventory( UP_MEDKIT, player->client->ps.stats );
+ }
+ }
+ }
+
+ //nothing left to heal so go back to idling
+ if( !self->enemy && self->active )
+ {
+ G_SetBuildableAnim( self, BANIM_CONSTRUCT2, qtrue );
+ G_SetIdleBuildableAnim( self, BANIM_IDLE1 );
+
+ self->active = qfalse;
+ }
+ else if( self->enemy && self->enemy->client ) //heal!
+ {
+ if( self->enemy->client->ps.stats[ STAT_STAMINA ] < STAMINA_MAX )
+ self->enemy->client->ps.stats[ STAT_STAMINA ] += STAMINA_MEDISTAT_RESTORE;
+
+ if( self->enemy->client->ps.stats[ STAT_STAMINA ] > STAMINA_MAX )
+ self->enemy->client->ps.stats[ STAT_STAMINA ] = STAMINA_MAX;
+
+ self->enemy->health++;
+
+ //if they're completely healed, give them a medkit
+ if( self->enemy->health >= self->enemy->client->ps.stats[ STAT_MAX_HEALTH ] )
+ {
+ self->enemy->health = self->enemy->client->ps.stats[ STAT_MAX_HEALTH ];
+ if( !BG_InventoryContainsUpgrade( UP_MEDKIT, self->enemy->client->ps.stats ) )
+ BG_AddUpgradeToInventory( UP_MEDKIT, self->enemy->client->ps.stats );
+ }
+ }
+ }
+}
+
+
+
+
+//==================================================================================
+
+
+
+
+/*
+================
+HMGTurret_CheckTarget
+
+Used by HMGTurret_Think to check enemies for validity
+================
+*/
+qboolean HMGTurret_CheckTarget( gentity_t *self, gentity_t *target,
+ qboolean los_check )
+{
+ trace_t tr;
+ vec3_t dir, end;
+
+ if( !target || target->health <= 0 || !target->client ||
+ target->client->pers.teamSelection != TEAM_ALIENS )
+ return qfalse;
+
+ if( target->flags & FL_NOTARGET )
+ return qfalse;
+
+ if( !los_check )
+ return qtrue;
+
+ // Accept target if we can line-trace to it
+ VectorSubtract( target->s.pos.trBase, self->s.pos.trBase, dir );
+ VectorNormalize( dir );
+ VectorMA( self->s.pos.trBase, MGTURRET_RANGE, dir, end );
+ trap_Trace( &tr, self->s.pos.trBase, NULL, NULL, end,
+ self->s.number, MASK_SHOT );
+ return tr.entityNum == target - g_entities;
+}
+
+
+/*
+================
+HMGTurret_TrackEnemy
+
+Used by HMGTurret_Think to track enemy location
+================
+*/
+qboolean HMGTurret_TrackEnemy( gentity_t *self )
+{
+ vec3_t dirToTarget, dttAdjusted, angleToTarget, angularDiff, xNormal;
+ vec3_t refNormal = { 0.0f, 0.0f, 1.0f };
+ float temp, rotAngle;
+
+ VectorSubtract( self->enemy->s.pos.trBase, self->s.pos.trBase, dirToTarget );
+ VectorNormalize( dirToTarget );
+
+ CrossProduct( self->s.origin2, refNormal, xNormal );
+ VectorNormalize( xNormal );
+ rotAngle = RAD2DEG( acos( DotProduct( self->s.origin2, refNormal ) ) );
+ RotatePointAroundVector( dttAdjusted, xNormal, dirToTarget, rotAngle );
+
+ vectoangles( dttAdjusted, angleToTarget );
+
+ angularDiff[ PITCH ] = AngleSubtract( self->s.angles2[ PITCH ], angleToTarget[ PITCH ] );
+ angularDiff[ YAW ] = AngleSubtract( self->s.angles2[ YAW ], angleToTarget[ YAW ] );
+
+ //if not pointing at our target then move accordingly
+ if( angularDiff[ PITCH ] < 0 && angularDiff[ PITCH ] < (-MGTURRET_ANGULARSPEED) )
+ self->s.angles2[ PITCH ] += MGTURRET_ANGULARSPEED;
+ else if( angularDiff[ PITCH ] > 0 && angularDiff[ PITCH ] > MGTURRET_ANGULARSPEED )
+ self->s.angles2[ PITCH ] -= MGTURRET_ANGULARSPEED;
+ else
+ self->s.angles2[ PITCH ] = angleToTarget[ PITCH ];
+
+ //disallow vertical movement past a certain limit
+ temp = fabs( self->s.angles2[ PITCH ] );
+ if( temp > 180 )
+ temp -= 360;
+
+ if( temp < -MGTURRET_VERTICALCAP )
+ self->s.angles2[ PITCH ] = (-360) + MGTURRET_VERTICALCAP;
+
+ //if not pointing at our target then move accordingly
+ if( angularDiff[ YAW ] < 0 && angularDiff[ YAW ] < ( -MGTURRET_ANGULARSPEED ) )
+ self->s.angles2[ YAW ] += MGTURRET_ANGULARSPEED;
+ else if( angularDiff[ YAW ] > 0 && angularDiff[ YAW ] > MGTURRET_ANGULARSPEED )
+ self->s.angles2[ YAW ] -= MGTURRET_ANGULARSPEED;
+ else
+ self->s.angles2[ YAW ] = angleToTarget[ YAW ];
+
+ AngleVectors( self->s.angles2, dttAdjusted, NULL, NULL );
+ RotatePointAroundVector( dirToTarget, xNormal, dttAdjusted, -rotAngle );
+ vectoangles( dirToTarget, self->turretAim );
+
+ //fire if target is within accuracy
+ return ( abs( angularDiff[ YAW ] ) - MGTURRET_ANGULARSPEED <=
+ MGTURRET_ACCURACY_TO_FIRE ) &&
+ ( abs( angularDiff[ PITCH ] ) - MGTURRET_ANGULARSPEED <=
+ MGTURRET_ACCURACY_TO_FIRE );
+}
+
+
+/*
+================
+HMGTurret_FindEnemy
+
+Used by HMGTurret_Think to locate enemy gentities
+================
+*/
+void HMGTurret_FindEnemy( gentity_t *self )
+{
+ int entityList[ MAX_GENTITIES ];
+ vec3_t range;
+ vec3_t mins, maxs;
+ int i, num;
+ gentity_t *target;
+ int start;
+
+ if( self->enemy )
+ self->enemy->targeted = NULL;
+
+ self->enemy = NULL;
+
+ // Look for targets in a box around the turret
+ VectorSet( range, MGTURRET_RANGE, MGTURRET_RANGE, MGTURRET_RANGE );
+ VectorAdd( self->s.origin, range, maxs );
+ VectorSubtract( self->s.origin, range, mins );
+ num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
+
+ if( num == 0 )
+ return;
+
+ start = rand( ) / ( RAND_MAX / num + 1 );
+ for( i = start; i < num + start ; i++ )
+ {
+ target = &g_entities[ entityList[ i % num ] ];
+ if( !HMGTurret_CheckTarget( self, target, qtrue ) )
+ continue;
+
+ self->enemy = target;
+ self->enemy->targeted = self;
+ return;
+ }
+}
+
+/*
+================
+HMGTurret_State
+
+Raise or lower MG turret towards desired state
+================
+*/
+enum {
+ MGT_STATE_INACTIVE,
+ MGT_STATE_DROP,
+ MGT_STATE_RISE,
+ MGT_STATE_ACTIVE
+};
+
+static qboolean HMGTurret_State( gentity_t *self, int state )
+{
+ float angle;
+
+ if( self->waterlevel == state )
+ return qfalse;
+
+ angle = AngleNormalize180( self->s.angles2[ PITCH ] );
+
+ if( state == MGT_STATE_INACTIVE )
+ {
+ if( angle < MGTURRET_VERTICALCAP )
+ {
+ if( self->waterlevel != MGT_STATE_DROP )
+ {
+ self->speed = 0.25f;
+ self->waterlevel = MGT_STATE_DROP;
+ }
+ else
+ self->speed *= 1.25f;
+
+ self->s.angles2[ PITCH ] =
+ MIN( MGTURRET_VERTICALCAP, angle + self->speed );
+ return qtrue;
+ }
+ else
+ self->waterlevel = MGT_STATE_INACTIVE;
+ }
+ else if( state == MGT_STATE_ACTIVE )
+ {
+ if( !self->enemy && angle > 0.0f )
+ {
+ self->waterlevel = MGT_STATE_RISE;
+ self->s.angles2[ PITCH ] =
+ MAX( 0.0f, angle - MGTURRET_ANGULARSPEED * 0.5f );
+ }
+ else
+ self->waterlevel = MGT_STATE_ACTIVE;
+ }
+
+ return qfalse;
+}
+
+/*
+================
+HMGTurret_Think
+
+Think function for MG turret
+================
+*/
+void HMGTurret_Think( gentity_t *self )
+{
+ self->nextthink = level.time +
+ BG_Buildable( self->s.modelindex, NULL )->nextthink;
+
+ // Turn off client side muzzle flashes
+ self->s.eFlags &= ~EF_FIRING;
+
+ self->powered = G_FindPower( self, qfalse );
+ if( G_SuicideIfNoPower( self ) )
+ return;
+ G_IdlePowerState( self );
+
+ // If not powered or spawned don't do anything
+ if( !self->powered )
+ {
+ // if power loss drop turret
+ if( self->spawned &&
+ HMGTurret_State( self, MGT_STATE_INACTIVE ) )
+ return;
+
+ self->nextthink = level.time + POWER_REFRESH_TIME;
+ return;
+ }
+ if( !self->spawned )
+ return;
+
+ // If the current target is not valid find a new enemy
+ if( !HMGTurret_CheckTarget( self, self->enemy, qtrue ) )
+ {
+ self->active = qfalse;
+ self->turretSpinupTime = -1;
+ HMGTurret_FindEnemy( self );
+ }
+ // if newly powered raise turret
+ HMGTurret_State( self, MGT_STATE_ACTIVE );
+ if( !self->enemy )
+ return;
+
+ // Track until we can hit the target
+ if( !HMGTurret_TrackEnemy( self ) )
+ {
+ self->active = qfalse;
+ self->turretSpinupTime = -1;
+ return;
+ }
+
+ // Update spin state
+ if( !self->active && self->timestamp < level.time )
+ {
+ self->active = qtrue;
+
+ self->turretSpinupTime = level.time + MGTURRET_SPINUP_TIME;
+ G_AddEvent( self, EV_MGTURRET_SPINUP, 0 );
+ }
+
+ // Not firing or haven't spun up yet
+ if( !self->active || self->turretSpinupTime > level.time )
+ return;
+
+ // Fire repeat delay
+ if( self->timestamp > level.time )
+ return;
+
+ FireWeapon( self );
+ self->s.eFlags |= EF_FIRING;
+ self->timestamp = level.time + BG_Buildable( self->s.modelindex, NULL )->turretFireSpeed;
+ G_AddEvent( self, EV_FIRE_WEAPON, 0 );
+ G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse );
+}
+
+
+
+
+//==================================================================================
+
+
+
+
+/*
+================
+HTeslaGen_Think
+
+Think function for Tesla Generator
+================
+*/
+void HTeslaGen_Think( gentity_t *self )
+{
+ self->nextthink = level.time + BG_Buildable( self->s.modelindex, NULL )->nextthink;
+
+ self->powered = G_FindPower( self, qfalse );
+ if( G_SuicideIfNoPower( self ) )
+ return;
+ G_IdlePowerState( self );
+
+ //if not powered don't do anything and check again for power next think
+ if( !self->powered )
+ {
+ self->s.eFlags &= ~EF_FIRING;
+ self->nextthink = level.time + POWER_REFRESH_TIME;
+ return;
+ }
+
+ if( self->spawned && self->timestamp < level.time )
+ {
+ vec3_t origin, range, mins, maxs;
+ int entityList[ MAX_GENTITIES ], i, num;
+
+ // Communicates firing state to client
+ self->s.eFlags &= ~EF_FIRING;
+
+ // Move the muzzle from the entity origin up a bit to fire over turrets
+ VectorMA( self->s.origin, self->r.maxs[ 2 ], self->s.origin2, origin );
+
+ VectorSet( range, TESLAGEN_RANGE, TESLAGEN_RANGE, TESLAGEN_RANGE );
+ VectorAdd( origin, range, maxs );
+ VectorSubtract( origin, range, mins );
+
+ // Attack nearby Aliens
+ num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
+ for( i = 0; i < num; i++ )
+ {
+ self->enemy = &g_entities[ entityList[ i ] ];
+
+ if( self->enemy->flags & FL_NOTARGET )
+ continue;
+
+ if( self->enemy->client && self->enemy->health > 0 &&
+ self->enemy->client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS &&
+ Distance( origin, self->enemy->s.pos.trBase ) <= TESLAGEN_RANGE )
+ FireWeapon( self );
+ }
+ self->enemy = NULL;
+
+ if( self->s.eFlags & EF_FIRING )
+ {
+ G_AddEvent( self, EV_FIRE_WEAPON, 0 );
+
+ //doesn't really need an anim
+ //G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse );
+
+ self->timestamp = level.time + TESLAGEN_REPEAT;
+ }
+ }
+}
+
+//==================================================================================
+// CUBOID FUNCTIONS
+// generic functions for all types of cuboids
+//==================================================================================
+
+void Cuboid_Think(gentity_t *self)
+{
+}
+
+//Cuboids need a new die function because of the cuboid explosion effects.
+void Cuboid_Die(gentity_t *self,gentity_t *inflictor,gentity_t *attacker,int damage,int mod)
+{
+ vec3_t dir;
+ qboolean event=qfalse;
+ const cuboidAttributes_t *cuboid;
+
+ cuboid=BG_CuboidAttributes(self->s.modelindex);
+ G_SetBuildableAnim(self,BANIM_DESTROY1,qtrue); // just for sound
+ self->die=nullDieFunction;
+ self->killedBy=attacker-g_entities;
+ self->powered=qfalse;
+ self->s.eFlags&=~EF_FIRING;
+ G_LogDestruction(self,attacker,mod);
+ dir[0]=dir[1]=0;
+ dir[2]=1;
+ self->timestamp = level.time;
+ G_QueueBuildPoints(self);
+ if(mod!=MOD_DECONSTRUCT&&self->spawned)
+ {
+ G_RewardAttackers(self);
+ G_RadiusDamage(self->s.pos.trBase,g_entities+self->killedBy,self->splashDamage,self->splashRadius,self,self->splashMethodOfDeath);
+ //NOTE: all cuboid info is already packed
+ self->s.eType=ET_EVENTS+EV_CUBOID_EXPLOSION;
+ self->freeAfterEvent = qtrue;
+ G_AddEvent(self,EV_HUMAN_BUILDABLE_EXPLOSION,DirToByte(dir));
+ event=qtrue;
+ self->r.contents=0;
+ trap_LinkEntity(self);
+ }
+ else
+ {
+ self->s.eType=0;
+ G_FreeEntity(self);
+ }
+}
+
+
+//==================================================================================
+
+
+
+
+/*
+============
+G_QueueValue
+============
+*/
+
+static int G_QueueValue( gentity_t *self )
+{
+ int i;
+ int damageTotal = 0;
+ int queuePoints;
+ double queueFraction = 0;
+
+ for( i = 0; i < level.maxclients; i++ )
+ {
+ gentity_t *player = g_entities + i;
+
+ damageTotal += self->credits[ i ];
+
+ if( self->buildableTeam != player->client->pers.teamSelection )
+ queueFraction += (double) self->credits[ i ];
+ }
+
+ if( damageTotal > 0 )
+ queueFraction = queueFraction / (double) damageTotal;
+ else // all damage was done by nonclients, so queue everything
+ queueFraction = 1.0;
+
+ queuePoints = (int) ( queueFraction * (double) BG_Buildable( self->s.modelindex, self->cuboidSize )->buildPoints );
+ return queuePoints;
+}
+
+/*
+============
+G_QueueBuildPoints
+============
+*/
+void G_QueueBuildPoints( gentity_t *self )
+{
+ gentity_t *powerEntity;
+ int queuePoints;
+
+ queuePoints = G_QueueValue( self );
+
+ if( !queuePoints )
+ return;
+
+ switch( self->buildableTeam )
+ {
+ default:
+ case TEAM_NONE:
+ return;
+
+ case TEAM_ALIENS:
+ if( !level.alienBuildPointQueue )
+ level.alienNextQueueTime = level.time + g_alienBuildQueueTime.integer;
+
+ level.alienBuildPointQueue += queuePoints;
+ break;
+
+ case TEAM_HUMANS:
+ powerEntity = G_PowerEntityForEntity( self );
+
+ if( powerEntity )
+ {
+ int nqt;
+ switch( powerEntity->s.modelindex )
+ {
+ case BA_H_REACTOR:
+ nqt = G_NextQueueTime( level.humanBuildPointQueue,
+ g_humanBuildPoints.integer,
+ g_humanBuildQueueTime.integer );
+ if( !level.humanBuildPointQueue ||
+ level.time + nqt < level.humanNextQueueTime )
+ level.humanNextQueueTime = level.time + nqt;
+
+ level.humanBuildPointQueue += queuePoints;
+ break;
+
+ case BA_H_REPEATER:
+ if( powerEntity->usesBuildPointZone &&
+ level.buildPointZones[ powerEntity->buildPointZone ].active )
+ {
+ buildPointZone_t *zone = &level.buildPointZones[ powerEntity->buildPointZone ];
+
+ nqt = G_NextQueueTime( zone->queuedBuildPoints,
+ zone->totalBuildPoints,
+ g_humanRepeaterBuildQueueTime.integer );
+
+ if( !zone->queuedBuildPoints ||
+ level.time + nqt < zone->nextQueueTime )
+ zone->nextQueueTime = level.time + nqt;
+
+ zone->queuedBuildPoints += queuePoints;
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+}
+
+/*
+============
+G_NextQueueTime
+============
+*/
+int G_NextQueueTime( int queuedBP, int totalBP, int queueBaseRate )
+{
+ float fractionQueued;
+
+ if( totalBP == 0 )
+ return 0;
+
+ fractionQueued = queuedBP / (float)totalBP;
+ return ( 1.0f - fractionQueued ) * queueBaseRate;
+}
+
+/*
+============
+G_BuildableTouchTriggers
+
+Find all trigger entities that a buildable touches.
+============
+*/
+void G_BuildableTouchTriggers( gentity_t *ent )
+{
+ int i, num;
+ int touch[ MAX_GENTITIES ];
+ gentity_t *hit;
+ trace_t trace;
+ vec3_t mins, maxs;
+ vec3_t bmins, bmaxs;
+ static vec3_t range = { 10, 10, 10 };
+
+ // dead buildables don't activate triggers!
+ if( ent->health <= 0 )
+ return;
+
+ BG_BuildableBoundingBox( ent->s.modelindex, bmins, bmaxs );
+
+ VectorAdd( ent->s.origin, bmins, mins );
+ VectorAdd( ent->s.origin, bmaxs, maxs );
+
+ VectorSubtract( mins, range, mins );
+ VectorAdd( maxs, range, maxs );
+
+ num = trap_EntitiesInBox( mins, maxs, touch, MAX_GENTITIES );
+
+ VectorAdd( ent->s.origin, bmins, mins );
+ VectorAdd( ent->s.origin, bmaxs, maxs );
+
+ for( i = 0; i < num; i++ )
+ {
+ hit = &g_entities[ touch[ i ] ];
+
+ if( !hit->touch )
+ continue;
+
+ if( !( hit->r.contents & CONTENTS_TRIGGER ) )
+ continue;
+
+ //ignore buildables not yet spawned
+ if( !ent->spawned )
+ continue;
+
+ if( !trap_EntityContact( mins, maxs, hit ) )
+ continue;
+
+ memset( &trace, 0, sizeof( trace ) );
+
+ if( hit->touch )
+ hit->touch( hit, ent, &trace );
+ }
+}
+
+
+/*
+===============
+G_BuildableThink
+
+General think function for buildables
+===============
+*/
+void G_BuildableThink( gentity_t *ent, int msec )
+{
+ int maxHealth = BG_Buildable( ent->s.modelindex, ent->cuboidSize )->health;
+ int regenRate = BG_Buildable( ent->s.modelindex, ent->cuboidSize )->regenRate;
+ int buildTime = BG_Buildable( ent->s.modelindex, ent->cuboidSize )->buildTime;
+ int buildRate;
+
+ //toggle spawned flag for buildables
+ if( !ent->spawned && ( ent->healthLeft<=0 || ent->health==maxHealth ) && !level.pausedTime )
+ {
+ ent->spawned = qtrue;
+ if( ent->s.modelindex == BA_A_OVERMIND )
+ {
+ G_TeamCommand( TEAM_ALIENS, "cp \"The Overmind has awakened!\"" );
+ }
+ }
+
+ // Timer actions
+ ent->time1000 += msec;
+ if( ent->time1000 >= 1000 )
+ {
+ ent->time1000 -= 1000;
+
+ if( !ent->spawned )
+ {
+ buildRate=(int)(ceil((float)maxHealth/(float)buildTime*1e3f));
+ ent->health=MIN(ent->health+buildRate,maxHealth);
+ ent->healthLeft=MAX(ent->healthLeft-buildRate,0);
+ }
+ else if( ent->health > 0 && ent->health < maxHealth )
+ {
+ if( ent->buildableTeam == TEAM_ALIENS && regenRate &&
+ ( ent->lastDamageTime + ALIEN_REGEN_DAMAGE_TIME ) < level.time )
+ {
+ ent->health += regenRate;
+ }
+ else if( ent->buildableTeam == TEAM_HUMANS && ent->dcc &&
+ ( ent->lastDamageTime + HUMAN_REGEN_DAMAGE_TIME ) < level.time )
+ {
+ ent->health += DC_HEALRATE * ent->dcc;
+ }
+ }
+
+ if( ent->health >= maxHealth )
+ {
+ int i;
+ ent->health = maxHealth;
+ for( i = 0; i < MAX_CLIENTS; i++ )
+ ent->credits[ i ] = 0;
+ }
+ }
+
+ if( ent->clientSpawnTime > 0 )
+ ent->clientSpawnTime -= msec;
+
+ if( ent->clientSpawnTime < 0 )
+ ent->clientSpawnTime = 0;
+
+ ent->dcc = ( ent->buildableTeam != TEAM_HUMANS ) ? 0 : G_FindDCC( ent );
+
+ // Set health
+ ent->s.generic1 = MIN(MAX(ent->health,0),999);
+
+ // Set health for cuboids (BG_CuboidPackHealthSafe does nothing if not a cuboid) !@#CUBOID
+ BG_CuboidPackHealthSafe(ent->s.modelindex,&ent->s,ent->health);
+
+ // Set flags
+ ent->s.eFlags &= ~( EF_B_POWERED | EF_B_SPAWNED | EF_B_MARKED );
+ if( ent->powered )
+ ent->s.eFlags |= EF_B_POWERED;
+
+ if( ent->spawned )
+ ent->s.eFlags |= EF_B_SPAWNED;
+
+ if( ent->deconstruct )
+ ent->s.eFlags |= EF_B_MARKED;
+
+ // Check if this buildable is touching any triggers
+ G_BuildableTouchTriggers( ent );
+
+ // Fall back on normal physics routines
+ G_Physics( ent, msec );
+}
+
+
+/*
+===============
+G_BuildableRange
+
+Check whether a point is within some range of a type of buildable
+===============
+*/
+qboolean G_BuildableRange( vec3_t origin, float r, buildable_t buildable )
+{
+ int entityList[ MAX_GENTITIES ];
+ vec3_t range;
+ vec3_t mins, maxs;
+ int i, num;
+ gentity_t *ent;
+
+ VectorSet( range, r, r, r );
+ VectorAdd( origin, range, maxs );
+ VectorSubtract( origin, range, mins );
+
+ num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES );
+ for( i = 0; i < num; i++ )
+ {
+ ent = &g_entities[ entityList[ i ] ];
+
+ if( ent->s.eType != ET_BUILDABLE )
+ continue;
+
+ if( ent->buildableTeam == TEAM_HUMANS && !ent->powered )
+ continue;
+
+ if( ent->s.modelindex == buildable && ent->spawned )
+ return qtrue;
+ }
+
+ return qfalse;
+}
+
+/*
+================
+G_FindBuildable
+
+Finds a buildable of the specified type
+================
+*/
+static gentity_t *G_FindBuildable( buildable_t buildable )
+{
+ int i;
+ gentity_t *ent;
+
+ for( i = MAX_CLIENTS, ent = g_entities + i;
+ i < level.num_entities; i++, ent++ )
+ {
+ if( ent->s.eType != ET_BUILDABLE )
+ continue;
+
+ if( ent->s.modelindex == buildable && !( ent->s.eFlags & EF_DEAD ) )
+ return ent;
+ }
+
+ return NULL;
+}
+
+/*
+===============
+G_BuildablesIntersect
+
+Test if two buildables intersect each other
+===============
+*/
+static qboolean G_BuildablesIntersect( buildable_t a, vec3_t originA, vec3_t cuboidA,
+ buildable_t b, vec3_t originB, vec3_t cuboidB )
+{
+ vec3_t minsA, maxsA;
+ vec3_t minsB, maxsB;
+
+ if(BG_Buildable(a,NULL)->cuboid)
+ BG_CuboidBBox(cuboidA,minsA,maxsA);
+ else
+ BG_BuildableBoundingBox( a, minsA, maxsA );
+ VectorAdd( minsA, originA, minsA );
+ VectorAdd( maxsA, originA, maxsA );
+
+ if(BG_Buildable(b,NULL)->cuboid)
+ BG_CuboidBBox(cuboidB,minsB,maxsB);
+ else
+ BG_BuildableBoundingBox( b, minsB, maxsB );
+ VectorAdd( minsB, originB, minsB );
+ VectorAdd( maxsB, originB, maxsB );
+
+ return BoundsIntersect( minsA, maxsA, minsB, maxsB );
+}
+
+/*
+===============
+G_CompareBuildablesForRemoval
+
+qsort comparison function for a buildable removal list
+===============
+*/
+static buildable_t cmpBuildable;
+static vec3_t cmpOrigin;
+static vec3_t cmpCuboid;
+static int G_CompareBuildablesForRemoval( const void *a, const void *b )
+{
+ int precedence[ ] =
+ {
+ BA_NONE,
+
+ BA_A_BARRICADE,
+ BA_A_ACIDTUBE,
+ BA_A_TRAPPER,
+ BA_A_HIVE,
+ BA_A_BOOSTER,
+ BA_A_SPAWN,
+ BA_A_CUBOID1,
+ BA_A_OVERMIND,
+
+ BA_H_MGTURRET,
+ BA_H_TESLAGEN,
+ BA_H_DCC,
+ BA_H_MEDISTAT,
+ BA_H_ARMOURY,
+ BA_H_SPAWN,
+ BA_H_REPEATER,
+ BA_H_CUBOID1,
+ BA_H_CUBOID2,
+ BA_H_REACTOR
+ };
+
+ gentity_t *buildableA, *buildableB;
+ int i;
+ int aPrecedence = 0, bPrecedence = 0;
+ qboolean aMatches = qfalse, bMatches = qfalse;
+
+ buildableA = *(gentity_t **)a;
+ buildableB = *(gentity_t **)b;
+
+ // Prefer the one that collides with the thing we're building
+ aMatches = G_BuildablesIntersect( cmpBuildable, cmpOrigin, cmpCuboid,
+ buildableA->s.modelindex, buildableA->s.origin, buildableA->cuboidSize );
+ bMatches = G_BuildablesIntersect( cmpBuildable, cmpOrigin, cmpCuboid,
+ buildableB->s.modelindex, buildableB->s.origin, buildableB->cuboidSize );
+ if( aMatches && !bMatches )
+ return -1;
+ else if( !aMatches && bMatches )
+ return 1;
+
+ // If the only spawn is marked, prefer it last
+ if( cmpBuildable == BA_A_SPAWN || cmpBuildable == BA_H_SPAWN )
+ {
+ if( ( buildableA->s.modelindex == BA_A_SPAWN && level.numAlienSpawns == 1 ) ||
+ ( buildableA->s.modelindex == BA_H_SPAWN && level.numHumanSpawns == 1 ) )
+ return 1;
+
+ if( ( buildableB->s.modelindex == BA_A_SPAWN && level.numAlienSpawns == 1 ) ||
+ ( buildableB->s.modelindex == BA_H_SPAWN && level.numHumanSpawns == 1 ) )
+ return -1;
+ }
+
+ // If one matches the thing we're building, prefer it
+ aMatches = ( buildableA->s.modelindex == cmpBuildable );
+ bMatches = ( buildableB->s.modelindex == cmpBuildable );
+ if( aMatches && !bMatches )
+ return -1;
+ else if( !aMatches && bMatches )
+ return 1;
+
+ // They're the same type
+ if( buildableA->s.modelindex == buildableB->s.modelindex )
+ {
+ gentity_t *powerEntity = G_PowerEntityForPoint( cmpOrigin );
+
+ // Prefer the entity that is providing power for this point
+ aMatches = ( powerEntity == buildableA );
+ bMatches = ( powerEntity == buildableB );
+ if( aMatches && !bMatches )
+ return -1;
+ else if( !aMatches && bMatches )
+ return 1;
+
+ // Pick the one marked earliest
+ return buildableA->deconstructTime - buildableB->deconstructTime;
+ }
+
+ // Resort to preference list
+ for( i = 0; i < sizeof( precedence ) / sizeof( precedence[ 0 ] ); i++ )
+ {
+ if( buildableA->s.modelindex == precedence[ i ] )
+ aPrecedence = i;
+
+ if( buildableB->s.modelindex == precedence[ i ] )
+ bPrecedence = i;
+ }
+
+ return aPrecedence - bPrecedence;
+}
+
+/*
+===============
+G_ClearDeconMarks
+
+Remove decon mark from all buildables
+===============
+*/
+void G_ClearDeconMarks( void )
+{
+ int i;
+ gentity_t *ent;
+
+ for( i = MAX_CLIENTS, ent = g_entities + i ; i < level.num_entities ; i++, ent++ )
+ {
+ if( !ent->inuse )
+ continue;
+
+ if( ent->s.eType != ET_BUILDABLE )
+ continue;
+
+ ent->deconstruct = qfalse;
+ }
+}
+
+/*
+===============
+G_FreeMarkedBuildables
+
+Free up build points for a team by deconstructing marked buildables
+===============
+*/
+void G_FreeMarkedBuildables( gentity_t *deconner, char *readable, int rsize,
+ char *nums, int nsize )
+{
+ int i;
+ int bNum;
+ int listItems = 0;
+ int totalListItems = 0;
+ gentity_t *ent;
+ int removalCounts[ BA_NUM_BUILDABLES ] = {0};
+
+ if( readable && rsize )
+ readable[ 0 ] = '\0';
+ if( nums && nsize )
+ nums[ 0 ] = '\0';
+
+ if( !g_markDeconstruct.integer )
+ return; // Not enabled, can't deconstruct anything
+
+ for( i = 0; i < level.numBuildablesForRemoval; i++ )
+ {
+ ent = level.markedBuildables[ i ];
+ bNum = BG_Buildable( ent->s.modelindex, NULL )->number;
+
+ if( removalCounts[ bNum ] == 0 )
+ totalListItems++;
+
+ G_Damage( ent, NULL, deconner, NULL, NULL, ent->health, 0, MOD_REPLACE );
+
+ removalCounts[ bNum ]++;
+
+ if( nums )
+ Q_strcat( nums, nsize, va( " %d", ent - g_entities ) );
+
+ G_FreeEntity( ent );
+ }
+
+ if( !readable )
+ return;
+
+ for( i = 0; i < BA_NUM_BUILDABLES; i++ )
+ {
+ if( removalCounts[ i ] )
+ {
+ if( listItems )
+ {
+ if( listItems == ( totalListItems - 1 ) )
+ Q_strcat( readable, rsize, va( "%s and ",
+ ( totalListItems > 2 ) ? "," : "" ) );
+ else
+ Q_strcat( readable, rsize, ", " );
+ }
+ Q_strcat( readable, rsize, va( "%s", BG_Buildable( i, NULL )->humanName ) );
+ if( removalCounts[ i ] > 1 )
+ Q_strcat( readable, rsize, va( " (%dx)", removalCounts[ i ] ) );
+ listItems++;
+ }
+ }
+}
+
+/*
+===============
+G_SufficientBPAvailable
+
+Determine if enough build points can be released for the buildable
+and list the buildables that must be destroyed if this is the case
+===============
+*/
+static itemBuildError_t G_SufficientBPAvailable( buildable_t buildable,
+ vec3_t origin,
+ vec3_t cuboidSize )
+{
+ int i;
+ int numBuildables = 0;
+ int numRequired = 0;
+ int pointsYielded = 0;
+ gentity_t *ent;
+ team_t team = BG_Buildable( buildable, NULL )->team;
+ int buildPoints = BG_Buildable( buildable, cuboidSize )->buildPoints;
+ int remainingBP, remainingSpawns;
+ qboolean collision = qfalse;
+ int collisionCount = 0;
+ qboolean repeaterInRange = qfalse;
+ int repeaterInRangeCount = 0;
+ itemBuildError_t bpError;
+ buildable_t spawn;
+ buildable_t core;
+ int spawnCount = 0;
+ qboolean changed = qtrue;
+
+ level.numBuildablesForRemoval = 0;
+
+ if( team == TEAM_ALIENS )
+ {
+ remainingBP = G_GetBuildPoints( origin, team );
+ remainingSpawns = level.numAlienSpawns;
+ bpError = IBE_NOALIENBP;
+ spawn = BA_A_SPAWN;
+ core = BA_A_OVERMIND;
+ }
+ else if( team == TEAM_HUMANS )
+ {
+ if( buildable == BA_H_REACTOR || buildable == BA_H_REPEATER )
+ remainingBP = level.humanBuildPoints;
+ else
+ remainingBP = G_GetBuildPoints( origin, team );
+
+ remainingSpawns = level.numHumanSpawns;
+ bpError = IBE_NOHUMANBP;
+ spawn = BA_H_SPAWN;
+ core = BA_H_REACTOR;
+ }
+ else
+ {
+ Com_Error( ERR_FATAL, "team is %d\n", team );
+ return IBE_NONE;
+ }
+
+ // Simple non-marking case
+ if( !g_markDeconstruct.integer )
+ {
+ if( remainingBP - buildPoints < 0 )
+ return bpError;
+
+ // Check for buildable<->buildable collisions
+ for( i = MAX_CLIENTS, ent = g_entities + i; i < level.num_entities; i++, ent++ )
+ {
+ if( ent->s.eType != ET_BUILDABLE )
+ continue;
+
+ if( G_BuildablesIntersect( buildable, origin, cuboidSize, ent->s.modelindex, ent->s.origin, ent->cuboidSize ) )
+ return IBE_NOROOM;
+
+ }
+
+ return IBE_NONE;
+ }
+
+ // Set buildPoints to the number extra that are required
+ buildPoints -= remainingBP;
+
+ // Build a list of buildable entities
+ for( i = MAX_CLIENTS, ent = g_entities + i; i < level.num_entities; i++, ent++ )
+ {
+ if( ent->s.eType != ET_BUILDABLE )
+ continue;
+
+ collision = G_BuildablesIntersect( buildable, origin, cuboidSize, ent->s.modelindex, ent->s.origin, ent->cuboidSize );
+
+ if( collision )
+ {
+ // Don't allow replacements at all
+ if( g_markDeconstruct.integer == 1 )
+ return IBE_NOROOM;
+
+ // Only allow replacements of the same type
+ if( g_markDeconstruct.integer == 2 && ent->s.modelindex != buildable )
+ return IBE_NOROOM;
+
+ // Any other setting means anything goes
+
+ collisionCount++;
+ }
+
+ // Check if this is a repeater and it's in range
+ if( buildable == BA_H_REPEATER &&
+ buildable == ent->s.modelindex &&
+ Distance( ent->s.origin, origin ) < REPEATER_BASESIZE )
+ {
+ repeaterInRange = qtrue;
+ repeaterInRangeCount++;
+ }
+ else
+ repeaterInRange = qfalse;
+
+ // Don't allow marked buildables to be replaced in another zone,
+ // unless the marked buildable isn't in a zone (and thus unpowered)
+ if( team == TEAM_HUMANS &&
+ buildable != BA_H_REACTOR &&
+ buildable != BA_H_REPEATER &&
+ ent->parentNode != G_PowerEntityForPoint( origin ) )
+ continue;
+
+ if( !ent->inuse )
+ continue;
+
+ if( ent->health <= 0 )
+ continue;
+
+ if( ent->buildableTeam != team )
+ continue;
+
+ // Explicitly disallow replacement of the core buildable with anything
+ // other than the core buildable
+ if( ent->s.modelindex == core && buildable != core )
+ continue;
+
+ // Don't allow a power source to be replaced by a dependant
+ if( team == TEAM_HUMANS &&
+ G_PowerEntityForPoint( origin ) == ent &&
+ buildable != BA_H_REPEATER &&
+ buildable != core )
+ continue;
+
+ // Don't include unpowered buildables
+ if( !collision && !ent->powered )
+ continue;
+
+ if( ent->deconstruct )
+ {
+ level.markedBuildables[ numBuildables++ ] = ent;
+
+ // Buildables that are marked here will always end up at the front of the
+ // removal list, so just incrementing numBuildablesForRemoval is sufficient
+ if( collision || repeaterInRange )
+ {
+ // Collided with something, so we definitely have to remove it or
+ // it's a repeater that intersects the new repeater's power area,
+ // so it must be removed
+
+ if( collision )
+ collisionCount--;
+
+ if( repeaterInRange )
+ repeaterInRangeCount--;
+
+ if( ent->powered )
+ pointsYielded += BG_Buildable( ent->s.modelindex, ent->cuboidSize )->buildPoints;
+ level.numBuildablesForRemoval++;
+ }
+ else if( BG_Buildable( ent->s.modelindex, NULL )->uniqueTest &&
+ ent->s.modelindex == buildable )
+ {
+ // If it's a unique buildable, it must be replaced by the same type
+ if( ent->powered )
+ pointsYielded += BG_Buildable( ent->s.modelindex, ent->cuboidSize )->buildPoints;
+ level.numBuildablesForRemoval++;
+ }
+ }
+ }
+
+ numRequired = level.numBuildablesForRemoval;
+
+ // We still need build points, but have no candidates for removal
+ if( buildPoints > 0 && numBuildables == 0 )
+ return bpError;
+
+ // Collided with something we can't remove
+ if( collisionCount > 0 )
+ return IBE_NOROOM;
+
+ // There are one or more repeaters we can't remove
+ if( repeaterInRangeCount > 0 )
+ return IBE_RPTPOWERHERE;
+
+ // Sort the list
+ cmpBuildable = buildable;
+ VectorCopy( origin, cmpOrigin );
+ VectorCopy( cuboidSize, cmpCuboid );
+ qsort( level.markedBuildables, numBuildables, sizeof( level.markedBuildables[ 0 ] ),
+ G_CompareBuildablesForRemoval );
+
+ // Determine if there are enough markees to yield the required BP
+ for( ; pointsYielded < buildPoints && level.numBuildablesForRemoval < numBuildables;
+ level.numBuildablesForRemoval++ )
+ {
+ ent = level.markedBuildables[ level.numBuildablesForRemoval ];
+ if( ent->powered )
+ pointsYielded += BG_Buildable( ent->s.modelindex, ent->cuboidSize )->buildPoints;
+ }
+
+ // Do another pass to see if we can meet quota with fewer buildables
+ // than we have now due to mismatches between priority and BP amounts
+ // by repeatedly testing if we can chop off the first thing that isn't
+ // required by rules of collision/uniqueness, which are always at the head
+ while( changed && level.numBuildablesForRemoval > 1 &&
+ level.numBuildablesForRemoval > numRequired )
+ {
+ int pointsUnYielded = 0;
+ changed = qfalse;
+ ent = level.markedBuildables[ numRequired ];
+ if( ent->powered )
+ pointsUnYielded = BG_Buildable( ent->s.modelindex, ent->cuboidSize )->buildPoints;
+
+ if( pointsYielded - pointsUnYielded >= buildPoints )
+ {
+ pointsYielded -= pointsUnYielded;
+ memmove( &level.markedBuildables[ numRequired ],
+ &level.markedBuildables[ numRequired + 1 ],
+ ( level.numBuildablesForRemoval - numRequired )
+ * sizeof( gentity_t * ) );
+ level.numBuildablesForRemoval--;
+ changed = qtrue;
+ }
+ }
+
+ for( i = 0; i < level.numBuildablesForRemoval; i++ )
+ {
+ if( level.markedBuildables[ i ]->s.modelindex == spawn )
+ spawnCount++;
+ }
+
+ // Make sure we're not removing the last spawn
+ if( !g_cheats.integer && remainingSpawns > 0 && ( remainingSpawns - spawnCount ) < 1 )
+ return IBE_LASTSPAWN;
+
+ // Not enough points yielded
+ if( pointsYielded < buildPoints )
+ return bpError;
+ else
+ return IBE_NONE;
+}
+
+/*
+================
+G_SetBuildableLinkState
+
+Links or unlinks all the buildable entities
+================
+*/
+static void G_SetBuildableLinkState( qboolean link )
+{
+ int i;
+ gentity_t *ent;
+
+ for ( i = 1, ent = g_entities + i; i < level.num_entities; i++, ent++ )
+ {
+ if( ent->s.eType != ET_BUILDABLE )
+ continue;
+
+ if( link )
+ trap_LinkEntity( ent );
+ else
+ trap_UnlinkEntity( ent );
+ }
+}
+
+static void G_SetBuildableMarkedLinkState( qboolean link )
+{
+ int i;
+ gentity_t *ent;
+
+ for( i = 0; i < level.numBuildablesForRemoval; i++ )
+ {
+ ent = level.markedBuildables[ i ];
+ if( link )
+ trap_LinkEntity( ent );
+ else
+ trap_UnlinkEntity( ent );
+ }
+}
+
+
+
+/*
+================
+G_CanBuild
+
+Checks to see if a buildable can be built
+================
+*/
+itemBuildError_t G_CanBuild( gentity_t *ent, buildable_t buildable, int distance, vec3_t origin, vec3_t normal, vec3_t cuboidSize )
+{
+ vec3_t angles;
+ vec3_t entity_origin;
+ vec3_t mins, maxs;
+ trace_t tr1, tr2, tr3;
+ itemBuildError_t reason = IBE_NONE, tempReason;
+ gentity_t *tempent;
+ float minNormal;
+ qboolean invert;
+ int contents;
+ playerState_t *ps = &ent->client->ps;
+
+ if( BG_Buildable(buildable,NULL)->cuboid )
+ BG_CuboidBBox(cuboidSize,mins,maxs);
+ else
+ BG_BuildableBoundingBox( buildable, mins, maxs );
+
+ if(!BG_PositionBuildableRelativeToPlayer( ps, BG_Buildable(buildable,NULL)->cuboid, mins, maxs, trap_Trace, entity_origin, angles, &tr1 ))
+ return IBE_NOSURF;
+ trap_Trace( &tr2, entity_origin, mins, maxs, entity_origin, -1, MASK_PLAYERSOLID );
+ trap_Trace( &tr3, ps->origin, NULL, NULL, entity_origin, ent->s.number, MASK_PLAYERSOLID );
+
+ VectorCopy( entity_origin, origin );
+
+ VectorCopy( tr1.plane.normal, normal );
+ minNormal = BG_Buildable( buildable, NULL )->minNormal;
+ invert = BG_Buildable( buildable, NULL )->invertNormal;
+
+ //can we build at this angle?
+ if(!BG_Buildable(buildable,NULL)->cuboid)
+ if( !( normal[ 2 ] >= minNormal || ( invert && normal[ 2 ] <= -minNormal ) ) )
+ reason = IBE_NORMAL;
+
+ //the only buildable one can build on is a cuboid
+ if( tr1.entityNum != ENTITYNUM_WORLD )
+ if( g_entities[ tr1.entityNum ].s.eType == ET_BUILDABLE &&
+ !BG_Buildable( g_entities[ tr1.entityNum ].s.modelindex, NULL )->cuboid )
+ reason = IBE_NORMAL;
+
+ contents = trap_PointContents( entity_origin, -1 );
+
+ if( ( tempReason = G_SufficientBPAvailable( buildable, origin, cuboidSize ) ) != IBE_NONE )
+ reason = tempReason;
+
+ if( ent->client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS )
+ {
+ //alien criteria
+
+ // Check there is an Overmind
+ if( buildable != BA_A_OVERMIND )
+ {
+ if( !G_Overmind( ) )
+ reason = IBE_NOOVERMIND;
+ }
+
+ //check there is creep near by for building on
+ if( BG_Buildable( buildable, NULL )->creepTest )
+ {
+ if( !G_IsCreepHere( entity_origin ) )
+ reason = IBE_NOCREEP;
+ }
+
+ // Check permission to build here
+ if( tr1.surfaceFlags & SURF_NOALIENBUILD || contents & CONTENTS_NOALIENBUILD )
+ reason = IBE_PERMISSION;
+ }
+ else if( ent->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS )
+ {
+ //human criteria
+
+ // Check for power
+ if( G_IsPowered( entity_origin ) == BA_NONE )
+ {
+ //tell player to build a repeater to provide power
+ if( buildable != BA_H_REACTOR && buildable != BA_H_REPEATER )
+ reason = IBE_NOPOWERHERE;
+ }
+
+ //this buildable requires a DCC
+ if( BG_Buildable( buildable, NULL )->dccTest && !G_IsDCCBuilt( ) )
+ reason = IBE_NODCC;
+
+ //check that there is a parent reactor when building a repeater
+ if( buildable == BA_H_REPEATER )
+ {
+ tempent = G_Reactor( );
+
+ if( tempent == NULL ) // No reactor
+ reason = IBE_RPTNOREAC;
+ else if( g_markDeconstruct.integer && G_IsPowered( entity_origin ) == BA_H_REACTOR )
+ reason = IBE_RPTPOWERHERE;
+ else if( !g_markDeconstruct.integer && G_IsPowered( entity_origin ) )
+ reason = IBE_RPTPOWERHERE;
+ }
+
+ // Check permission to build here
+ if( tr1.surfaceFlags & SURF_NOHUMANBUILD || contents & CONTENTS_NOHUMANBUILD )
+ reason = IBE_PERMISSION;
+ }
+
+ // Check permission to build here
+ if( tr1.surfaceFlags & SURF_NOBUILD || contents & CONTENTS_NOBUILD )
+ reason = IBE_PERMISSION;
+
+ // Can we only have one of these?
+ if( BG_Buildable( buildable, NULL )->uniqueTest )
+ {
+ tempent = G_FindBuildable( buildable );
+ if( tempent && !tempent->deconstruct )
+ {
+ switch( buildable )
+ {
+ case BA_A_OVERMIND:
+ reason = IBE_ONEOVERMIND;
+ break;
+
+ case BA_H_REACTOR:
+ reason = IBE_ONEREACTOR;
+ break;
+
+ default:
+ Com_Error( ERR_FATAL, "No reason for denying build of %d\n", buildable );
+ break;
+ }
+ }
+ }
+
+ //check there is enough room to spawn from (presuming this is a spawn)
+ if( reason == IBE_NONE )
+ {
+ G_SetBuildableMarkedLinkState( qfalse );
+ if( G_CheckSpawnPoint( ENTITYNUM_NONE, origin, normal, buildable, NULL ) != NULL )
+ reason = IBE_NORMAL;
+ G_SetBuildableMarkedLinkState( qtrue );
+ }
+
+ //this item does not fit here
+ if( reason == IBE_NONE && ( tr2.startsolid || !COMPARE_FLOAT_EPSILON(tr3.fraction,1.0f) ) )
+ reason = IBE_NOROOM;
+ if( reason != IBE_NONE )
+ level.numBuildablesForRemoval = 0;
+
+ if( g_buildableDensityLimit.integer > 0 )
+ {
+ int i, count, numents, ents[ MAX_GENTITIES ];
+ vec3_t rmins, rmaxs;
+ gentity_t *other;
+
+ for( i = 0; i < 3; i++ )
+ rmins[ i ] = origin[ i ] - g_buildableDensityLimitRange.value,
+ rmaxs[ i ] = origin[ i ] + g_buildableDensityLimitRange.value;
+
+ numents = trap_EntitiesInBox( rmins, rmaxs, ents, MAX_GENTITIES );
+
+ for( count = 0, i = 0; i < numents ; i++ )
+ {
+ other = &g_entities[ ents[ i ] ];
+
+ if( other->s.eType != ET_BUILDABLE )
+ continue;
+
+ if( other->buildableTeam != ent->client->ps.stats[ STAT_TEAM ] )
+ continue;
+
+ if( Distance( origin, other->s.origin ) > g_buildableDensityLimitRange.value )
+ continue;
+
+ if( !trap_InPVSIgnorePortals( origin, other->s.origin ) )
+ continue;
+
+ if( ++count > g_buildableDensityLimit.integer )
+ {
+ reason = IBE_TOODENSE;
+ break;
+ }
+ }
+ }
+
+ return reason;
+}
+
+/*
+================
+G_CuboidName
+
+For standard buildables the function returns humanName.
+In case of cuboids, a "dynamic" one is generated.
+================
+*/
+
+const char *G_CuboidName(buildable_t buildable, const vec3_t cuboidSize, qboolean verbose)
+{
+ static char buffer[100];
+ const buildableAttributes_t *battr;
+
+ if((battr=BG_Buildable(buildable,cuboidSize))->cuboid)
+ {
+ if(verbose)
+ Com_sprintf(buffer,sizeof(buffer),"%s %fx%fx%f %ibp",battr->humanName,cuboidSize[0],cuboidSize[1],cuboidSize[2],battr->buildPoints);
+ else
+ Com_sprintf(buffer,sizeof(buffer),"%s %s (%i bp)",BG_CuboidAttributes(buildable)->icon,battr->humanName,battr->buildPoints);
+ return buffer;
+ }
+ else
+ return battr->humanName;
+}
+
+/*
+================
+G_Build
+
+Spawns a buildable
+================
+*/
+static gentity_t *G_Build( gentity_t *builder, buildable_t buildable,
+ const vec3_t origin, const vec3_t normal, const vec3_t angles,
+ const vec3_t cuboidSize )
+{
+ gentity_t *built;
+ vec3_t localOrigin;
+ char readable[ MAX_STRING_CHARS ];
+ char buildnums[ MAX_STRING_CHARS ];
+ buildLog_t *log;
+ qboolean cuboid;
+
+ cuboid=BG_Buildable(buildable,NULL)->cuboid;
+
+ VectorCopy( origin, localOrigin );
+
+ if( builder->client )
+ log = G_BuildLogNew( builder, BF_CONSTRUCT );
+ else
+ log = NULL;
+
+ // Free existing buildables
+ G_FreeMarkedBuildables( builder, readable, sizeof( readable ),
+ buildnums, sizeof( buildnums ) );
+
+ // Spawn the buildable
+ built = G_Spawn();
+ built->s.eType = ET_BUILDABLE;
+ built->killedBy = ENTITYNUM_NONE;
+ built->classname = BG_Buildable( buildable, NULL )->entityName;
+ built->s.modelindex = buildable;
+ built->buildableTeam = built->s.modelindex2 = BG_Buildable( buildable, NULL )->team;
+
+ // when building the initial layout, spawn the entity slightly off its
+ // target surface so that it can be "dropped" onto it
+ if( !builder->client )
+ VectorMA( localOrigin, 1.0f, normal, localOrigin );
+
+ if(cuboid)
+ {
+ BG_CuboidBBox(cuboidSize,built->r.mins,built->r.maxs);
+ VectorCopy(cuboidSize,&built->cuboidSize);
+ }
+ else
+ BG_BuildableBoundingBox(buildable,built->r.mins,built->r.maxs);
+
+ built->health = 1;
+ built->healthLeft = BG_Buildable( buildable, cuboidSize )->health-1;
+ built->s.generic1 = MIN(MAX(built->health,0),999);
+ built->splashDamage = BG_Buildable( buildable, cuboidSize )->splashDamage;
+ built->splashRadius = BG_Buildable( buildable, cuboidSize )->splashRadius;
+ built->splashMethodOfDeath = BG_Buildable( buildable, cuboidSize )->meansOfDeath;
+
+ built->nextthink = BG_Buildable( buildable, NULL )->nextthink;
+
+ built->takedamage = qtrue;
+ built->spawned = qfalse;
+ built->buildTime = built->s.time = level.time;
+
+ // build instantly in cheat mode
+ if( builder->client && (g_cheats.integer || g_instantBuild.integer) )
+ {
+ built->health = BG_Buildable( buildable, cuboidSize )->health;
+ built->buildTime = built->s.time =
+ level.time -BG_Buildable( buildable, cuboidSize )->buildTime;
+ }
+
+ //things that vary for each buildable that aren't in the dbase
+ switch( buildable )
+ {
+ case BA_A_SPAWN:
+ built->die = AGeneric_Die;
+ built->think = ASpawn_Think;
+ built->pain = AGeneric_Pain;
+ break;
+
+ case BA_A_BARRICADE:
+ built->die = ABarricade_Die;
+ built->think = ABarricade_Think;
+ built->pain = ABarricade_Pain;
+ built->touch = ABarricade_Touch;
+ built->shrunkTime = 0;
+ ABarricade_Shrink( built, qtrue );
+ break;
+
+ case BA_A_BOOSTER:
+ built->die = AGeneric_Die;
+ built->think = AGeneric_Think;
+ built->pain = AGeneric_Pain;
+ built->touch = ABooster_Touch;
+ break;
+
+ case BA_A_ACIDTUBE:
+ built->die = AGeneric_Die;
+ built->think = AAcidTube_Think;
+ built->pain = AGeneric_Pain;
+ break;
+
+ case BA_A_HIVE:
+ built->die = AGeneric_Die;
+ built->think = AHive_Think;
+ built->pain = AHive_Pain;
+ break;
+
+ case BA_A_TRAPPER:
+ built->die = AGeneric_Die;
+ built->think = ATrapper_Think;
+ built->pain = AGeneric_Pain;
+ break;
+
+ case BA_A_OVERMIND:
+ built->die = AGeneric_Die;
+ built->think = AOvermind_Think;
+ built->pain = AGeneric_Pain;
+ break;
+
+ case BA_H_SPAWN:
+ built->die = HSpawn_Die;
+ built->think = HSpawn_Think;
+ break;
+
+ case BA_H_MGTURRET:
+ built->die = HSpawn_Die;
+ built->think = HMGTurret_Think;
+ break;
+
+ case BA_H_TESLAGEN:
+ built->die = HSpawn_Die;
+ built->think = HTeslaGen_Think;
+ break;
+
+ case BA_H_ARMOURY:
+ built->think = HArmoury_Think;
+ built->die = HSpawn_Die;
+ built->use = HArmoury_Activate;
+ break;
+
+ case BA_H_DCC:
+ built->think = HDCC_Think;
+ built->die = HSpawn_Die;
+ break;
+
+ case BA_H_MEDISTAT:
+ built->think = HMedistat_Think;
+ built->die = HMedistat_Die;
+ break;
+
+ case BA_H_REACTOR:
+ built->think = HReactor_Think;
+ built->die = HSpawn_Die;
+ built->use = HRepeater_Use;
+ built->powered = built->active = qtrue;
+ break;
+
+ case BA_H_REPEATER:
+ built->think = HRepeater_Think;
+ built->die = HRepeater_Die;
+ built->use = HRepeater_Use;
+ built->count = -1;
+ break;
+
+ default:
+ //erk
+ break;
+ }
+
+ if(buildable>=CUBOID_FIRST && buildable<=CUBOID_LAST)
+ {
+ built->think=Cuboid_Think;
+ built->die=Cuboid_Die;
+ }
+
+ built->clipmask = MASK_PLAYERSOLID;
+ built->r.contents=CONTENTS_BODY;
+ built->s.number = built - g_entities;
+ built->enemy = NULL;
+ built->s.weapon = BG_Buildable( buildable, NULL )->turretProjType;
+
+ if( builder->client )
+ built->builtBy = builder->client->ps.clientNum;
+ else
+ built->builtBy = -1;
+
+ G_SetOrigin( built, localOrigin );
+
+ // roughly nudge the buildable onto the surface D:< !@#CUBOID
+ VectorScale( normal, -512.0f, built->s.pos.trDelta );
+
+ if(BG_Buildable(buildable, NULL)->cuboid)
+ VectorCopy(cuboidSize,built->s.angles);
+ else
+ {
+ VectorCopy( angles, built->s.angles );
+ built->s.angles[ PITCH ] = 0.0f;
+ }
+
+ built->s.pos.trType = BG_Buildable( buildable, NULL )->traj;
+ built->s.pos.trTime = level.time;
+ built->physicsBounce = BG_Buildable( buildable, NULL )->bounce;
+ built->s.groundEntityNum = -1;
+ //turret angles
+ VectorCopy( builder->s.angles2, built->s.angles2 );
+ built->s.angles2[ YAW ] = angles[ YAW ];
+ built->s.angles2[ PITCH ] = MGTURRET_VERTICALCAP;
+
+ if( BG_Buildable( buildable, NULL )->team == TEAM_ALIENS )
+ {
+ built->powered = qtrue;
+ built->s.eFlags |= EF_B_POWERED;
+ }
+ else if( ( built->powered = G_FindPower( built, qfalse ) ) )
+ built->s.eFlags |= EF_B_POWERED;
+
+ built->s.eFlags &= ~EF_B_SPAWNED;
+
+ VectorCopy( normal, built->s.origin2 );
+
+ G_AddEvent( built, EV_BUILD_CONSTRUCT, 0 );
+
+ G_SetIdleBuildableAnim( built, BG_Buildable( buildable, NULL )->idleAnim );
+
+ if( built->builtBy >= 0 )
+ G_SetBuildableAnim( built, BANIM_CONSTRUCT1, qtrue );
+
+ trap_LinkEntity( built );
+
+ if( builder && builder->client )
+ {
+ G_TeamCommand( builder->client->ps.stats[ STAT_TEAM ],
+ va( "print \"%s ^2built^7 by %s%s%s\n\"",
+ G_CuboidName(built->s.modelindex,cuboidSize,qfalse),
+ builder->client->pers.netname,
+ ( readable[ 0 ] ) ? "^7, ^3replacing^7 " : "",
+ readable ) );
+ G_LogPrintf( "Construct: %d %d %s%s: %s" S_COLOR_WHITE " is building "
+ "%s%s%s\n",
+ builder - g_entities,
+ built - g_entities,
+ BG_Buildable( built->s.modelindex, NULL )->name,
+ buildnums,
+ builder->client->pers.netname,
+ G_CuboidName(built->s.modelindex,built->cuboidSize,qtrue),
+ readable[ 0 ] ? ", replacing " : "",
+ readable );
+ }
+
+ if( log )
+ G_BuildLogSet( log, built );
+
+ BG_CuboidPackHealthSafe(built->s.modelindex,&built->s,built->health);
+
+ return built;
+}
+
+/*
+=================
+G_BuildIfValid
+=================
+*/
+qboolean G_BuildIfValid( gentity_t *ent, buildable_t buildable, vec3_t cuboidSize )
+{
+ float dist;
+ vec3_t origin, normal;
+
+ dist = BG_Class( ent->client->ps.stats[ STAT_CLASS ] )->buildDist;
+
+ switch( G_CanBuild( ent, buildable, dist, origin, normal, cuboidSize ) )
+ {
+ case IBE_NONE:
+ G_Build( ent, buildable, origin, normal, ent->s.apos.trBase, cuboidSize );
+ return qtrue;
+
+ case IBE_NOALIENBP:
+ G_TriggerMenu( ent->client->ps.clientNum, MN_A_NOBP );
+ return qfalse;
+
+ case IBE_NOOVERMIND:
+ G_TriggerMenu( ent->client->ps.clientNum, MN_A_NOOVMND );
+ return qfalse;
+
+ case IBE_NOCREEP:
+ G_TriggerMenu( ent->client->ps.clientNum, MN_A_NOCREEP );
+ return qfalse;
+
+ case IBE_ONEOVERMIND:
+ G_TriggerMenu( ent->client->ps.clientNum, MN_A_ONEOVERMIND );
+ return qfalse;
+
+ case IBE_NORMAL:
+ G_TriggerMenu( ent->client->ps.clientNum, MN_B_NORMAL );
+ return qfalse;
+
+ case IBE_PERMISSION:
+ G_TriggerMenu( ent->client->ps.clientNum, MN_B_NORMAL );
+ return qfalse;
+
+ case IBE_ONEREACTOR:
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_ONEREACTOR );
+ return qfalse;
+
+ case IBE_NOPOWERHERE:
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOPOWERHERE );
+ return qfalse;
+
+ case IBE_NOROOM:
+ G_TriggerMenu( ent->client->ps.clientNum, MN_B_NOROOM );
+ return qfalse;
+
+ case IBE_NOHUMANBP:
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOBP);
+ return qfalse;
+
+ case IBE_NODCC:
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_NODCC );
+ return qfalse;
+
+ case IBE_RPTPOWERHERE:
+ G_TriggerMenu( ent->client->ps.clientNum, MN_H_RPTPOWERHERE );
+ return qfalse;
+
+ case IBE_LASTSPAWN:
+ G_TriggerMenu( ent->client->ps.clientNum, MN_B_LASTSPAWN );
+ return qfalse;
+
+ case IBE_NOSURF:
+ G_TriggerMenu( ent->client->ps.clientNum, MN_B_NOSURF );
+ return qfalse;
+
+ case IBE_TOODENSE:
+ G_TriggerMenu( ent->client->ps.clientNum, MN_B_TOODENSE );
+ return qfalse;
+
+ default:
+ break;
+ }
+
+ return qfalse;
+}
+
+/*
+================
+G_FinishSpawningBuildable
+
+Traces down to find where an item should rest, instead of letting them
+free fall from their spawn points
+================
+*/
+static gentity_t *G_FinishSpawningBuildable( gentity_t *ent, qboolean force )
+{
+ trace_t tr;
+ vec3_t normal, dest;
+ gentity_t *built;
+ buildable_t buildable = ent->s.modelindex;
+
+ if( ent->s.origin2[ 0 ] || ent->s.origin2[ 1 ] || ent->s.origin2[ 2 ] )
+ VectorCopy( ent->s.origin2, normal );
+ else if( BG_Buildable( buildable, NULL )->traj == TR_BUOYANCY )
+ VectorSet( normal, 0.0f, 0.0f, -1.0f );
+ else
+ VectorSet( normal, 0.0f, 0.0f, 1.0f );
+
+ built = G_Build( ent, buildable, ent->s.pos.trBase, normal, ent->s.angles, built->cuboidSize );
+
+ built->takedamage = qtrue;
+ built->spawned = qtrue; //map entities are already spawned
+ built->health = BG_Buildable( buildable, built->cuboidSize )->health;
+ built->s.eFlags |= EF_B_SPAWNED;
+
+ // drop towards normal surface
+ VectorScale( built->s.origin2, -4096.0f, dest );
+ VectorAdd( dest, built->s.origin, dest );
+
+ trap_Trace( &tr, built->s.origin, built->r.mins, built->r.maxs, dest, built->s.number, built->clipmask );
+
+ if( tr.startsolid && !force )
+ {
+ G_Printf( S_COLOR_YELLOW "G_FinishSpawningBuildable: %s startsolid at %s\n",
+ built->classname, vtos( built->s.origin ) );
+ G_FreeEntity( built );
+ return NULL;
+ }
+
+ //point items in the correct direction
+ VectorCopy( tr.plane.normal, built->s.origin2 );
+
+ // allow to ride movers
+ built->s.groundEntityNum = tr.entityNum;
+
+ G_SetOrigin( built, tr.endpos );
+
+ trap_LinkEntity( built );
+ return built;
+}
+
+/*
+============
+G_SpawnBuildableThink
+
+Complete spawning a buildable using it's placeholder
+============
+*/
+static void G_SpawnBuildableThink( gentity_t *ent )
+{
+ G_FinishSpawningBuildable( ent, qfalse );
+ G_FreeEntity( ent );
+}
+
+/*
+============
+G_SpawnBuildable
+
+Sets the clipping size and plants the object on the floor.
+
+Items can't be immediately dropped to floor, because they might
+be on an entity that hasn't spawned yet.
+============
+*/
+void G_SpawnBuildable( gentity_t *ent, buildable_t buildable )
+{
+ ent->s.modelindex = buildable;
+
+ // some movers spawn on the second frame, so delay item
+ // spawns until the third frame so they can ride trains
+ ent->nextthink = level.time + FRAMETIME * 2;
+ ent->think = G_SpawnBuildableThink;
+}
+
+/*
+============
+G_LayoutSave
+============
+*/
+void G_LayoutSave( char *name )
+{
+ char map[ MAX_QPATH ];
+ char fileName[ MAX_OSPATH ];
+ fileHandle_t f;
+ int len;
+ int i;
+ gentity_t *ent;
+ char *s;
+
+ trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) );
+ if( !map[ 0 ] )
+ {
+ G_Printf( "LayoutSave( ): no map is loaded\n" );
+ return;
+ }
+ Com_sprintf( fileName, sizeof( fileName ), "layouts/%s/%s.dat", map, name );
+
+ len = trap_FS_FOpenFile( fileName, &f, FS_WRITE );
+ if( len < 0 )
+ {
+ G_Printf( "layoutsave: could not open %s\n", fileName );
+ return;
+ }
+
+ G_Printf( "layoutsave: saving layout to %s\n", fileName );
+
+ for( i = MAX_CLIENTS; i < level.num_entities; i++ )
+ {
+ ent = &level.gentities[ i ];
+ if( ent->s.eType != ET_BUILDABLE )
+ continue;
+
+ s = va( "%s %f %f %f %f %f %f %f %f %f %f %f %f\n",
+ BG_Buildable( ent->s.modelindex, NULL )->name,
+ ent->s.pos.trBase[ 0 ],
+ ent->s.pos.trBase[ 1 ],
+ ent->s.pos.trBase[ 2 ],
+ ent->s.angles[ 0 ],
+ ent->s.angles[ 1 ],
+ ent->s.angles[ 2 ],
+ ent->s.origin2[ 0 ],
+ ent->s.origin2[ 1 ],
+ ent->s.origin2[ 2 ],
+ ent->s.angles2[ 0 ],
+ ent->s.angles2[ 1 ],
+ ent->s.angles2[ 2 ] );
+ trap_FS_Write( s, strlen( s ), f );
+ }
+ trap_FS_FCloseFile( f );
+}
+
+/*
+============
+G_LayoutList
+============
+*/
+int G_LayoutList( const char *map, char *list, int len )
+{
+ // up to 128 single character layout names could fit in layouts
+ char fileList[ ( MAX_CVAR_VALUE_STRING / 2 ) * 5 ] = {""};
+ char layouts[ MAX_CVAR_VALUE_STRING ] = {""};
+ int numFiles, i, fileLen = 0, listLen;
+ int count = 0;
+ char *filePtr;
+
+ Q_strcat( layouts, sizeof( layouts ), "*BUILTIN* " );
+ numFiles = trap_FS_GetFileList( va( "layouts/%s", map ), ".dat",
+ fileList, sizeof( fileList ) );
+ filePtr = fileList;
+ for( i = 0; i < numFiles; i++, filePtr += fileLen + 1 )
+ {
+ fileLen = strlen( filePtr );
+ listLen = strlen( layouts );
+ if( fileLen < 5 )
+ continue;
+
+ // list is full, stop trying to add to it
+ if( ( listLen + fileLen ) >= sizeof( layouts ) )
+ break;
+
+ Q_strcat( layouts, sizeof( layouts ), filePtr );
+ listLen = strlen( layouts );
+
+ // strip extension and add space delimiter
+ layouts[ listLen - 4 ] = ' ';
+ layouts[ listLen - 3 ] = '\0';
+ count++;
+ }
+ if( count != numFiles )
+ {
+ G_Printf( S_COLOR_YELLOW "WARNING: layout list was truncated to %d "
+ "layouts, but %d layout files exist in layouts/%s/.\n",
+ count, numFiles, map );
+ }
+ Q_strncpyz( list, layouts, len );
+ return count + 1;
+}
+
+/*
+============
+G_LayoutSelect
+
+set level.layout based on g_layouts or g_layoutAuto
+============
+*/
+void G_LayoutSelect( void )
+{
+ char fileName[ MAX_OSPATH ];
+ char layouts[ MAX_CVAR_VALUE_STRING ];
+ char layouts2[ MAX_CVAR_VALUE_STRING ];
+ char *l;
+ char map[ MAX_QPATH ];
+ char *s;
+ int cnt = 0;
+ int layoutNum;
+
+ Q_strncpyz( layouts, g_layouts.string, sizeof( layouts ) );
+ trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) );
+
+ // one time use cvar
+ trap_Cvar_Set( "g_layouts", "" );
+
+ // pick an included layout at random if no list has been provided
+ if( !layouts[ 0 ] && g_layoutAuto.integer )
+ {
+ G_LayoutList( map, layouts, sizeof( layouts ) );
+ }
+
+ if( !layouts[ 0 ] )
+ return;
+
+ Q_strncpyz( layouts2, layouts, sizeof( layouts2 ) );
+ l = &layouts2[ 0 ];
+ layouts[ 0 ] = '\0';
+ while( 1 )
+ {
+ s = COM_ParseExt( &l, qfalse );
+ if( !*s )
+ break;
+
+ if( !Q_stricmp( s, "*BUILTIN*" ) )
+ {
+ Q_strcat( layouts, sizeof( layouts ), s );
+ Q_strcat( layouts, sizeof( layouts ), " " );
+ cnt++;
+ continue;
+ }
+
+ Com_sprintf( fileName, sizeof( fileName ), "layouts/%s/%s.dat", map, s );
+ if( trap_FS_FOpenFile( fileName, NULL, FS_READ ) > 0 )
+ {
+ Q_strcat( layouts, sizeof( layouts ), s );
+ Q_strcat( layouts, sizeof( layouts ), " " );
+ cnt++;
+ }
+ else
+ G_Printf( S_COLOR_YELLOW "WARNING: layout \"%s\" does not exist\n", s );
+ }
+ if( !cnt )
+ {
+ G_Printf( S_COLOR_RED "ERROR: none of the specified layouts could be "
+ "found, using map default\n" );
+ return;
+ }
+ layoutNum = rand( ) / ( RAND_MAX / cnt + 1 ) + 1;
+ cnt = 0;
+
+ Q_strncpyz( layouts2, layouts, sizeof( layouts2 ) );
+ l = &layouts2[ 0 ];
+ while( 1 )
+ {
+ s = COM_ParseExt( &l, qfalse );
+ if( !*s )
+ break;
+
+ Q_strncpyz( level.layout, s, sizeof( level.layout ) );
+ cnt++;
+ if( cnt >= layoutNum )
+ break;
+ }
+ G_Printf( "using layout \"%s\" from list (%s)\n", level.layout, layouts );
+}
+
+/*
+============
+G_LayoutBuildItem
+============
+*/
+void G_LayoutBuildItem( buildable_t buildable, vec3_t origin,
+ vec3_t angles, vec3_t origin2, vec3_t angles2 )
+{
+ gentity_t *builder;
+
+ builder = G_Spawn( );
+ builder->client = 0;
+ VectorCopy( origin, builder->s.pos.trBase );
+ VectorCopy( angles, builder->s.angles );
+ VectorCopy( angles, builder->cuboidSize );
+ VectorCopy( origin2, builder->s.origin2 );
+ VectorCopy( angles2, builder->s.angles2 );
+ G_SpawnBuildable( builder, buildable );
+}
+
+/*
+============
+G_LayoutLoad
+
+load the layout .dat file indicated by level.layout and spawn buildables
+as if a builder was creating them
+============
+*/
+void G_LayoutLoad( void )
+{
+ fileHandle_t f;
+ int len;
+ char *layout, *layoutHead;
+ char map[ MAX_QPATH ];
+ char buildName[ MAX_TOKEN_CHARS ];
+ int buildable;
+ vec3_t origin = { 0.0f, 0.0f, 0.0f };
+ vec3_t angles = { 0.0f, 0.0f, 0.0f };
+ vec3_t origin2 = { 0.0f, 0.0f, 0.0f };
+ vec3_t angles2 = { 0.0f, 0.0f, 0.0f };
+ char line[ MAX_STRING_CHARS ];
+ int i = 0;
+
+ if( !level.layout[ 0 ] || !Q_stricmp( level.layout, "*BUILTIN*" ) )
+ return;
+
+ trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) );
+ len = trap_FS_FOpenFile( va( "layouts/%s/%s.dat", map, level.layout ),
+ &f, FS_READ );
+ if( len < 0 )
+ {
+ G_Printf( "ERROR: layout %s could not be opened\n", level.layout );
+ return;
+ }
+ layoutHead = layout = BG_Alloc( len + 1 );
+ trap_FS_Read( layout, len, f );
+ layout[ len ] = '\0';
+ trap_FS_FCloseFile( f );
+ while( *layout )
+ {
+ if( i >= sizeof( line ) - 1 )
+ {
+ G_Printf( S_COLOR_RED "ERROR: line overflow in %s before \"%s\"\n",
+ va( "layouts/%s/%s.dat", map, level.layout ), line );
+ break;
+ }
+ line[ i++ ] = *layout;
+ line[ i ] = '\0';
+ if( *layout == '\n' )
+ {
+ i = 0;
+ sscanf( line, "%s %f %f %f %f %f %f %f %f %f %f %f %f\n",
+ buildName,
+ &origin[ 0 ], &origin[ 1 ], &origin[ 2 ],
+ &angles[ 0 ], &angles[ 1 ], &angles[ 2 ],
+ &origin2[ 0 ], &origin2[ 1 ], &origin2[ 2 ],
+ &angles2[ 0 ], &angles2[ 1 ], &angles2[ 2 ] );
+
+ buildable = BG_BuildableByName( buildName )->number;
+ if( buildable <= BA_NONE || buildable >= BA_NUM_BUILDABLES )
+ G_Printf( S_COLOR_YELLOW "WARNING: bad buildable name (%s) in layout."
+ " skipping\n", buildName );
+ else
+ G_LayoutBuildItem( buildable, origin, angles, origin2, angles2 );
+ }
+ layout++;
+ }
+ BG_Free( layoutHead );
+}
+
+/*
+============
+G_BaseSelfDestruct
+============
+*/
+void G_BaseSelfDestruct( team_t team )
+{
+ int i;
+ gentity_t *ent;
+
+ for( i = MAX_CLIENTS; i < level.num_entities; i++ )
+ {
+ ent = &level.gentities[ i ];
+ if( ent->health <= 0 )
+ continue;
+ if( ent->s.eType != ET_BUILDABLE )
+ continue;
+ if( ent->buildableTeam != team )
+ continue;
+ G_Damage( ent, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE );
+ }
+}
+
+/*
+============
+build log
+============
+*/
+buildLog_t *G_BuildLogNew( gentity_t *actor, buildFate_t fate )
+{
+ buildLog_t *log = &level.buildLog[ level.buildId++ % MAX_BUILDLOG ];
+
+ if( level.numBuildLogs < MAX_BUILDLOG )
+ level.numBuildLogs++;
+ log->time = level.time;
+ log->fate = fate;
+ log->actor = actor && actor->client ? actor->client->pers.namelog : NULL;
+ return log;
+}
+
+void G_BuildLogSet( buildLog_t *log, gentity_t *ent )
+{
+ log->modelindex = ent->s.modelindex;
+ log->deconstruct = log->deconstruct;
+ log->deconstructTime = ent->deconstructTime;
+ VectorCopy( ent->s.pos.trBase, log->origin );
+ VectorCopy( ent->s.angles, log->angles );
+ VectorCopy( ent->s.origin2, log->origin2 );
+ VectorCopy( ent->s.angles2, log->angles2 );
+ log->powerSource = ent->parentNode ? ent->parentNode->s.modelindex : BA_NONE;
+ log->powerValue = G_QueueValue( ent );
+}
+
+void G_BuildLogAuto( gentity_t *actor, gentity_t *buildable, buildFate_t fate )
+{
+ G_BuildLogSet( G_BuildLogNew( actor, fate ), buildable );
+}
+
+void G_BuildLogRevertThink( gentity_t *ent )
+{
+ gentity_t *built;
+ vec3_t mins, maxs;
+ int blockers[ MAX_GENTITIES ];
+ int num;
+ int victims = 0;
+ int i;
+
+ if( ent->suicideTime > 0 )
+ {
+ BG_BuildableBoundingBox( ent->s.modelindex, mins, maxs );
+ VectorAdd( ent->s.pos.trBase, mins, mins );
+ VectorAdd( ent->s.pos.trBase, maxs, maxs );
+ num = trap_EntitiesInBox( mins, maxs, blockers, MAX_GENTITIES );
+ for( i = 0; i < num; i++ )
+ {
+ gentity_t *targ;
+ vec3_t push;
+
+ targ = g_entities + blockers[ i ];
+ if( targ->client )
+ {
+ float val = ( targ->client->ps.eFlags & EF_WALLCLIMB) ? 300.0 : 150.0;
+
+ VectorSet( push, crandom() * val, crandom() * val, random() * val );
+ VectorAdd( targ->client->ps.velocity, push, targ->client->ps.velocity );
+ victims++;
+ }
+ }
+
+ ent->suicideTime--;
+
+ if( victims )
+ {
+ // still a blocker
+ ent->nextthink = level.time + FRAMETIME;
+ return;
+ }
+ }
+
+ built = G_FinishSpawningBuildable( ent, qtrue );
+ if( ( built->deconstruct = ent->deconstruct ) )
+ built->deconstructTime = ent->deconstructTime;
+ built->buildTime = built->s.time = 0;
+ G_KillBox( built );
+
+ G_LogPrintf( "revert: restore %d %s\n",
+ built - g_entities, BG_Buildable( built->s.modelindex, NULL )->name );
+
+ G_FreeEntity( ent );
+}
+
+void G_BuildLogRevert( int id )
+{
+ buildLog_t *log;
+ gentity_t *ent;
+ int i;
+ vec3_t dist;
+
+ level.numBuildablesForRemoval = 0;
+
+ level.numBuildLogs -= level.buildId - id;
+ while( level.buildId > id )
+ {
+ log = &level.buildLog[ --level.buildId % MAX_BUILDLOG ];
+ if( log->fate == BF_CONSTRUCT )
+ {
+ for( i = MAX_CLIENTS; i < level.num_entities; i++ )
+ {
+ ent = &g_entities[ i ];
+ if( ( ( ent->s.eType == ET_BUILDABLE &&
+ ent->health > 0 ) ||
+ ( ent->s.eType == ET_GENERAL &&
+ ent->think == G_BuildLogRevertThink ) ) &&
+ ent->s.modelindex == log->modelindex )
+ {
+ VectorSubtract( ent->s.pos.trBase, log->origin, dist );
+ if( VectorLengthSquared( dist ) <= 2.0f )
+ {
+ if( ent->s.eType == ET_BUILDABLE )
+ G_LogPrintf( "revert: remove %d %s\n",
+ ent - g_entities, BG_Buildable( ent->s.modelindex, NULL )->name );
+ G_FreeEntity( ent );
+ break;
+ }
+ }
+ }
+ }
+ else
+ {
+ gentity_t *builder = G_Spawn();
+
+ builder->client = NULL;
+ VectorCopy( log->origin, builder->s.pos.trBase );
+ VectorCopy( log->angles, builder->s.angles );
+ VectorCopy( log->origin2, builder->s.origin2 );
+ VectorCopy( log->angles2, builder->s.angles2 );
+ builder->s.modelindex = log->modelindex;
+ builder->deconstruct = log->deconstruct;
+ builder->deconstructTime = log->deconstructTime;
+
+ builder->think = G_BuildLogRevertThink;
+ builder->nextthink = level.time + FRAMETIME;
+
+ // Number of thinks before giving up and killing players in the way
+ builder->suicideTime = 30;
+
+ if( log->fate == BF_DESTROY || log->fate == BF_TEAMKILL )
+ {
+ int value = log->powerValue;
+
+ if( BG_Buildable( log->modelindex, NULL )->team == TEAM_ALIENS )
+ {
+ level.alienBuildPointQueue =
+ MAX( 0, level.alienBuildPointQueue - value );
+ }
+ else
+ {
+ if( log->powerSource == BA_H_REACTOR )
+ {
+ level.humanBuildPointQueue =
+ MAX( 0, level.humanBuildPointQueue - value );
+ }
+ else if( log->powerSource == BA_H_REPEATER )
+ {
+ gentity_t *source;
+ buildPointZone_t *zone;
+
+ source = G_PowerEntityForPoint( log->origin );
+ if( source && source->usesBuildPointZone )
+ {
+ zone = &level.buildPointZones[ source->buildPointZone ];
+ zone->queuedBuildPoints =
+ MAX( 0, zone->queuedBuildPoints - value );
+ }
+ }
+ }
+ }
+ }
+ }
+}
+