mirror of
https://github.com/id-Software/quake2-rerelease-dll.git
synced 2025-02-25 04:30:45 +00:00
710 lines
16 KiB
C++
710 lines
16 KiB
C++
// Copyright (c) ZeniMax Media Inc.
|
|
// Licensed under the GNU General Public License 2.0.
|
|
// g_sphere.c
|
|
// pmack
|
|
// april 1998
|
|
|
|
// defender - actively finds and shoots at enemies
|
|
// hunter - waits until < 25% health and vore ball tracks person who hurt you
|
|
// vengeance - kills person who killed you.
|
|
|
|
#include "../g_local.h"
|
|
|
|
constexpr gtime_t DEFENDER_LIFESPAN = 30_sec;
|
|
constexpr gtime_t HUNTER_LIFESPAN = 30_sec;
|
|
constexpr gtime_t VENGEANCE_LIFESPAN = 30_sec;
|
|
constexpr gtime_t MINIMUM_FLY_TIME = 15_sec;
|
|
|
|
void LookAtKiller(edict_t *self, edict_t *inflictor, edict_t *attacker);
|
|
|
|
void vengeance_touch(edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self);
|
|
void hunter_touch(edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self);
|
|
|
|
// *************************
|
|
// General Sphere Code
|
|
// *************************
|
|
|
|
// =================
|
|
// =================
|
|
THINK(sphere_think_explode) (edict_t *self) -> void
|
|
{
|
|
if (self->owner && self->owner->client && !(self->spawnflags & SPHERE_DOPPLEGANGER))
|
|
{
|
|
self->owner->client->owned_sphere = nullptr;
|
|
}
|
|
BecomeExplosion1(self);
|
|
}
|
|
|
|
// =================
|
|
// sphere_explode
|
|
// =================
|
|
DIE(sphere_explode) (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void
|
|
{
|
|
sphere_think_explode(self);
|
|
}
|
|
|
|
// =================
|
|
// sphere_if_idle_die - if the sphere is not currently attacking, blow up.
|
|
// =================
|
|
DIE(sphere_if_idle_die) (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void
|
|
{
|
|
if (!self->enemy)
|
|
sphere_think_explode(self);
|
|
}
|
|
|
|
// *************************
|
|
// Sphere Movement
|
|
// *************************
|
|
|
|
// =================
|
|
// =================
|
|
void sphere_fly(edict_t *self)
|
|
{
|
|
vec3_t dest;
|
|
vec3_t dir;
|
|
|
|
if (level.time >= gtime_t::from_sec(self->wait))
|
|
{
|
|
sphere_think_explode(self);
|
|
return;
|
|
}
|
|
|
|
dest = self->owner->s.origin;
|
|
dest[2] = self->owner->absmax[2] + 4;
|
|
|
|
if (level.time.seconds() == level.time.seconds<int>())
|
|
{
|
|
if (!visible(self, self->owner))
|
|
{
|
|
self->s.origin = dest;
|
|
gi.linkentity(self);
|
|
return;
|
|
}
|
|
}
|
|
|
|
dir = dest - self->s.origin;
|
|
self->velocity = dir * 5;
|
|
}
|
|
|
|
// =================
|
|
// =================
|
|
void sphere_chase(edict_t *self, int stupidChase)
|
|
{
|
|
vec3_t dest;
|
|
vec3_t dir;
|
|
float dist;
|
|
|
|
if (level.time >= gtime_t::from_sec(self->wait) || (self->enemy && self->enemy->health < 1))
|
|
{
|
|
sphere_think_explode(self);
|
|
return;
|
|
}
|
|
|
|
dest = self->enemy->s.origin;
|
|
if (self->enemy->client)
|
|
dest[2] += self->enemy->viewheight;
|
|
|
|
if (visible(self, self->enemy) || stupidChase)
|
|
{
|
|
// if moving, hunter sphere uses active sound
|
|
if (!stupidChase)
|
|
self->s.sound = gi.soundindex("spheres/h_active.wav");
|
|
|
|
dir = dest - self->s.origin;
|
|
dir.normalize();
|
|
self->s.angles = vectoangles(dir);
|
|
self->velocity = dir * 500;
|
|
self->monsterinfo.saved_goal = dest;
|
|
}
|
|
else if (!self->monsterinfo.saved_goal)
|
|
{
|
|
dir = self->enemy->s.origin - self->s.origin;
|
|
dist = dir.normalize();
|
|
self->s.angles = vectoangles(dir);
|
|
|
|
// if lurking, hunter sphere uses lurking sound
|
|
self->s.sound = gi.soundindex("spheres/h_lurk.wav");
|
|
self->velocity = {};
|
|
}
|
|
else
|
|
{
|
|
dir = self->monsterinfo.saved_goal - self->s.origin;
|
|
dist = dir.normalize();
|
|
|
|
if (dist > 1)
|
|
{
|
|
self->s.angles = vectoangles(dir);
|
|
|
|
if (dist > 500)
|
|
self->velocity = dir * 500;
|
|
else if (dist < 20)
|
|
self->velocity = dir * (dist / gi.frame_time_s);
|
|
else
|
|
self->velocity = dir * dist;
|
|
|
|
// if moving, hunter sphere uses active sound
|
|
if (!stupidChase)
|
|
self->s.sound = gi.soundindex("spheres/h_active.wav");
|
|
}
|
|
else
|
|
{
|
|
dir = self->enemy->s.origin - self->s.origin;
|
|
dist = dir.normalize();
|
|
self->s.angles = vectoangles(dir);
|
|
|
|
// if not moving, hunter sphere uses lurk sound
|
|
if (!stupidChase)
|
|
self->s.sound = gi.soundindex("spheres/h_lurk.wav");
|
|
|
|
self->velocity = {};
|
|
}
|
|
}
|
|
}
|
|
|
|
// *************************
|
|
// Attack related stuff
|
|
// *************************
|
|
|
|
// =================
|
|
// =================
|
|
void sphere_fire(edict_t *self, edict_t *enemy)
|
|
{
|
|
vec3_t dest;
|
|
vec3_t dir;
|
|
|
|
if (!enemy || level.time >= gtime_t::from_sec(self->wait))
|
|
{
|
|
sphere_think_explode(self);
|
|
return;
|
|
}
|
|
|
|
dest = enemy->s.origin;
|
|
self->s.effects |= EF_ROCKET;
|
|
|
|
dir = dest - self->s.origin;
|
|
dir.normalize();
|
|
self->s.angles = vectoangles(dir);
|
|
self->velocity = dir * 1000;
|
|
|
|
self->touch = vengeance_touch;
|
|
self->think = sphere_think_explode;
|
|
self->nextthink = gtime_t::from_sec(self->wait);
|
|
}
|
|
|
|
// =================
|
|
// =================
|
|
void sphere_touch(edict_t *self, edict_t *other, const trace_t &tr, mod_t mod)
|
|
{
|
|
if (self->spawnflags.has(SPHERE_DOPPLEGANGER))
|
|
{
|
|
if (other == self->teammaster)
|
|
return;
|
|
|
|
self->takedamage = false;
|
|
self->owner = self->teammaster;
|
|
self->teammaster = nullptr;
|
|
}
|
|
else
|
|
{
|
|
if (other == self->owner)
|
|
return;
|
|
// PMM - don't blow up on bodies
|
|
if (!strcmp(other->classname, "bodyque"))
|
|
return;
|
|
}
|
|
|
|
if (tr.surface && (tr.surface->flags & SURF_SKY))
|
|
{
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
if (self->owner)
|
|
{
|
|
if (other->takedamage)
|
|
{
|
|
T_Damage(other, self, self->owner, self->velocity, self->s.origin, tr.plane.normal,
|
|
10000, 1, DAMAGE_DESTROY_ARMOR, mod);
|
|
}
|
|
else
|
|
{
|
|
T_RadiusDamage(self, self->owner, 512, self->owner, 256, DAMAGE_NONE, mod);
|
|
}
|
|
}
|
|
|
|
sphere_think_explode(self);
|
|
}
|
|
|
|
// =================
|
|
// =================
|
|
TOUCH(vengeance_touch) (edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self) -> void
|
|
{
|
|
if (self->spawnflags.has(SPHERE_DOPPLEGANGER))
|
|
sphere_touch(self, other, tr, MOD_DOPPLE_VENGEANCE);
|
|
else
|
|
sphere_touch(self, other, tr, MOD_VENGEANCE_SPHERE);
|
|
}
|
|
|
|
// =================
|
|
// =================
|
|
TOUCH(hunter_touch) (edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self) -> void
|
|
{
|
|
edict_t *owner;
|
|
|
|
// don't blow up if you hit the world.... sheesh.
|
|
if (other == world)
|
|
return;
|
|
|
|
if (self->owner)
|
|
{
|
|
// if owner is flying with us, make sure they stop too.
|
|
owner = self->owner;
|
|
if (owner->flags & FL_SAM_RAIMI)
|
|
{
|
|
owner->velocity = {};
|
|
owner->movetype = MOVETYPE_NONE;
|
|
gi.linkentity(owner);
|
|
}
|
|
}
|
|
|
|
if (self->spawnflags.has(SPHERE_DOPPLEGANGER))
|
|
sphere_touch(self, other, tr, MOD_DOPPLE_HUNTER);
|
|
else
|
|
sphere_touch(self, other, tr, MOD_HUNTER_SPHERE);
|
|
}
|
|
|
|
// =================
|
|
// =================
|
|
void defender_shoot(edict_t *self, edict_t *enemy)
|
|
{
|
|
vec3_t dir;
|
|
vec3_t start;
|
|
|
|
if (!(enemy->inuse) || enemy->health <= 0)
|
|
return;
|
|
|
|
if (enemy == self->owner)
|
|
return;
|
|
|
|
dir = enemy->s.origin - self->s.origin;
|
|
dir.normalize();
|
|
|
|
if (self->monsterinfo.attack_finished > level.time)
|
|
return;
|
|
|
|
if (!visible(self, self->enemy))
|
|
return;
|
|
|
|
start = self->s.origin;
|
|
start[2] += 2;
|
|
fire_blaster2(self->owner, start, dir, 10, 1000, EF_BLASTER, 0);
|
|
|
|
self->monsterinfo.attack_finished = level.time + 400_ms;
|
|
}
|
|
|
|
// *************************
|
|
// Activation Related Stuff
|
|
// *************************
|
|
|
|
// =================
|
|
// =================
|
|
void body_gib(edict_t *self)
|
|
{
|
|
gi.sound(self, CHAN_BODY, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0);
|
|
ThrowGibs(self, 50, {
|
|
{ 4, "models/objects/gibs/sm_meat/tris.md2" },
|
|
{ "models/objects/gibs/skull/tris.md2" }
|
|
});
|
|
}
|
|
|
|
// =================
|
|
// =================
|
|
PAIN(hunter_pain) (edict_t *self, edict_t *other, float kick, int damage, const mod_t &mod) -> void
|
|
{
|
|
edict_t *owner;
|
|
float dist;
|
|
vec3_t dir;
|
|
|
|
if (self->enemy)
|
|
return;
|
|
|
|
owner = self->owner;
|
|
|
|
if (!(self->spawnflags & SPHERE_DOPPLEGANGER))
|
|
{
|
|
if (owner && (owner->health > 0))
|
|
return;
|
|
|
|
// PMM
|
|
if (other == owner)
|
|
return;
|
|
// pmm
|
|
}
|
|
else
|
|
{
|
|
// if fired by a doppleganger, set it to 10 second timeout
|
|
self->wait = (level.time + MINIMUM_FLY_TIME).seconds();
|
|
}
|
|
|
|
if ((gtime_t::from_sec(self->wait) - level.time) < MINIMUM_FLY_TIME)
|
|
self->wait = (level.time + MINIMUM_FLY_TIME).seconds();
|
|
self->s.effects |= EF_BLASTER | EF_TRACKER;
|
|
self->touch = hunter_touch;
|
|
self->enemy = other;
|
|
|
|
// if we're not owned by a player, no sam raimi
|
|
// if we're spawned by a doppleganger, no sam raimi
|
|
if (self->spawnflags.has(SPHERE_DOPPLEGANGER) || !(owner && owner->client))
|
|
return;
|
|
|
|
// sam raimi cam is disabled if FORCE_RESPAWN is set.
|
|
// sam raimi cam is also disabled if huntercam->value is 0.
|
|
if (!g_dm_force_respawn->integer && huntercam->integer)
|
|
{
|
|
dir = other->s.origin - self->s.origin;
|
|
dist = dir.length();
|
|
|
|
if (owner && (dist >= 192))
|
|
{
|
|
// detach owner from body and send him flying
|
|
owner->movetype = MOVETYPE_FLYMISSILE;
|
|
|
|
// gib like we just died, even though we didn't, really.
|
|
body_gib(owner);
|
|
|
|
// move the sphere to the owner's current viewpoint.
|
|
// we know it's a valid spot (or will be momentarily)
|
|
self->s.origin = owner->s.origin;
|
|
self->s.origin[2] += owner->viewheight;
|
|
|
|
// move the player's origin to the sphere's new origin
|
|
owner->s.origin = self->s.origin;
|
|
owner->s.angles = self->s.angles;
|
|
owner->client->v_angle = self->s.angles;
|
|
owner->mins = { -5, -5, -5 };
|
|
owner->maxs = { 5, 5, 5 };
|
|
owner->client->ps.fov = 140;
|
|
owner->s.modelindex = 0;
|
|
owner->s.modelindex2 = 0;
|
|
owner->viewheight = 8;
|
|
owner->solid = SOLID_NOT;
|
|
owner->flags |= FL_SAM_RAIMI;
|
|
gi.linkentity(owner);
|
|
|
|
self->solid = SOLID_BBOX;
|
|
gi.linkentity(self);
|
|
}
|
|
}
|
|
}
|
|
|
|
// =================
|
|
// =================
|
|
PAIN(defender_pain) (edict_t *self, edict_t *other, float kick, int damage, const mod_t &mod) -> void
|
|
{
|
|
// PMM
|
|
if (other == self->owner)
|
|
return;
|
|
|
|
// pmm
|
|
self->enemy = other;
|
|
}
|
|
|
|
// =================
|
|
// =================
|
|
PAIN(vengeance_pain) (edict_t *self, edict_t *other, float kick, int damage, const mod_t &mod) -> void
|
|
{
|
|
if (self->enemy)
|
|
return;
|
|
|
|
if (!(self->spawnflags & SPHERE_DOPPLEGANGER))
|
|
{
|
|
if (self->owner && self->owner->health >= 25)
|
|
return;
|
|
|
|
// PMM
|
|
if (other == self->owner)
|
|
return;
|
|
// pmm
|
|
}
|
|
else
|
|
{
|
|
self->wait = (level.time + MINIMUM_FLY_TIME).seconds();
|
|
}
|
|
|
|
if ((gtime_t::from_sec(self->wait) - level.time) < MINIMUM_FLY_TIME)
|
|
self->wait = (level.time + MINIMUM_FLY_TIME).seconds();
|
|
self->s.effects |= EF_ROCKET;
|
|
self->touch = vengeance_touch;
|
|
self->enemy = other;
|
|
}
|
|
|
|
// *************************
|
|
// Think Functions
|
|
// *************************
|
|
|
|
// ===================
|
|
// ===================
|
|
THINK(defender_think) (edict_t *self) -> void
|
|
{
|
|
if (!self->owner)
|
|
{
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
// if we've exited the level, just remove ourselves.
|
|
if (level.intermissiontime)
|
|
{
|
|
sphere_think_explode(self);
|
|
return;
|
|
}
|
|
|
|
if (self->owner->health <= 0)
|
|
{
|
|
sphere_think_explode(self);
|
|
return;
|
|
}
|
|
|
|
self->s.frame++;
|
|
if (self->s.frame > 19)
|
|
self->s.frame = 0;
|
|
|
|
if (self->enemy)
|
|
{
|
|
if (self->enemy->health > 0)
|
|
defender_shoot(self, self->enemy);
|
|
else
|
|
self->enemy = nullptr;
|
|
}
|
|
|
|
sphere_fly(self);
|
|
|
|
if (self->inuse)
|
|
self->nextthink = level.time + 10_hz;
|
|
}
|
|
|
|
// =================
|
|
// =================
|
|
THINK(hunter_think) (edict_t *self) -> void
|
|
{
|
|
// if we've exited the level, just remove ourselves.
|
|
if (level.intermissiontime)
|
|
{
|
|
sphere_think_explode(self);
|
|
return;
|
|
}
|
|
|
|
edict_t *owner = self->owner;
|
|
|
|
if (!owner && !(self->spawnflags & SPHERE_DOPPLEGANGER))
|
|
{
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
if (owner)
|
|
self->ideal_yaw = owner->s.angles[YAW];
|
|
else if (self->enemy) // fired by doppleganger
|
|
{
|
|
vec3_t dir = self->enemy->s.origin - self->s.origin;
|
|
self->ideal_yaw = vectoyaw(dir);
|
|
}
|
|
|
|
M_ChangeYaw(self);
|
|
|
|
if (self->enemy)
|
|
{
|
|
sphere_chase(self, 0);
|
|
|
|
// deal with sam raimi cam
|
|
if (owner && (owner->flags & FL_SAM_RAIMI))
|
|
{
|
|
if (self->inuse)
|
|
{
|
|
owner->movetype = MOVETYPE_FLYMISSILE;
|
|
LookAtKiller(owner, self, self->enemy);
|
|
// owner is flying with us, move him too
|
|
owner->movetype = MOVETYPE_FLYMISSILE;
|
|
owner->viewheight = (int) (self->s.origin[2] - owner->s.origin[2]);
|
|
owner->s.origin = self->s.origin;
|
|
owner->velocity = self->velocity;
|
|
owner->mins = {};
|
|
owner->maxs = {};
|
|
gi.linkentity(owner);
|
|
}
|
|
else // sphere timed out
|
|
{
|
|
owner->velocity = {};
|
|
owner->movetype = MOVETYPE_NONE;
|
|
gi.linkentity(owner);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
sphere_fly(self);
|
|
|
|
if (self->inuse)
|
|
self->nextthink = level.time + 10_hz;
|
|
}
|
|
|
|
// =================
|
|
// =================
|
|
THINK(vengeance_think) (edict_t *self) -> void
|
|
{
|
|
// if we've exited the level, just remove ourselves.
|
|
if (level.intermissiontime)
|
|
{
|
|
sphere_think_explode(self);
|
|
return;
|
|
}
|
|
|
|
if (!(self->owner) && !(self->spawnflags & SPHERE_DOPPLEGANGER))
|
|
{
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
if (self->enemy)
|
|
sphere_chase(self, 1);
|
|
else
|
|
sphere_fly(self);
|
|
|
|
if (self->inuse)
|
|
self->nextthink = level.time + 10_hz;
|
|
}
|
|
|
|
// *************************
|
|
// Spawning / Creation
|
|
// *************************
|
|
|
|
// monsterinfo_t
|
|
// =================
|
|
// =================
|
|
edict_t *Sphere_Spawn(edict_t *owner, spawnflags_t spawnflags)
|
|
{
|
|
edict_t *sphere;
|
|
|
|
sphere = G_Spawn();
|
|
sphere->s.origin = owner->s.origin;
|
|
sphere->s.origin[2] = owner->absmax[2];
|
|
sphere->s.angles[YAW] = owner->s.angles[YAW];
|
|
sphere->solid = SOLID_BBOX;
|
|
sphere->clipmask = MASK_PROJECTILE;
|
|
sphere->s.renderfx = RF_FULLBRIGHT | RF_IR_VISIBLE;
|
|
sphere->movetype = MOVETYPE_FLYMISSILE;
|
|
|
|
if (spawnflags.has(SPHERE_DOPPLEGANGER))
|
|
sphere->teammaster = owner->teammaster;
|
|
else
|
|
sphere->owner = owner;
|
|
|
|
sphere->classname = "sphere";
|
|
sphere->yaw_speed = 40;
|
|
sphere->monsterinfo.attack_finished = 0_ms;
|
|
sphere->spawnflags = spawnflags; // need this for the HUD to recognize sphere
|
|
// PMM
|
|
sphere->takedamage = false;
|
|
|
|
switch ((spawnflags & SPHERE_TYPE).value)
|
|
{
|
|
case SPHERE_DEFENDER.value:
|
|
sphere->s.modelindex = gi.modelindex("models/items/defender/tris.md2");
|
|
sphere->s.modelindex2 = gi.modelindex("models/items/shell/tris.md2");
|
|
sphere->s.sound = gi.soundindex("spheres/d_idle.wav");
|
|
sphere->pain = defender_pain;
|
|
sphere->wait = (level.time + DEFENDER_LIFESPAN).seconds();
|
|
sphere->die = sphere_explode;
|
|
sphere->think = defender_think;
|
|
break;
|
|
case SPHERE_HUNTER.value:
|
|
sphere->s.modelindex = gi.modelindex("models/items/hunter/tris.md2");
|
|
sphere->s.sound = gi.soundindex("spheres/h_idle.wav");
|
|
sphere->wait = (level.time + HUNTER_LIFESPAN).seconds();
|
|
sphere->pain = hunter_pain;
|
|
sphere->die = sphere_if_idle_die;
|
|
sphere->think = hunter_think;
|
|
break;
|
|
case SPHERE_VENGEANCE.value:
|
|
sphere->s.modelindex = gi.modelindex("models/items/vengnce/tris.md2");
|
|
sphere->s.sound = gi.soundindex("spheres/v_idle.wav");
|
|
sphere->wait = (level.time + VENGEANCE_LIFESPAN).seconds();
|
|
sphere->pain = vengeance_pain;
|
|
sphere->die = sphere_if_idle_die;
|
|
sphere->think = vengeance_think;
|
|
sphere->avelocity = { 30, 30, 0 };
|
|
break;
|
|
default:
|
|
gi.Com_Print("Tried to create an invalid sphere\n");
|
|
G_FreeEdict(sphere);
|
|
return nullptr;
|
|
}
|
|
|
|
sphere->nextthink = level.time + 10_hz;
|
|
|
|
gi.linkentity(sphere);
|
|
|
|
return sphere;
|
|
}
|
|
|
|
// =================
|
|
// Own_Sphere - attach the sphere to the client so we can
|
|
// directly access it later
|
|
// =================
|
|
void Own_Sphere(edict_t *self, edict_t *sphere)
|
|
{
|
|
if (!sphere)
|
|
return;
|
|
|
|
// ownership only for players
|
|
if (self->client)
|
|
{
|
|
// if they don't have one
|
|
if (!(self->client->owned_sphere))
|
|
{
|
|
self->client->owned_sphere = sphere;
|
|
}
|
|
// they already have one, take care of the old one
|
|
else
|
|
{
|
|
if (self->client->owned_sphere->inuse)
|
|
{
|
|
G_FreeEdict(self->client->owned_sphere);
|
|
self->client->owned_sphere = sphere;
|
|
}
|
|
else
|
|
{
|
|
self->client->owned_sphere = sphere;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// =================
|
|
// =================
|
|
void Defender_Launch(edict_t *self)
|
|
{
|
|
edict_t *sphere;
|
|
|
|
sphere = Sphere_Spawn(self, SPHERE_DEFENDER);
|
|
Own_Sphere(self, sphere);
|
|
}
|
|
|
|
// =================
|
|
// =================
|
|
void Hunter_Launch(edict_t *self)
|
|
{
|
|
edict_t *sphere;
|
|
|
|
sphere = Sphere_Spawn(self, SPHERE_HUNTER);
|
|
Own_Sphere(self, sphere);
|
|
}
|
|
|
|
// =================
|
|
// =================
|
|
void Vengeance_Launch(edict_t *self)
|
|
{
|
|
edict_t *sphere;
|
|
|
|
sphere = Sphere_Spawn(self, SPHERE_VENGEANCE);
|
|
Own_Sphere(self, sphere);
|
|
}
|