1670 lines
44 KiB
C
1670 lines
44 KiB
C
// Copyright (C) 1999-2000 Id Software, Inc.
|
|
//
|
|
|
|
#include "g_local.h"
|
|
|
|
|
|
/*
|
|
===============
|
|
G_DamageFeedback
|
|
|
|
Called just before a snapshot is sent to the given player.
|
|
Totals up all damage and generates both the player_state_t
|
|
damage values to that client for pain blends and kicks, and
|
|
global pain sound events for all clients.
|
|
===============
|
|
*/
|
|
void P_DamageFeedback( gentity_t *player ) {
|
|
gclient_t *client;
|
|
float count;
|
|
vec3_t angles;
|
|
|
|
client = player->client;
|
|
if ( client->ps.pm_type == PM_DEAD ) {
|
|
return;
|
|
}
|
|
|
|
// total points of damage shot at the player this frame
|
|
count = client->damage_blood + client->damage_armor;
|
|
if ( count == 0 ) {
|
|
return; // didn't take any damage
|
|
}
|
|
|
|
if ( count > 255 ) {
|
|
count = 255;
|
|
}
|
|
|
|
// send the information to the client
|
|
|
|
// world damage (falling, slime, etc) uses a special code
|
|
// to make the blend blob centered instead of positional
|
|
if ( client->damage_fromWorld ) {
|
|
client->ps.damagePitch = 255;
|
|
client->ps.damageYaw = 255;
|
|
|
|
client->damage_fromWorld = qfalse;
|
|
} else {
|
|
vectoangles( client->damage_from, angles );
|
|
client->ps.damagePitch = angles[PITCH]/360.0 * 256;
|
|
client->ps.damageYaw = angles[YAW]/360.0 * 256;
|
|
}
|
|
|
|
// play an apropriate pain sound
|
|
if ( (level.time > player->pain_debounce_time) && !(player->flags & FL_GODMODE) ) {
|
|
player->pain_debounce_time = level.time + 700;
|
|
G_AddEvent( player, EV_PAIN, player->health );
|
|
client->ps.damageEvent++;
|
|
}
|
|
|
|
client->ps.damageCount = client->damage_blood;
|
|
if (client->ps.damageCount > 255)
|
|
{
|
|
client->ps.damageCount = 255;
|
|
}
|
|
|
|
client->ps.damageShieldCount = client->damage_armor;
|
|
if (client->ps.damageShieldCount > 255)
|
|
{
|
|
client->ps.damageShieldCount = 255;
|
|
}
|
|
|
|
//
|
|
// clear totals
|
|
//
|
|
client->damage_blood = 0;
|
|
client->damage_armor = 0;
|
|
client->damage_knockback = 0;
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
=============
|
|
P_WorldEffects
|
|
|
|
Check for lava / slime contents and drowning
|
|
=============
|
|
*/
|
|
void P_WorldEffects( gentity_t *ent ) {
|
|
qboolean envirosuit;
|
|
int waterlevel;
|
|
|
|
if ( ent->client->noclip ) {
|
|
ent->client->airOutTime = level.time + 12000; // don't need air
|
|
return;
|
|
}
|
|
|
|
waterlevel = ent->waterlevel;
|
|
|
|
envirosuit = ent->client->ps.powerups[PW_BATTLESUIT] > level.time;
|
|
|
|
//
|
|
// check for drowning
|
|
//
|
|
if ( waterlevel == 3 && !(ent->watertype&CONTENTS_LADDER)) {
|
|
// envirosuit give air
|
|
if ( envirosuit ) {
|
|
ent->client->airOutTime = level.time + 10000;
|
|
}
|
|
|
|
// if out of air, start drowning
|
|
if ( ent->client->airOutTime < level.time) {
|
|
// drown!
|
|
ent->client->airOutTime += 1000;
|
|
if ( ent->health > 0 ) {
|
|
// take more damage the longer underwater
|
|
ent->damage += 2;
|
|
if (ent->damage > 15)
|
|
ent->damage = 15;
|
|
|
|
// play a gurp sound instead of a normal pain sound
|
|
if (ent->health <= ent->damage) {
|
|
G_Sound(ent, G_SoundIndex("*drown.wav"));
|
|
} else if (rand()&1) {
|
|
G_Sound(ent, G_SoundIndex("sound/player/gurp1.wav"));
|
|
} else {
|
|
G_Sound(ent, G_SoundIndex("sound/player/gurp2.wav"));
|
|
}
|
|
|
|
// don't play a normal pain sound
|
|
ent->pain_debounce_time = level.time + 200;
|
|
|
|
G_Damage (ent, NULL, NULL, NULL, NULL,
|
|
ent->damage, DAMAGE_NO_ARMOR, MOD_WATER);
|
|
}
|
|
}
|
|
} else {
|
|
ent->client->airOutTime = level.time + 12000;
|
|
ent->damage = 2;
|
|
}
|
|
|
|
//
|
|
// check for sizzle damage (move to pmove?)
|
|
//
|
|
if (waterlevel &&
|
|
(ent->watertype&(CONTENTS_LAVA|CONTENTS_SLIME)) ) {
|
|
if (ent->health > 0
|
|
&& ent->pain_debounce_time < level.time ) {
|
|
|
|
if ( envirosuit ) {
|
|
G_AddEvent( ent, EV_POWERUP_BATTLESUIT, 0 );
|
|
} else {
|
|
if (ent->watertype & CONTENTS_LAVA) {
|
|
G_Damage (ent, NULL, NULL, NULL, NULL,
|
|
30*waterlevel, 0, MOD_LAVA);
|
|
}
|
|
|
|
if (ent->watertype & CONTENTS_SLIME) {
|
|
G_Damage (ent, NULL, NULL, NULL, NULL,
|
|
10*waterlevel, 0, MOD_SLIME);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
===============
|
|
G_SetClientSound
|
|
===============
|
|
*/
|
|
void G_SetClientSound( gentity_t *ent )
|
|
{ // 3/28/00 kef -- this is dumb.
|
|
if (ent->waterlevel && (ent->watertype&(CONTENTS_LAVA|CONTENTS_SLIME)) )
|
|
ent->s.loopSound = level.snd_fry;
|
|
else
|
|
ent->s.loopSound = 0;
|
|
}
|
|
|
|
|
|
|
|
//==============================================================
|
|
|
|
/*
|
|
==============
|
|
ClientImpacts
|
|
==============
|
|
*/
|
|
void ClientImpacts( gentity_t *ent, pmove_t *pm ) {
|
|
int i, j;
|
|
trace_t trace;
|
|
gentity_t *other;
|
|
|
|
memset( &trace, 0, sizeof( trace ) );
|
|
for (i=0 ; i<pm->numtouch ; i++) {
|
|
for (j=0 ; j<i ; j++) {
|
|
if (pm->touchents[j] == pm->touchents[i] ) {
|
|
break;
|
|
}
|
|
}
|
|
if (j != i) {
|
|
continue; // duplicated
|
|
}
|
|
other = &g_entities[ pm->touchents[i] ];
|
|
|
|
if ( ( ent->r.svFlags & SVF_BOT ) && ( ent->touch ) ) {
|
|
ent->touch( ent, other, &trace );
|
|
}
|
|
|
|
if ( !other->touch ) {
|
|
continue;
|
|
}
|
|
|
|
other->touch( other, ent, &trace );
|
|
}
|
|
|
|
}
|
|
|
|
/*
|
|
============
|
|
G_TouchTriggers
|
|
|
|
Find all trigger entities that ent's current position touches.
|
|
Spectators will only interact with teleporters.
|
|
============
|
|
*/
|
|
void G_TouchTriggers( gentity_t *ent ) {
|
|
int i, num;
|
|
int touch[MAX_GENTITIES];
|
|
gentity_t *hit;
|
|
trace_t trace;
|
|
vec3_t mins, maxs;
|
|
static vec3_t range = { 40, 40, 52 };
|
|
|
|
if ( !ent->client ) {
|
|
return;
|
|
}
|
|
|
|
// dead clients don't activate triggers!
|
|
if ( ent->client->ps.stats[STAT_HEALTH] <= 0 ) {
|
|
return;
|
|
}
|
|
|
|
VectorSubtract( ent->client->ps.origin, range, mins );
|
|
VectorAdd( ent->client->ps.origin, range, maxs );
|
|
|
|
num = trap_EntitiesInBox( mins, maxs, touch, MAX_GENTITIES );
|
|
|
|
// can't use ent->absmin, because that has a one unit pad
|
|
VectorAdd( ent->client->ps.origin, ent->r.mins, mins );
|
|
VectorAdd( ent->client->ps.origin, ent->r.maxs, maxs );
|
|
|
|
for ( i=0 ; i<num ; i++ ) {
|
|
hit = &g_entities[touch[i]];
|
|
|
|
if ( !hit->touch && !ent->touch ) {
|
|
continue;
|
|
}
|
|
if ( !( hit->r.contents & CONTENTS_TRIGGER ) ) {
|
|
continue;
|
|
}
|
|
|
|
// ignore most entities if a spectator
|
|
if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) {
|
|
if ( hit->s.eType != ET_TELEPORT_TRIGGER &&
|
|
// this is ugly but adding a new ET_? type will
|
|
// most likely cause network incompatibilities
|
|
hit->touch != Touch_DoorTrigger) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// use seperate code for determining if an item is picked up
|
|
// so you don't have to actually contact its bounding box
|
|
if ( hit->s.eType == ET_ITEM ) {
|
|
if ( !BG_PlayerTouchesItem( &ent->client->ps, &hit->s, level.time ) ) {
|
|
continue;
|
|
}
|
|
} else {
|
|
if ( !trap_EntityContact( mins, maxs, hit ) ) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
memset( &trace, 0, sizeof(trace) );
|
|
|
|
if ( hit->touch ) {
|
|
hit->touch (hit, ent, &trace);
|
|
}
|
|
|
|
if ( ( ent->r.svFlags & SVF_BOT ) && ( ent->touch ) ) {
|
|
ent->touch( ent, hit, &trace );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
=================
|
|
SpectatorThink
|
|
=================
|
|
*/
|
|
void SpectatorThink( gentity_t *ent, usercmd_t *ucmd ) {
|
|
pmove_t pm;
|
|
gclient_t *client;
|
|
|
|
client = ent->client;
|
|
|
|
if ( client->sess.spectatorState != SPECTATOR_FOLLOW ) {
|
|
client->ps.pm_type = PM_SPECTATOR;
|
|
client->ps.speed = 400; // faster than normal
|
|
|
|
// set up for pmove
|
|
memset (&pm, 0, sizeof(pm));
|
|
pm.ps = &client->ps;
|
|
pm.cmd = *ucmd;
|
|
pm.tracemask = MASK_PLAYERSOLID & ~CONTENTS_BODY; // spectators can fly through bodies
|
|
pm.trace = trap_Trace;
|
|
pm.pointcontents = trap_PointContents;
|
|
|
|
// perform a pmove
|
|
Pmove (&pm);
|
|
|
|
// save results of pmove
|
|
VectorCopy( client->ps.origin, ent->s.origin );
|
|
|
|
G_TouchTriggers( ent );
|
|
trap_UnlinkEntity( ent );
|
|
}
|
|
|
|
client->oldbuttons = client->buttons;
|
|
client->buttons = ucmd->buttons;
|
|
|
|
// attack button cycles through spectators
|
|
if ( ( client->buttons & BUTTON_ATTACK ) && ! ( client->oldbuttons & BUTTON_ATTACK ) ) {
|
|
Cmd_FollowCycle_f( ent, 1 );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
=================
|
|
ClientInactivityTimer
|
|
|
|
Returns qfalse if the client is dropped
|
|
=================
|
|
*/
|
|
qboolean ClientInactivityTimer( gclient_t *client )
|
|
{
|
|
if ( ! g_inactivity.integer )
|
|
{
|
|
// give everyone some time, so if the operator sets g_inactivity during
|
|
// gameplay, everyone isn't kicked
|
|
client->inactivityTime = level.time + 60 * 1000;
|
|
client->inactivityWarning = qfalse;
|
|
}
|
|
else if ( client->pers.cmd.forwardmove ||
|
|
client->pers.cmd.rightmove ||
|
|
client->pers.cmd.upmove ||
|
|
(client->pers.cmd.buttons & BUTTON_ATTACK) ||
|
|
(client->pers.cmd.buttons & BUTTON_ALT_ATTACK) )
|
|
{
|
|
client->inactivityTime = level.time + g_inactivity.integer * 1000;
|
|
client->inactivityWarning = qfalse;
|
|
}
|
|
else if ( !client->pers.localClient )
|
|
{
|
|
if ( level.time > client->inactivityTime )
|
|
{
|
|
trap_DropClient( client - level.clients, "Dropped due to inactivity" );
|
|
return qfalse;
|
|
}
|
|
if ( level.time > client->inactivityTime - 10000 && !client->inactivityWarning )
|
|
{
|
|
client->inactivityWarning = qtrue;
|
|
trap_SendServerCommand( client - level.clients, "cp \"Ten seconds until inactivity drop!\n\"" );
|
|
}
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
ClientTimerActions
|
|
|
|
Actions that happen once a second
|
|
==================
|
|
*/
|
|
void ClientTimerActions( gentity_t *ent, int msec ) {
|
|
gclient_t *client;
|
|
|
|
client = ent->client;
|
|
client->timeResidual += msec;
|
|
|
|
while ( client->timeResidual >= 1000 ) {
|
|
client->timeResidual -= 1000;
|
|
|
|
// regenerate
|
|
if ( client->ps.powerups[PW_REGEN] ) {
|
|
if ( ent->health < client->ps.stats[STAT_MAX_HEALTH]) {
|
|
ent->health += 15;
|
|
if ( ent->health > client->ps.stats[STAT_MAX_HEALTH] * 1.1 ) {
|
|
ent->health = client->ps.stats[STAT_MAX_HEALTH] * 1.1;
|
|
}
|
|
G_AddEvent( ent, EV_POWERUP_REGEN, 0 );
|
|
} else if ( ent->health < client->ps.stats[STAT_MAX_HEALTH] * 2) {
|
|
ent->health += 5;
|
|
if ( ent->health > client->ps.stats[STAT_MAX_HEALTH] * 2 ) {
|
|
ent->health = client->ps.stats[STAT_MAX_HEALTH] * 2;
|
|
}
|
|
G_AddEvent( ent, EV_POWERUP_REGEN, 0 );
|
|
}
|
|
} else {
|
|
// count down health when over max
|
|
if ( ent->health > client->ps.stats[STAT_MAX_HEALTH] ) {
|
|
ent->health--;
|
|
}
|
|
}
|
|
|
|
// THIS USED TO count down armor when over max, more slowly now (every other second)
|
|
// if ( client->ps.stats[STAT_ARMOR] > client->ps.stats[STAT_MAX_HEALTH] && ((int)(level.time/1000))&0x01) {
|
|
|
|
// NOW IT ONCE AGAIN counts down armor when over max, once per second
|
|
if ( client->ps.stats[STAT_ARMOR] > client->ps.stats[STAT_MAX_HEALTH]) {
|
|
client->ps.stats[STAT_ARMOR]--;
|
|
}
|
|
|
|
// if we've got the seeker powerup, see if we can shoot it at someone
|
|
if ( client->ps.powerups[PW_SEEKER] )
|
|
{
|
|
vec3_t seekerPos;
|
|
|
|
if (SeekerAcquiresTarget(ent, seekerPos)) // set the client's enemy to a valid target
|
|
{
|
|
FireSeeker( ent, ent->enemy, seekerPos );
|
|
G_AddEvent( ent, EV_POWERUP_SEEKER_FIRE, 0 ); // draw the thingy
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/*
|
|
====================
|
|
ClientIntermissionThink
|
|
====================
|
|
*/
|
|
void ClientIntermissionThink( gclient_t *client ) {
|
|
client->ps.eFlags &= ~EF_TALK;
|
|
client->ps.eFlags &= ~EF_FIRING;
|
|
|
|
// the level will exit when everyone wants to or after timeouts
|
|
|
|
// swap and latch button actions
|
|
client->oldbuttons = client->buttons;
|
|
client->buttons = client->pers.cmd.buttons;
|
|
if (g_gametype.integer != GT_SINGLE_PLAYER)
|
|
{
|
|
if ( client->buttons & ( BUTTON_ATTACK | BUTTON_USE_HOLDABLE ) & ( client->oldbuttons ^ client->buttons ) ) {
|
|
client->readyToExit ^= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
====================
|
|
Cmd_Ready_f
|
|
====================
|
|
|
|
This function is called from the ui_sp_postgame.c as a result of clicking on the
|
|
"next" button in non GT_TOURNAMENT games. This replaces the old system of waiting
|
|
for the user to click an ATTACK or USE button to signal ready
|
|
(see ClientIntermissionThink() above)
|
|
|
|
when all clients have signaled ready, the game continues to the next match.
|
|
*/
|
|
void Cmd_Ready_f (gentity_t *ent)
|
|
{
|
|
gclient_t *client;
|
|
client = ent->client;
|
|
|
|
client->readyToExit ^= 1;
|
|
}
|
|
|
|
|
|
|
|
typedef struct detHit_s
|
|
{
|
|
gentity_t *detpack;
|
|
gentity_t *player;
|
|
int time;
|
|
} detHit_t;
|
|
|
|
#define MAX_DETHITS 32 // never define this to be 0
|
|
|
|
detHit_t detHits[MAX_DETHITS];
|
|
qboolean bDetInit = qfalse;
|
|
|
|
extern void FinishSpawningDetpack( gentity_t *ent, int itemIndex );
|
|
extern void FinishSpawningTransporter( gentity_t *ent, int itemIndex );
|
|
//-----------------------------------------------------------------------------DECOY TEMP
|
|
extern void FinishSpawningDecoy( gentity_t *ent, int itemIndex );
|
|
//-----------------------------------------------------------------------------DECOY TEMP
|
|
void DetonateDetpack(gentity_t *ent);
|
|
|
|
#define DETPACK_DAMAGE 750
|
|
#define DETPACK_RADIUS 500
|
|
|
|
void detpack_shot( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int meansOfDeath )
|
|
{
|
|
int i = 0;
|
|
gentity_t *ent = NULL;
|
|
|
|
G_TempEntity(self->s.origin, EV_GRENADE_EXPLODE);
|
|
G_RadiusDamage( self->s.origin, self->parent?self->parent:self, DETPACK_DAMAGE*0.125, DETPACK_RADIUS*0.25,
|
|
self, DAMAGE_ALL_TEAMS, MOD_DETPACK );
|
|
// we're blowing up cuz we've been shot, so make sure we remove ourselves
|
|
//from our parent's inventory (so to speak)
|
|
for (i = 0; i < MAX_CLIENTS; i++)
|
|
{
|
|
if (((ent = &g_entities[i])!=NULL) && ent->inuse && (self->parent == ent))
|
|
{
|
|
ent->client->ps.stats[STAT_USEABLE_PLACED] = 0;
|
|
ent->client->ps.stats[STAT_HOLDABLE_ITEM] = 0;
|
|
break;
|
|
}
|
|
}
|
|
G_FreeEntity(self);
|
|
}
|
|
|
|
qboolean PlaceDetpack(gentity_t *ent)
|
|
{
|
|
gentity_t *detpack = NULL;
|
|
gitem_t *item = NULL;
|
|
float detDistance = 80;
|
|
int i = 0;
|
|
trace_t tr;
|
|
vec3_t fwd, right, up, end, mins = {-16,-16,-16}, maxs = {16,16,16};
|
|
|
|
// make sure our detHit info is init'd
|
|
if (!bDetInit)
|
|
{
|
|
memset(detHits, 0, MAX_DETHITS*sizeof(detHit_t));
|
|
bDetInit = 1;
|
|
}
|
|
|
|
// what item are we looking for?
|
|
for (i = 0; i < bg_numItems; i++)
|
|
{
|
|
item = &bg_itemlist[i];
|
|
if ( (IT_HOLDABLE == item->giType) && (HI_DETPACK == item->giTag) )
|
|
{
|
|
// can we place this in front of us?
|
|
AngleVectors (ent->client->ps.viewangles, fwd, right, up);
|
|
fwd[2] = 0;
|
|
VectorMA(ent->client->ps.origin, detDistance, fwd, end);
|
|
trap_Trace (&tr, ent->client->ps.origin, mins, maxs, end, ent->s.number, MASK_SHOT );
|
|
if (tr.fraction > 0.9)
|
|
{
|
|
// got enough room so place the detpack
|
|
detpack = G_Spawn();
|
|
G_SpawnItem(detpack, item);
|
|
VectorMA(ent->client->ps.origin, detDistance + mins[0], fwd, detpack->s.origin);
|
|
FinishSpawningDetpack(detpack, i);
|
|
VectorNegate(fwd, fwd);
|
|
vectoangles(fwd, detpack->s.angles);
|
|
detpack->think = DetonateDetpack;
|
|
detpack->nextthink = level.time + 120000; // if not used after 2 minutes it blows up anyway
|
|
detpack->parent = ent;
|
|
return qtrue;
|
|
}
|
|
else
|
|
{
|
|
// no room
|
|
return qfalse;
|
|
}
|
|
}
|
|
}
|
|
/******** no model for detpack. bad *********/
|
|
return qfalse;
|
|
}
|
|
|
|
qboolean PlayerHitByDet(gentity_t *det, gentity_t *player)
|
|
{
|
|
int i = 0;
|
|
|
|
if (!bDetInit)
|
|
{
|
|
// detHit stuff not initialized. who knows what's going on?
|
|
return qfalse;
|
|
}
|
|
for (i = 0; i < MAX_DETHITS; i++)
|
|
{
|
|
if ( (detHits[i].detpack == det) && (detHits[i].player == player) )
|
|
{
|
|
return qtrue;
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
void AddPlayerToDetHits(gentity_t *det, gentity_t *player)
|
|
{
|
|
int i = 0;
|
|
detHit_t *lastHit = NULL, *curHit = NULL;
|
|
|
|
for (i = 0; i < MAX_DETHITS; i++)
|
|
{
|
|
if (0 == detHits[i].time)
|
|
{
|
|
// empty slot. add our player here.
|
|
detHits[i].detpack = det;
|
|
detHits[i].player = player;
|
|
detHits[i].time = level.time;
|
|
}
|
|
lastHit = &detHits[i];
|
|
}
|
|
// getting here means we've filled our list of detHits, so begin recycling them, starting with the oldest hit.
|
|
curHit = &detHits[0];
|
|
while (lastHit->time < curHit->time)
|
|
{
|
|
lastHit = curHit;
|
|
curHit++;
|
|
// just a safety check here
|
|
if (curHit == &detHits[0])
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
curHit->detpack = det;
|
|
curHit->player = player;
|
|
curHit->time = level.time;
|
|
}
|
|
|
|
void ClearThisDetpacksHits(gentity_t *det)
|
|
{
|
|
int i = 0;
|
|
|
|
for (i = 0; i < MAX_DETHITS; i++)
|
|
{
|
|
if (detHits[i].detpack == det)
|
|
{
|
|
detHits[i].player = NULL;
|
|
detHits[i].detpack = NULL;
|
|
detHits[i].time = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
void DetpackBlammoThink(gentity_t *ent)
|
|
{
|
|
int i = 0, lifetime = 3000; // how long (in msec) the shockwave lasts
|
|
int knockback = 400;
|
|
float curBlastRadius = 50.0*ent->count, radius = 0;
|
|
vec3_t vTemp;
|
|
trace_t tr;
|
|
gentity_t *pl = NULL;
|
|
|
|
if (ent->count++ > (lifetime*0.01))
|
|
{
|
|
ClearThisDetpacksHits(ent);
|
|
G_FreeEntity(ent);
|
|
return;
|
|
}
|
|
|
|
// get a list of players within the blast radius at this time.
|
|
// only hit the ones we can see from the center of the explosion.
|
|
for (i=0; i<MAX_CLIENTS; i++)
|
|
{
|
|
if ( g_entities[i].client && g_entities[i].takedamage)
|
|
{
|
|
pl = &g_entities[i];
|
|
VectorSubtract(pl->s.pos.trBase, ent->s.origin, vTemp);
|
|
radius = VectorNormalize(vTemp);
|
|
if ( (radius <= curBlastRadius) && !PlayerHitByDet(ent, pl) )
|
|
{
|
|
// within the proper radius. do we have LOS to the player?
|
|
trap_Trace (&tr, ent->s.origin, NULL, NULL, pl->s.pos.trBase, ent->s.number, MASK_SHOT );
|
|
if (tr.entityNum == i)
|
|
{
|
|
// oh yeah. you're gettin' hit.
|
|
AddPlayerToDetHits(ent, pl);
|
|
VectorMA(pl->client->ps.velocity, knockback, vTemp, pl->client->ps.velocity);
|
|
// make sure the player goes up some
|
|
if (pl->client->ps.velocity[2] < 100)
|
|
{
|
|
pl->client->ps.velocity[2] = 100;
|
|
}
|
|
if ( !pl->client->ps.pm_time )
|
|
{
|
|
int t;
|
|
|
|
t = knockback * 2;
|
|
if ( t < 50 ) {
|
|
t = 50;
|
|
}
|
|
if ( t > 200 ) {
|
|
t = 200;
|
|
}
|
|
pl->client->ps.pm_time = t;
|
|
pl->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
|
|
}
|
|
}
|
|
}
|
|
// this was just for testing. looks pretty neat, though.
|
|
/*
|
|
else
|
|
{
|
|
VectorMA(ent->s.origin, curBlastRadius, vTemp, vTemp);
|
|
G_TempEntity(vTemp, EV_FX_STEAM);
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
|
|
ent->nextthink = level.time + FRAMETIME;
|
|
}
|
|
|
|
void DetonateDetpack(gentity_t *ent)
|
|
{
|
|
// find all detpacks. the one whose parent is ent...blow up
|
|
gentity_t *detpack = NULL;
|
|
char *classname = BG_FindClassnameForHoldable(HI_DETPACK);
|
|
|
|
if (!classname)
|
|
{
|
|
return;
|
|
}
|
|
while ((detpack = G_Find (detpack, FOFS(classname), classname)) != NULL)
|
|
{
|
|
if (detpack->parent == ent)
|
|
{
|
|
// found it. BLAMMO!
|
|
// play explosion sound to all clients
|
|
gentity_t *te = NULL;
|
|
|
|
te = G_TempEntity( detpack->s.pos.trBase, EV_GLOBAL_SOUND );
|
|
te->s.eventParm = G_SoundIndex( "sound/weapons/explosions/detpakexplode.wav" );//cgs.media.detpackExplodeSound
|
|
te->r.svFlags |= SVF_BROADCAST;
|
|
|
|
G_AddEvent(detpack, EV_DETPACK, 0);
|
|
G_RadiusDamage( detpack->s.origin, detpack->parent, DETPACK_DAMAGE, DETPACK_RADIUS,
|
|
detpack, DAMAGE_HALF_NOTLOS|DAMAGE_ALL_TEAMS, MOD_DETPACK );
|
|
// just turn the model invisible and let the entity think for a bit to deliver a shockwave
|
|
//G_FreeEntity(detpack);
|
|
detpack->classname = NULL;
|
|
detpack->s.modelindex = 0;
|
|
detpack->think = DetpackBlammoThink;
|
|
detpack->count = 1;
|
|
detpack->nextthink = level.time + FRAMETIME;
|
|
return;
|
|
}
|
|
else if (detpack == ent) // if detpack == ent, we're blowing up this detpack cuz it's been sitting too long
|
|
{
|
|
detpack_shot(detpack, NULL, NULL, 0, 0);
|
|
return;
|
|
}
|
|
}
|
|
// hmm. couldn't find it.
|
|
detpack = NULL;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#define SHIELD_HEALTH 250
|
|
#define SHIELD_HEALTH_DEC 10 // 25 seconds
|
|
#define MAX_SHIELD_HEIGHT 254
|
|
#define MAX_SHIELD_HALFWIDTH 255
|
|
#define SHIELD_HALFTHICKNESS 4
|
|
#define SHIELD_PLACEDIST 64
|
|
|
|
|
|
static qhandle_t shieldAttachSound;
|
|
static qhandle_t shieldActivateSound;
|
|
static qhandle_t shieldDamageSound;
|
|
|
|
|
|
void ShieldRemove(gentity_t *self)
|
|
{
|
|
self->think = G_FreeEntity;
|
|
self->nextthink = level.time + 100;
|
|
|
|
// Play raising sound...
|
|
G_AddEvent(self, EV_GENERAL_SOUND, shieldActivateSound);
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
// Count down the health of the shield.
|
|
void ShieldThink(gentity_t *self)
|
|
{
|
|
self->s.eFlags &= ~(EF_ITEMPLACEHOLDER | EF_NODRAW);
|
|
self->health -= SHIELD_HEALTH_DEC;
|
|
self->nextthink = level.time + 1000;
|
|
if (self->health <= 0)
|
|
{
|
|
ShieldRemove(self);
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
// The shield was damaged to below zero health.
|
|
void ShieldDie(gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod)
|
|
{
|
|
// Play damaging sound...
|
|
G_AddEvent(self, EV_GENERAL_SOUND, shieldDamageSound);
|
|
|
|
ShieldRemove(self);
|
|
}
|
|
|
|
|
|
// The shield had damage done to it. Make it flicker.
|
|
void ShieldPain(gentity_t *self, gentity_t *attacker, int damage)
|
|
{
|
|
// Set the itemplaceholder flag to indicate the the shield drawing that the shield pain should be drawn.
|
|
self->s.eFlags |= EF_ITEMPLACEHOLDER;
|
|
self->think = ShieldThink;
|
|
self->nextthink = level.time + 400;
|
|
|
|
// Play damaging sound...
|
|
G_AddEvent(self, EV_GENERAL_SOUND, shieldDamageSound);
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
// Try to turn the shield back on after a delay.
|
|
void ShieldGoSolid(gentity_t *self)
|
|
{
|
|
trace_t tr;
|
|
|
|
// see if we're valid
|
|
self->health--;
|
|
if (self->health <= 0)
|
|
{
|
|
ShieldRemove(self);
|
|
return;
|
|
}
|
|
|
|
trap_Trace (&tr, self->r.currentOrigin, self->r.mins, self->r.maxs, self->r.currentOrigin, self->s.number, CONTENTS_BODY );
|
|
if(tr.startsolid)
|
|
{ // gah, we can't activate yet
|
|
self->nextthink = level.time + 200;
|
|
self->think = ShieldGoSolid;
|
|
trap_LinkEntity(self);
|
|
}
|
|
else
|
|
{ // get hard... huh-huh...
|
|
self->r.contents = CONTENTS_SOLID;
|
|
self->s.eFlags &= ~(EF_NODRAW | EF_ITEMPLACEHOLDER);
|
|
self->nextthink = level.time + 1000;
|
|
self->think = ShieldThink;
|
|
self->takedamage = qtrue;
|
|
trap_LinkEntity(self);
|
|
|
|
// Play raising sound...
|
|
G_AddEvent(self, EV_GENERAL_SOUND, shieldActivateSound);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
// Turn the shield off to allow a friend to pass through.
|
|
void ShieldGoNotSolid(gentity_t *self)
|
|
{
|
|
// make the shield non-solid very briefly
|
|
self->r.contents = CONTENTS_NONE;
|
|
self->s.eFlags |= EF_NODRAW;
|
|
// nextthink needs to have a large enough interval to avoid excess accumulation of Activate messages
|
|
self->nextthink = level.time + 200;
|
|
self->think = ShieldGoSolid;
|
|
self->takedamage = qfalse;
|
|
trap_LinkEntity(self);
|
|
|
|
// Play raising sound...
|
|
G_AddEvent(self, EV_GENERAL_SOUND, shieldActivateSound);
|
|
}
|
|
|
|
|
|
// Somebody (a player) has touched the shield. See if it is a "friend".
|
|
void ShieldTouch(gentity_t *self, gentity_t *other, trace_t *trace)
|
|
{
|
|
if (g_gametype.integer >= GT_TEAM)
|
|
{ // let teammates through
|
|
// compare the parent's team to the "other's" team
|
|
if (( self->parent->client) && (other->client))
|
|
{
|
|
if (self->parent->client->sess.sessionTeam == other->client->sess.sessionTeam)
|
|
{
|
|
ShieldGoNotSolid(self);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{//let the person who dropped the shield through
|
|
if (self->parent->s.number == other->s.number)
|
|
{
|
|
ShieldGoNotSolid(self);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// After a short delay, create the shield by expanding in all directions.
|
|
void CreateShield(gentity_t *ent)
|
|
{
|
|
trace_t tr;
|
|
vec3_t mins, maxs, end, posTraceEnd, negTraceEnd, start;
|
|
int height, posWidth, negWidth, halfWidth = 0;
|
|
qboolean xaxis;
|
|
int paramData = 0;
|
|
static int shieldID;
|
|
|
|
// trace upward to find height of shield
|
|
VectorCopy(ent->r.currentOrigin, end);
|
|
end[2] += MAX_SHIELD_HEIGHT;
|
|
trap_Trace (&tr, ent->r.currentOrigin, NULL, NULL, end, ent->s.number, MASK_SHOT );
|
|
height = (int)(MAX_SHIELD_HEIGHT * tr.fraction);
|
|
|
|
// use angles to find the proper axis along which to align the shield
|
|
VectorSet(mins, -SHIELD_HALFTHICKNESS, -SHIELD_HALFTHICKNESS, 0);
|
|
VectorSet(maxs, SHIELD_HALFTHICKNESS, SHIELD_HALFTHICKNESS, height);
|
|
VectorCopy(ent->r.currentOrigin, posTraceEnd);
|
|
VectorCopy(ent->r.currentOrigin, negTraceEnd);
|
|
|
|
if ((int)(ent->s.angles[YAW]) == 0) // shield runs along y-axis
|
|
{
|
|
posTraceEnd[1]+=MAX_SHIELD_HALFWIDTH;
|
|
negTraceEnd[1]-=MAX_SHIELD_HALFWIDTH;
|
|
xaxis = qfalse;
|
|
}
|
|
else // shield runs along x-axis
|
|
{
|
|
posTraceEnd[0]+=MAX_SHIELD_HALFWIDTH;
|
|
negTraceEnd[0]-=MAX_SHIELD_HALFWIDTH;
|
|
xaxis = qtrue;
|
|
}
|
|
|
|
// trace horizontally to find extend of shield
|
|
// positive trace
|
|
VectorCopy(ent->r.currentOrigin, start);
|
|
start[2] += (height>>1);
|
|
trap_Trace (&tr, start, 0, 0, posTraceEnd, ent->s.number, MASK_SHOT );
|
|
posWidth = MAX_SHIELD_HALFWIDTH * tr.fraction;
|
|
// negative trace
|
|
trap_Trace (&tr, start, 0, 0, negTraceEnd, ent->s.number, MASK_SHOT );
|
|
negWidth = MAX_SHIELD_HALFWIDTH * tr.fraction;
|
|
|
|
// kef -- monkey with dimensions and place origin in center
|
|
halfWidth = (posWidth + negWidth)>>1;
|
|
if (xaxis)
|
|
{
|
|
ent->r.currentOrigin[0] = ent->r.currentOrigin[0] - negWidth + halfWidth;
|
|
}
|
|
else
|
|
{
|
|
ent->r.currentOrigin[1] = ent->r.currentOrigin[1] - negWidth + halfWidth;
|
|
}
|
|
ent->r.currentOrigin[2] += (height>>1);
|
|
|
|
// set entity's mins and maxs to new values, make it solid, and link it
|
|
if (xaxis)
|
|
{
|
|
VectorSet(ent->r.mins, -halfWidth, -SHIELD_HALFTHICKNESS, -(height>>1));
|
|
VectorSet(ent->r.maxs, halfWidth, SHIELD_HALFTHICKNESS, height>>1);
|
|
}
|
|
else
|
|
{
|
|
VectorSet(ent->r.mins, -SHIELD_HALFTHICKNESS, -halfWidth, -(height>>1));
|
|
VectorSet(ent->r.maxs, SHIELD_HALFTHICKNESS, halfWidth, height);
|
|
}
|
|
ent->clipmask = MASK_SHOT;
|
|
|
|
// Information for shield rendering.
|
|
|
|
// xaxis - 1 bit
|
|
// height - 0-254 8 bits
|
|
// posWidth - 0-255 8 bits
|
|
// negWidth - 0 - 255 8 bits
|
|
|
|
paramData = (xaxis << 24) | (height << 16) | (posWidth << 8) | (negWidth);
|
|
ent->s.time2 = paramData;
|
|
ent->s.time =
|
|
|
|
ent->health = SHIELD_HEALTH;
|
|
ent->pain = ShieldPain;
|
|
ent->die = ShieldDie;
|
|
ent->touch = ShieldTouch;
|
|
|
|
ent->r.svFlags |= SVF_SHIELD_BBOX;
|
|
|
|
// see if we're valid
|
|
trap_Trace (&tr, ent->r.currentOrigin, ent->r.mins, ent->r.maxs, ent->r.currentOrigin, ent->s.number, CONTENTS_BODY );
|
|
|
|
if (tr.startsolid)
|
|
{ // Something in the way!
|
|
// make the shield non-solid very briefly
|
|
ent->r.contents = CONTENTS_NONE;
|
|
ent->s.eFlags |= EF_NODRAW;
|
|
// nextthink needs to have a large enough interval to avoid excess accumulation of Activate messages
|
|
ent->nextthink = level.time + 200;
|
|
ent->think = ShieldGoSolid;
|
|
ent->takedamage = qfalse;
|
|
trap_LinkEntity(ent);
|
|
}
|
|
else
|
|
{ // Get solid.
|
|
ent->r.contents = CONTENTS_SOLID;
|
|
|
|
ent->nextthink = level.time + 1000;
|
|
ent->think = ShieldThink;
|
|
|
|
ent->takedamage = qtrue;
|
|
trap_LinkEntity(ent);
|
|
|
|
// Play raising sound...
|
|
G_AddEvent(ent, EV_GENERAL_SOUND, shieldActivateSound);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
qboolean PlaceShield(gentity_t *playerent)
|
|
{
|
|
gentity_t *shield = NULL;
|
|
gitem_t *item = NULL;
|
|
int i = 0;
|
|
trace_t tr;
|
|
vec3_t fwd, pos, dest, mins = {-16,-16, 0}, maxs = {16,16,16};
|
|
|
|
if (shieldAttachSound==0)
|
|
{
|
|
shieldAttachSound = G_SoundIndex("sound/weapons/detpacklatch.wav");
|
|
shieldActivateSound = G_SoundIndex("sound/movers/forceup.wav");
|
|
shieldDamageSound = G_SoundIndex("sound/ambience/spark5.wav");
|
|
}
|
|
|
|
for (i = 0; i < bg_numItems; i++)
|
|
{
|
|
item = &bg_itemlist[i];
|
|
if ( (IT_HOLDABLE == item->giType) && (HI_SHIELD == item->giTag) )
|
|
{
|
|
// can we place this in front of us?
|
|
AngleVectors (playerent->client->ps.viewangles, fwd, NULL, NULL);
|
|
fwd[2] = 0;
|
|
VectorMA(playerent->client->ps.origin, SHIELD_PLACEDIST, fwd, dest);
|
|
trap_Trace (&tr, playerent->client->ps.origin, mins, maxs, dest, playerent->s.number, MASK_SHOT );
|
|
if (tr.fraction > 0.9)
|
|
{ // got enough room so place the portable shield
|
|
shield = G_Spawn();
|
|
|
|
VectorCopy(tr.endpos, pos);
|
|
// Figure out what direction the shield is facing.
|
|
if (fabs(fwd[0]) > fabs(fwd[1]))
|
|
{ // shield is north/south, facing east.
|
|
shield->s.angles[YAW] = 0;
|
|
}
|
|
else
|
|
{ // shield is along the east/west axis, facing north
|
|
shield->s.angles[YAW] = 90;
|
|
}
|
|
shield->think = CreateShield;
|
|
shield->nextthink = level.time + 500; // power up after .5 seconds
|
|
shield->parent = playerent;
|
|
|
|
// Set team number.
|
|
shield->s.otherEntityNum2 = playerent->client->sess.sessionTeam;
|
|
|
|
shield->s.eType = ET_USEABLE;
|
|
shield->s.modelindex = item->giTag; // this'll be used in CG_Useable() for rendering.
|
|
shield->classname = item->classname;
|
|
|
|
shield->r.contents = CONTENTS_TRIGGER;
|
|
|
|
shield->touch = 0;
|
|
// using an item causes it to respawn
|
|
shield->use = 0; //Use_Item;
|
|
|
|
// drop to floor
|
|
VectorSet( dest, pos[0], pos[1], pos[2] - 4096 );
|
|
trap_Trace( &tr, pos, mins, maxs, dest, playerent->s.number, MASK_SOLID );
|
|
if ( tr.startsolid )
|
|
{
|
|
G_FreeEntity( shield );
|
|
return qfalse;
|
|
}
|
|
|
|
// allow to ride movers
|
|
shield->s.groundEntityNum = tr.entityNum;
|
|
|
|
G_SetOrigin( shield, tr.endpos );
|
|
|
|
shield->s.eFlags &= ~EF_NODRAW;
|
|
shield->r.svFlags &= ~SVF_NOCLIENT;
|
|
|
|
trap_LinkEntity (shield);
|
|
|
|
// Play placing sound...
|
|
G_AddEvent(shield, EV_GENERAL_SOUND, shieldAttachSound);
|
|
|
|
return qtrue;
|
|
}
|
|
else
|
|
{
|
|
// no room
|
|
return qfalse;
|
|
}
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//-------------------------------------------------------------- DECOY ACTIVITIES
|
|
void DecoyThink(gentity_t *ent)
|
|
{
|
|
ent->s.apos =(ent->parent)->s.apos; // Update Current Rotation
|
|
ent->nextthink = level.time + irandom(2000, 6000); // Next think between 2 & 8 seconds
|
|
|
|
(ent->count) --; // Count Down
|
|
if (ent->count<0) G_FreeEntity(ent); // Time To Erase The Ent
|
|
}
|
|
|
|
qboolean PlaceDecoy(gentity_t *ent)
|
|
{
|
|
gentity_t *decoy = NULL;
|
|
gitem_t *item = NULL;
|
|
float detDistance = 80; // Distance the object will be placed from player
|
|
int i = 0;
|
|
trace_t tr;
|
|
vec3_t fwd, right, up, end, mins = {-16,-16,-16}, maxs = {16,16,16};
|
|
|
|
// get a pointer to the correct item structure in bg_misc.c
|
|
for (i = 0; i < bg_numItems; i++)
|
|
{
|
|
item = &bg_itemlist[i];
|
|
if ( (IT_HOLDABLE == item->giType) && (HI_DECOY == item->giTag) )
|
|
{
|
|
// can we place this in front of us?
|
|
AngleVectors (ent->client->ps.viewangles, fwd, right, up);
|
|
fwd[2] = 0;
|
|
VectorMA(ent->client->ps.origin, detDistance, fwd, end);
|
|
trap_Trace (&tr, ent->client->ps.origin, mins, maxs, end, ent->s.number, MASK_SHOT );
|
|
if (tr.fraction > 0.9)
|
|
{
|
|
//--------------------------- SPAWN AND PLACE DECOY ON GROUND
|
|
decoy = G_Spawn();
|
|
G_SpawnItem(decoy, item); // Generate it as an item, temporarly
|
|
VectorMA(ent->client->ps.origin, detDistance + mins[0], fwd, decoy->s.origin);
|
|
VectorNegate(fwd, fwd); // ??? What does this do??
|
|
vectoangles(fwd, decoy->s.angles);
|
|
FinishSpawningDecoy(decoy, i); // Drop to ground and trap to clients
|
|
decoy->s.number = decoy - g_entities;
|
|
|
|
//--------------------------- SPECIALIZED DECOY SETUP
|
|
decoy->think = DecoyThink;
|
|
decoy->count = 12; // about 1 minute before dissapear
|
|
decoy->nextthink = level.time + 4000; // think after 4 seconds
|
|
decoy->parent = ent;
|
|
|
|
(decoy->s).eType = (ent->s).eType; // set to type PLAYER
|
|
(decoy->s).eFlags= (ent->s).eFlags;
|
|
(decoy->s).eFlags |= EF_ITEMPLACEHOLDER;// set the HOLOGRAM FLAG to ON
|
|
|
|
decoy->s.weapon = ent->s.weapon; // get Player's Wepon Type
|
|
// decoy->s.constantLight = 10 + (10 << 8) + (10 << 16) + (9 << 24);
|
|
|
|
decoy->s.pos.trBase[2] += 24; // shift up to adjust origin of body
|
|
decoy->s.apos = ent->s.apos; // copy angle of player to decoy
|
|
|
|
decoy->s.legsAnim = LEGS_IDLE; // Just standing
|
|
decoy->s.torsoAnim = TORSO_STAND;
|
|
|
|
//--------------------------- WEAPON ADJUST
|
|
if (decoy->s.weapon==WP_PHASER || decoy->s.weapon==WP_DREADNOUGHT)
|
|
decoy->s.weapon = WP_COMPRESSION_RIFLE;
|
|
|
|
// The Phaser and Dreadnought (Arc Welder) weapons are rendered on the
|
|
// client side differently, and cannot be used by the decoy
|
|
|
|
return qtrue; // SUCCESS
|
|
}
|
|
else
|
|
{ return qfalse; // FAILURE: no room
|
|
}
|
|
}
|
|
}
|
|
/******** no model for decoy. bad *********/
|
|
return qfalse;
|
|
}
|
|
//-------------------------------------------------------------- DECOY ACTIVITIES
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
================
|
|
ClientEvents
|
|
|
|
Events will be passed on to the clients for presentation,
|
|
but any server game effects are handled here
|
|
================
|
|
*/
|
|
void ClientEvents( gentity_t *ent, int oldEventSequence ) {
|
|
int i;
|
|
int event;
|
|
gclient_t *client;
|
|
int damage;
|
|
|
|
client = ent->client;
|
|
|
|
if ( oldEventSequence < client->ps.eventSequence - MAX_PS_EVENTS ) {
|
|
oldEventSequence = client->ps.eventSequence - MAX_PS_EVENTS;
|
|
}
|
|
for ( i = oldEventSequence ; i < client->ps.eventSequence ; i++ ) {
|
|
event = client->ps.events[ i & (MAX_PS_EVENTS-1) ];
|
|
|
|
switch ( event ) {
|
|
case EV_FALL_MEDIUM:
|
|
case EV_FALL_FAR:
|
|
if ( ent->s.eType != ET_PLAYER ) {
|
|
break; // not in the player model
|
|
}
|
|
if ( g_dmflags.integer & DF_NO_FALLING ) {
|
|
break;
|
|
}
|
|
if ( event == EV_FALL_FAR ) {
|
|
damage = 10;
|
|
} else {
|
|
damage = 5;
|
|
}
|
|
ent->pain_debounce_time = level.time + 200; // no normal pain sound
|
|
G_Damage (ent, NULL, NULL, NULL, NULL, damage, DAMAGE_ARMOR_PIERCING, MOD_FALLING);
|
|
break;
|
|
|
|
case EV_FIRE_WEAPON:
|
|
FireWeapon( ent, qfalse );
|
|
break;
|
|
|
|
case EV_ALT_FIRE:
|
|
FireWeapon( ent, qtrue );
|
|
break;
|
|
|
|
case EV_FIRE_EMPTY_PHASER:
|
|
FireWeapon( ent, qfalse );
|
|
break;
|
|
|
|
case EV_USE_ITEM1: // transporter
|
|
// get rid of transporter and go to random spawnpoint
|
|
{
|
|
vec3_t origin, angles;
|
|
|
|
//------------------------------------------------------- DROP FLAGS
|
|
if ( ent->client->ps.powerups[PW_REDFLAG] ) {
|
|
Team_ReturnFlag(TEAM_RED);
|
|
}
|
|
else if ( ent->client->ps.powerups[PW_BLUEFLAG] ) {
|
|
Team_ReturnFlag(TEAM_BLUE);
|
|
}
|
|
ent->client->ps.powerups[PW_BLUEFLAG] = 0;
|
|
ent->client->ps.powerups[PW_REDFLAG] = 0;
|
|
//------------------------------------------------------- DROP FLAGS
|
|
|
|
ent->client->ps.stats[STAT_USEABLE_PLACED] = 0;
|
|
SelectSpawnPoint( ent->client->ps.origin, origin, angles );
|
|
TeleportPlayer( ent, origin, angles );
|
|
}
|
|
break;
|
|
|
|
case EV_USE_ITEM2: // medkit
|
|
// New set of rules. You get either 100 health, or an extra 25, whichever is higher.
|
|
// Give 1/4 health.
|
|
ent->health += ent->client->ps.stats[STAT_MAX_HEALTH]*0.25;
|
|
|
|
if (ent->health < ent->client->ps.stats[STAT_MAX_HEALTH])
|
|
{ // If that doesn't bring us up to 100, make it go up to 100.
|
|
ent->health = ent->client->ps.stats[STAT_MAX_HEALTH];
|
|
}
|
|
else if (ent->health > ent->client->ps.stats[STAT_MAX_HEALTH]*2)
|
|
{ // Otherwise, 25 is all you get. Just make sure we don't go above 200.
|
|
ent->health = ent->client->ps.stats[STAT_MAX_HEALTH]*2;
|
|
}
|
|
break;
|
|
|
|
case EV_USE_ITEM3: // detpack
|
|
// if we haven't placed it yet, place it
|
|
if (0 == ent->client->ps.stats[STAT_USEABLE_PLACED])
|
|
{
|
|
if (PlaceDetpack(ent))
|
|
{
|
|
ent->client->ps.stats[STAT_USEABLE_PLACED] = 1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// ok, we placed it earlier. blow it up.
|
|
ent->client->ps.stats[STAT_USEABLE_PLACED] = 0;
|
|
DetonateDetpack(ent);
|
|
}
|
|
break;
|
|
|
|
case EV_USE_ITEM4: // portable shield
|
|
PlaceShield(ent); // fixme if we fail, perhaps just spawn it as a pickup
|
|
break;
|
|
|
|
case EV_USE_ITEM5: // decoy
|
|
PlaceDecoy(ent);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void BotTestSolid(vec3_t origin);
|
|
|
|
/*
|
|
==============
|
|
ClientThink
|
|
|
|
This will be called once for each client frame, which will
|
|
usually be a couple times for each server frame on fast clients.
|
|
|
|
If "g_syncronousClients 1" is set, this will be called exactly
|
|
once for each server frame, which makes for smooth demo recording.
|
|
==============
|
|
*/
|
|
void ClientThink_real( gentity_t *ent ) {
|
|
gclient_t *client;
|
|
pmove_t pm;
|
|
vec3_t oldOrigin;
|
|
int oldEventSequence;
|
|
int msec;
|
|
usercmd_t *ucmd;
|
|
|
|
client = ent->client;
|
|
|
|
// mark the time, so the connection sprite can be removed
|
|
ucmd = &ent->client->pers.cmd;
|
|
|
|
// sanity check the command time to prevent speedup cheating
|
|
if ( ucmd->serverTime > level.time + 200 ) {
|
|
ucmd->serverTime = level.time + 200;
|
|
// G_Printf("serverTime <<<<<\n" );
|
|
}
|
|
if ( ucmd->serverTime < level.time - 1000 ) {
|
|
ucmd->serverTime = level.time - 1000;
|
|
// G_Printf("serverTime >>>>>\n" );
|
|
}
|
|
|
|
msec = ucmd->serverTime - client->ps.commandTime;
|
|
// following others may result in bad times, but we still want
|
|
// to check for follow toggles
|
|
if ( msec < 1 && client->sess.spectatorState != SPECTATOR_FOLLOW ) {
|
|
return;
|
|
}
|
|
if ( msec > 200 ) {
|
|
msec = 200;
|
|
}
|
|
|
|
//
|
|
// check for exiting intermission
|
|
//
|
|
if ( level.intermissiontime ) {
|
|
ClientIntermissionThink( client );
|
|
return;
|
|
}
|
|
|
|
// Don't move while under intro sequence.
|
|
if (client->ps.introTime > level.time)
|
|
{ // Don't be visible either.
|
|
ent->s.eFlags |= EF_NODRAW;
|
|
SetClientViewAngle( ent, ent->s.angles );
|
|
ucmd->buttons = 0;
|
|
ucmd->weapon = 0;
|
|
// ucmd->angles[0] = ucmd->angles[1] = ucmd->angles[2] = 0;
|
|
ucmd->forwardmove = ucmd->rightmove = ucmd->upmove = 0;
|
|
// return;
|
|
}
|
|
/* else if (client->ps.pm_type == PM_FREEZE)
|
|
{
|
|
client->ps.pm_type = PM_NORMAL;
|
|
SetClientViewAngle( ent, ent->s.angles );
|
|
ent->s.eFlags &= ~EF_NODRAW;
|
|
}
|
|
*/
|
|
// spectators don't do much
|
|
if ( client->sess.sessionTeam == TEAM_SPECTATOR ) {
|
|
if ( client->sess.spectatorState == SPECTATOR_SCOREBOARD ) {
|
|
return;
|
|
}
|
|
SpectatorThink( ent, ucmd );
|
|
return;
|
|
}
|
|
|
|
// check for inactivity timer, but never drop the local client of a non-dedicated server
|
|
if ( !ClientInactivityTimer( client ) ) {
|
|
return;
|
|
}
|
|
|
|
// clear the rewards if time
|
|
if ( level.time > client->rewardTime ) {
|
|
client->ps.eFlags &= ~EF_AWARD_MASK;
|
|
}
|
|
|
|
if ( client->noclip ) {
|
|
client->ps.pm_type = PM_NOCLIP;
|
|
} else if ( client->ps.stats[STAT_HEALTH] <= 0 ) {
|
|
client->ps.pm_type = PM_DEAD;
|
|
} else {
|
|
client->ps.pm_type = PM_NORMAL;
|
|
}
|
|
|
|
client->ps.gravity = g_gravity.value;
|
|
|
|
// set speed
|
|
client->ps.speed = g_speed.value;
|
|
|
|
if ( client->ps.powerups[PW_HASTE] )
|
|
{
|
|
client->ps.speed *= 1.5;
|
|
}
|
|
else if (client->ps.powerups[PW_FLIGHT] )
|
|
{
|
|
client->ps.speed *= 1.25;
|
|
}
|
|
|
|
// set up for pmove
|
|
oldEventSequence = client->ps.eventSequence;
|
|
|
|
memset (&pm, 0, sizeof(pm));
|
|
|
|
pm.ps = &client->ps;
|
|
pm.cmd = *ucmd;
|
|
if ( pm.ps->pm_type == PM_DEAD ) {
|
|
pm.tracemask = MASK_PLAYERSOLID & ~CONTENTS_BODY;
|
|
}
|
|
else {
|
|
pm.tracemask = MASK_PLAYERSOLID;
|
|
}
|
|
pm.trace = trap_Trace;
|
|
pm.pointcontents = trap_PointContents;
|
|
pm.debugLevel = g_debugMove.integer;
|
|
pm.noFootsteps = ( g_dmflags.integer & DF_NO_FOOTSTEPS ) > 0;
|
|
|
|
VectorCopy( client->ps.origin, oldOrigin );
|
|
|
|
// perform a pmove
|
|
Pmove (&pm);
|
|
|
|
// save results of pmove
|
|
if ( ent->client->ps.eventSequence != oldEventSequence ) {
|
|
ent->eventTime = level.time;
|
|
}
|
|
BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue );
|
|
|
|
#if 1
|
|
// use the precise origin for linking
|
|
VectorCopy( ent->client->ps.origin, ent->r.currentOrigin );
|
|
#else
|
|
// use the snapped origin for linking so it matches client predicted versions
|
|
VectorCopy( ent->s.pos.trBase, ent->r.currentOrigin );
|
|
#endif
|
|
|
|
VectorCopy (pm.mins, ent->r.mins);
|
|
VectorCopy (pm.maxs, ent->r.maxs);
|
|
|
|
ent->waterlevel = pm.waterlevel;
|
|
ent->watertype = pm.watertype;
|
|
|
|
// execute client events
|
|
ClientEvents( ent, oldEventSequence );
|
|
|
|
// link entity now, after any personal teleporters have been used
|
|
trap_LinkEntity (ent);
|
|
if ( !ent->client->noclip ) {
|
|
G_TouchTriggers( ent );
|
|
}
|
|
|
|
//test for solid areas in the AAS file
|
|
BotTestSolid(ent->r.currentOrigin);
|
|
|
|
// touch other objects
|
|
ClientImpacts( ent, &pm );
|
|
|
|
// swap and latch button actions
|
|
client->oldbuttons = client->buttons;
|
|
client->buttons = ucmd->buttons;
|
|
client->latched_buttons |= client->buttons & ~client->oldbuttons;
|
|
|
|
// check for respawning
|
|
if ( client->ps.stats[STAT_HEALTH] <= 0 ) {
|
|
// wait for the attack button to be pressed
|
|
if ( level.time > client->respawnTime ) {
|
|
// forcerespawn is to prevent users from waiting out powerups
|
|
if ( g_forcerespawn.integer > 0 &&
|
|
( level.time - client->respawnTime ) > g_forcerespawn.integer * 1000 ) {
|
|
respawn( ent );
|
|
return;
|
|
}
|
|
|
|
// pressing attack or use is the normal respawn method
|
|
if ( ucmd->buttons & ( BUTTON_ATTACK | BUTTON_USE_HOLDABLE ) ) {
|
|
respawn( ent );
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// perform once-a-second actions
|
|
ClientTimerActions( ent, msec );
|
|
}
|
|
|
|
/*
|
|
==================
|
|
ClientThink
|
|
|
|
A new command has arrived from the client
|
|
==================
|
|
*/
|
|
void ClientThink( int clientNum ) {
|
|
gentity_t *ent;
|
|
|
|
ent = g_entities + clientNum;
|
|
trap_GetUsercmd( clientNum, &ent->client->pers.cmd );
|
|
|
|
// mark the time we got info, so we can display the
|
|
// phone jack if they don't get any for a while
|
|
ent->client->lastCmdTime = level.time;
|
|
|
|
if ( !g_syncronousClients.integer ) {
|
|
ClientThink_real( ent );
|
|
}
|
|
}
|
|
|
|
|
|
void G_RunClient( gentity_t *ent ) {
|
|
if ( !g_syncronousClients.integer ) {
|
|
return;
|
|
}
|
|
ent->client->pers.cmd.serverTime = level.time;
|
|
ClientThink_real( ent );
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
SpectatorClientEndFrame
|
|
|
|
==================
|
|
*/
|
|
void SpectatorClientEndFrame( gentity_t *ent ) {
|
|
gclient_t *cl;
|
|
|
|
// if we are doing a chase cam or a remote view, grab the latest info
|
|
if ( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) {
|
|
int clientNum;
|
|
|
|
clientNum = ent->client->sess.spectatorClient;
|
|
|
|
// team follow1 and team follow2 go to whatever clients are playing
|
|
if ( clientNum == -1 ) {
|
|
clientNum = level.follow1;
|
|
} else if ( clientNum == -2 ) {
|
|
clientNum = level.follow2;
|
|
}
|
|
if ( clientNum >= 0 ) {
|
|
cl = &level.clients[ clientNum ];
|
|
if ( cl->pers.connected == CON_CONNECTED && cl->sess.sessionTeam != TEAM_SPECTATOR ) {
|
|
ent->client->ps = cl->ps;
|
|
ent->client->ps.pm_flags |= PMF_FOLLOW;
|
|
return;
|
|
} else {
|
|
// drop them to free spectators unless they are dedicated camera followers
|
|
if ( ent->client->sess.spectatorClient >= 0 ) {
|
|
ent->client->sess.spectatorState = SPECTATOR_FREE;
|
|
ClientBegin( ent->client - level.clients );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( ent->client->sess.spectatorState == SPECTATOR_SCOREBOARD ) {
|
|
ent->client->ps.pm_flags |= PMF_SCOREBOARD;
|
|
} else {
|
|
ent->client->ps.pm_flags &= ~PMF_SCOREBOARD;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==============
|
|
ClientEndFrame
|
|
|
|
Called at the end of each server frame for each connected client
|
|
A fast client will have multiple ClientThink for each ClientEdFrame,
|
|
while a slow client may have multiple ClientEndFrame between ClientThink.
|
|
==============
|
|
*/
|
|
void ClientEndFrame( gentity_t *ent ) {
|
|
int i;
|
|
clientPersistant_t *pers;
|
|
|
|
if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) {
|
|
SpectatorClientEndFrame( ent );
|
|
return;
|
|
}
|
|
|
|
pers = &ent->client->pers;
|
|
|
|
// turn off any expired powerups
|
|
for ( i = 0 ; i < MAX_POWERUPS ; i++ ) {
|
|
if ( ent->client->ps.powerups[ i ] < level.time ) {
|
|
ent->client->ps.powerups[ i ] = 0;
|
|
}
|
|
}
|
|
|
|
// save network bandwidth
|
|
#if 0
|
|
if ( !g_syncronousClients->integer && ent->client->ps.pm_type == PM_NORMAL ) {
|
|
// FIXME: this must change eventually for non-sync demo recording
|
|
VectorClear( ent->client->ps.viewangles );
|
|
}
|
|
#endif
|
|
|
|
//
|
|
// If the end of unit layout is displayed, don't give
|
|
// the player any normal movement attributes
|
|
//
|
|
if ( level.intermissiontime ) {
|
|
return;
|
|
}
|
|
|
|
// burn from lava, etc
|
|
P_WorldEffects (ent);
|
|
|
|
// apply all the damage taken this frame
|
|
P_DamageFeedback (ent);
|
|
|
|
// add the EF_CONNECTION flag if we haven't gotten commands recently
|
|
if ( level.time - ent->client->lastCmdTime > 1000 ) {
|
|
ent->s.eFlags |= EF_CONNECTION;
|
|
} else {
|
|
ent->s.eFlags &= ~EF_CONNECTION;
|
|
}
|
|
|
|
ent->client->ps.stats[STAT_HEALTH] = ent->health; // FIXME: get rid of ent->health...
|
|
|
|
G_SetClientSound (ent);
|
|
|
|
// set the latest infor
|
|
BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue );
|
|
}
|
|
|
|
|