thirtyflightsofloving/awaken2/g_weapon.c
Knightmare66 59157b413d Clamped vid_fullscreen to 0, 1, or 2 before copying to dispType_t var.
Fixed railgun infinite loop (hang) bug in Awakening2 game DLL.
Added unique armor shard icon in 3ZB2 and Awakening2 game DLLs.
Updated changelog with missing changes from development changelog.
2021-07-20 22:14:01 -04:00

3843 lines
99 KiB
C

// g_weapon.c
#include "g_local.h"
//CW++
void Fireball_CheckEnv(edict_t *self);
//CW--
/*
=================
Fire_Lead
This is an internal support routine used for bullet/pellet based weapons.
=================
*/
static void Fire_Lead(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick, int te_impact, int hspread, int vspread, int mod)
{
trace_t tr;
vec3_t dir;
vec3_t forward;
vec3_t right;
vec3_t up;
vec3_t end;
vec3_t water_start;
float r;
float u;
qboolean water = false;
int content_mask = MASK_SHOT | MASK_WATER;
tr = gi.trace(self->s.origin, NULL, NULL, start, self, MASK_SHOT);
if (!(tr.fraction < 1.0))
{
vectoangles(aimdir, dir);
AngleVectors(dir, forward, right, up);
r = crandom() * hspread;
u = crandom() * vspread;
// Knightmare- adjust spread for expanded world size
#ifdef KMQUAKE2_ENGINE_MOD
r *= (WORLD_SIZE / 8192);
u *= (WORLD_SIZE / 8192);
#endif
VectorMA(start, WORLD_SIZE, forward, end); // was 8192.0
VectorMA(end, r, right, end);
VectorMA(end, u, up, end);
if (gi.pointcontents(start) & MASK_WATER)
{
water = true;
VectorCopy(start, water_start);
content_mask &= ~MASK_WATER;
}
tr = gi.trace(start, NULL, NULL, end, self, content_mask);
// see if we hit water
if (tr.contents & MASK_WATER)
{
int color;
water = true;
VectorCopy(tr.endpos, water_start);
if (!VectorCompare(start, tr.endpos))
{
if (tr.contents & CONTENTS_WATER)
{
if (strcmp(tr.surface->name, "*brwater") == 0)
color = SPLASH_BROWN_WATER;
else
color = SPLASH_BLUE_WATER;
}
else if (tr.contents & CONTENTS_SLIME)
color = SPLASH_SLIME;
else if (tr.contents & CONTENTS_LAVA)
color = SPLASH_LAVA;
else
color = SPLASH_UNKNOWN;
if (color != SPLASH_UNKNOWN)
{
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_SPLASH);
gi.WriteByte(8);
gi.WritePosition(tr.endpos);
gi.WriteDir(tr.plane.normal);
gi.WriteByte(color);
gi.multicast(tr.endpos, MULTICAST_PVS);
}
// change bullet's course when it enters water
VectorSubtract(end, start, dir);
vectoangles(dir, dir);
AngleVectors(dir, forward, right, up);
r = crandom() * hspread * 2.0;
u = crandom() * vspread * 2.0;
// Knightmare- adjust spread for expanded world size
#ifdef KMQUAKE2_ENGINE_MOD
r *= (WORLD_SIZE / 8192);
u *= (WORLD_SIZE / 8192);
#endif
VectorMA(water_start, WORLD_SIZE, forward, end); // was 8192.0
VectorMA(end, r, right, end);
VectorMA(end, u, up, end);
}
// re-trace ignoring water this time
tr = gi.trace(water_start, NULL, NULL, end, self, MASK_SHOT);
}
}
// send gun puff / flash
if (!(tr.surface && (tr.surface->flags & SURF_SKY)))
{
if (tr.fraction < 1.0)
{
//CW++
// Destroy player's nearby Traps and C4 bundles (as gi.trace will ignore player's own entities).
if (self->next_node)
{
edict_t *check;
edict_t *index;
qboolean finished = false;
index = self->next_node;
while (index && !finished)
{
check = index;
if (index->next_node)
index = index->next_node;
else
finished = true;
if (VecRange(tr.endpos, check->s.origin) < 10.0)
{
if (check->die == C4_DieFromDamage)
C4_Die(check);
else if (check->die == Trap_DieFromDamage)
Trap_Die(check);
else
gi.dprintf("BUG: Invalid next_node pointer in Fire_Lead().\nPlease contact musashi@planetquake.com\n");
}
}
}
//CW--
if (tr.ent->takedamage)
T_Damage(tr.ent, self, self, aimdir, tr.endpos, tr.plane.normal, damage, kick, DAMAGE_BULLET, mod);
else
{
//CW++
if (tr.surface && !(tr.surface->flags & SURF_SKY))
//CW--
{
gi.WriteByte(svc_temp_entity);
gi.WriteByte(te_impact);
gi.WritePosition(tr.endpos);
gi.WriteDir(tr.plane.normal);
gi.multicast(tr.endpos, MULTICAST_PVS);
//CW++
if (self->client)
PlayerNoise(self, tr.endpos, PNOISE_IMPACT);
//CW--
}
}
}
}
// if went through water, determine where the end and make a bubble trail
if (water)
{
vec3_t pos;
VectorSubtract(tr.endpos, water_start, dir);
VectorNormalize(dir);
VectorMA(tr.endpos, -2.0, dir, pos);
if (gi.pointcontents(pos) & MASK_WATER)
VectorCopy(pos, tr.endpos);
else
tr = gi.trace(pos, NULL, NULL, water_start, tr.ent, MASK_WATER);
VectorAdd(water_start, tr.endpos, pos);
VectorScale(pos, 0.5, pos);
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_BUBBLETRAIL2); //CW
gi.WritePosition(water_start);
gi.WritePosition(tr.endpos);
gi.multicast(pos, MULTICAST_PVS);
}
}
/*
=================
Fire_Bullet
Fires a single round.
=================
*/
void Fire_Bullet(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick, int hspread, int vspread, int mod)
{
Fire_Lead(self, start, aimdir, damage, kick, TE_GUNSHOT, hspread, vspread, mod);
}
/*
=================
Fire_Shotgun
Shoots shotgun pellets.
=================
*/
void Fire_Shotgun(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick, int hspread, int vspread, int count, int mod)
{
int i;
for (i = 0; i < count; i++)
Fire_Lead(self, start, aimdir, damage, kick, TE_SHOTGUN, hspread, vspread, mod);
}
/*
=================
Fire_Blaster
Fires a single blaster bolt.
=================
*/
void Blaster_Touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
if (other == self->owner)
return;
if (surf && (surf->flags & SURF_SKY))
{
G_FreeEdict(self);
return;
}
//CW++
if (self->owner && self->owner->client)
PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT);
//CW--
if (other->takedamage)
T_Damage(other, self, self->owner, self->velocity, self->s.origin, plane->normal, self->dmg, 1, DAMAGE_ENERGY, MOD_TARGET_BLASTER); //CW
else
{
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_BLASTER);
gi.WritePosition(self->s.origin);
if (!plane)
gi.WriteDir(vec3_origin);
else
gi.WriteDir(plane->normal);
gi.multicast(self->s.origin, MULTICAST_PVS);
}
G_FreeEdict(self);
}
void Fire_Blaster(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int speed, int effect) //CW
{
edict_t *bolt;
trace_t tr;
bolt = G_Spawn();
bolt->solid = SOLID_BBOX;
bolt->movetype = MOVETYPE_FLYMISSILE;
bolt->clipmask = MASK_SHOT;
bolt->svflags = SVF_DEADMONSTER | SVF_PROJECTILE; //CW
bolt->s.effects |= effect;
bolt->s.sound = gi.soundindex("misc/lasfly.wav");
bolt->owner = self;
bolt->dmg = damage;
bolt->classname = "bolt";
bolt->touch = Blaster_Touch;
bolt->nextthink = level.time + BLASTER_LIVETIME; //CW
bolt->think = G_FreeEdict;
bolt->s.modelindex = gi.modelindex("models/objects/laser/tris.md2");
VectorCopy(start, bolt->s.origin);
VectorCopy(start, bolt->s.old_origin);
vectoangles(aimdir, bolt->s.angles);
VectorScale(aimdir, speed, bolt->velocity);
gi.linkentity(bolt);
tr = gi.trace(self->s.origin, NULL, NULL, bolt->s.origin, bolt, MASK_SHOT);
if (tr.fraction < 1.0)
{
VectorMA(bolt->s.origin, -10.0, aimdir, bolt->s.origin);
bolt->touch(bolt, tr.ent, NULL, NULL);
}
}
/*
=================
Fire_Rocket
=================
*/
//CW++
void Guided_Rocket_Think(edict_t *self)
{
// Based on code by David Hyde.
trace_t tr;
vec3_t dir;
vec3_t forward;
vec3_t right;
vec3_t end;
float speed;
// Trace a line along player's current viewing direction; if it intersects a solid, reset
// the target to be this endpoint. Otherwise, the target is the end of the line.
AngleVectors(self->owner->client->v_angle, forward, right, NULL);
VectorMA(self->owner->s.origin, WORLD_SIZE, forward, end); // was 8192.0
tr = gi.trace(self->owner->s.origin, NULL, NULL, end, self, MASK_SHOT);
// Adjust rocket's velocity and angles to aim towards the target point.
VectorSubtract(tr.endpos, self->s.origin, dir);
VectorNormalize(dir);
VectorAdd(dir, self->movedir, dir);
VectorNormalize(dir);
VectorCopy(dir, self->movedir);
vectoangles(dir, self->s.angles);
speed = VectorLength(self->velocity);
VectorScale(dir, speed, self->velocity);
gi.linkentity(self);
self->nextthink = level.time + FRAMETIME;
}
//CW--
void Rocket_Touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
vec3_t origin;
if (other == self->owner)
return;
if (surf && (surf->flags & SURF_SKY))
{
G_FreeEdict(self);
return;
}
// calculate position for the explosion entity
VectorMA(self->s.origin, -0.02, self->velocity, origin);
if (other->takedamage)
T_Damage(other, self, self->owner, self->velocity, self->s.origin, plane->normal, self->dmg, 0, 0, MOD_ROCKET);
T_RadiusDamage(self, self->owner, self->radius_dmg, other, self->dmg_radius, MOD_R_SPLASH);
gi.WriteByte(svc_temp_entity);
gi.WriteByte((self->waterlevel)?TE_ROCKET_EXPLOSION_WATER:TE_ROCKET_EXPLOSION);
gi.WritePosition(origin);
gi.multicast(self->s.origin, MULTICAST_PHS);
//CW++
if (self->owner && self->owner->client)
PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT);
//CW--
G_FreeEdict(self);
}
void Fire_Rocket(edict_t *self, vec3_t start, vec3_t aimdir, int damage, float speed, float damage_radius, int radius_damage, qboolean guided) //CW
{
edict_t *rocket;
rocket = G_Spawn();
rocket->solid = SOLID_BBOX;
rocket->movetype = MOVETYPE_FLYMISSILE;
rocket->clipmask = MASK_SHOT;
rocket->s.effects |= EF_ROCKET;
rocket->owner = self;
rocket->touch = Rocket_Touch;
rocket->dmg = damage;
rocket->radius_dmg = radius_damage;
rocket->dmg_radius = damage_radius;
rocket->s.sound = gi.soundindex("weapons/rockfly.wav");
rocket->classname = "rocket";
rocket->s.modelindex = gi.modelindex("models/objects/rocket/tris.md2");
VectorCopy(start, rocket->s.origin);
VectorCopy(aimdir, rocket->movedir);
vectoangles(aimdir, rocket->s.angles);
VectorScale(aimdir, speed, rocket->velocity);
//CW++
rocket->svflags = SVF_DEADMONSTER;
rocket->wep_proj = true;
if (!guided)
{
rocket->spawnflags = 0;
rocket->svflags |= SVF_PROJECTILE;
rocket->nextthink = level.time + (8000.0 / speed);
rocket->think = G_FreeEdict;
}
else
{
rocket->spawnflags = 1;
rocket->nextthink = level.time + FRAMETIME;
rocket->think = Guided_Rocket_Think;
}
//CW--
gi.linkentity(rocket);
}
/*
=================
Fire_Rail
=================
*/
void Fire_Rail (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick)
{
vec3_t from;
vec3_t end;
trace_t tr;
edict_t *ignore;
int mask, i=0;
qboolean water;
VectorMA (start, WORLD_SIZE, aimdir, end); // was 8192.0
VectorCopy (start, from);
ignore = self;
water = false;
mask = MASK_SHOT | CONTENTS_SLIME | CONTENTS_LAVA;
while (ignore && i<256)
{
tr = gi.trace(from, NULL, NULL, end, ignore, mask);
if (tr.contents & (CONTENTS_SLIME | CONTENTS_LAVA))
{
mask &= ~(CONTENTS_SLIME | CONTENTS_LAVA);
water = true;
}
else
{
//ZOID--added so rail goes through SOLID_BBOX entities (gibs, etc)
if (tr.ent->client || (tr.ent->solid == SOLID_BBOX) || (tr.ent->svflags & SVF_MONSTER))
ignore = tr.ent;
else
ignore = NULL;
if (tr.ent->takedamage && (tr.ent != self))
T_Damage(tr.ent, self, self, aimdir, tr.endpos, tr.plane.normal, damage, kick, 0, MOD_RAILGUN);
}
VectorCopy(tr.endpos, from);
i++;
}
//CW++
// Destroy player's nearby Traps and C4 bundles (as gi.trace will ignore player's own entities).
if (self->next_node)
{
edict_t *check;
edict_t *index;
qboolean finished = false;
index = self->next_node;
while (index && !finished)
{
check = index;
if (index->next_node)
index = index->next_node;
else
finished = true;
if (VecRange(tr.endpos, check->s.origin) < 10.0)
{
if (check->die == C4_DieFromDamage)
C4_Die(check);
else if (check->die == Trap_DieFromDamage)
Trap_Die(check);
else
gi.dprintf("BUG: Invalid next_node pointer in Fire_Rail().\nPlease contact musashi@planetquake.com\n");
}
}
}
//CW--
// send gun puff / flash
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_RAILTRAIL);
gi.WritePosition(start);
gi.WritePosition(tr.endpos);
gi.multicast(self->s.origin, MULTICAST_PHS);
if (water)
{
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_RAILTRAIL);
gi.WritePosition(start);
gi.WritePosition(tr.endpos);
gi.multicast(tr.endpos, MULTICAST_PHS);
}
//CW++
if (self->client)
PlayerNoise(self, tr.endpos, PNOISE_IMPACT);
//CW--
}
//CW++
//-------------------
// AWAKENING WEAPONS
//-------------------
/*
======================================================================
C4 subroutines
spawnflags (internal use only):
1 = held too long by player
4 = stuck to a rotating brush model
8 = stuck to a moving brush model
16 = proximity fuse has been activated
32 = proximity fuse lifetime was exceeded
======================================================================
*/
void C4_Explode(edict_t *self)
{
vec3_t origin;
int mod;
if (self->enemy)
{
float points;
vec3_t v;
vec3_t dir;
VectorAdd(self->enemy->mins, self->enemy->maxs, v);
VectorMA(self->enemy->s.origin, 0.5, v, v);
VectorSubtract(self->s.origin, v, v);
points = self->dmg - (0.5 * VectorLength(v));
VectorSubtract(self->enemy->s.origin, self->s.origin, dir);
T_Damage(self->enemy, self, self->owner, dir, self->s.origin, vec3_origin, (int)points, (int)points, DAMAGE_RADIUS, MOD_C4);
}
if (self->spawnflags & 1)
mod = MOD_C4_HELD;
else if (self->spawnflags & 16)
mod = MOD_C4_PROXIMITY;
else if (self->spawnflags & 32)
mod = MOD_C4_LIFETIME;
else
mod = MOD_C4;
T_RadiusDamage(self, self->owner, self->dmg, self->enemy, self->dmg_radius, mod);
VectorMA(self->s.origin, -0.02, self->velocity, origin);
gi.WriteByte(svc_temp_entity);
if (self->waterlevel)
gi.WriteByte((self->groundentity)?TE_GRENADE_EXPLOSION_WATER:TE_ROCKET_EXPLOSION_WATER);
else
gi.WriteByte((self->groundentity)?TE_GRENADE_EXPLOSION:TE_ROCKET_EXPLOSION);
gi.WritePosition(origin);
gi.multicast(self->s.origin, MULTICAST_PHS);
if (self->owner && self->owner->client)
PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT);
TList_DelNode(self);
G_FreeEdict(self);
}
void C4_ProximityCheck(edict_t *self)
{
edict_t *cl_ent = NULL;
// If the proximity fuse lifetime has been exceeded, and hasn't yet been triggered, go BOOM!
if (self->wait && (level.time >= self->wait) && !self->delay)
{
self->spawnflags = 32;
C4_Explode(self);
return;
}
self->nextthink = level.time + FRAMETIME;
// Valid targets are live players who aren't our owner, or team members of our owner, within our
// detection range.
while ((cl_ent = FindRadius(cl_ent, self->s.origin, sv_c4_proximity_range->value)) != NULL)
{
if (!cl_ent->client)
continue;
if (cl_ent == self->owner)
continue;
if (cl_ent->health < 1)
continue;
if (!visible(self, cl_ent))
continue;
if ((int)sv_gametype->value > G_FFA)
{
if (self->owner->client && (cl_ent->client->resp.ctf_team == self->owner->client->resp.ctf_team))
continue;
}
// Target is valid, so trigger the fuse.
// NB: Explosion-timing must be done differently if stuck to a moving brush model, as the think
// function is already taken by C4_MoveWithEnt().
self->spawnflags |= 16;
gi.sound(self, CHAN_VOICE, gi.soundindex("weapons/c4/timer.wav"), 1, ATTN_NORM, 0);
if (!(self->spawnflags & 4) && !(self->spawnflags & 8))
{
self->think = C4_Explode;
self->nextthink = level.time + sv_c4_proximity_delay->value;
}
else
self->delay = level.time + sv_c4_proximity_delay->value;
break;
}
}
void C4_Die(edict_t *self)
{
int n;
if (self == NULL)
{
gi_centerprintf(self->owner, "BUG: C4_Die() called with null edict.\nPlease contact musashi@planetquake.com\n");
return;
}
if (self->die != C4_DieFromDamage)
{
gi.dprintf("BUG: C4_Die() called for a non-trap edict.\n");
gi.dprintf(" classname = %s\n", self->classname);
if (self->owner && self->owner->client)
gi.dprintf(" owner->name = %s\n", self->owner->client->pers.netname);
return;
}
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_SPARKS);
gi.WritePosition(self->s.origin);
gi.WriteDir(vec3_origin);
gi.multicast(self->s.origin, MULTICAST_PVS);
n = rand() % 4;
while (n--)
ThrowDebris(self, "models/objects/debris2/tris.md2", 2, self->s.origin);
TList_DelNode(self);
G_FreeEdict(self);
}
void C4_DieFromDamage(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point)
{
C4_Die(self);
}
void C4_CheckEnv(edict_t *self)
{
vec3_t point;
vec3_t puff_start;
int cont;
// Pop if we detect slime or lava just below us (this makes the puff more visible compared with
// checking the pointcontents at our actual origin).
point[0] = self->s.origin[0];
point[1] = self->s.origin[1];
point[2] = self->s.origin[2] + self->mins[2];
cont = gi.pointcontents(point);
if (cont & (CONTENTS_LAVA | CONTENTS_SLIME))
{
puff_start[0] = self->s.origin[0];
puff_start[1] = self->s.origin[1];
puff_start[2] = self->s.origin[2] + 20.0;
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_HEATBEAM_SPARKS);
gi.WritePosition(puff_start);
gi.WriteDir(vec3_up);
gi.multicast(puff_start, MULTICAST_PVS);
C4_Die(self);
return;
}
else if (cont & CONTENTS_SOLID)
{
TList_DelNode(self);
G_FreeEdict(self);
return;
}
self->nextthink = level.time + FRAMETIME;
}
void C4_RotateWithEnt(edict_t *self)
{
edict_t *t;
float vec1;
float vec2;
float radius;
float angle;
// Sanity check.
if (!self->target_ent)
{
gi_centerprintf(self->owner, "BUG: C4_RotateWithEnt() called with no target_ent.\nPlease contact musashi@planetquake.com\n");
C4_Die(self);
return;
}
// Adjust origin based on the axis of rotation.
t = self->target_ent;
if (t->movedir[0]) // y-axis
{
vec1 = self->s.origin[0] - t->s.origin[0];
vec2 = self->s.origin[2] - t->s.origin[2];
radius = sqrt(vec1*vec1 + vec2*vec2);
angle = atan2(vec2, vec1) - DEG2RAD(t->avelocity[0] * FRAMETIME);
self->s.origin[0] = t->s.origin[0] + (radius * cos(angle));
self->s.origin[2] = t->s.origin[2] + (radius * sin(angle));
self->s.angles[0] = -RAD2DEG(angle);
}
else if (t->movedir[1]) // z-axis
{
vec1 = self->s.origin[0] - t->s.origin[0];
vec2 = self->s.origin[1] - t->s.origin[1];
radius = sqrt(vec1*vec1 + vec2*vec2);
angle = atan2(vec2, vec1) + DEG2RAD(t->avelocity[1] * FRAMETIME);
self->s.origin[0] = t->s.origin[0] + (radius * cos(angle));
self->s.origin[1] = t->s.origin[1] + (radius * sin(angle));
self->s.angles[1] = RAD2DEG(angle);
}
else // x-axis
{
vec1 = self->s.origin[1] - t->s.origin[1];
vec2 = self->s.origin[2] - t->s.origin[2];
radius = sqrt(vec1*vec1 + vec2*vec2);
angle = atan2(vec2, vec1) + DEG2RAD(t->avelocity[2] * FRAMETIME);
self->s.origin[1] = t->s.origin[1] + (radius * cos(angle));
self->s.origin[2] = t->s.origin[2] + (radius * sin(angle));
self->s.angles[2] = -RAD2DEG(angle);
}
}
void C4_MoveWithEnt(edict_t *self)
{
// Sanity check.
if (!self->target_ent)
{
gi_centerprintf(self->owner, "BUG: C4_MoveWithEnt() called with no target_ent.\nPlease contact musashi@planetquake.com\n");
C4_Die(self);
return;
}
// If the proximity fuse has been activated, and the time is right, go BOOM!
if ((self->spawnflags & 16) && (level.time >= self->delay))
{
C4_Explode(self);
return;
}
// If the proximity fuse lifetime has been exceeded, and hasn't yet been triggered, go BOOM!
if ((int)sv_c4_proximity->value && self->wait && (level.time >= self->wait) && !self->delay)
{
self->spawnflags = 32;
C4_Explode(self);
return;
}
// Handle C4 movement for the case that it's stuck to a moving brush model.
if (self->spawnflags & 4) // rotating brush model
C4_RotateWithEnt(self);
else if (self->spawnflags & 8) // non-rotating brush model
VectorAdd(self->target_ent->s.origin, self->move_origin, self->s.origin);
// Destroy ourself if we're inside a solid (eg. scraped against the side of a lift shaft),
// or in slime/lava.
if (gi.pointcontents(self->s.origin) & (CONTENTS_SOLID | CONTENTS_LAVA | CONTENTS_SLIME))
{
C4_Die(self);
return;
}
// Perform proximity detection if flagged to do so.
if ((int)sv_c4_proximity->value && !(self->spawnflags & 16))
C4_ProximityCheck(self);
self->nextthink = level.time + FRAMETIME;
gi.linkentity(self);
}
void C4_Touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
edict_t *ent = NULL;
// If the other entity has been freed (eg. as happens when two touchbang C4s touch each other),
// then don't bother running the rest of the touch function (see next code fragment).
if (!other->inuse)
return;
// Pop if we hit another C4.
if (other->die == C4_DieFromDamage)
{
C4_Die(self);
return;
}
// Ignore weapon projectiles.
if (other->wep_proj)
return;
// Don't explode if it's our owner who is doing the touching.
if (other == self->owner)
return;
// Don't explode if we're a touchbang C4 and it's a team-mate of our owner who is doing the
// touching, as long as we've already landed on the ground.
if ((int)sv_c4_touchbang->value && self->show_hostile && ((int)sv_gametype->value > G_FFA))
{
if (self->owner->client && other->client && (other->client->resp.ctf_team == self->owner->client->resp.ctf_team))
return;
}
// Vanish if we hit a sky surface.
if (surf && (surf->flags & SURF_SKY))
{
TList_DelNode(self);
G_FreeEdict(self);
return;
}
// Don't explode if we hit something that can't be damaged (eg. the floor). Instead, stick to it.
if (!other->takedamage)
{
// First, a final check that we're not in slime or lava.
if (gi.pointcontents(self->s.origin) & (CONTENTS_SOLID | CONTENTS_LAVA | CONTENTS_SLIME))
{
C4_Die(self);
return;
}
// Next, check that we're not too close to a spawn point or teleporter destination
// (just for proximity and touch-sensitive C4s).
if (((int)sv_c4_proximity->value || (int)sv_c4_touchbang->value) && ((int)sv_c4_spawn_range->value > 0))
{
while ((ent = FindPointRadius(ent, self->s.origin, sv_c4_spawn_range->value)) != NULL)
{
if (ent->client)
continue;
if (ent->wep_proj)
continue;
if (!ent->classname)
continue;
if (ent->style == ENT_ID_INTERMISSION)
continue;
if ((ent->style == ENT_ID_PLAYER_SPAWN) || (ent->style == ENT_ID_TELE_DEST))
{
C4_Die(self);
return;
}
}
}
// Seems OK, so let's get on with the sticking!
self->s.effects = 0;
VectorMA(self->s.origin, -0.001, self->velocity, self->s.origin);
VectorClear(self->velocity);
VectorClear(self->avelocity);
self->movetype = MOVETYPE_NONE;
self->show_hostile = true; // abuse flag
VectorClear(self->s.angles);
if (plane->normal)
{
if ((plane->normal[2] > 0.8) || (plane->normal[2] < -0.8))
self->s.angles[0] += 90.0;
}
// If we hit a brush model that might be in motion, then move with it.
if ((other->blocked == plat_blocked) || (other->blocked == door_blocked) || (other->blocked == train_blocked) || (other->blocked == rotating_blocked))
{
VectorSubtract(self->s.origin, other->s.origin, self->move_origin);
self->target_ent = other;
self->think = C4_MoveWithEnt;
self->nextthink = level.time + FRAMETIME;
if ((other->blocked == rotating_blocked) || (other->style == ENT_ID_DOOR_ROTATING))
{
self->spawnflags |= 4;
if (other->movedir[0] || other->movedir[2])
self->s.angles[0] = 0;
}
else
self->spawnflags |= 8;
}
// Start scanning for nearby enemies if proximity detection is flagged, unless stuck
// to a moving brush model (function will be called separately).
if ((int)sv_c4_proximity->value)
{
if (!(self->spawnflags & 4) && !(self->spawnflags & 8))
{
self->think = C4_ProximityCheck;
self->nextthink = level.time + FRAMETIME;
}
// Proximity C4 bundles have a limited lifetime, if flagged.
if (sv_c4_proximity_life->value)
self->wait = level.time + sv_c4_proximity_life->value;
}
gi.linkentity(self);
if (random() > 0.5)
gi.sound(self, CHAN_VOICE, gi.soundindex("weapons/c4/bounce1.wav"), 1, ATTN_NORM, 0);
else
gi.sound(self, CHAN_VOICE, gi.soundindex("weapons/c4/bounce2.wav"), 1, ATTN_NORM, 0);
}
// else go BOOM!
else
{
self->enemy = other;
C4_Explode(self);
}
// A stickied C4 bundle will now only explode on contact if flagged to do so.
if (!(int)sv_c4_touchbang->value)
self->touch = NULL;
}
void Fire_C4(edict_t *self, vec3_t start, vec3_t aimdir, int damage, float speed, float damage_radius, qboolean held)
{
edict_t *c4;
vec3_t dir;
vec3_t forward;
vec3_t right;
vec3_t up;
c4 = G_Spawn();
c4->solid = SOLID_BBOX;
c4->movetype = MOVETYPE_TOSS;
c4->takedamage = DAMAGE_YES;
c4->clipmask = MASK_SHOT;
c4->monsterinfo.aiflags = AI_NOSTEP;
c4->health = 1;
c4->mass = 2;
c4->s.effects |= EF_GRENADE;
c4->dmg = damage;
c4->dmg_radius = damage_radius;
c4->classname = "c4";
c4->owner = self;
c4->die = C4_DieFromDamage;
c4->touch = C4_Touch;
c4->think = C4_CheckEnv;
c4->nextthink = level.time + FRAMETIME;
c4->model = "models/objects/grenade/tris.md2";
c4->s.modelindex = gi.modelindex(c4->model);
VectorSet(c4->mins, -3.0, -3.0, -3.0);
VectorSet(c4->maxs, 3.0, 3.0, 3.0);
if ((int)sv_gametype->value > G_FFA)
{
if (self->client->resp.ctf_team == CTF_TEAM1)
c4->s.skinnum = 1;
else if (self->client->resp.ctf_team == CTF_TEAM2)
c4->s.skinnum = 2;
}
vectoangles(aimdir, dir);
AngleVectors(dir, forward, right, up);
VectorCopy(start, c4->s.origin);
VectorScale(aimdir, speed, c4->velocity);
VectorMA(c4->velocity, 200.0, up, c4->velocity);
VectorSet(c4->avelocity, 100.0, 100.0, 100.0);
TList_AddNode(c4);
if (held)
{
c4->spawnflags = 1;
C4_Explode(c4);
}
else
gi.linkentity(c4);
}
/*
======================================================================
TRAPS subroutines
spawnflags (internal use only):
1 = held too long by player, so auto-tractoring them
2 = tractor beam pulling power is zero (apply damage only)
4 = stuck to a rotating brush model [*]
8 = stuck to a moving brush model
16 = proximity-detection has been activated
32 = hook has been fired at enemy
64 = tractor beam has been activated
128 = [*] on positive radial surface
256 = [*] on negative radial surface
512 = [*] on circumferential surface
======================================================================
*/
void Trap_CheckEnv(edict_t *self);
void Trap_TractorBeam(edict_t *self)
{
vec3_t start;
vec3_t end;
vec3_t dir;
int power;
int mod;
// Sanity checks.
if (!self->enemy)
{
gi_centerprintf(self->owner, "BUG: Trap_TractorBeam() called with no enemy.\nPlease contact musashi@planetquake.com\n");
Trap_Die(self);
return;
}
if (!self->enemy->inuse)
{
Trap_Die(self);
return;
}
// Draw a laser beam between the Trap and the player who's caught. Apply damage every frame, but only
// play the ripping sound once per second. Apply a momentum change to the target if flagged to do so.
VectorCopy(self->s.origin, start);
VectorCopy(self->enemy->s.origin, end);
end[2] += self->enemy->viewheight - 8.0;
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_BFG_LASER);
gi.WritePosition(start);
gi.WritePosition(end);
gi.multicast(start, MULTICAST_PHS);
VectorSubtract(start, end, dir);
mod = (self->spawnflags & 1) ? MOD_TRAP_HELD : MOD_TRAP;
power = (self->spawnflags & 2) ? 0 : self->count;
T_Damage(self->enemy, self, self->owner, dir, end, vec3_origin, self->radius_dmg, power, 0, mod);
if (level.time > self->wait)
{
gi.sound(self, CHAN_WEAPON, gi.soundindex("weapons/hook/rip.wav"), 1, ATTN_NORM, 0);
self->wait = level.time + TRAP_DMG_RATE;
}
self->nextthink = level.time + FRAMETIME;
if (self->spawnflags & 1)
Trap_CheckEnv(self);
}
void Trap_Hook_Die(edict_t *self)
{
gi.sound(self, CHAN_WEAPON, gi.soundindex("weapons/hook/hit_wall.wav"), 1, ATTN_NORM, 0);
Trap_Die(self->owner);
self->touch = NULL;
self->think = G_FreeEdict;
self->nextthink = level.time + FRAMETIME;
}
void Trap_Hook_Touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
qboolean hit_fake = false;
// Ignore the Trap that fired us.
if (other == self->owner)
return;
// Ignore weapon projectiles.
if (other->wep_proj)
return;
// Destroy ourself and our parent Trap if...
// ...our parent Trap has been destroyed in the meantime.
if (!self->owner->inuse) // prevents server crashes
{
if (other->takedamage)
{
T_Damage(other, self, self->goalentity, self->velocity, self->s.origin, plane->normal, self->dmg, 1, 0, MOD_TRAP);
gi.sound(self, CHAN_WEAPON, gi.soundindex("weapons/hook/hit_body.wav"), 1, ATTN_NORM, 0);
}
else
gi.sound(self, CHAN_WEAPON, gi.soundindex("weapons/hook/hit_wall.wav"), 1, ATTN_NORM, 0);
self->think = G_FreeEdict;
self->nextthink = level.time + FRAMETIME;
self->touch = NULL;
return;
}
// ...we hit someone who's invulnerable.
if (other->client && (other->client->invincible_framenum > level.framenum))
{
Trap_Hook_Die(self);
return;
}
// ...we hit a sky surface.
if (surf && (surf->flags & SURF_SKY))
{
Trap_Hook_Die(self);
return;
}
// ...we hit something that can't be damaged (eg. the floor).
if (!other->takedamage)
{
Trap_Hook_Die(self);
return;
}
// ...we hit a dead body.
if (other->health < 1)
{
T_Damage(other, self, self->goalentity, self->velocity, self->s.origin, plane->normal, self->dmg, 1, 0, MOD_TRAP);
Trap_Hook_Die(self);
return;
}
// ... we hit team members of our parent Trap's owner in CTF/TDM/ASLT.
if ((int)sv_gametype->value > G_FFA)
{
if (other->client && self->owner->owner->client && (other->client->resp.ctf_team == self->owner->owner->client->resp.ctf_team))
{
Trap_Hook_Die(self);
return;
}
}
// Mark the entity as being the target of the parent Trap's tractor beam if they're a player.
if (other->client)
{
other->tractored = true;
self->owner->enemy = other;
self->owner->wait = level.time + TRAP_DMG_RATE;
self->owner->nextthink = level.time + FRAMETIME;
if (!(self->owner->spawnflags & 4) && !(self->owner->spawnflags & 8))
self->owner->think = Trap_TractorBeam;
else
self->owner->spawnflags |= 64;
if (other->target_ent && other->target_ent->client && other->target_ent->client->spycam)
hit_fake = true;
}
T_Damage(other, self, self->goalentity, self->velocity, self->s.origin, plane->normal, self->dmg, 1, 0, MOD_TRAP);
gi.sound(self, CHAN_WEAPON, gi.soundindex("weapons/hook/hit_body.wav"), 1, ATTN_NORM, 0);
// If we've hit something damage-able that isn't a player, pop our parent Trap.
if (!other->client && !hit_fake)
Trap_Die(self->owner);
// Vanish ("like an old oak table").
self->think = G_FreeEdict;
self->nextthink = level.time + FRAMETIME;
self->touch = NULL;
}
void Fire_Trap_Hook(edict_t *self, vec3_t start, vec3_t aimdir, int damage, float speed)
{
edict_t *hook;
hook = G_Spawn();
hook->solid = SOLID_BBOX;
hook->movetype = MOVETYPE_FLYMISSILE;
hook->clipmask = MASK_SHOT;
hook->svflags = SVF_DEADMONSTER | SVF_PROJECTILE;
hook->s.effects |= EF_GREENGIB;
hook->s.renderfx = RF_FULLBRIGHT;
hook->dmg = damage;
hook->classname = "trap_hook";
hook->wep_proj = true;
hook->owner = self;
hook->goalentity = self->owner;
hook->touch = Trap_Hook_Touch;
hook->model = "models/objects/hook/tris.md2";
hook->s.modelindex = hook_index;
VectorCopy(start, hook->s.origin);
VectorCopy(start, hook->s.old_origin);
vectoangles(aimdir, hook->s.angles);
VectorScale(aimdir, speed, hook->velocity);
gi.linkentity(hook);
}
void Trap_ShootHook(edict_t *self)
{
vec3_t end;
vec3_t dir;
float time;
VectorCopy(self->enemy->s.origin, end);
// Lead the target.
VectorSubtract(end, self->s.origin, dir);
time = VectorLength(dir) / sv_trap_hook_speed->value;
end[0] += (time * self->enemy->velocity[0]);
end[1] += (time * self->enemy->velocity[1]);
VectorSubtract(end, self->s.origin, dir);
VectorNormalize(dir);
gi.sound(self, CHAN_WEAPON, gi.soundindex("makron/blaster.wav"), 1, ATTN_NORM, 0);
if (self->owner && self->owner->client)
PlayerNoise(self->owner, self->s.origin, PNOISE_WEAPON);
Fire_Trap_Hook(self, self->s.origin, dir, self->dmg, sv_trap_hook_speed->value);
}
void Trap_ProximityCheck(edict_t *self)
{
edict_t *ent = NULL;
while ((ent = FindRadius(ent, self->s.origin, sv_trap_proximity_range->value)) != NULL)
{
// Don't target ourself.
if (ent == self)
continue;
// Don't target noclipped players.
if (ent->movetype == MOVETYPE_NOCLIP)
continue;
// Don't target team members of our owner in CTF/TDM/ASLT, or their Traps and C4 bundles,
// or our owner's.
if ((int)sv_gametype->value > G_FFA)
{
if (ent->client && self->owner->client && (ent->client->resp.ctf_team == self->owner->client->resp.ctf_team))
continue;
}
if ((ent->die == Trap_DieFromDamage) || (ent->die == C4_DieFromDamage))
{
if (ent->owner == self->owner)
continue;
if ((int)sv_gametype->value > G_FFA)
{
if (ent->owner->client && self->owner->client && (ent->owner->client->resp.ctf_team == self->owner->client->resp.ctf_team))
continue;
}
}
// Target any visible enemy players, Traps or C4s (apart from ourself or our owner).
if (visible(self, ent) && ent->takedamage && (ent != self->owner) && (ent->health > 0))
{
if (!ent->client && (ent->die != Trap_DieFromDamage) && (ent->die != C4_DieFromDamage))
continue;
self->enemy = ent;
if (!(self->spawnflags & 4) && !(self->spawnflags & 8))
self->think = Trap_ShootHook;
break;
}
}
self->nextthink = level.time + FRAMETIME;
}
void Trap_Arm_ProximityCheck(edict_t *self)
{
gi.sound(self, CHAN_WEAPON, gi.soundindex("weapons/trap/timer.wav"), 1, ATTN_NORM, 0);
self->think = Trap_ProximityCheck;
self->nextthink = level.time + FRAMETIME;
}
void Trap_Die(edict_t *self)
{
int n;
if (self == NULL)
{
gi_centerprintf(self->owner, "BUG: Trap_Die() called with null edict.\nPlease contact musashi@planetquake.com\n");
return;
}
if (self->die != Trap_DieFromDamage)
{
gi.dprintf("BUG: Trap_Die() called for a non-trap edict.\n");
gi.dprintf(" classname = %s\n", self->classname);
if (self->owner && self->owner->client)
gi.dprintf(" owner->name = %s\n", self->owner->client->pers.netname);
return;
}
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_SPARKS);
gi.WritePosition(self->s.origin);
gi.WriteDir(vec3_origin);
gi.multicast(self->s.origin, MULTICAST_PVS);
n = rand() % 4;
while (n--)
ThrowDebris(self, "models/objects/debris2/tris.md2", 2, self->s.origin);
if (self->enemy)
self->enemy->tractored = false;
TList_DelNode(self);
G_FreeEdict(self);
}
void Trap_DieFromDamage(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point)
{
Trap_Die(self);
}
void Trap_CheckEnv(edict_t *self)
{
vec3_t point;
vec3_t puff_start;
int cont;
// Pop if we detect slime or lava just below us (this makes the puff more visible compared with
// checking the pointcontents at our actual origin).
point[0] = self->s.origin[0];
point[1] = self->s.origin[1];
point[2] = self->s.origin[2] + self->mins[2];
cont = gi.pointcontents(point);
if (cont & (CONTENTS_LAVA | CONTENTS_SLIME))
{
puff_start[0] = self->s.origin[0];
puff_start[1] = self->s.origin[1];
puff_start[2] = self->s.origin[2] + 20.0;
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_HEATBEAM_SPARKS);
gi.WritePosition(puff_start);
gi.WriteDir(vec3_up);
gi.multicast(puff_start, MULTICAST_PVS);
Trap_Die(self);
return;
}
else if (cont & CONTENTS_SOLID)
{
TList_DelNode(self);
G_FreeEdict(self);
return;
}
self->nextthink = level.time + FRAMETIME;
}
void Trap_RotateWithEnt(edict_t *self)
{
edict_t *t;
float vec1;
float vec2;
float radius;
float angle;
// Sanity check.
if (!self->target_ent)
{
gi_centerprintf(self->owner, "BUG: Trap_RotateWithEnt() called with no target_ent.\nPlease contact musashi@planetquake.com\n");
Trap_Die(self);
return;
}
// Adjust origin based on the axis of rotation.
t = self->target_ent;
if (t->movedir[0]) // y-axis
{
vec1 = self->s.origin[0] - t->s.origin[0];
vec2 = self->s.origin[2] - t->s.origin[2];
radius = sqrt(vec1*vec1 + vec2*vec2);
angle = atan2(vec2, vec1) - DEG2RAD(t->avelocity[0] * FRAMETIME);
self->s.origin[0] = t->s.origin[0] + (radius * cos(angle));
self->s.origin[2] = t->s.origin[2] + (radius * sin(angle));
if (self->spawnflags & 128)
self->s.angles[2] = RAD2DEG(angle);
else if (self->spawnflags & 256)
self->s.angles[2] = -RAD2DEG(angle);
else if (self->spawnflags & 512)
{
trace_t tr = gi.trace(self->s.origin, NULL, NULL, t->s.origin, self, MASK_SOLID);
if (tr.surface)
vectoangles(tr.plane.normal, self->s.angles);
}
}
else if (t->movedir[1]) // z-axis
{
vec1 = self->s.origin[0] - t->s.origin[0];
vec2 = self->s.origin[1] - t->s.origin[1];
radius = sqrt(vec1*vec1 + vec2*vec2);
angle = atan2(vec2, vec1) + DEG2RAD(t->avelocity[1] * FRAMETIME);
self->s.origin[0] = t->s.origin[0] + (radius * cos(angle));
self->s.origin[1] = t->s.origin[1] + (radius * sin(angle));
if (self->spawnflags & 128)
self->s.angles[1] = RAD2DEG(angle);
else if (self->spawnflags & 256)
self->s.angles[1] = RAD2DEG(angle);
else if (self->spawnflags & 512)
{
trace_t tr = gi.trace(self->s.origin, NULL, NULL, t->s.origin, self, MASK_SOLID);
if (tr.surface)
vectoangles(tr.plane.normal, self->s.angles);
}
}
else // x-axis
{
vec1 = self->s.origin[1] - t->s.origin[1];
vec2 = self->s.origin[2] - t->s.origin[2];
radius = sqrt(vec1*vec1 + vec2*vec2);
angle = atan2(vec2, vec1) + DEG2RAD(t->avelocity[2] * FRAMETIME);
self->s.origin[1] = t->s.origin[1] + (radius * cos(angle));
self->s.origin[2] = t->s.origin[2] + (radius * sin(angle));
if (self->spawnflags & 128)
self->s.angles[2] = -RAD2DEG(angle);
else if (self->spawnflags & 256)
self->s.angles[2] = RAD2DEG(angle);
else if (self->spawnflags & 512)
{
trace_t tr = gi.trace(self->s.origin, NULL, NULL, t->s.origin, self, MASK_SOLID);
if (tr.surface)
vectoangles(tr.plane.normal, self->s.angles);
}
}
}
void Trap_MoveWithEnt(edict_t *self)
{
// Sanity check.
if (!self->target_ent)
{
gi_centerprintf(self->owner, "BUG: Trap_MoveWithEnt() called with no target_ent.\nPlease contact musashi@planetquake.com\n");
Trap_Die(self);
return;
}
// Handle Trap movement for the case that it's stuck to a moving brush model.
if (self->spawnflags & 4) // rotating brush model
Trap_RotateWithEnt(self);
else if (self->spawnflags & 8) // non-rotating brush model
VectorAdd(self->target_ent->s.origin, self->move_origin, self->s.origin);
// Destroy ourself if we're inside a solid (eg. scraped against the side of a lift shaft),
// or in slime/lava.
if (gi.pointcontents(self->s.origin) & (CONTENTS_SOLID | CONTENTS_LAVA | CONTENTS_SLIME))
{
Trap_Die(self);
return;
}
gi.linkentity(self);
// Apply tractor beam effects if flagged to do so.
if (self->spawnflags & 64)
{
Trap_TractorBeam(self);
return;
}
if (self->spawnflags & 32)
return;
// Check for nearby enemies if proximity detection has been activated, otherwise call
// Trap_Arm_ProximityCheck() at the appropriate time.
if (self->spawnflags & 16)
Trap_ProximityCheck(self);
else
{
if (level.time >= self->delay)
{
gi.sound(self, CHAN_WEAPON, gi.soundindex("weapons/trap/timer.wav"), 1, ATTN_NORM, 0);
self->spawnflags |= 16;
}
}
// Launch hook if a target has been acquired (and the hook hasn't already been fired).
if (self->enemy && !(self->spawnflags & 32))
{
Trap_ShootHook(self);
self->spawnflags |= 32;
}
self->nextthink = level.time + FRAMETIME;
}
void Trap_Touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
// Pop if we hit another Trap or C4 bundle.
if ((other->die == Trap_DieFromDamage) || (other->die == C4_DieFromDamage))
{
Trap_Die(self);
return;
}
// Ignore weapon projectiles.
if (other->wep_proj)
return;
// Don't stick to our owner.
if (other == self->owner)
return;
// Vanish if we hit a sky surface.
if (surf && (surf->flags & SURF_SKY))
{
TList_DelNode(self);
G_FreeEdict(self);
return;
}
// If we hit something that can't be damaged (eg. the floor), stick to it.
if (!other->takedamage)
{
// First, a final check that we're not in slime or lava.
if (gi.pointcontents(self->s.origin) & (CONTENTS_SOLID | CONTENTS_LAVA | CONTENTS_SLIME))
{
Trap_Die(self);
return;
}
// Seems OK, so let's get on with the sticking!
self->s.effects = 0;
VectorClear(self->velocity);
VectorClear(self->avelocity);
self->movetype = MOVETYPE_NONE;
if (plane->normal)
vectoangles(plane->normal, self->s.angles);
else
VectorClear(self->s.angles);
// If we hit a brush model that might be in motion, then move with it.
if ((other->blocked == plat_blocked) || (other->blocked == door_blocked) || (other->blocked == train_blocked) || (other->blocked == rotating_blocked))
{
VectorSubtract(self->s.origin, other->s.origin, self->move_origin);
self->target_ent = other;
self->think = Trap_MoveWithEnt;
self->nextthink = level.time + FRAMETIME;
if ((other->blocked == rotating_blocked) || (other->style == ENT_ID_DOOR_ROTATING))
{
self->spawnflags |= 4;
if (other->movedir[0]) // y-axis
{
if (plane->normal[1] > 0.9) // +ve radial
self->spawnflags |= 128;
else if (plane->normal[1] < -0.9) // -ve radial
self->spawnflags |= 256;
else // circumferential
self->spawnflags |= 512;
}
else if (other->movedir[1]) // z-axis
{
if (plane->normal[2] > 0.9) // +ve radial
self->spawnflags |= 128;
else if (plane->normal[2] < -0.9) // -ve radial
self->spawnflags |= 256;
else // circumferential
self->spawnflags |= 512;
}
else // x-axis
{
if (plane->normal[0] > 0.9) // +ve radial
self->spawnflags |= 128;
else if (plane->normal[0] < -0.9) // -ve radial
self->spawnflags |= 256;
else // circumferential
self->spawnflags |= 512;
}
}
else
self->spawnflags |= 8;
}
// If we're flagged as having been held too long, start tractoring our owner on full power...
if (self->spawnflags & 1)
{
self->spawnflags &= ~1; // stops Trap_CheckEnv() call in Trap_TractorBeam()
self->spawnflags &= ~2;
if ((self->spawnflags & 4) || (self->spawnflags & 8)) // keep tractoring
self->spawnflags |= 64;
}
// ...otherwise, start scanning for enemies.
// NB: Proximity-checking must be handled differently if moving with a bmodel, as the think
// function is already taken by Trap_MoveWithEnt().
else
{
if (!(self->spawnflags & 4) && !(self->spawnflags & 8))
{
self->think = Trap_Arm_ProximityCheck;
self->nextthink = level.time + sv_trap_activate_delay->value;
self->svflags = SVF_DEADMONSTER | SVF_PROJECTILE;
}
else
self->delay = level.time + sv_trap_activate_delay->value;
}
self->touch = NULL;
gi.linkentity(self);
if (random() > 0.5)
gi.sound(self, CHAN_VOICE, gi.soundindex("weapons/trap/bounce1.wav"), 1, ATTN_NORM, 0);
else
gi.sound(self, CHAN_VOICE, gi.soundindex("weapons/trap/bounce2.wav"), 1, ATTN_NORM, 0);
}
// Otherwise, pop.
else
Trap_Die(self);
}
void Fire_Trap(edict_t *self, vec3_t start, vec3_t aimdir, int hook_damage, float speed, int beam_damage, int beam_power, qboolean held)
{
edict_t *trap;
vec3_t dir;
vec3_t forward;
vec3_t right;
vec3_t up;
trap = G_Spawn();
trap->solid = SOLID_BBOX;
trap->movetype = MOVETYPE_TOSS;
trap->takedamage = DAMAGE_YES;
trap->clipmask = MASK_SHOT;
trap->monsterinfo.aiflags = AI_NOSTEP;
trap->health = 1;
trap->mass = 10;
trap->dmg = hook_damage;
trap->radius_dmg = beam_damage;
trap->count = beam_power;
trap->classname = "trap";
trap->owner = self;
trap->die = Trap_DieFromDamage;
trap->touch = Trap_Touch;
trap->model = "models/objects/trap/tris.md2";
trap->s.modelindex = gi.modelindex(trap->model);
VectorSet(trap->mins, -6.0, -6.0, -6.0);
VectorSet(trap->maxs, 6.0, 6.0, 6.0);
if ((int)sv_gametype->value > G_FFA)
{
if (self->client->resp.ctf_team == CTF_TEAM1)
trap->s.skinnum = 1;
else if (self->client->resp.ctf_team == CTF_TEAM2)
trap->s.skinnum = 2;
}
vectoangles(aimdir, dir);
AngleVectors(dir, forward, right, up);
VectorCopy(start, trap->s.origin);
VectorScale(aimdir, speed, trap->velocity);
VectorMA(trap->velocity, 200.0, up, trap->velocity);
VectorSet(trap->avelocity, 300.0, 300.0, 300.0);
gi.linkentity(trap);
TList_AddNode(trap);
trap->nextthink = level.time + FRAMETIME;
if (held)
{
trap->spawnflags = 3; //bit 0 => held; bit 1 => don't pull
trap->think = Trap_TractorBeam;
trap->enemy = self;
self->tractored = true;
}
else
trap->think = Trap_CheckEnv;
}
/*
======================================================================
EXPLOSIVE SPIKE GUN subroutines
spawnflags (internal use only):
4 = stuck to a rotating brush model [*]
8 = stuck to a moving brush model
128 = [*] on positive radial surface
256 = [*] on negative radial surface
512 = [*] on circumferential surface
======================================================================
*/
void Spike_Explode(edict_t *self)
{
if (self->enemy)
{
float points;
vec3_t v;
vec3_t dir;
VectorAdd(self->enemy->mins, self->enemy->maxs, v);
VectorMA(self->enemy->s.origin, 0.5, v, v);
VectorSubtract(self->s.origin, v, v);
points = self->radius_dmg - (0.5 * VectorLength(v));
VectorSubtract(self->enemy->s.origin, self->s.origin, dir);
T_Damage(self->enemy, self, self->owner, dir, self->s.origin, vec3_origin, (int)points, (int)points, DAMAGE_RADIUS, MOD_SPIKE_SPLASH);
}
T_RadiusDamage(self, self->owner, self->radius_dmg, self->enemy, self->dmg_radius, MOD_SPIKE_SPLASH);
gi.WriteByte(svc_temp_entity);
gi.WriteByte((self->waterlevel)?TE_ROCKET_EXPLOSION_WATER:TE_ROCKET_EXPLOSION);
gi.WritePosition(self->s.origin);
gi.multicast(self->s.origin, MULTICAST_PHS);
G_FreeEdict(self);
}
void Spike_RotateWithEnt(edict_t *self)
{
edict_t *t;
vec3_t normal;
float vec1;
float vec2;
float radius;
float angle;
// Sanity check.
if (!self->enemy)
{
gi_centerprintf(self->owner, "BUG: Spike_RotateWithEnt() called with no enemy.\nPlease contact musashi@planetquake.com\n");
G_FreeEdict(self);
return;
}
// Adjust origin based on the axis of rotation.
t = self->enemy;
if (t->movedir[0]) // y-axis
{
vec1 = self->s.origin[0] - t->s.origin[0];
vec2 = self->s.origin[2] - t->s.origin[2];
radius = sqrt(vec1*vec1 + vec2*vec2);
angle = atan2(vec2, vec1) - DEG2RAD(t->avelocity[0] * FRAMETIME);
self->s.origin[0] = t->s.origin[0] + (radius * cos(angle));
self->s.origin[2] = t->s.origin[2] + (radius * sin(angle));
if (self->spawnflags & 128)
self->s.angles[2] = RAD2DEG(angle);
else if (self->spawnflags & 256)
self->s.angles[2] = -RAD2DEG(angle);
else if (self->spawnflags & 512)
{
trace_t tr = gi.trace(self->s.origin, NULL, NULL, t->s.origin, self, MASK_SOLID);
if (tr.surface)
{
VectorNegate(tr.plane.normal, normal);
vectoangles(normal, self->s.angles);
}
}
}
else if (t->movedir[1]) // z-axis
{
vec1 = self->s.origin[0] - t->s.origin[0];
vec2 = self->s.origin[1] - t->s.origin[1];
radius = sqrt(vec1*vec1 + vec2*vec2);
angle = atan2(vec2, vec1) + DEG2RAD(t->avelocity[1] * FRAMETIME);
self->s.origin[0] = t->s.origin[0] + (radius * cos(angle));
self->s.origin[1] = t->s.origin[1] + (radius * sin(angle));
if (self->spawnflags & 128)
self->s.angles[1] = RAD2DEG(angle);
else if (self->spawnflags & 256)
self->s.angles[1] = RAD2DEG(angle);
else if (self->spawnflags & 512)
{
trace_t tr = gi.trace(self->s.origin, NULL, NULL, t->s.origin, self, MASK_SOLID);
if (tr.surface)
{
VectorNegate(tr.plane.normal, normal);
vectoangles(normal, self->s.angles);
}
}
}
else // x-axis
{
vec1 = self->s.origin[1] - t->s.origin[1];
vec2 = self->s.origin[2] - t->s.origin[2];
radius = sqrt(vec1*vec1 + vec2*vec2);
angle = atan2(vec2, vec1) + DEG2RAD(t->avelocity[2] * FRAMETIME);
self->s.origin[1] = t->s.origin[1] + (radius * cos(angle));
self->s.origin[2] = t->s.origin[2] + (radius * sin(angle));
if (self->spawnflags & 128)
self->s.angles[2] = -RAD2DEG(angle);
else if (self->spawnflags & 256)
self->s.angles[2] = RAD2DEG(angle);
else if (self->spawnflags & 512)
{
trace_t tr = gi.trace(self->s.origin, NULL, NULL, t->s.origin, self, MASK_SOLID);
if (tr.surface)
{
VectorNegate(tr.plane.normal, normal);
vectoangles(normal, self->s.angles);
}
}
}
}
void Spike_MoveWithEnt(edict_t *self)
{
edict_t *t;
float z_offset;
if (level.time >= self->delay)
self->think = Spike_Explode;
self->nextthink = level.time + FRAMETIME;
// Sanity check.
if (!self->enemy)
{
gi_centerprintf(self->owner, "BUG: Spike_MoveWithEnt() called with no enemy.\nPlease contact musashi@planetquake.com\n");
G_FreeEdict(self);
return;
}
// Handle spike movement for the case that it's stuck to a player. Note that the
// z-position needs to be adjusted if the player is crouching or jumping.
t = self->enemy;
if (t->client)
{
if ((t->health < 1) || !t->inuse)
{
self->movetype = MOVETYPE_TOSS;
self->think = Spike_Explode;
self->nextthink = self->delay;
self->enemy = NULL;
}
else
{
VectorAdd(t->s.origin, self->move_origin, self->s.origin);
if (t->client->ps.pmove.pm_flags & PMF_DUCKED)
{
z_offset = 32.0 * ((self->s.origin[2] - (t->s.origin[2] - 24.0)) / 56.0);
self->s.origin[2] = t->s.origin[2] - 24.0 + z_offset;
}
else if (t->client->anim_priority == ANIM_JUMP)
{
z_offset = 32.0 * (((t->s.origin[2] + 32.0) - self->s.origin[2]) / 56.0);
self->s.origin[2] = t->s.origin[2] + 32.0 - z_offset;
}
}
}
else
{
// Handle spike movement for the case that it's stuck to a moving brush model.
if (self->spawnflags & 4) // rotating brush model
Spike_RotateWithEnt(self);
else if (self->spawnflags & 8) // non-rotating brush model
VectorAdd(t->s.origin, self->move_origin, self->s.origin);
}
gi.linkentity(self);
}
void Spike_Touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
if (other == self->owner)
return;
// Vanish if we hit a sky surface.
if (surf && (surf->flags & SURF_SKY))
{
G_FreeEdict(self);
return;
}
// Ignore weapon projectiles.
if (other->wep_proj)
return;
// Stick to whatever we hit; move in step with those entities which may be in motion themselves.
self->movetype = MOVETYPE_NONE;
self->s.effects = 0;
self->solid = SOLID_NOT;
self->touch = NULL;
if (!other->takedamage)
{
if ((other->blocked == plat_blocked) || (other->blocked == door_blocked) || (other->blocked == train_blocked) || (other->blocked == rotating_blocked))
{
VectorSubtract(self->s.origin, other->s.origin, self->move_origin);
self->enemy = other;
self->delay = level.time + sv_spike_bang_delay->value;
self->think = Spike_MoveWithEnt;
self->nextthink = level.time + FRAMETIME;
if ((other->blocked == rotating_blocked) || (other->style == ENT_ID_DOOR_ROTATING))
{
self->spawnflags |= 4;
if (other->movedir[0]) // y-axis
{
if (plane->normal[1] > 0.9) // +ve radial
self->spawnflags |= 128;
else if (plane->normal[1] < -0.9) // -ve radial
self->spawnflags |= 256;
else // circumferential
self->spawnflags |= 512;
}
else if (other->movedir[1]) // z-axis
{
if (plane->normal[2] > 0.9) // +ve radial
self->spawnflags |= 128;
else if (plane->normal[2] < -0.9) // -ve radial
self->spawnflags |= 256;
else // circumferential
self->spawnflags |= 512;
}
else // x-axis
{
if (plane->normal[0] > 0.9) // +ve radial
self->spawnflags |= 128;
else if (plane->normal[0] < -0.9) // -ve radial
self->spawnflags |= 256;
else // circumferential
self->spawnflags |= 512;
}
}
else
self->spawnflags |= 8;
}
else
{
self->nextthink = level.time + sv_spike_bang_delay->value;
self->think = Spike_Explode;
}
gi.sound(self, CHAN_VOICE, gi.soundindex("weapons/esg/hit_wall.wav"), 1, ATTN_NORM, 0);
}
else
{
gi.sound(self, CHAN_VOICE, gi.soundindex("weapons/esg/hit_body.wav"), 1, ATTN_NORM, 0);
T_Damage(other, self, self->owner, self->velocity, self->s.origin, plane->normal, self->dmg, self->count, 0, MOD_SPIKE);
if (other->health > 0) // could be a player or a func_plat/door/train
{
self->enemy = other;
self->delay = level.time + sv_spike_bang_delay->value;
self->think = Spike_MoveWithEnt;
self->nextthink = level.time + FRAMETIME;
if (other->client)
VectorMA(self->s.origin, 0.1, self->velocity, self->s.origin);
VectorSubtract(self->s.origin, other->s.origin, self->move_origin);
}
else // assume it's a deady body
{
self->movetype = MOVETYPE_TOSS;
self->think = Spike_Explode;
self->nextthink = level.time + sv_spike_bang_delay->value;
}
}
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_SPARKS);
gi.WritePosition(self->s.origin);
gi.WriteDir(vec3_origin);
gi.multicast(self->s.origin, MULTICAST_PVS);
if (self->owner && self->owner->client)
PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT);
VectorClear(self->velocity);
VectorClear(self->avelocity);
gi.linkentity(self);
}
void Fire_Spike(edict_t *self, vec3_t start, vec3_t aimdir, int damage, float speed, int kick, float damage_radius, int radius_damage)
{
edict_t *spike;
trace_t tr;
spike = G_Spawn();
spike->solid = SOLID_BBOX;
spike->movetype = MOVETYPE_FLYMISSILE;
spike->clipmask = MASK_SHOT;
spike->svflags = SVF_DEADMONSTER;
spike->s.effects |= EF_IONRIPPER;
spike->s.renderfx = RF_FULLBRIGHT;
spike->dmg = damage;
spike->count = kick;
spike->radius_dmg = radius_damage;
spike->dmg_radius = damage_radius;
spike->classname = "spike";
spike->wep_proj = true;
spike->owner = self;
spike->touch = Spike_Touch;
spike->think = Spike_Explode;
spike->nextthink = level.time + SPIKE_LIVETIME;
spike->model = "models/objects/spike/tris.md2";
spike->s.modelindex = spike_index;
VectorCopy(start, spike->s.origin);
VectorCopy(start, spike->s.old_origin);
vectoangles(aimdir, spike->s.angles);
VectorScale(aimdir, speed, spike->velocity);
gi.linkentity(spike);
tr = gi.trace(self->s.origin, NULL, NULL, spike->s.origin, spike, MASK_SHOT);
if (tr.fraction < 1.0)
{
VectorMA(spike->s.origin, -10.0, aimdir, spike->s.origin);
spike->touch(spike, tr.ent, NULL, NULL);
}
}
/*
======================================================================
FLAMETHROWER subroutines
spawnflags (internal use only):
1 = fireball has already halted on something
2 = fireball has already done damage to something
======================================================================
*/
//
// Flame_* subroutines are for the larger fires, for burning players.
//
void Flame_Expire(edict_t *self)
{
vec3_t puff_start;
if (Q_stricmp(self->classname, "flame"))
{
gi.dprintf("BUG: Flame_Expire() called for a non-flame edict.\n");
gi.dprintf(" classname = %s\n", self->classname);
if (self->owner && self->owner->client)
gi.dprintf(" owner->name = %s\n", self->owner->client->pers.netname);
return;
}
if (self->s.frame < 34)
self->s.frame = 34;
else if (self->s.frame < 38)
++self->s.frame;
else
{
puff_start[0] = self->s.origin[0];
puff_start[1] = self->s.origin[1];
puff_start[2] = self->s.origin[2] + 32;
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_HEATBEAM_SPARKS);
gi.WritePosition(puff_start);
gi.WriteDir(vec3_up);
gi.multicast(puff_start, MULTICAST_PVS);
G_FreeEdict(self);
return;
}
self->touch = NULL;
self->think = Flame_Expire;
self->nextthink = level.time + FRAMETIME;
}
void Flame_Burn(edict_t *self)
{
vec3_t offset;
++self->s.frame;
if (self->s.frame == 34)
self->s.frame = 21;
self->nextthink = level.time + FRAMETIME;
if (!self->enemy)
{
gi_centerprintf(self->owner, "BUG: Flame_Burn() called with no enemy.\nPlease contact musashi@planetquake.com\n");
G_FreeEdict(self);
return;
}
// Sanity check - if our enemy isn't flagged as being on fire (eg. they've jumped into water),
// then extinguish ourself.
if (!self->enemy->burning)
{
self->touch = NULL;
self->think = Flame_Expire;
return;
}
// If the enemy is gibbed, snuff out.
if (self->enemy->health < -40)
{
self->touch = NULL;
self->think = Flame_Expire;
return;
}
// Otherwise, follow them around and apply burning damage every second.
VectorCopy(self->enemy->s.origin, self->s.origin);
if (self->enemy->health <= 0)
{
VectorSet(offset, 0.0, 0.0, -18.0);
VectorAdd(self->s.origin, offset, self->s.origin);
}
gi.linkentity(self);
if (level.time >= self->wait)
{
T_Damage(self->enemy, self, self->owner, self->velocity, self->s.origin, vec3_origin, self->dmg, 0, 0, MOD_FLAME);
self->wait = level.time + FLAME_DMG_RATE;
}
}
void Flame_Touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
if (!other->client)
return;
// Don't ignite someone who is already nicely warm and toasty.
if (other->burning)
return;
// The environment suit and Invulnerability will protect against fire damage and personal ignition.
if (other->client->enviro_framenum > level.framenum)
return;
if (other->client->invincible_framenum > level.framenum)
return;
// Don't ignite team members of our owner in CTF or TDM.
if (CheckTeamDamage(other, self->owner))
return;
// Otherwise, let them burn!
other->burning = true;
T_Damage(other, self, self->owner, self->velocity, self->s.origin, plane->normal, self->dmg, 0, 0, MOD_FLAME);
Spawn_Flame(self, other, other->s.origin, self->dmg);
}
void Spawn_Flame(edict_t *self, edict_t *other, vec3_t start, int damage)
{
edict_t *flame;
flame = G_Spawn();
flame->solid = SOLID_TRIGGER;
flame->movetype = MOVETYPE_NONE;
flame->dmg = damage;
flame->classname = "flame";
flame->wep_proj = true;
flame->takedamage = DAMAGE_NO;
flame->s.effects |= EF_PLASMA | EF_HYPERBLASTER;
flame->s.renderfx = RF_FULLBRIGHT;
flame->owner = self->owner;
flame->think = Flame_Burn;
flame->touch = Flame_Touch;
flame->enemy = other;
other->flame = flame;
flame->nextthink = level.time + FRAMETIME;
flame->wait = level.time + FLAME_DMG_RATE;
VectorSet(flame->mins, -18, -18, 0);
VectorSet(flame->maxs, 18, 18, 24);
flame->model = "models/fire/tris.md2";
flame->s.modelindex = gi.modelindex(flame->model);
flame->s.frame = 16;
flame->s.sound = gi.soundindex("weapons/flamer/fire1.wav");
VectorCopy(start, flame->s.origin);
VectorCopy(start, flame->s.old_origin);
VectorClear(flame->velocity);
gi.linkentity(flame);
}
//
// Flame_Small_* subroutines are for the smaller fires that burn on the ground.
//
void Flame_Small_Expire(edict_t *self)
{
vec3_t puff_start;
puff_start[0] = self->s.origin[0];
puff_start[1] = self->s.origin[1];
puff_start[2] = self->s.origin[2] + 16;
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_HEATBEAM_SPARKS);
gi.WritePosition(puff_start);
gi.WriteDir(vec3_up);
gi.multicast(puff_start, MULTICAST_PVS);
G_FreeEdict(self);
}
void Flame_Small_Touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
vec3_t point;
int cont;
// Vanish if we hit a sky surface.
if (surf && (surf->flags & SURF_SKY))
{
G_FreeEdict(self);
return;
}
// Fizzle out if we hit water.
point[0] = self->s.origin[0];
point[1] = self->s.origin[1];
point[2] = self->s.origin[2] + self->mins[2];
if ((cont = gi.pointcontents(point)) & MASK_WATER)
{
self->think = Flame_Small_Expire;
self->nextthink = level.time + FRAMETIME;
self->touch = NULL;
return;
}
// If a player touches the flame, damage them and then snuff out. There is a 20% chance of igniting the
// player if they're not already burning.
if (other->client)
{
self->touch = NULL;
self->think = Flame_Small_Expire;
self->nextthink = level.time + FRAMETIME;
// The environment suit and Invulnerability protect against small fire damage and personal ignition.
if (other->client && (other->client->enviro_framenum > level.framenum))
return;
if (other->client && (other->client->invincible_framenum > level.framenum))
return;
// Don't ignite team members of our owner in CTF or TDM.
if (CheckTeamDamage(other, self->owner))
return;
// If it's a non-burning player, and they fail their saving throw vs combustion, make them burn!
if (other->client && !other->burning && (random() < FT_SMALL_IGNITE_PROB))
{
other->burning = true;
Spawn_Flame(self, other, other->s.origin, self->dmg);
}
T_Damage(other, self, self->owner, self->velocity, self->s.origin, plane->normal, self->radius_dmg, 0, 0, MOD_FLAME);
}
}
void Flame_Small_Burn(edict_t *self)
{
++self->s.frame;
if (self->s.frame == 15)
self->s.frame = 3;
if (level.time >= self->wait)
self->think = Flame_Small_Expire;
self->nextthink = level.time + FRAMETIME;
Fireball_CheckEnv(self);
}
void Spawn_Flame_Small(edict_t *self, vec3_t start, int damage, int damage_smallflame)
{
edict_t *flame;
flame = G_Spawn();
flame->solid = SOLID_TRIGGER;
flame->svflags = SVF_DEADMONSTER;
flame->movetype = MOVETYPE_TOSS;
flame->clipmask = MASK_SHOT;
flame->takedamage = DAMAGE_NO;
flame->s.effects |= EF_PLASMA;
flame->s.renderfx = RF_FULLBRIGHT;
flame->dmg = damage;
flame->radius_dmg = damage_smallflame;
flame->classname = "flame_small";
flame->wep_proj = true;
flame->owner = self->owner;
flame->touch = Flame_Small_Touch;
flame->think = Flame_Small_Burn;
flame->nextthink = level.time + FRAMETIME;
flame->wait = level.time + FLAME_LIVETIME_0 + (FLAME_LIVETIME_R * random());
VectorSet(flame->mins, -2.0, -2.0, 0.0);
VectorSet(flame->maxs, 2.0, 2.0, 4.0);
flame->model = "models/fire/tris.md2";
flame->s.modelindex = gi.modelindex(flame->model);
flame->s.frame = 0;
VectorCopy(start, flame->s.origin);
VectorCopy(start, flame->s.old_origin);
VectorClear(flame->velocity);
gi.linkentity(flame);
}
//
// Fireball_* subroutines are for the burning globs emitted by the Flamethrower.
//
void Fireball_Expire(edict_t *self)
{
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_HEATBEAM_SPARKS);
gi.WritePosition(self->s.origin);
gi.WriteDir(vec3_up);
gi.multicast(self->s.origin, MULTICAST_PVS);
G_FreeEdict(self);
}
void Fireball_CheckBurn(edict_t *self)
{
edict_t *cl_ent = NULL;
while ((cl_ent = FindClientRadius(cl_ent, self->s.origin, FT_IGNITE_RADIUS)) != NULL)
{
if (cl_ent == self->owner)
continue;
if (cl_ent->burning)
continue;
if (CheckTeamDamage(cl_ent, self->owner))
continue;
if (cl_ent->client->enviro_framenum > level.framenum)
continue;
if (cl_ent->client->invincible_framenum > level.framenum)
continue;
if (random() < FT_IGNITE_PROB)
{
cl_ent->burning = true;
Spawn_Flame(self, cl_ent, cl_ent->s.origin, self->dmg);
}
}
}
void Fireball_CheckEnv(edict_t *self)
{ //NB: could free self, so be careful how you call this function!
vec3_t point;
vec3_t puff_start;
// Fizzle out if we're in liquid.
point[0] = self->s.origin[0];
point[1] = self->s.origin[1];
point[2] = self->s.origin[2];
if (gi.pointcontents(point) & MASK_WATER)
{
puff_start[0] = self->s.origin[0];
puff_start[1] = self->s.origin[1];
puff_start[2] = self->s.origin[2] + 20.0;
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_HEATBEAM_SPARKS);
gi.WritePosition(puff_start);
gi.WriteDir(vec3_up);
gi.multicast(puff_start, MULTICAST_PVS);
G_FreeEdict(self);
}
}
void Fireball_Think(edict_t *self)
{
self->nextthink = level.time + FRAMETIME;
if (level.time < self->delay)
{
Fireball_CheckBurn(self);
Fireball_CheckEnv(self);
return;
}
if (self->s.skinnum < 6)
++self->s.skinnum;
if (self->s.frame == 0)
self->s.frame = 15;
else
++self->s.frame;
if (self->s.frame == 18)
self->s.effects |= EF_SPHERETRANS;
if (self->s.frame == 21)
self->think = G_FreeEdict;
Fireball_CheckBurn(self);
Fireball_CheckEnv(self);
}
void Fireball_Touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
edict_t *ent;
vec3_t point;
int cont;
if (other == self)
return;
// Don't ignite owner.
if (other == self->owner)
return;
// Vanish if we hit a sky surface.
if (surf && (surf->flags & SURF_SKY))
{
G_FreeEdict(self);
return;
}
// Fizzle out if we hit water.
point[0] = self->s.origin[0];
point[1] = self->s.origin[1];
point[2] = self->s.origin[2] - 2.0;
if ((cont = gi.pointcontents(point)) & MASK_WATER)
{
self->think = Fireball_Expire;
self->nextthink = level.time + FRAMETIME;
self->touch = NULL;
return;
}
if (other->wep_proj)
return;
// There's a 10% chance that a small burning globule will be produced.
if (!other->takedamage)
{
if (self->spawnflags & 1) // already halted (and maybe spawned a small flame)
return;
else
{
VectorMA(self->s.origin, -0.001, self->velocity, self->s.origin);
VectorClear(self->velocity);
self->spawnflags |= 1;
gi.linkentity(self);
if (random() < FT_SMALLFLAME_PROB)
Spawn_Flame_Small(self, self->s.origin, self->dmg, self->radius_dmg);
// Destroy nearby Traps and C4 bundles (beyond fireball's bounding box).
ent = NULL;
while ((ent = FindRadius(ent, self->s.origin, 40.0)) != NULL)
{
if ((ent->die == C4_DieFromDamage) || (ent->die == Trap_DieFromDamage))
T_Damage(ent, self, self->owner, self->velocity, ent->s.origin, vec3_origin, 999, 0, 0, MOD_FLAME);
}
}
return;
}
if (self->spawnflags & 2) // already done damage
return;
if (other->client && (other->client->invincible_framenum > level.framenum))
return;
if (CheckTeamDamage(other, self->owner))
return;
// Otherwise, cause damage; there is a 50% chance of igniting the player if they're not already burning.
// NB: The environment suit reduces flamethrower damage, and prevents personal ignition.
if (other->client && (other->client->enviro_framenum > level.framenum))
T_Damage(other, self, self->owner, self->velocity, self->s.origin, vec3_origin, (int)(0.2*self->dmg), 0, 0, MOD_FLAMETHROWER);
else
{
if (other->client && !other->burning && (random() < FT_IGNITE_PROB))
{
other->burning = true;
Spawn_Flame(self, other, other->s.origin, self->dmg);
}
T_Damage(other, self, self->owner, self->velocity, self->s.origin, vec3_origin, self->dmg, 0, 0, MOD_FLAMETHROWER);
}
self->spawnflags |= 2;
}
void Fire_Fireball(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int damage_smallflame, float speed, qboolean glow)
{
edict_t *fireball;
trace_t tr;
fireball = G_Spawn();
fireball->svflags = SVF_DEADMONSTER;
fireball->solid = SOLID_BBOX;
fireball->movetype = MOVETYPE_FLYMISSILE;
fireball->clipmask = MASK_SHOT;
fireball->takedamage = DAMAGE_NO;
fireball->s.effects |= EF_PLASMA | ((glow)?EF_HYPERBLASTER:0);
fireball->s.renderfx = RF_FULLBRIGHT;
fireball->dmg = damage;
fireball->radius_dmg = damage_smallflame;
fireball->classname = "fireball";
fireball->owner = self;
fireball->touch = Fireball_Touch;
fireball->think = Fireball_Think;
fireball->nextthink = level.time + FRAMETIME;
fireball->spawnflags = 0;
fireball->wep_proj = true;
fireball->delay = level.time + 0.2;
fireball->model = "models/objects/r_explode/tris.md2";
fireball->s.modelindex = r_explode_index;
fireball->s.frame = 0;
fireball->s.skinnum = 1;
VectorCopy(start, fireball->s.origin);
VectorCopy(start, fireball->s.old_origin);
vectoangles(aimdir, fireball->s.angles);
VectorScale(aimdir, speed, fireball->velocity);
gi.linkentity(fireball);
tr = gi.trace(self->s.origin, NULL, NULL, fireball->s.origin, fireball, MASK_SHOT);
if (tr.fraction < 1.0)
{
VectorMA(fireball->s.origin, -10.0, aimdir, fireball->s.origin);
fireball->touch(fireball, tr.ent, NULL, NULL);
}
}
//------- Firebomb-sepcific functions -------------
void Firebomb_Explode(edict_t *self)
{
self->nextthink = level.time + FRAMETIME;
if (self->s.frame < 4)
{
++self->s.frame;
++self->s.skinnum;
if (self->s.frame == 1)
self->s.effects |= EF_PLASMA;
else if (self->s.frame == 3)
self->s.effects |= EF_SPHERETRANS;
}
else
{
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_HEATBEAM_SPARKS);
gi.WritePosition(self->s.origin);
gi.WriteDir(vec3_up);
gi.multicast(self->s.origin, MULTICAST_PVS);
G_FreeEdict(self);
}
}
void Firebomb_Touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
vec3_t point;
int cont;
if (other == self)
return;
// Don't ignite owner.
if (other == self->owner)
return;
// Vanish if we hit a sky surface.
if (surf && (surf->flags & SURF_SKY))
{
G_FreeEdict(self);
return;
}
// Ignore other weapon projectiles.
if (other->wep_proj)
return;
// Fizzle out if we hit water.
point[0] = self->s.origin[0];
point[1] = self->s.origin[1];
point[2] = self->s.origin[2] + self->mins[2];
if ((cont = gi.pointcontents(point)) & MASK_WATER)
{
self->think = Fireball_Expire;
self->nextthink = level.time + FRAMETIME;
self->touch = NULL;
return;
}
// Stop and explode.
VectorMA(self->s.origin, -0.001, self->velocity, self->s.origin);
VectorClear(self->velocity);
self->think = Firebomb_Explode;
self->nextthink = level.time + FRAMETIME;
self->touch = NULL;
self->solid = SOLID_NOT;
self->movetype = MOVETYPE_NONE;
gi.linkentity(self);
if (other->takedamage)
{
// Otherwise, cause damage; ignite the player if they're not already burning.
// NB: The environment suit prevents ignition.
if (other->client && !other->burning)
{
if (!CheckTeamDamage(other, self->owner) && (other->client->enviro_framenum <= level.framenum) &&
(other->client->invincible_framenum <= level.framenum))
{
other->burning = true;
Spawn_Flame(self, other, other->s.origin, self->radius_dmg);
}
}
T_Damage(other, self, self->owner, self->velocity, self->s.origin, vec3_origin, self->dmg, self->count, 0, MOD_FIREBOMB);
}
T_RadiusDamage(self, self->owner, self->dmg, other, self->dmg_radius, MOD_FIREBOMB_SPLASH);
gi.sound(self, CHAN_WEAPON, gi.soundindex("weapons/rocklx1a.wav"), 1, ATTN_NORM, 0);
if (self->owner && self->owner->client)
PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT);
}
void Firebomb_Think(edict_t *self)
{
self->nextthink = level.time + FRAMETIME;
Fireball_CheckEnv(self);
}
void Fire_Firebomb(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int damage_minor, int kick, float damage_radius, float speed)
{
edict_t *firebomb;
trace_t tr;
firebomb = G_Spawn();
firebomb->svflags = SVF_PROJECTILE | SVF_DEADMONSTER;
firebomb->solid = SOLID_BBOX;
firebomb->movetype = MOVETYPE_FLYMISSILE;
firebomb->clipmask = MASK_SHOT;
firebomb->s.effects = EF_ROCKET;
firebomb->s.renderfx = RF_FULLBRIGHT;
firebomb->dmg = damage;
firebomb->radius_dmg = damage_minor; // actually flame burning dmg, not radius damage
firebomb->dmg_radius = damage_radius;
firebomb->count = kick;
firebomb->classname = "firebomb";
firebomb->wep_proj = true;
firebomb->owner = self;
firebomb->touch = Firebomb_Touch;
firebomb->think = Firebomb_Think;
firebomb->nextthink = level.time + FRAMETIME;
firebomb->model = "models/objects/firebomb/tris.md2";
firebomb->s.modelindex = gi.modelindex(firebomb->model);
firebomb->s.frame = 0;
firebomb->s.skinnum = 0;
VectorCopy(start, firebomb->s.origin);
VectorCopy(start, firebomb->s.old_origin);
vectoangles(aimdir, firebomb->s.angles);
VectorScale(aimdir, speed, firebomb->velocity);
gi.linkentity(firebomb);
tr = gi.trace(self->s.origin, NULL, NULL, firebomb->s.origin, firebomb, MASK_SHOT);
if (tr.fraction < 1.0)
{
VectorMA(firebomb->s.origin, -10.0, aimdir, firebomb->s.origin);
firebomb->touch(firebomb, tr.ent, NULL, NULL);
}
}
/*
======================================================================
SHOCKRIFLE subroutines
======================================================================
*/
void Shock_SeekEnemy(edict_t *self);
void Shock_HomeIn(edict_t *self)
{
vec3_t end;
vec3_t dir;
vec3_t deltav;
float range;
self->nextthink = level.time + FRAMETIME;
if (level.time > self->wait)
{
self->think = G_FreeEdict;
self->touch = NULL;
return;
}
// Sanity check.
if (!self->enemy)
{
gi_centerprintf(self->owner, "BUG: Shock_HomeIn() called with no enemy.\nPlease contact musashi@planetquake.com\n");
G_FreeEdict(self);
return;
}
// Set our target to be the enemy player's cold, black heart.
end[0] = self->enemy->s.origin[0];
end[1] = self->enemy->s.origin[1];
end[2] = self->enemy->s.origin[2] + (self->enemy->viewheight - 8.0);
// Obtain an attack vector that points from us to the target. If the length of this vector is greater than
// our detection range, lose the current target and search for another one.
VectorSubtract(end, self->s.origin, dir);
range = VectorLength(dir);
if (range > sv_shock_homing_range->value)
{
self->think = Shock_SeekEnemy;
self->enemy = NULL;
return;
}
// Otherwise, keep homing! Adjust our orientation to point towards the target.
VectorNormalize(dir);
VectorCopy(dir, self->movedir);
vectoangles(dir, self->s.angles);
// Apply a delta-V to our current velocity based on the attack vector.
VectorScale(dir, self->speed, deltav);
VectorAdd(deltav, self->velocity, self->velocity);
VectorNormalize(self->velocity);
VectorScale(self->velocity, self->speed, self->velocity);
gi.linkentity(self);
}
void Shock_SeekEnemy(edict_t *self)
{
edict_t *cl_ent = NULL;
self->nextthink = level.time + FRAMETIME;
if (level.time > self->wait)
{
self->think = G_FreeEdict;
self->touch = NULL;
return;
}
// Valid targets are live players who aren't our owner, or team members of our owner, within our
// detection range.
while ((cl_ent = FindRadius(cl_ent, self->s.origin, sv_shock_homing_range->value)) != NULL)
{
if (!cl_ent->client)
continue;
if (cl_ent == self->owner)
continue;
if (cl_ent->health < 1)
continue;
if (cl_ent->client->antibeam_framenum > level.framenum)
continue;
if ((int)sv_gametype->value > G_FFA)
{
if (self->owner->client && (cl_ent->client->resp.ctf_team == self->owner->client->resp.ctf_team))
continue;
}
// Target is valid, so home in on them.
self->enemy = cl_ent;
self->think = Shock_HomeIn;
break;
}
}
void Shock_HomingTouch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
vec3_t origin;
// Vanish if we hit a sky surface.
if (surf && (surf->flags & SURF_SKY))
{
G_FreeEdict(self);
return;
}
// Ignore weapon projectiles.
if (other->wep_proj)
return;
// Explode and do damage.
VectorMA(self->s.origin, -0.02, self->velocity, origin);
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_BFG_BIGEXPLOSION);
gi.WritePosition(origin);
gi.multicast(origin, MULTICAST_PVS);
gi.sound(self, CHAN_VOICE, gi.soundindex("weapons/shock/shockhit.wav"), 1, ATTN_NORM, 0);
if (self->owner && self->owner->client)
PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT);
if (other->takedamage)
T_Damage(other, self, self->owner, self->velocity, self->s.origin, plane->normal, self->dmg, self->count, DAMAGE_ENERGY, MOD_SR_HOMING);
T_RadiusDamage(self, self->owner, self->dmg, other, self->dmg_radius, MOD_SR_HOMING);
G_FreeEdict(self);
}
void Shock_DisintegratorTouch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
vec3_t origin;
// Vanish if we hit a sky surface.
if (surf && (surf->flags & SURF_SKY))
{
G_FreeEdict(self);
return;
}
// Ignore weapon projectiles.
if (other->wep_proj)
return;
// Explode and do damage.
VectorMA(self->s.origin, -0.02, self->velocity, origin);
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_NUKEBLAST);
gi.WritePosition(origin);
gi.multicast(origin, MULTICAST_PVS);
gi.sound(self, CHAN_VOICE, gi.soundindex("weapons/shock/shockhit.wav"), 1, ATTN_NORM, 0);
if (self->owner && self->owner->client)
PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT);
if (other->takedamage)
{
if (other->client)
other->disintegrated = true;
T_Damage(other, self, self->owner, self->velocity, self->s.origin, plane->normal, 10000, self->count, DAMAGE_ENERGY, MOD_SR_DISINT);
}
T_RadiusDamage(self, self->owner, self->radius_dmg, other, self->dmg_radius, MOD_SR_DISINT_WAVE);
G_FreeEdict(self);
}
void Fire_Shock(edict_t *self, vec3_t start, vec3_t aimdir, int damage_plasma, float speed, int kick, int damage_shockbolt, float damage_radius, qboolean homing)
{
edict_t *shock;
trace_t tr;
shock = G_Spawn();
shock->solid = SOLID_BBOX;
shock->movetype = MOVETYPE_FLYMISSILE;
shock->svflags = SVF_DEADMONSTER;
shock->clipmask = MASK_SHOT;
shock->s.renderfx = RF_FULLBRIGHT;
shock->dmg = damage_plasma;
shock->count = kick;
shock->speed = speed;
shock->radius_dmg = damage_shockbolt;
shock->dmg_radius = damage_radius;
shock->owner = self;
shock->s.sound = gi.soundindex("weapons/shock/shockfly.wav");
shock->wep_proj = true;
shock->model = "models/objects/shock/tris.md2";
shock->s.modelindex = gi.modelindex(shock->model);
if (homing)
{
shock->classname = "shock_homing";
shock->touch = Shock_HomingTouch;
shock->think = Shock_SeekEnemy;
shock->nextthink = level.time + FRAMETIME;
shock->wait = level.time + sv_shock_live_time->value;
shock->s.skinnum = 1;
shock->s.effects |= EF_BFG | EF_COLOR_SHELL;
shock->s.effects |= EF_BLASTER | EF_TRACKER;
shock->s.renderfx |= RF_SHELL_GREEN;
}
else
{
shock->svflags |= SVF_PROJECTILE;
shock->classname = "shock_disint";
shock->touch = Shock_DisintegratorTouch;
shock->think = G_FreeEdict;
shock->nextthink = level.time + sv_shock_live_time->value;
shock->s.skinnum = 0;
shock->s.effects |= EF_BLUEHYPERBLASTER | EF_QUAD | EF_FLAG2;
}
VectorCopy(start, shock->s.origin);
VectorCopy(start, shock->s.old_origin);
vectoangles(aimdir, shock->s.angles);
VectorScale(aimdir, speed, shock->velocity);
gi.linkentity(shock);
tr = gi.trace(self->s.origin, NULL, NULL, shock->s.origin, shock, MASK_SHOT);
if (tr.fraction < 1.0)
{
VectorMA(shock->s.origin, -10.0, aimdir, shock->s.origin);
shock->touch(shock, tr.ent, NULL, NULL);
}
}
/*
======================================================================
GAUSS PISTOL subroutines
======================================================================
*/
void Draw_Beam(edict_t *self)
{
if (level.time > self->wait)
{
G_FreeEdict(self);
return;
}
self->nextthink = level.time + FRAMETIME;
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_BFG_LASER);
gi.WritePosition(self->s.origin);
gi.WritePosition(self->pos1);
gi.multicast(self->s.origin, MULTICAST_PHS);
}
qboolean Particle_Can_Hit(vec3_t start, vec3_t aimdir, edict_t *targ)
{
vec3_t vec;
vec3_t end[3];
float range;
float dotp;
float tolerance;
qboolean hit = false;
int i;
// Take into account the target entity's bounding box - generate two endpoints.
VectorCopy(targ->s.origin, end[0]);
VectorAdd(targ->s.origin, targ->mins, end[1]);
VectorAdd(targ->s.origin, targ->maxs, end[2]);
// Compare the muzzle->endpoint vectors with the aiming vector; if they're close enough together,
// call it a hit.
// NB: the numbers used here were arrived at after playing around to see what felt best in-game.
for (i = 0; i < 3; ++i)
{
VectorSubtract(end[i], start, vec);
range = VectorLength(vec);
if (range < 40.0)
tolerance = 0.5;
else if (range < 200.0)
tolerance = 0.5 + (0.00309375 * (range - 40.0));
else
tolerance = 0.995;
VectorNormalize(vec);
dotp = DotProduct(vec, aimdir);
if (dotp >= tolerance)
{
hit = true;
break;
}
}
return hit;
}
void Fire_Particle (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick)
{
trace_t tr;
edict_t *ignore = self;
edict_t *index;
edict_t *check;
edict_t *refkiller = NULL;
edict_t *beam;
vec3_t from;
vec3_t end;
vec3_t beam_mins;
vec3_t beam_maxs;
qboolean finished = false;
qboolean reflected = false;
float scale = WORLD_SIZE; // was 8192.0
int i;
// Extend a long line from the muzzle, and clip it against the world cube boundary; this is the
// endpoint for the particle beam.
VectorMA(start, scale, aimdir, end);
for (i = 0; i < 3; ++i)
{
if (end[i] >= MAX_WORLD_COORD) // was 4095.0
{
scale *= (MAX_WORLD_COORD - start[i]) / (end[i] - start[i]); // was 4095.0
VectorMA(start, scale, aimdir, end);
}
else if (end[i] <= MIN_WORLD_COORD) // was -4095.0
{
scale *= (MAX_WORLD_COORD + start[i]) / (start[i] - end[i]); // was 4095.0
VectorMA(start, scale, aimdir, end);
}
}
// Trace a line from the muzzle to the endpoint, and apply damage to those entities that can be
// damaged. (The contents mask for gi.trace will ensure that the line will pass through world brushes).
VectorSet(beam_mins, -16.0, -16.0, -16.0);
VectorSet(beam_maxs, 16.0, 16.0, 16.0);
VectorCopy(start, from);
while (ignore)
{
tr = gi.trace(from, beam_mins, beam_maxs, end, ignore, CONTENTS_MONSTER | CONTENTS_DEADMONSTER);
if (tr.ent->client || (tr.ent->solid == SOLID_BBOX))
ignore = tr.ent;
else
ignore = NULL;
if ((tr.ent != self) && (tr.ent->takedamage))
{
T_Damage(tr.ent, self, self, aimdir, tr.endpos, tr.plane.normal, damage, kick, DAMAGE_ENERGY, MOD_GAUSS_BEAM);
if (tr.ent->client && (tr.ent->client->antibeam_framenum > level.framenum))
{
reflected = true;
refkiller = tr.ent;
VectorCopy(tr.endpos, from);
break;
}
}
if (VectorCompare(tr.endpos, from))
break;
else
VectorCopy(tr.endpos, from);
}
// Search through the player's linked list of Trap and C4 entities, and destroy them if they are
// within the particle beam's cone of effect (as they will have been ignored by the above trace).
if (self->next_node)
{
index = self->next_node;
while (index && !finished)
{
check = index;
if (index->next_node)
index = index->next_node;
else
finished = true;
if (Particle_Can_Hit(start, aimdir, check))
{
if (check->die == C4_DieFromDamage)
C4_Die(check);
else if (check->die == Trap_DieFromDamage)
Trap_Die(check);
else
gi.dprintf("BUG: Invalid next_node pointer in Fire_Particle().\nPlease contact musashi@planetquake.com\n");
}
}
}
// Draw the particle beam.
for (i = 0; i < 2; ++i)
{
gi.WriteByte(svc_temp_entity);
gi.WriteByte(i?TE_BUBBLETRAIL:TE_BFG_LASER);
gi.WritePosition(start);
gi.WritePosition((reflected)?from:end);
gi.multicast(start, MULTICAST_PHS);
}
// If the beam has been reflected, we're doomed!
if (reflected && (refkiller != NULL))
{
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_BFG_LASER);
gi.WritePosition(refkiller->s.origin);
gi.WritePosition(start);
gi.multicast(start, MULTICAST_PHS);
T_Damage(self, refkiller, refkiller, aimdir, self->s.origin, vec3_origin, damage, kick, DAMAGE_ENERGY, MOD_GAUSS_BEAM_REF);
}
else
// Spawn a temporary entity that will continue to draw the beam for a few more frames.
{
beam = G_Spawn();
beam->svflags |= SVF_NOCLIENT | SVF_PROJECTILE;
beam->classname = "particle_beam";
beam->think = Draw_Beam;
beam->nextthink = level.time + FRAMETIME;
beam->wait = level.time + 0.3;
VectorCopy(start, beam->s.origin);
VectorCopy(end, beam->pos1);
gi.linkentity(beam);
}
}
void Fire_Instabolt(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick)
{
edict_t *check;
edict_t *index;
vec3_t end;
trace_t tr;
qboolean finished = false;
// Trace a line until we reach something that can be shot, and damage it if possible.
VectorMA(start, WORLD_SIZE, aimdir, end); // was 8192.0
tr = gi.trace(start, NULL, NULL, end, self, MASK_SHOT);
if ((tr.ent != self) && (tr.ent->takedamage))
T_Damage(tr.ent, self, self, aimdir, tr.endpos, tr.plane.normal, damage, kick, DAMAGE_ENERGY, MOD_GAUSS_BLASTER);
// Destroy nearby Traps and C4 bundles (as gi.trace will ignore player's own entities).
if (self->next_node)
{
index = self->next_node;
while (index && !finished)
{
check = index;
if (index->next_node)
index = index->next_node;
else
finished = true;
if (VecRange(tr.endpos, check->s.origin) < 10.0)
{
if (check->die == C4_DieFromDamage)
C4_Die(check);
else if (check->die == Trap_DieFromDamage)
Trap_Die(check);
else
gi.dprintf("BUG: Invalid next_node pointer in Fire_Instabolt().\nPlease contact musashi@planetquake.com\n");
}
}
}
// Don't generate an impact effect if the endpoint is on a sky surface, otherwise generate
// a blaster bolt puff.
if (tr.surface && (tr.surface->flags & SURF_SKY))
return;
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_FLECHETTE);
gi.WritePosition(tr.endpos);
if (!tr.plane.normal)
gi.WriteDir(vec3_origin);
else
gi.WriteDir(tr.plane.normal);
gi.multicast(tr.endpos, MULTICAST_PVS);
if (self->owner && self->owner->client)
PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT);
}
/*
======================================================================
Anti-Grav Manipulator subroutines
======================================================================
*/
void Move_AGM(edict_t *self, vec3_t start, vec3_t aimdir)
{
gclient_t *cl = self->client;
trace_t tr;
vec3_t end;
vec3_t dir;
float speed;
if (cl->agm_target == NULL)
{
gi.dprintf("BUG: Move_AGM() called with null target\n");
return;
}
// Move our target towards the beam endpoint (make the delta-v proportional to target's
// distance from the endpoint).
// Note that we want to do it here, rather than after the checks for beam-breakage, to make
// it easier for players to throw their target into a hard surface (this way, it gives the
// target a last velocity boost before shutting off the beam).
VectorMA(start, cl->agm_range, aimdir, end);
VectorSubtract(end, cl->agm_target->s.origin, dir);
speed = AGM_MOVE_SCALE * VectorLength(dir);
VectorNormalize(dir);
VectorScale(dir, speed, cl->agm_target->velocity);
gi.linkentity(cl->agm_target);
// Trace a line from the player to the fixed end of the AGM beam. If the trace meets something that isn't
// the intended client, disconnect the beam.
cl->agm_target->client->flung_by_agm = false;
tr = gi.trace(start, NULL, NULL, end, self, MASK_SHOT);
if (tr.ent && (tr.ent != cl->agm_target) && Q_stricmp(tr.ent->classname, "worldspawn"))
{
cl->agm_target->client->held_by_agm = false;
cl->agm_target->client->thrown_by_agm = true;
cl->agm_target = NULL;
return;
}
if ((tr.fraction < 1.0) && !Q_stricmp(tr.ent->classname, "worldspawn"))
{
cl->agm_target->client->held_by_agm = false;
cl->agm_target->client->thrown_by_agm = true;
cl->agm_target = NULL;
return;
}
cl->agm_target->client->held_by_agm = true;
cl->agm_target->client->thrown_by_agm = false;
// If the target is being held against a wall, make sure we can't screw up the range calcs by
// extending the beam endpoint into the wall.
tr = gi.trace(cl->agm_target->s.origin, NULL, NULL, end, cl->agm_target, MASK_SHOT);
if (tr.fraction < 1.0)
{
VectorSubtract(tr.endpos, self->s.origin, dir);
self->client->agm_range = VectorLength(dir);
}
}
void Fire_AGM(edict_t *self, vec3_t start, vec3_t aimdir, qboolean disrupt)
{
trace_t tr;
vec3_t end;
vec3_t vbeam;
float dist;
int damage;
int kick;
// Trace a line to try and find something that can be manipulated.
self->client->agm_target = NULL;
VectorMA(start, WORLD_SIZE, aimdir, end); // was 8192.0
tr = gi.trace(start, NULL, NULL, end, self, MASK_SHOT);
if (tr.startsolid && !tr.ent->client)
return;
// As long as we're not firing point-blank at something, draw the tracer beam.
if (!tr.startsolid)
{
edict_t *beam = G_Spawn();
beam->movetype = MOVETYPE_NOCLIP;
beam->solid = SOLID_NOT;
beam->s.renderfx |= RF_BEAM;
beam->s.modelindex = 1;
beam->s.frame = 2;
beam->s.skinnum = (disrupt)?0xf0f2f2f2:0xf1f3f1f1;
beam->think = G_FreeEdict;
beam->nextthink = level.time + (1.1 * FRAMETIME); // give it enough time to be seen
VectorCopy(start, beam->s.origin);
VectorSubtract(tr.endpos, start, vbeam);
dist = 10.0 * VectorLength(vbeam);
VectorScale(aimdir, dist, beam->velocity);
gi.linkentity(beam);
}
// A living player is a valid target.
if (tr.ent->client && tr.ent->inuse && (tr.ent->health > 0))
{
VectorSubtract(tr.endpos, start, vbeam);
// If the target is currently using an AGM on us, cut our AGM beam and get damaged.
if (tr.ent->client->agm_target && (tr.ent->client->agm_target == self))
{
damage = (int)sv_agm_cross_damage->value;
if (self->client->quad_framenum > level.framenum)
damage *= (int)sv_quad_factor->value;
T_Damage(self, tr.ent, tr.ent, vbeam, start, vec3_origin, damage, -damage, 0, MOD_AGM_FEEDBACK);
self->client->agm_target = NULL;
self->client->agm_charge = 0;
gi.sound(self, CHAN_ITEM, gi.soundindex("weapons/agm/agm_cross.wav"), 1, ATTN_NORM, 0);
}
// The beam also cuts out if our target is currently being held by someone else's AGM,
// and we get damaged if the AGM wielder is not on our team.
else if (tr.ent->client->held_by_agm)
{
if (!CheckTeamDamage(tr.ent->client->agm_enemy, self))
{
damage = (int)sv_agm_cross_damage->value;
if (self->client->quad_framenum > level.framenum)
damage *= (int)sv_quad_factor->value;
T_Damage(self, tr.ent->client->agm_enemy, tr.ent->client->agm_enemy, vbeam, start, vec3_origin, damage, -damage, 0, MOD_AGM_FEEDBACK);
}
self->client->agm_target = NULL;
self->client->agm_charge = 0;
gi.sound(self, CHAN_ITEM, gi.soundindex("weapons/agm/agm_cross.wav"), 1, ATTN_NORM, 0);
}
// If our target has a Beam Reflector, we get bounced back and hurt if they're not on our team.
else if (tr.ent->client->antibeam_framenum > level.framenum)
{
if (!CheckTeamDamage(tr.ent, self))
{
damage = (int)sv_agm_reflect_damage->value;
if (self->client->quad_framenum > level.framenum)
damage *= (int)sv_quad_factor->value;
T_Damage(self, tr.ent, tr.ent, vbeam, start, vec3_origin, damage, -200, 0, MOD_AGM_BEAM_REF);
}
self->client->agm_target = NULL;
self->client->agm_charge = 0;
gi.sound(tr.ent, CHAN_ITEM, gi.soundindex("ctf/tech1.wav"), 1, ATTN_NORM, 0);
}
else
{
// Do cellular disruption effect rather than manipulation, if flagged.
// NB: armour doesn't protect against this effect.
if (disrupt)
{
if (!CheckTeamDamage(tr.ent, self))
{
damage = (int)sv_agm_disrupt_damage->value;
kick = -20;
// Apply powerup and Tech effects.
if (self->client->quad_framenum > level.framenum)
{
damage *= (int)sv_quad_factor->value;
kick *= (int)sv_quad_factor->value;
}
if ((self->client->haste_framenum > level.framenum) || CTFApplyHaste(self))
{
damage *= 2;
kick *= 2;
}
T_Damage(tr.ent, self, self, vbeam, tr.endpos, tr.plane.normal, damage, kick, (DAMAGE_ENERGY | DAMAGE_NO_ARMOR), MOD_AGM_DISRUPT);
}
return;
}
// As long as they don't have the Invulnerability, or are on our team, they're ours to play with
// like a cheap toy.
if ((tr.ent->client->invincible_framenum > level.framenum) && ((int)sv_agm_invuln_cells->value < 0))
return;
if (CheckTeamDamage(tr.ent, self))
{
self->client->agm_target = NULL;
return;
}
if (tr.ent->target_ent && tr.ent->target_ent->client && tr.ent->target_ent->client->spycam)
{
tr.ent = tr.ent->target_ent;
camera_off(tr.ent);
}
self->client->agm_target = tr.ent;
self->client->agm_range = VectorLength(vbeam);
if ((self->client->agm_range < AGM_RANGE_MIN) || tr.startsolid)
self->client->agm_range = AGM_RANGE_MIN;
tr.ent->client->agm_enemy = self;
tr.ent->client->held_by_agm = true;
tr.ent->client->flung_by_agm = false;
tr.ent->client->thrown_by_agm = false;
if (tr.ent->isabot)
tr.ent->client->movestate |= STS_AGMMOVE;
gi.sound(self, CHAN_WEAPON, gi.soundindex("brain/melee3.wav"), 1, ATTN_NORM, 0);
gi.sound(tr.ent, CHAN_BODY, gi.soundindex("brain/melee3.wav"), 1, ATTN_NORM, 0);
gi_centerprintf(tr.ent, "You're being manipulated by %s\n", self->client->pers.netname);
}
}
}
/*
======================================================================
DISC LAUNCHER subroutines
======================================================================
*/
void Disc_Pop(edict_t *self)
{
int n;
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_SPARKS);
gi.WritePosition(self->s.origin);
gi.WriteDir(vec3_origin);
gi.multicast(self->s.origin, MULTICAST_PVS);
n = rand() % 4;
while (n--)
ThrowDebris(self, "models/objects/debris2/tris.md2", 2, self->s.origin);
G_FreeEdict(self);
}
void Disc_Touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
// Vanish if we hit a sky surface.
if (surf && (surf->flags & SURF_SKY))
{
G_FreeEdict(self);
return;
}
// Ignore weapon projectiles.
if (other->wep_proj)
return;
// If it's a player then damage them, otherwise ricochet.
if (!other->takedamage)
{
int r = 1 + (rand() % 3);
gi.sound(self, CHAN_VOICE, gi.soundindex(va("weapons/disc/ric%i.wav", r)), 1, ATTN_NORM, 0);
if (self->owner && self->owner->client)
PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT);
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_SPARKS);
gi.WritePosition(self->s.origin);
if (!plane)
gi.WriteDir(vec3_origin);
else
gi.WriteDir(plane->normal);
gi.multicast(self->s.origin, MULTICAST_PVS);
self->owner = self; // reset for collision detection
}
else
{
gi.sound(self, CHAN_VOICE, gi.soundindex("weapons/disc/hit.wav"), 1, ATTN_NORM, 0);
if (self->owner && self->owner->client)
PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT);
T_Damage(other, self, self->oldenemy, self->velocity, self->s.origin, plane->normal, self->dmg, self->count, 0, MOD_DISC);
G_FreeEdict(self);
}
}
void Fire_Disc(edict_t *self, vec3_t start, vec3_t aimdir, int damage, float speed, int kick)
{
edict_t *disc;
disc = G_Spawn();
disc->solid = SOLID_BBOX;
disc->movetype = MOVETYPE_FLYBOUNCE;
disc->clipmask = MASK_SHOT;
disc->svflags = SVF_DEADMONSTER;
disc->s.effects |= EF_GRENADE;
disc->s.renderfx = RF_FULLBRIGHT;
disc->dmg = damage;
disc->count = kick;
disc->classname = "disc";
disc->wep_proj = true;
disc->oldenemy = self; // used for T_Damage() call
disc->owner = self;
disc->touch = Disc_Touch;
disc->think = Disc_Pop;
disc->nextthink = level.time + sv_disc_live_time->value;
disc->model = "models/objects/disc/tris.md2";
disc->s.modelindex = gi.modelindex(disc->model);
VectorSet(disc->mins, -8.0, -8.0, -2.0);
VectorSet(disc->maxs, 8.0, 8.0, 2.0);
VectorCopy(start, disc->s.origin);
VectorCopy(start, disc->s.old_origin);
vectoangles(aimdir, disc->s.angles);
VectorScale(aimdir, speed, disc->velocity);
gi.linkentity(disc);
}
/*
======================================================================
CHAINSAW subroutines
======================================================================
*/
void Fire_Chainsaw(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick)
{
trace_t tr;
vec3_t end;
vec3_t dmgdir;
// Trace a line from the start point to an imaginary chainsaw tip (it actually extends further
// than a chainsaw would, in order to make the weapon useful). If any entities are detected
// then damage them if possible, otherwise draw a shower of sparks.
VectorMA(start, RANGE_CHAINSAW, aimdir, end);
tr = gi.trace(start, NULL, NULL, end, self, MASK_SHOT);
if (tr.fraction < 1.0)
{
if (!tr.ent->takedamage)
{
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_SPARKS);
gi.WritePosition(tr.endpos);
if (!tr.plane.normal)
gi.WriteDir(vec3_origin);
else
gi.WriteDir(tr.plane.normal);
gi.multicast(tr.endpos, MULTICAST_PVS);
}
else
{
// Prevent dead bodies from being pulled off the ground.
if (tr.ent->die == body_die)
kick = 0;
// Set damage direction to be towards the attacking player, and apply damage.
VectorNegate(aimdir, dmgdir);
T_Damage(tr.ent, self, self, dmgdir, tr.endpos, tr.plane.normal, damage, kick, 0, MOD_CHAINSAW);
// If it's other players or dead bodies that are being chainsawed, produce lots of blood and gibs!
if (tr.ent->client || (tr.ent->die == body_die))
{
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_MOREBLOOD);
gi.WritePosition(tr.ent->s.origin);
gi.WriteDir(vec3_up);
gi.multicast(tr.ent->s.origin, MULTICAST_PVS);
if (random() < 0.5)
ThrowGib(tr.ent, "models/objects/gibs/sm_meat/tris.md2", 50, GIB_ORGANIC, 1.0);
}
}
}
// Destroy nearby Traps and C4 bundles (as gi.trace will ignore the player's own entities).
if (self->next_node)
{
edict_t *check;
edict_t *index;
vec3_t entdir;
qboolean finished = false;
index = self->next_node;
while (index && !finished)
{
check = index;
if (index->next_node)
index = index->next_node;
else
finished = true;
VectorSubtract(check->s.origin, start, entdir);
VectorNormalize(entdir);
if ((VecRange(start, check->s.origin) < RANGE_CHAINSAW) && (DotProduct(entdir, aimdir) > 0.9))
{
if (check->die == C4_DieFromDamage)
C4_Die(check);
else if (check->die == Trap_DieFromDamage)
Trap_Die(check);
else
gi.dprintf("BUG: Invalid next_node pointer in Fire_Chainsaw().\nPlease contact musashi@planetquake.com\n");
}
}
}
}
/*
======================================================================
MG TRACER ROUND subroutines
======================================================================
*/
void Tracer_Touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
if (self == other)
return;
if (other->classname && (!Q_stricmp(other->classname, "worldspawn") || !Q_stricmp(other->classname, "player")))
G_FreeEdict(self);
}
void Fire_Tracer(edict_t *self, vec3_t start, vec3_t aimdir, float speed, float lifetime)
{
edict_t *tracer;
tracer = G_Spawn();
tracer->svflags = SVF_DEADMONSTER | SVF_PROJECTILE;
tracer->solid = SOLID_TRIGGER;
tracer->movetype = MOVETYPE_FLY;
tracer->clipmask = 0;
tracer->classname = "tracer";
tracer->wep_proj = true;
tracer->s.renderfx |= RF_FULLBRIGHT;
tracer->touch = Tracer_Touch;
tracer->think = G_FreeEdict;
tracer->nextthink = level.time + lifetime;
tracer->model = "models/objects/tracer/tris.md2";
tracer->s.modelindex = tracer_index;
VectorCopy(start, tracer->s.origin);
VectorCopy(start, tracer->s.old_origin);
vectoangles(aimdir, tracer->s.angles);
VectorScale(aimdir, speed, tracer->velocity);
gi.linkentity(tracer);
}
/*
=================
PLASMA BOLT subroutines
=================
*/
void Plasma_Touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
// Ignore ourself and our owner.
if (other == self)
return;
if (other == self->owner)
return;
// Ignore weapon projectiles.
if (other->wep_proj)
return;
// Vanish if we hit a sky surface.
if (surf && (surf->flags & SURF_SKY))
{
G_FreeEdict(self);
return;
}
// Apply damage if appropriate.
if (other->takedamage)
T_Damage(other, self, self->owner, self->velocity, self->s.origin, plane->normal, self->dmg, 5, DAMAGE_ENERGY, MOD_PLASMA);
else
{
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_SHIELD_SPARKS);
gi.WritePosition (self->s.origin);
if (!plane)
gi.WriteDir(vec3_origin);
else
gi.WriteDir(plane->normal);
gi.multicast(self->s.origin, MULTICAST_PVS);
}
gi.sound(self, CHAN_WEAPON, gi.soundindex("world/explod2.wav"), 1, ATTN_NORM, 0);
if (self->owner && self->owner->client)
PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT);
G_FreeEdict(self);
}
void Fire_Plasma(edict_t *self, vec3_t start, vec3_t aimdir, int damage, float speed)
{
edict_t *bolt;
trace_t tr;
bolt = G_Spawn();
bolt->svflags = SVF_DEADMONSTER | SVF_PROJECTILE;
bolt->solid = SOLID_TRIGGER;
bolt->movetype = MOVETYPE_FLY;
bolt->clipmask = MASK_SHOT;
bolt->s.effects = EF_BLUEHYPERBLASTER;
bolt->s.renderfx = RF_FULLBRIGHT;
bolt->dmg = damage;
bolt->classname = "plasma_bolt";
bolt->owner = self;
bolt->touch = Plasma_Touch;
bolt->think = G_FreeEdict;
bolt->nextthink = level.time + PLASMA_LIVETIME;
bolt->s.modelindex = gi.modelindex("sprites/s_plasma.sp2");
bolt->s.sound = gi.soundindex("misc/lasfly.wav");
bolt->s.frame = 0;
bolt->s.skinnum = 0;
VectorCopy(start, bolt->s.origin);
VectorCopy(start, bolt->s.old_origin);
vectoangles(aimdir, bolt->s.angles);
VectorScale(aimdir, speed, bolt->velocity);
gi.linkentity(bolt);
tr = gi.trace(self->s.origin, NULL, NULL, bolt->s.origin, bolt, MASK_SHOT);
if (tr.fraction < 1.0)
{
VectorMA(bolt->s.origin, -10.0, aimdir, bolt->s.origin);
bolt->touch(bolt, tr.ent, NULL, NULL);
}
}
//CW--