thirtyflightsofloving/awaken2/g_weapon.c
Knightmare66 9481c7c513 Rewrote Com_strcpy() and Com_strcat() in game DLLs to not be based on strncpy() and to return size copied.
Changed Zaero and 3ZB2 game DLLs to use WORLD_SIZE for various calculations instead of 8192.
Cleaned up string handling in 3ZB2 game DLL.
Added func_plat2, func_door_secret2, and func_force_wall from Rogue to 3ZB2 game DLL.
Added alternate attack contact explode for grenade launcher in 3ZB2 game DLL.
Added awakening2 game DLL source.
2021-02-01 20:19:52 -05: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;
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)
{
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);
}
//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--