rogue/src/g_newweap.c
Yamagi 6196f3fb9a
Merge pull request #52 from BjossiAlfreds/plane
Safely use plane normal in rest of touch functions
2019-10-07 18:58:42 +02:00

1927 lines
39 KiB
C

/*
* =======================================================================
*
* Rogue specific weapons.
*
* =======================================================================
*/
#include "header/local.h"
#define NUKE_DELAY 4
#define NUKE_TIME_TO_LIVE 6
#define NUKE_RADIUS 512
#define NUKE_DAMAGE 400
#define NUKE_QUAKE_TIME 3
#define NUKE_QUAKE_STRENGTH 100
#define PROX_TIME_TO_LIVE 45
#define PROX_TIME_DELAY 0.5
#define PROX_BOUND_SIZE 96
#define PROX_DAMAGE_RADIUS 192
#define PROX_HEALTH 20
#define PROX_DAMAGE 90
#define TESLA_TIME_TO_LIVE 30
#define TESLA_DAMAGE_RADIUS 128
#define TESLA_DAMAGE 3
#define TESLA_KNOCKBACK 8
#define TESLA_ACTIVATE_TIME 3
#define TESLA_EXPLOSION_DAMAGE_MULT 50
#define TESLA_EXPLOSION_RADIUS 200
#define TRACKER_DAMAGE_FLAGS (DAMAGE_NO_POWER_ARMOR | DAMAGE_ENERGY | DAMAGE_NO_KNOCKBACK)
#define TRACKER_IMPACT_FLAGS (DAMAGE_NO_POWER_ARMOR | DAMAGE_ENERGY)
#define TRACKER_DAMAGE_TIME 0.5
extern byte P_DamageModifier(edict_t *ent);
extern void check_dodge(edict_t *self, vec3_t start, vec3_t dir, int speed);
extern void hurt_touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf);
extern void droptofloor(edict_t *ent);
extern void Grenade_Explode(edict_t *ent);
extern void drawbbox(edict_t *ent);
void
flechette_touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
vec3_t dir;
vec3_t normal;
if (!self || !other)
{
return;
}
if (other == self->owner)
{
return;
}
if (surf && (surf->flags & SURF_SKY))
{
G_FreeEdict(self);
return;
}
if (self->client)
{
PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT);
}
get_normal_vector(plane, normal);
if (other->takedamage)
{
T_Damage(other, self, self->owner, self->velocity, self->s.origin,
normal, self->dmg, self->dmg_radius, DAMAGE_NO_REG_ARMOR,
MOD_ETF_RIFLE);
}
else
{
VectorScale(normal, 256, dir);
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_FLECHETTE);
gi.WritePosition(self->s.origin);
gi.WriteDir(dir);
gi.multicast(self->s.origin, MULTICAST_PVS);
}
G_FreeEdict(self);
}
void
fire_flechette(edict_t *self, vec3_t start, vec3_t dir, int damage,
int speed, int kick)
{
edict_t *flechette;
if (!self)
{
return;
}
VectorNormalize(dir);
flechette = G_Spawn();
VectorCopy(start, flechette->s.origin);
VectorCopy(start, flechette->s.old_origin);
vectoangles2(dir, flechette->s.angles);
VectorScale(dir, speed, flechette->velocity);
flechette->movetype = MOVETYPE_FLYMISSILE;
flechette->clipmask = MASK_SHOT;
flechette->solid = SOLID_BBOX;
flechette->s.renderfx = RF_FULLBRIGHT;
VectorClear(flechette->mins);
VectorClear(flechette->maxs);
flechette->s.modelindex = gi.modelindex("models/proj/flechette/tris.md2");
flechette->owner = self;
flechette->touch = flechette_touch;
flechette->nextthink = level.time + (8000.0f / (float)speed);
flechette->think = G_FreeEdict;
flechette->dmg = damage;
flechette->dmg_radius = kick;
gi.linkentity(flechette);
if (self->client)
{
check_dodge(self, flechette->s.origin, dir, speed);
}
}
void
Prox_Explode(edict_t *ent)
{
vec3_t origin;
edict_t *owner;
if (!ent)
{
return;
}
/* free the trigger field */
if (ent->teamchain && (ent->teamchain->owner == ent))
{
G_FreeEdict(ent->teamchain);
}
owner = ent;
if (ent->teammaster)
{
owner = ent->teammaster;
PlayerNoise(owner, ent->s.origin, PNOISE_IMPACT);
}
/* play double/quad sound if appopriate */
if (ent->dmg >= (PROX_DAMAGE * 4))
{
gi.sound(ent, CHAN_ITEM, gi.soundindex("items/damage3.wav"), 1, ATTN_NORM, 0);
}
else if (ent->dmg == (PROX_DAMAGE * 2))
{
gi.sound(ent, CHAN_ITEM, gi.soundindex("misc/ddamage3.wav"), 1, ATTN_NORM, 0);
}
ent->takedamage = DAMAGE_NO;
T_RadiusDamage(ent, owner, ent->dmg, ent, PROX_DAMAGE_RADIUS, MOD_PROX);
VectorMA(ent->s.origin, -0.02, ent->velocity, origin);
gi.WriteByte(svc_temp_entity);
if (ent->groundentity)
{
gi.WriteByte(TE_GRENADE_EXPLOSION);
}
else
{
gi.WriteByte(TE_ROCKET_EXPLOSION);
}
gi.WritePosition(origin);
gi.multicast(ent->s.origin, MULTICAST_PVS);
G_FreeEdict(ent);
}
void
prox_die(edict_t *self, edict_t *inflictor, edict_t *attacker /* unused */,
int damage, vec3_t point)
{
if (!self || !inflictor)
{
return;
}
if (strcmp(inflictor->classname, "prox"))
{
self->takedamage = DAMAGE_NO;
Prox_Explode(self);
}
else
{
self->takedamage = DAMAGE_NO;
self->think = Prox_Explode;
self->nextthink = level.time + FRAMETIME;
}
}
void
Prox_Field_Touch(edict_t *ent, edict_t *other, cplane_t *plane /* unused */,
csurface_t *surf /* unused */)
{
edict_t *prox;
if (!ent || !other)
{
return;
}
if (!(other->svflags & SVF_MONSTER) && !other->client)
{
return;
}
/* trigger the prox mine if it's still there, and still mine. */
prox = ent->owner;
if (other == prox) /* don't set self off */
{
return;
}
if (prox->think == Prox_Explode) /* we're set to blow! */
{
return;
}
if (prox->teamchain == ent)
{
gi.sound(ent, CHAN_VOICE, gi.soundindex("weapons/proxwarn.wav"), 1, ATTN_NORM, 0);
prox->think = Prox_Explode;
prox->nextthink = level.time + PROX_TIME_DELAY;
return;
}
ent->solid = SOLID_NOT;
G_FreeEdict(ent);
}
void
prox_seek(edict_t *ent)
{
if (!ent)
{
return;
}
if (level.time > ent->wait)
{
Prox_Explode(ent);
}
else
{
ent->s.frame++;
if (ent->s.frame > 13)
{
ent->s.frame = 9;
}
ent->think = prox_seek;
ent->nextthink = level.time + 0.1;
}
}
void
prox_open(edict_t *ent)
{
edict_t *search;
if (!ent)
{
return;
}
search = NULL;
if (ent->s.frame == 9) /* end of opening animation */
{
/* set the owner to NULL so the owner can shoot it, etc.
needs to be done here so the owner doesn't get stuck on
it while it's opening if fired at point blank wall */
ent->s.sound = 0;
ent->owner = NULL;
if (ent->teamchain)
{
ent->teamchain->touch = Prox_Field_Touch;
}
while ((search = findradius(search, ent->s.origin, PROX_DAMAGE_RADIUS + 10)) != NULL)
{
if (!search->classname) /* tag token and other weird shit */
{
continue;
}
/* if it's a monster or player with health > 0
or it's a player start point and we can see it
blow up */
if (((((search->svflags & SVF_MONSTER) ||
(search->client)) && (search->health > 0)) ||
((deathmatch->value) &&((!strcmp(search->classname, "info_player_deathmatch")) ||
(!strcmp(search->classname, "info_player_start")) ||
(!strcmp(search->classname, "info_player_coop")) ||
(!strcmp(search->classname, "misc_teleporter_dest"))))) &&
(visible(search, ent)))
{
gi.sound(ent, CHAN_VOICE, gi.soundindex("weapons/proxwarn.wav"), 1, ATTN_NORM, 0);
Prox_Explode(ent);
return;
}
}
if (strong_mines && (strong_mines->value))
{
ent->wait = level.time + PROX_TIME_TO_LIVE;
}
else
{
switch (ent->dmg / PROX_DAMAGE)
{
case 1:
ent->wait = level.time + PROX_TIME_TO_LIVE;
break;
case 2:
ent->wait = level.time + 30;
break;
case 4:
ent->wait = level.time + 15;
break;
case 8:
ent->wait = level.time + 10;
break;
default:
ent->wait = level.time + PROX_TIME_TO_LIVE;
break;
}
}
ent->think = prox_seek;
ent->nextthink = level.time + 0.2;
}
else
{
if (ent->s.frame == 0)
{
gi.sound(ent, CHAN_VOICE, gi.soundindex("weapons/proxopen.wav"), 1, ATTN_NORM, 0);
}
ent->s.frame++;
ent->think = prox_open;
ent->nextthink = level.time + 0.05;
}
}
void
prox_land(edict_t *ent, edict_t *other, cplane_t *plane, csurface_t *surf)
{
edict_t *field;
vec3_t dir;
vec3_t normal;
vec3_t forward, right, up;
int movetype = MOVETYPE_NONE;
int stick_ok = 0;
vec3_t land_point;
if (!ent || !other)
{
return;
}
/* must turn off owner so owner can shoot it and set it off
moved to prox_open so owner can get away from it if fired
at pointblank range into wall */
if (surf && (surf->flags & SURF_SKY))
{
G_FreeEdict(ent);
return;
}
get_normal_vector(plane, normal);
VectorMA(ent->s.origin, -10.0, normal, land_point);
if (gi.pointcontents(land_point) & (CONTENTS_SLIME | CONTENTS_LAVA))
{
Prox_Explode(ent);
return;
}
if ((other->svflags & (SVF_MONSTER|SVF_DAMAGEABLE)) || other->client)
{
if (other != ent->teammaster)
{
Prox_Explode(ent);
}
return;
}
#define STOP_EPSILON 0.1
else if (other != world)
{
/* Here we need to check to see if we can stop on this entity. */
vec3_t out;
float backoff, change;
int i;
if ((other->movetype == MOVETYPE_PUSH) && (normal[2] > 0.7))
{
stick_ok = 1;
}
else
{
stick_ok = 0;
}
backoff = DotProduct(ent->velocity, normal) * 1.5;
for (i = 0; i < 3; i++)
{
change = normal[i] * backoff;
out[i] = ent->velocity[i] - change;
if ((out[i] > -STOP_EPSILON) && (out[i] < STOP_EPSILON))
{
out[i] = 0;
}
}
if (out[2] > 60)
{
return;
}
movetype = MOVETYPE_BOUNCE;
/* if we're here, we're going to stop on an entity */
if (stick_ok)
{
/* it's a happy entity */
VectorCopy(vec3_origin, ent->velocity);
VectorCopy(vec3_origin, ent->avelocity);
}
else /* no-stick. teflon time */
{
if (normal[2] > 0.7)
{
Prox_Explode(ent);
return;
}
return;
}
}
else if (other->s.modelindex != 1)
{
return;
}
vectoangles2(normal, dir);
AngleVectors(dir, forward, right, up);
if (gi.pointcontents(ent->s.origin) & (CONTENTS_LAVA | CONTENTS_SLIME))
{
Prox_Explode(ent);
return;
}
field = G_Spawn();
VectorCopy(ent->s.origin, field->s.origin);
VectorClear(field->velocity);
VectorClear(field->avelocity);
VectorSet(field->mins, -PROX_BOUND_SIZE, -PROX_BOUND_SIZE, -PROX_BOUND_SIZE);
VectorSet(field->maxs, PROX_BOUND_SIZE, PROX_BOUND_SIZE, PROX_BOUND_SIZE);
field->movetype = MOVETYPE_NONE;
field->solid = SOLID_TRIGGER;
field->owner = ent;
field->classname = "prox_field";
field->teammaster = ent;
gi.linkentity(field);
VectorClear(ent->velocity);
VectorClear(ent->avelocity);
/* rotate to vertical */
dir[PITCH] = dir[PITCH] + 90;
VectorCopy(dir, ent->s.angles);
ent->takedamage = DAMAGE_AIM;
ent->movetype = movetype; /* either bounce or none, depending on whether we stuck to something */
ent->die = prox_die;
ent->teamchain = field;
ent->health = PROX_HEALTH;
ent->nextthink = level.time + 0.05;
ent->think = prox_open;
ent->touch = NULL;
ent->solid = SOLID_BBOX;
/* record who we're attached to */
gi.linkentity(ent);
}
void
fire_prox(edict_t *self, vec3_t start, vec3_t aimdir, int damage_multiplier, int speed)
{
edict_t *prox;
vec3_t dir;
vec3_t forward, right, up;
if (!self)
{
return;
}
vectoangles2(aimdir, dir);
AngleVectors(dir, forward, right, up);
prox = G_Spawn();
VectorCopy(start, prox->s.origin);
VectorScale(aimdir, speed, prox->velocity);
VectorMA(prox->velocity, 200 + crandom() * 10.0, up, prox->velocity);
VectorMA(prox->velocity, crandom() * 10.0, right, prox->velocity);
VectorCopy(dir, prox->s.angles);
prox->s.angles[PITCH] -= 90;
prox->movetype = MOVETYPE_BOUNCE;
prox->solid = SOLID_BBOX;
prox->s.effects |= EF_GRENADE;
prox->clipmask = MASK_SHOT | CONTENTS_LAVA | CONTENTS_SLIME;
prox->s.renderfx |= RF_IR_VISIBLE;
VectorSet(prox->mins, -6, -6, -6);
VectorSet(prox->maxs, 6, 6, 6);
prox->s.modelindex = gi.modelindex("models/weapons/g_prox/tris.md2");
prox->owner = self;
prox->teammaster = self;
prox->touch = prox_land;
prox->think = Prox_Explode;
prox->dmg = PROX_DAMAGE * damage_multiplier;
prox->classname = "prox";
prox->svflags |= SVF_DAMAGEABLE;
prox->flags |= FL_MECHANICAL;
switch (damage_multiplier)
{
case 1:
prox->nextthink = level.time + PROX_TIME_TO_LIVE;
break;
case 2:
prox->nextthink = level.time + 30;
break;
case 4:
prox->nextthink = level.time + 15;
break;
case 8:
prox->nextthink = level.time + 10;
break;
default:
prox->nextthink = level.time + PROX_TIME_TO_LIVE;
break;
}
gi.linkentity(prox);
}
void
fire_player_melee(edict_t *self, vec3_t start, vec3_t aim, int reach,
int damage, int kick, int quiet, int mod)
{
vec3_t forward, right, up;
vec3_t v;
vec3_t point;
trace_t tr;
if (!self)
{
return;
}
vectoangles2(aim, v);
AngleVectors(v, forward, right, up);
VectorNormalize(forward);
VectorMA(start, reach, forward, point);
/* see if the hit connects */
tr = gi.trace(start, NULL, NULL, point, self, MASK_SHOT);
if (tr.fraction == 1.0)
{
if (!quiet)
{
gi.sound(self, CHAN_WEAPON, gi.soundindex("weapons/swish.wav"), 1, ATTN_NORM, 0);
}
return;
}
if ((tr.ent->takedamage == DAMAGE_YES) ||
(tr.ent->takedamage == DAMAGE_AIM))
{
/* pull the player forward if you do damage */
VectorMA(self->velocity, 75, forward, self->velocity);
VectorMA(self->velocity, 75, up, self->velocity);
/* do the damage */
if (mod == MOD_CHAINFIST)
{
T_Damage(tr.ent, self, self, vec3_origin, tr.ent->s.origin, vec3_origin,
damage, kick / 2, DAMAGE_DESTROY_ARMOR | DAMAGE_NO_KNOCKBACK, mod);
}
else
{
T_Damage(tr.ent, self, self, vec3_origin, tr.ent->s.origin, vec3_origin,
damage, kick / 2, DAMAGE_NO_KNOCKBACK, mod);
}
if (!quiet)
{
gi.sound(self, CHAN_WEAPON, gi.soundindex("weapons/meatht.wav"), 1, ATTN_NORM, 0);
}
}
else
{
if (!quiet)
{
gi.sound(self, CHAN_WEAPON, gi.soundindex("weapons/tink1.wav"), 1, ATTN_NORM, 0);
}
VectorScale(tr.plane.normal, 256, point);
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_GUNSHOT);
gi.WritePosition(tr.endpos);
gi.WriteDir(point);
gi.multicast(tr.endpos, MULTICAST_PVS);
}
}
void
Nuke_Quake(edict_t *self)
{
int i;
edict_t *e;
if (!self)
{
return;
}
if (self->last_move_time < level.time)
{
gi.positioned_sound(self->s.origin, self, CHAN_AUTO, self->noise_index,
0.75, ATTN_NONE, 0);
self->last_move_time = level.time + 0.5;
}
for (i = 1, e = g_edicts + i; i < globals.num_edicts; i++, e++)
{
if (!e->inuse)
{
continue;
}
if (!e->client)
{
continue;
}
if (!e->groundentity)
{
continue;
}
e->groundentity = NULL;
e->velocity[0] += crandom() * 150;
e->velocity[1] += crandom() * 150;
e->velocity[2] = self->speed * (100.0 / e->mass);
}
if (level.time < self->timestamp)
{
self->nextthink = level.time + FRAMETIME;
}
else
{
G_FreeEdict(self);
}
}
void
Nuke_Explode(edict_t *ent)
{
if (!ent)
{
return;
}
if (ent->teammaster->client)
{
PlayerNoise(ent->teammaster, ent->s.origin, PNOISE_IMPACT);
}
T_RadiusNukeDamage(ent, ent->teammaster, ent->dmg,
ent, ent->dmg_radius, MOD_NUKE);
if (ent->dmg >= (NUKE_DAMAGE * 4))
{
gi.sound(ent, CHAN_ITEM, gi.soundindex("items/damage3.wav"), 1, ATTN_NORM, 0);
}
else if (ent->dmg == (NUKE_DAMAGE * 2))
{
gi.sound(ent, CHAN_ITEM, gi.soundindex("misc/ddamage3.wav"), 1, ATTN_NORM, 0);
}
gi.sound(ent, CHAN_NO_PHS_ADD + CHAN_VOICE, gi.soundindex("weapons/grenlx1a.wav"), 1, ATTN_NONE, 0);
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_EXPLOSION1_BIG);
gi.WritePosition(ent->s.origin);
gi.multicast(ent->s.origin, MULTICAST_PVS);
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_NUKEBLAST);
gi.WritePosition(ent->s.origin);
gi.multicast(ent->s.origin, MULTICAST_ALL);
/* become a quake */
ent->svflags |= SVF_NOCLIENT;
ent->noise_index = gi.soundindex("world/rumble.wav");
ent->think = Nuke_Quake;
ent->speed = NUKE_QUAKE_STRENGTH;
ent->timestamp = level.time + NUKE_QUAKE_TIME;
ent->nextthink = level.time + FRAMETIME;
ent->last_move_time = 0;
}
void
nuke_die(edict_t *self, edict_t *inflictor /* unused */,
edict_t *attacker, int damage, vec3_t point)
{
if (!self)
{
return;
}
self->takedamage = DAMAGE_NO;
if ((attacker) && !(strcmp(attacker->classname, "nuke")))
{
G_FreeEdict(self);
return;
}
Nuke_Explode(self);
}
void
Nuke_Think(edict_t *ent)
{
float attenuation, default_atten = 1.8;
int damage_multiplier, muzzleflash;
if (!ent)
{
return;
}
damage_multiplier = ent->dmg / NUKE_DAMAGE;
switch (damage_multiplier)
{
case 1:
attenuation = default_atten / 1.4;
muzzleflash = MZ_NUKE1;
break;
case 2:
attenuation = default_atten / 2.0;
muzzleflash = MZ_NUKE2;
break;
case 4:
attenuation = default_atten / 3.0;
muzzleflash = MZ_NUKE4;
break;
case 8:
attenuation = default_atten / 5.0;
muzzleflash = MZ_NUKE8;
break;
default:
attenuation = default_atten;
muzzleflash = MZ_NUKE1;
break;
}
if (ent->wait < level.time)
{
Nuke_Explode(ent);
}
else if (level.time >= (ent->wait - NUKE_TIME_TO_LIVE))
{
ent->s.frame++;
if (ent->s.frame > 11)
{
ent->s.frame = 6;
}
if (gi.pointcontents(ent->s.origin) & (CONTENTS_SLIME | CONTENTS_LAVA))
{
Nuke_Explode(ent);
return;
}
ent->think = Nuke_Think;
ent->nextthink = level.time + 0.1;
ent->health = 1;
ent->owner = NULL;
gi.WriteByte(svc_muzzleflash);
gi.WriteShort(ent - g_edicts);
gi.WriteByte(muzzleflash);
gi.multicast(ent->s.origin, MULTICAST_PVS);
if (ent->timestamp <= level.time)
{
if ((ent->wait - level.time) <= (NUKE_TIME_TO_LIVE / 2.0))
{
gi.sound(ent, CHAN_NO_PHS_ADD + CHAN_VOICE, gi.soundindex("weapons/nukewarn2.wav"), 1, attenuation, 0);
ent->timestamp = level.time + 0.3;
}
else
{
gi.sound(ent, CHAN_NO_PHS_ADD + CHAN_VOICE, gi.soundindex("weapons/nukewarn2.wav"), 1, attenuation, 0);
ent->timestamp = level.time + 0.5;
}
}
}
else
{
if (ent->timestamp <= level.time)
{
gi.sound(ent, CHAN_NO_PHS_ADD + CHAN_VOICE, gi.soundindex("weapons/nukewarn2.wav"), 1, attenuation, 0);
ent->timestamp = level.time + 1.0;
}
ent->nextthink = level.time + FRAMETIME;
}
}
void
nuke_bounce(edict_t *ent, edict_t *other /* unused */, cplane_t *plane /* unused */,
csurface_t *surf /* unused */)
{
if (!ent)
{
return;
}
if (random() > 0.5)
{
gi.sound(ent, CHAN_VOICE, gi.soundindex("weapons/hgrenb1a.wav"), 1, ATTN_NORM, 0);
}
else
{
gi.sound(ent, CHAN_VOICE, gi.soundindex("weapons/hgrenb2a.wav"), 1, ATTN_NORM, 0);
}
}
void
fire_nuke(edict_t *self, vec3_t start, vec3_t aimdir, int speed)
{
edict_t *nuke;
vec3_t dir;
vec3_t forward, right, up;
int damage_modifier;
if (!self)
{
return;
}
damage_modifier = (int)P_DamageModifier(self);
vectoangles2(aimdir, dir);
AngleVectors(dir, forward, right, up);
nuke = G_Spawn();
VectorCopy(start, nuke->s.origin);
VectorScale(aimdir, speed, nuke->velocity);
VectorMA(nuke->velocity, 200 + crandom() * 10.0, up, nuke->velocity);
VectorMA(nuke->velocity, crandom() * 10.0, right, nuke->velocity);
VectorClear(nuke->avelocity);
VectorClear(nuke->s.angles);
nuke->movetype = MOVETYPE_BOUNCE;
nuke->clipmask = MASK_SHOT;
nuke->solid = SOLID_BBOX;
nuke->s.effects |= EF_GRENADE;
nuke->s.renderfx |= RF_IR_VISIBLE;
VectorSet(nuke->mins, -8, -8, 0);
VectorSet(nuke->maxs, 8, 8, 16);
nuke->s.modelindex = gi.modelindex("models/weapons/g_nuke/tris.md2");
nuke->owner = self;
nuke->teammaster = self;
nuke->nextthink = level.time + FRAMETIME;
nuke->wait = level.time + NUKE_DELAY + NUKE_TIME_TO_LIVE;
nuke->think = Nuke_Think;
nuke->touch = nuke_bounce;
nuke->health = 10000;
nuke->takedamage = DAMAGE_YES;
nuke->svflags |= SVF_DAMAGEABLE;
nuke->dmg = NUKE_DAMAGE * damage_modifier;
if (damage_modifier == 1)
{
nuke->dmg_radius = NUKE_RADIUS;
}
else
{
nuke->dmg_radius = NUKE_RADIUS + NUKE_RADIUS * (0.25 * (float)damage_modifier);
}
nuke->classname = "nuke";
nuke->die = nuke_die;
gi.linkentity(nuke);
}
void
tesla_remove(edict_t *self)
{
edict_t *cur, *next;
if (!self)
{
return;
}
self->takedamage = DAMAGE_NO;
if (self->teamchain)
{
cur = self->teamchain;
while (cur)
{
next = cur->teamchain;
G_FreeEdict(cur);
cur = next;
}
}
else if (self->air_finished)
{
gi.dprintf("tesla without a field!\n");
}
self->owner = self->teammaster; /* Going away, set the owner correctly. */
self->enemy = NULL;
/* play double/quad sound if doubled/quadded and an underwater explosion */
if (self->dmg_radius)
{
if (self->dmg >= (TESLA_DAMAGE * TESLA_EXPLOSION_DAMAGE_MULT * 4))
{
gi.sound(self, CHAN_ITEM, gi.soundindex("items/damage3.wav"), 1, ATTN_NORM, 0);
}
else if (self->dmg == (TESLA_DAMAGE * TESLA_EXPLOSION_DAMAGE_MULT * 2))
{
gi.sound(self, CHAN_ITEM, gi.soundindex("misc/ddamage3.wav"), 1, ATTN_NORM, 0);
}
}
Grenade_Explode(self);
}
void
tesla_die(edict_t *self, edict_t *inflictor /* unused */, edict_t *attacker /* unused */,
int damage /* unused */, vec3_t point /* unused */)
{
if (!self)
{
return;
}
tesla_remove(self);
}
void
tesla_blow(edict_t *self)
{
if (!self)
{
return;
}
self->dmg = self->dmg * TESLA_EXPLOSION_DAMAGE_MULT;
self->dmg_radius = TESLA_EXPLOSION_RADIUS;
tesla_remove(self);
}
void
tesla_zap(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
}
void
tesla_think_active(edict_t *self)
{
int i, num;
edict_t *touch[MAX_EDICTS], *hit;
vec3_t dir, start;
trace_t tr;
if (!self)
{
return;
}
if (level.time > self->air_finished)
{
tesla_remove(self);
return;
}
VectorCopy(self->s.origin, start);
start[2] += 16;
num = gi.BoxEdicts(self->teamchain->absmin, self->teamchain->absmax,
touch, MAX_EDICTS, AREA_SOLID);
for (i = 0; i < num; i++)
{
/* if the tesla died while zapping things, stop zapping. */
if (!(self->inuse))
{
break;
}
hit = touch[i];
if (!hit->inuse)
{
continue;
}
if (hit == self)
{
continue;
}
if (hit->health < 1)
{
continue;
}
/* don't hit clients in single-player or coop */
if (hit->client)
{
if (coop->value || !deathmatch->value)
{
continue;
}
}
if (!(hit->svflags & (SVF_MONSTER | SVF_DAMAGEABLE)) && !hit->client)
{
continue;
}
tr = gi.trace(start, vec3_origin, vec3_origin, hit->s.origin,
self, MASK_SHOT);
if ((tr.fraction == 1) || (tr.ent == hit))
{
VectorSubtract(hit->s.origin, start, dir);
/* play double/quad sound if it's above the "normal" damage */
if (self->dmg >= (TESLA_DAMAGE * 4))
{
gi.sound(self, CHAN_ITEM, gi.soundindex("items/damage3.wav"), 1, ATTN_NORM, 0);
}
else if (self->dmg == (TESLA_DAMAGE * 2))
{
gi.sound(self, CHAN_ITEM, gi.soundindex("misc/ddamage3.wav"), 1, ATTN_NORM, 0);
}
/* don't do knockback to walking monsters */
if ((hit->svflags & SVF_MONSTER) &&
!(hit->flags & (FL_FLY | FL_SWIM)))
{
T_Damage(hit, self, self->teammaster, dir, tr.endpos,
tr.plane.normal, self->dmg, 0, 0, MOD_TESLA);
}
else
{
T_Damage(hit, self, self->teammaster, dir, tr.endpos, tr.plane.normal,
self->dmg, TESLA_KNOCKBACK, 0, MOD_TESLA);
}
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_LIGHTNING);
gi.WriteShort(hit - g_edicts); /* destination entity */
gi.WriteShort(self - g_edicts); /* source entity */
gi.WritePosition(tr.endpos);
gi.WritePosition(start);
gi.multicast(start, MULTICAST_PVS);
}
}
if (self->inuse)
{
self->think = tesla_think_active;
self->nextthink = level.time + FRAMETIME;
}
}
void
tesla_activate(edict_t *self)
{
edict_t *trigger;
edict_t *search;
if (!self)
{
return;
}
if (gi.pointcontents(self->s.origin) & (CONTENTS_SLIME | CONTENTS_LAVA | CONTENTS_WATER))
{
tesla_blow(self);
return;
}
/* only check for spawn points in deathmatch */
if (deathmatch->value)
{
search = NULL;
while ((search = findradius(search, self->s.origin, 1.5 * TESLA_DAMAGE_RADIUS)) != NULL)
{
if (search->classname)
{
if (((!strcmp(search->classname, "info_player_deathmatch")) ||
(!strcmp(search->classname, "info_player_start")) ||
(!strcmp(search->classname, "info_player_coop")) ||
(!strcmp(search->classname, "misc_teleporter_dest"))) &&
(visible(search, self)))
{
tesla_remove(self);
return;
}
}
}
}
trigger = G_Spawn();
VectorCopy(self->s.origin, trigger->s.origin);
VectorSet(trigger->mins, -TESLA_DAMAGE_RADIUS, -TESLA_DAMAGE_RADIUS, self->mins[2]);
VectorSet(trigger->maxs, TESLA_DAMAGE_RADIUS, TESLA_DAMAGE_RADIUS, TESLA_DAMAGE_RADIUS);
trigger->movetype = MOVETYPE_NONE;
trigger->solid = SOLID_TRIGGER;
trigger->owner = self;
trigger->touch = tesla_zap;
trigger->classname = "tesla trigger";
/* doesn't need to be marked as a teamslave since the move code for bounce looks for teamchains */
gi.linkentity(trigger);
VectorClear(self->s.angles);
/* clear the owner if in deathmatch */
if (deathmatch->value)
{
self->owner = NULL;
}
self->teamchain = trigger;
self->think = tesla_think_active;
self->nextthink = level.time + FRAMETIME;
self->air_finished = level.time + TESLA_TIME_TO_LIVE;
}
void
tesla_think(edict_t *ent)
{
if (!ent)
{
return;
}
if (gi.pointcontents(ent->s.origin) & (CONTENTS_SLIME | CONTENTS_LAVA))
{
tesla_remove(ent);
return;
}
VectorClear(ent->s.angles);
if (!(ent->s.frame))
{
gi.sound(ent, CHAN_VOICE, gi.soundindex("weapons/teslaopen.wav"), 1, ATTN_NORM, 0);
}
ent->s.frame++;
if (ent->s.frame > 14)
{
ent->s.frame = 14;
ent->think = tesla_activate;
ent->nextthink = level.time + 0.1;
}
else
{
if (ent->s.frame > 9)
{
if (ent->s.frame == 10)
{
if (ent->owner && ent->owner->client)
{
PlayerNoise(ent->owner, ent->s.origin, PNOISE_WEAPON); /* PGM */
}
ent->s.skinnum = 1;
}
else if (ent->s.frame == 12)
{
ent->s.skinnum = 2;
}
else if (ent->s.frame == 14)
{
ent->s.skinnum = 3;
}
}
ent->think = tesla_think;
ent->nextthink = level.time + 0.1;
}
}
void
tesla_lava(edict_t *ent, edict_t *other /* unused */, cplane_t *plane, csurface_t *surf /* unused */)
{
vec3_t land_point;
vec3_t normal;
if (!ent)
{
return;
}
get_normal_vector(plane, normal);
VectorMA(ent->s.origin, -20.0, normal, land_point);
if (gi.pointcontents(land_point) & (CONTENTS_SLIME | CONTENTS_LAVA))
{
tesla_blow(ent);
return;
}
if (random() > 0.5)
{
gi.sound(ent, CHAN_VOICE, gi.soundindex("weapons/hgrenb1a.wav"), 1, ATTN_NORM, 0);
}
else
{
gi.sound(ent, CHAN_VOICE, gi.soundindex("weapons/hgrenb2a.wav"), 1, ATTN_NORM, 0);
}
}
void
fire_tesla(edict_t *self, vec3_t start, vec3_t aimdir,
int damage_multiplier, int speed)
{
edict_t *tesla;
vec3_t dir;
vec3_t forward, right, up;
if (!self)
{
return;
}
vectoangles2(aimdir, dir);
AngleVectors(dir, forward, right, up);
tesla = G_Spawn();
VectorCopy(start, tesla->s.origin);
VectorScale(aimdir, speed, tesla->velocity);
VectorMA(tesla->velocity, 200 + crandom() * 10.0, up, tesla->velocity);
VectorMA(tesla->velocity, crandom() * 10.0, right, tesla->velocity);
VectorClear(tesla->s.angles);
tesla->movetype = MOVETYPE_BOUNCE;
tesla->solid = SOLID_BBOX;
tesla->s.effects |= EF_GRENADE;
tesla->s.renderfx |= RF_IR_VISIBLE;
VectorSet(tesla->mins, -12, -12, 0);
VectorSet(tesla->maxs, 12, 12, 20);
tesla->s.modelindex = gi.modelindex("models/weapons/g_tesla/tris.md2");
tesla->owner = self;
tesla->teammaster = self;
tesla->wait = level.time + TESLA_TIME_TO_LIVE;
tesla->think = tesla_think;
tesla->nextthink = level.time + TESLA_ACTIVATE_TIME;
/* blow up on contact with lava & slime code */
tesla->touch = tesla_lava;
if (deathmatch->value)
{
tesla->health = 20;
}
else
{
tesla->health = 30;
}
tesla->takedamage = DAMAGE_YES;
tesla->die = tesla_die;
tesla->dmg = TESLA_DAMAGE * damage_multiplier;
tesla->classname = "tesla";
tesla->svflags |= SVF_DAMAGEABLE;
tesla->clipmask = MASK_SHOT | CONTENTS_SLIME | CONTENTS_LAVA;
tesla->flags |= FL_MECHANICAL;
gi.linkentity(tesla);
}
void
fire_beams(edict_t *self, vec3_t start, vec3_t aimdir, vec3_t offset,
int damage, int kick, int te_beam, int te_impact, int mod)
{
trace_t tr;
vec3_t dir;
vec3_t forward, right, up;
vec3_t end;
vec3_t water_start, endpoint;
qboolean water = false, underwater = false;
int content_mask = MASK_SHOT | MASK_WATER;
vec3_t beam_endpt;
if (!self)
{
return;
}
vectoangles2(aimdir, dir);
AngleVectors(dir, forward, right, up);
VectorMA(start, 8192, forward, end);
if (gi.pointcontents(start) & MASK_WATER)
{
underwater = 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)
{
water = true;
VectorCopy(tr.endpos, water_start);
if (!VectorCompare(start, tr.endpos))
{
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_HEATBEAM_SPARKS);
gi.WritePosition(water_start);
gi.WriteDir(tr.plane.normal);
gi.multicast(tr.endpos, MULTICAST_PVS);
}
/* re-trace ignoring water this time */
tr = gi.trace(water_start, NULL, NULL, end, self, MASK_SHOT);
}
VectorCopy(tr.endpos, endpoint);
/* halve the damage if target underwater */
if (water)
{
damage = damage / 2;
}
/* send gun puff / flash */
if (!((tr.surface) && (tr.surface->flags & SURF_SKY)))
{
if (tr.fraction < 1.0)
{
if (tr.ent->takedamage)
{
T_Damage(tr.ent, self, self, aimdir, tr.endpos, tr.plane.normal,
damage, kick, DAMAGE_ENERGY, mod);
}
else
{
if ((!water) && (strncmp(tr.surface->name, "sky", 3)))
{
/* This is the truncated steam entry - uses 1+1+2 extra bytes of data */
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_HEATBEAM_STEAM);
gi.WritePosition(tr.endpos);
gi.WriteDir(tr.plane.normal);
gi.multicast(tr.endpos, MULTICAST_PVS);
if (self->client)
{
PlayerNoise(self, tr.endpos, PNOISE_IMPACT);
}
}
}
}
}
/* if went through water, determine where the end and make a bubble trail */
if ((water) || (underwater))
{
vec3_t pos;
VectorSubtract(tr.endpos, water_start, dir);
VectorNormalize(dir);
VectorMA(tr.endpos, -2, 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);
gi.WritePosition(water_start);
gi.WritePosition(tr.endpos);
gi.multicast(pos, MULTICAST_PVS);
}
if ((!underwater) && (!water))
{
VectorCopy(tr.endpos, beam_endpt);
}
else
{
VectorCopy(endpoint, beam_endpt);
}
gi.WriteByte(svc_temp_entity);
gi.WriteByte(te_beam);
gi.WriteShort(self - g_edicts);
gi.WritePosition(start);
gi.WritePosition(beam_endpt);
gi.multicast(self->s.origin, MULTICAST_ALL);
}
void
fire_heat(edict_t *self, vec3_t start, vec3_t aimdir, vec3_t offset,
int damage, int kick, qboolean monster)
{
if (!self)
{
return;
}
if (monster)
{
fire_beams(self, start, aimdir, offset, damage, kick,
TE_MONSTER_HEATBEAM, TE_HEATBEAM_SPARKS, MOD_HEATBEAM);
}
else
{
fire_beams(self, start, aimdir, offset, damage,
kick, TE_HEATBEAM, TE_HEATBEAM_SPARKS, MOD_HEATBEAM);
}
}
/*
* Fires a single green blaster bolt. Used by monsters, generally.
*/
void
blaster2_touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
int mod;
int damagestat;
vec3_t normal;
if (!self || !other)
{
return;
}
if (other == self->owner)
{
return;
}
if (surf && (surf->flags & SURF_SKY))
{
G_FreeEdict(self);
return;
}
if (self->owner && self->owner->client)
{
PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT);
}
get_normal_vector(plane, normal);
if (other->takedamage)
{
mod = MOD_BLASTER2;
if (self->owner)
{
/* the only time players will be firing blaster2
bolts will be from the defender sphere. */
if (self->owner->client)
{
mod = MOD_DEFENDER_SPHERE;
}
damagestat = self->owner->takedamage;
self->owner->takedamage = DAMAGE_NO;
if (self->dmg >= 5)
{
T_RadiusDamage(self, self->owner, self->dmg * 3, other,
self->dmg_radius, 0);
}
T_Damage(other, self, self->owner, self->velocity, self->s.origin, normal,
self->dmg, 1, DAMAGE_ENERGY, mod);
self->owner->takedamage = damagestat;
}
else
{
if (self->dmg >= 5)
{
T_RadiusDamage(self, self->owner, self->dmg * 3, other,
self->dmg_radius, 0);
}
T_Damage(other, self, self->owner, self->velocity, self->s.origin,
normal, self->dmg, 1, DAMAGE_ENERGY, mod);
}
}
else
{
/* yeowch this will get expensive */
if (self->dmg >= 5)
{
T_RadiusDamage(self, self->owner, self->dmg * 3, self->owner,
self->dmg_radius, 0);
}
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_BLASTER2);
gi.WritePosition(self->s.origin);
gi.WriteDir(normal);
gi.multicast(self->s.origin, MULTICAST_PVS);
}
G_FreeEdict(self);
}
void
fire_blaster2(edict_t *self, vec3_t start, vec3_t dir, int damage,
int speed, int effect, qboolean hyper)
{
edict_t *bolt;
trace_t tr;
if (!self)
{
return;
}
VectorNormalize(dir);
bolt = G_Spawn();
VectorCopy(start, bolt->s.origin);
VectorCopy(start, bolt->s.old_origin);
vectoangles2(dir, bolt->s.angles);
VectorScale(dir, speed, bolt->velocity);
bolt->movetype = MOVETYPE_FLYMISSILE;
bolt->clipmask = MASK_SHOT;
bolt->solid = SOLID_BBOX;
bolt->s.effects |= effect;
VectorClear(bolt->mins);
VectorClear(bolt->maxs);
if (effect)
{
bolt->s.effects |= EF_TRACKER;
}
bolt->dmg_radius = 128;
bolt->s.modelindex = gi.modelindex("models/proj/laser2/tris.md2");
bolt->touch = blaster2_touch;
bolt->owner = self;
bolt->nextthink = level.time + 2;
bolt->think = G_FreeEdict;
bolt->dmg = damage;
bolt->classname = "bolt";
gi.linkentity(bolt);
if (self->client)
{
check_dodge(self, bolt->s.origin, dir, speed);
}
tr = gi.trace(self->s.origin, NULL, NULL, bolt->s.origin, bolt, MASK_SHOT);
if (tr.fraction < 1.0)
{
VectorMA(bolt->s.origin, -10, dir, bolt->s.origin);
bolt->touch(bolt, tr.ent, NULL, NULL);
}
}
void
tracker_pain_daemon_think(edict_t *self)
{
static vec3_t pain_normal = {0, 0, 1};
int hurt;
if (!self)
{
return;
}
if (!self->inuse)
{
return;
}
if ((level.time - self->timestamp) > TRACKER_DAMAGE_TIME)
{
if (!self->enemy->client)
{
self->enemy->s.effects &= ~EF_TRACKERTRAIL;
}
G_FreeEdict(self);
}
else
{
if (self->enemy->health > 0)
{
T_Damage(self->enemy, self, self->owner, vec3_origin, self->enemy->s.origin,
pain_normal, self->dmg, 0, TRACKER_DAMAGE_FLAGS, MOD_TRACKER);
/* if we kill the player, we'll be removed. */
if (self->inuse)
{
/* if we killed a monster, gib them. */
if (self->enemy->health < 1)
{
if (self->enemy->gib_health)
{
hurt = -self->enemy->gib_health;
}
else
{
hurt = 500;
}
T_Damage(self->enemy, self, self->owner, vec3_origin, self->enemy->s.origin,
pain_normal, hurt, 0, TRACKER_DAMAGE_FLAGS, MOD_TRACKER);
}
if (self->enemy->client)
{
self->enemy->client->tracker_pain_framenum = level.framenum + 1;
}
else
{
self->enemy->s.effects |= EF_TRACKERTRAIL;
}
self->nextthink = level.time + FRAMETIME;
}
}
else
{
if (!self->enemy->client)
{
self->enemy->s.effects &= ~EF_TRACKERTRAIL;
}
G_FreeEdict(self);
}
}
}
void
tracker_pain_daemon_spawn(edict_t *owner, edict_t *enemy, int damage)
{
edict_t *daemon;
if (!owner || !enemy)
{
return;
}
daemon = G_Spawn();
daemon->classname = "pain daemon";
daemon->think = tracker_pain_daemon_think;
daemon->nextthink = level.time + FRAMETIME;
daemon->timestamp = level.time;
daemon->owner = owner;
daemon->enemy = enemy;
daemon->dmg = damage;
}
void
tracker_explode(edict_t *self)
{
if (!self)
{
return;
}
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_TRACKER_EXPLOSION);
gi.WritePosition(self->s.origin);
gi.multicast(self->s.origin, MULTICAST_PVS);
G_FreeEdict(self);
}
void
tracker_touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
float damagetime;
vec3_t normal;
if (!self || !other)
{
return;
}
if (other == self->owner)
{
return;
}
if (surf && (surf->flags & SURF_SKY))
{
G_FreeEdict(self);
return;
}
if (self->client)
{
PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT);
}
if (other->takedamage)
{
get_normal_vector(plane, normal);
if ((other->svflags & SVF_MONSTER) || other->client)
{
if (other->health > 0) /* knockback only for living creatures */
{
T_Damage(other, self, self->owner, self->velocity, self->s.origin,
normal, 0, (self->dmg * 3), TRACKER_IMPACT_FLAGS,
MOD_TRACKER);
if (!(other->flags & (FL_FLY | FL_SWIM)))
{
other->velocity[2] += 140;
}
damagetime = ((float)self->dmg) * FRAMETIME;
damagetime = damagetime / TRACKER_DAMAGE_TIME;
tracker_pain_daemon_spawn(self->owner, other, (int)damagetime);
}
else /* lots of damage (almost autogib) for dead bodies */
{
T_Damage(other, self, self->owner, self->velocity, self->s.origin, normal,
self->dmg * 4, (self->dmg * 3), TRACKER_IMPACT_FLAGS, MOD_TRACKER);
}
}
else /* full damage in one shot for inanimate objects */
{
T_Damage(other, self, self->owner, self->velocity, self->s.origin, normal,
self->dmg, (self->dmg * 3), TRACKER_IMPACT_FLAGS, MOD_TRACKER);
}
}
tracker_explode(self);
}
void
tracker_fly(edict_t *self)
{
vec3_t dest;
vec3_t dir;
vec3_t center;
if (!self)
{
return;
}
if ((!self->enemy) || (!self->enemy->inuse) || (self->enemy->health < 1))
{
tracker_explode(self);
return;
}
/* try to hunt for center of enemy, if possible and not client */
if (self->enemy->client)
{
VectorCopy(self->enemy->s.origin, dest);
dest[2] += self->enemy->viewheight;
}
else if (VectorCompare(self->enemy->absmin, vec3_origin) ||
VectorCompare(self->enemy->absmax, vec3_origin))
{
VectorCopy(self->enemy->s.origin, dest);
}
else
{
VectorMA(vec3_origin, 0.5, self->enemy->absmin, center);
VectorMA(center, 0.5, self->enemy->absmax, center);
VectorCopy(center, dest);
}
VectorSubtract(dest, self->s.origin, dir);
VectorNormalize(dir);
vectoangles2(dir, self->s.angles);
VectorScale(dir, self->speed, self->velocity);
VectorCopy(dest, self->monsterinfo.saved_goal);
self->nextthink = level.time + 0.1;
}
void
fire_tracker(edict_t *self, vec3_t start, vec3_t dir, int damage,
int speed, edict_t *enemy)
{
edict_t *bolt;
trace_t tr;
if (!self)
{
return;
}
VectorNormalize(dir);
bolt = G_Spawn();
VectorCopy(start, bolt->s.origin);
VectorCopy(start, bolt->s.old_origin);
vectoangles2(dir, bolt->s.angles);
VectorScale(dir, speed, bolt->velocity);
bolt->movetype = MOVETYPE_FLYMISSILE;
bolt->clipmask = MASK_SHOT;
bolt->solid = SOLID_BBOX;
bolt->speed = speed;
bolt->s.effects = EF_TRACKER;
bolt->s.sound = gi.soundindex("weapons/disrupt.wav");
VectorClear(bolt->mins);
VectorClear(bolt->maxs);
bolt->s.modelindex = gi.modelindex("models/proj/disintegrator/tris.md2");
bolt->touch = tracker_touch;
bolt->enemy = enemy;
bolt->owner = self;
bolt->dmg = damage;
bolt->classname = "tracker";
gi.linkentity(bolt);
if (enemy)
{
bolt->nextthink = level.time + 0.1;
bolt->think = tracker_fly;
}
else
{
bolt->nextthink = level.time + 10;
bolt->think = G_FreeEdict;
}
if (self->client)
{
check_dodge(self, bolt->s.origin, dir, speed);
}
tr = gi.trace(self->s.origin, NULL, NULL, bolt->s.origin, bolt, MASK_SHOT);
if (tr.fraction < 1.0)
{
VectorMA(bolt->s.origin, -10, dir, bolt->s.origin);
bolt->touch(bolt, tr.ent, NULL, NULL);
}
}