quake2-rerelease-dll/rerelease/g_misc.cpp
2023-08-07 14:48:30 -05:00

2485 lines
No EOL
62 KiB
C++

// Copyright (c) ZeniMax Media Inc.
// Licensed under the GNU General Public License 2.0.
// g_misc.c
#include "g_local.h"
/*QUAKED func_group (0 0 0) ?
Used to group brushes together just for editor convenience.
*/
//=====================================================
USE(Use_Areaportal) (edict_t *ent, edict_t *other, edict_t *activator) -> void
{
ent->count ^= 1; // toggle state
gi.SetAreaPortalState(ent->style, ent->count);
}
/*QUAKED func_areaportal (0 0 0) ?
This is a non-visible object that divides the world into
areas that are seperated when this portal is not activated.
Usually enclosed in the middle of a door.
*/
void SP_func_areaportal(edict_t *ent)
{
ent->use = Use_Areaportal;
ent->count = 0; // always start closed;
}
//=====================================================
/*
=================
Misc functions
=================
*/
void VelocityForDamage(int damage, vec3_t &v)
{
v[0] = 100.0f * crandom();
v[1] = 100.0f * crandom();
v[2] = frandom(200.0f, 300.0f);
if (damage < 50)
v = v * 0.7f;
else
v = v * 1.2f;
}
void ClipGibVelocity(edict_t *ent)
{
if (ent->velocity[0] < -300)
ent->velocity[0] = -300;
else if (ent->velocity[0] > 300)
ent->velocity[0] = 300;
if (ent->velocity[1] < -300)
ent->velocity[1] = -300;
else if (ent->velocity[1] > 300)
ent->velocity[1] = 300;
if (ent->velocity[2] < 200)
ent->velocity[2] = 200; // always some upwards
else if (ent->velocity[2] > 500)
ent->velocity[2] = 500;
}
/*
=================
gibs
=================
*/
DIE(gib_die) (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void
{
if (mod.id == MOD_CRUSH)
G_FreeEdict(self);
}
TOUCH(gib_touch) (edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self) -> void
{
if (tr.plane.normal[2] > 0.7f)
{
self->s.angles[0] = clamp(self->s.angles[0], -5.0f, 5.0f);
self->s.angles[2] = clamp(self->s.angles[2], -5.0f, 5.0f);
}
}
edict_t *ThrowGib(edict_t *self, const char *gibname, int damage, gib_type_t type, float scale)
{
edict_t *gib;
vec3_t vd;
vec3_t origin;
vec3_t size;
float vscale;
if (type & GIB_HEAD)
{
gib = self;
gib->s.event = EV_OTHER_TELEPORT;
// remove setskin so that it doesn't set the skin wrongly later
self->monsterinfo.setskin = nullptr;
}
else
gib = G_Spawn();
size = self->size * 0.5f;
// since absmin is bloated by 1, un-bloat it here
origin = (self->absmin + vec3_t { 1, 1, 1 }) + size;
int32_t i;
for (i = 0; i < 3; i++)
{
gib->s.origin = origin + vec3_t { crandom(), crandom(), crandom() }.scaled(size);
// try 3 times to get a good, non-solid position
if (!(gi.pointcontents(gib->s.origin) & MASK_SOLID))
break;
}
if (i == 3)
{
// only free us if we're not being turned into the gib, otherwise
// just spawn inside a wall
if (gib != self)
{
G_FreeEdict(gib);
return nullptr;
}
}
gib->s.modelindex = gi.modelindex(gibname);
gib->s.modelindex2 = 0;
gib->s.scale = scale;
gib->solid = SOLID_NOT;
gib->svflags |= SVF_DEADMONSTER;
gib->svflags &= ~SVF_MONSTER;
gib->clipmask = MASK_SOLID;
gib->s.effects = EF_NONE;
gib->s.renderfx = RF_LOW_PRIORITY;
gib->s.renderfx |= RF_NOSHADOW;
if (!(type & GIB_DEBRIS))
{
if (type & GIB_ACID)
gib->s.effects |= EF_GREENGIB;
else
gib->s.effects |= EF_GIB;
gib->s.renderfx |= RF_IR_VISIBLE;
}
gib->flags |= FL_NO_KNOCKBACK | FL_NO_DAMAGE_EFFECTS;
gib->takedamage = true;
gib->die = gib_die;
gib->classname = "gib";
if (type & GIB_SKINNED)
gib->s.skinnum = self->s.skinnum;
else
gib->s.skinnum = 0;
gib->s.frame = 0;
gib->mins = gib->maxs = {};
gib->s.sound = 0;
gib->monsterinfo.engine_sound = 0;
if (!(type & GIB_METALLIC))
{
gib->movetype = MOVETYPE_TOSS;
vscale = (type & GIB_ACID) ? 3.0 : 0.5;
}
else
{
gib->movetype = MOVETYPE_BOUNCE;
vscale = 1.0;
}
if (type & GIB_DEBRIS)
{
vec3_t v;
v[0] = 100 * crandom();
v[1] = 100 * crandom();
v[2] = 100 + 100 * crandom();
gib->velocity = self->velocity + (v * damage);
}
else
{
VelocityForDamage(damage, vd);
gib->velocity = self->velocity + (vd * vscale);
ClipGibVelocity(gib);
}
if (type & GIB_UPRIGHT)
{
gib->touch = gib_touch;
gib->flags |= FL_ALWAYS_TOUCH;
}
gib->avelocity[0] = frandom(600);
gib->avelocity[1] = frandom(600);
gib->avelocity[2] = frandom(600);
gib->s.angles[0] = frandom(359);
gib->s.angles[1] = frandom(359);
gib->s.angles[2] = frandom(359);
gib->think = G_FreeEdict;
if (g_instagib->integer)
gib->nextthink = level.time + random_time(1_sec, 5_sec);
else
gib->nextthink = level.time + random_time(10_sec, 20_sec);
gi.linkentity(gib);
gib->watertype = gi.pointcontents(gib->s.origin);
if (gib->watertype & MASK_WATER)
gib->waterlevel = WATER_FEET;
else
gib->waterlevel = WATER_NONE;
return gib;
}
void ThrowClientHead(edict_t *self, int damage)
{
vec3_t vd;
const char *gibname;
if (brandom())
{
gibname = "models/objects/gibs/head2/tris.md2";
self->s.skinnum = 1; // second skin is player
}
else
{
gibname = "models/objects/gibs/skull/tris.md2";
self->s.skinnum = 0;
}
self->s.origin[2] += 32;
self->s.frame = 0;
gi.setmodel(self, gibname);
self->mins = { -16, -16, 0 };
self->maxs = { 16, 16, 16 };
self->takedamage = true; // [Paril-KEX] allow takedamage so we get crushed
self->solid = SOLID_TRIGGER; // [Paril-KEX] make 'trigger' so we still move but don't block shots/explode
self->svflags |= SVF_DEADMONSTER;
self->s.effects = EF_GIB;
// PGM
self->s.renderfx |= RF_IR_VISIBLE;
// PGM
self->s.sound = 0;
self->flags |= FL_NO_KNOCKBACK | FL_NO_DAMAGE_EFFECTS;
self->movetype = MOVETYPE_BOUNCE;
VelocityForDamage(damage, vd);
self->velocity += vd;
if (self->client) // bodies in the queue don't have a client anymore
{
self->client->anim_priority = ANIM_DEATH;
self->client->anim_end = self->s.frame;
}
else
{
self->think = nullptr;
self->nextthink = 0_ms;
}
gi.linkentity(self);
}
void BecomeExplosion1(edict_t *self)
{
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_EXPLOSION1);
gi.WritePosition(self->s.origin);
gi.multicast(self->s.origin, MULTICAST_PHS, false);
G_FreeEdict(self);
}
void BecomeExplosion2(edict_t *self)
{
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_EXPLOSION2);
gi.WritePosition(self->s.origin);
gi.multicast(self->s.origin, MULTICAST_PHS, false);
G_FreeEdict(self);
}
/*QUAKED path_corner (.5 .3 0) (-8 -8 -8) (8 8 8) TELEPORT
Target: next path corner
Pathtarget: gets used when an entity that has
this path_corner targeted touches it
*/
TOUCH(path_corner_touch) (edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self) -> void
{
vec3_t v;
edict_t *next;
if (other->movetarget != self)
return;
if (other->enemy)
return;
if (self->pathtarget)
{
const char *savetarget;
savetarget = self->target;
self->target = self->pathtarget;
G_UseTargets(self, other);
self->target = savetarget;
}
// see m_move; this is just so we don't needlessly check it
self->flags |= FL_PARTIALGROUND;
if (self->target)
next = G_PickTarget(self->target);
else
next = nullptr;
// [Paril-KEX] don't teleport to a point_combat, it means HOLD for them.
if ((next) && !strcmp(next->classname, "path_corner") && next->spawnflags.has(SPAWNFLAG_PATH_CORNER_TELEPORT))
{
v = next->s.origin;
v[2] += next->mins[2];
v[2] -= other->mins[2];
other->s.origin = v;
next = G_PickTarget(next->target);
other->s.event = EV_OTHER_TELEPORT;
}
other->goalentity = other->movetarget = next;
if (self->wait)
{
other->monsterinfo.pausetime = level.time + gtime_t::from_sec(self->wait);
other->monsterinfo.stand(other);
return;
}
if (!other->movetarget)
{
// N64 cutscene behavior
if (other->hackflags & HACKFLAG_END_CUTSCENE)
{
G_FreeEdict(other);
return;
}
other->monsterinfo.pausetime = HOLD_FOREVER;
other->monsterinfo.stand(other);
}
else
{
v = other->goalentity->s.origin - other->s.origin;
other->ideal_yaw = vectoyaw(v);
}
}
void SP_path_corner(edict_t *self)
{
if (!self->targetname)
{
gi.Com_PrintFmt("{} with no targetname\n", *self);
G_FreeEdict(self);
return;
}
self->solid = SOLID_TRIGGER;
self->touch = path_corner_touch;
self->mins = { -8, -8, -8 };
self->maxs = { 8, 8, 8 };
self->svflags |= SVF_NOCLIENT;
gi.linkentity(self);
}
/*QUAKED point_combat (0.5 0.3 0) (-8 -8 -8) (8 8 8) Hold
Makes this the target of a monster and it will head here
when first activated before going after the activator. If
hold is selected, it will stay here.
*/
TOUCH(point_combat_touch) (edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self) -> void
{
edict_t *activator;
if (other->movetarget != self)
return;
if (self->target)
{
other->target = self->target;
other->goalentity = other->movetarget = G_PickTarget(other->target);
if (!other->goalentity)
{
gi.Com_PrintFmt("{} target {} does not exist\n", *self, self->target);
other->movetarget = self;
}
// [Paril-KEX] allow them to be re-used
//self->target = nullptr;
}
else if (self->spawnflags.has(SPAWNFLAG_POINT_COMBAT_HOLD) && !(other->flags & (FL_SWIM | FL_FLY)))
{
// already standing
if (other->monsterinfo.aiflags & AI_STAND_GROUND)
return;
other->monsterinfo.pausetime = HOLD_FOREVER;
other->monsterinfo.aiflags |= AI_STAND_GROUND | AI_REACHED_HOLD_COMBAT | AI_THIRD_EYE;
other->monsterinfo.stand(other);
}
if (other->movetarget == self)
{
// [Paril-KEX] if we're holding, keep movetarget set; we will
// use this to make sure we haven't moved too far from where
// we want to "guard".
if (!self->spawnflags.has(SPAWNFLAG_POINT_COMBAT_HOLD))
{
other->target = nullptr;
other->movetarget = nullptr;
}
other->goalentity = other->enemy;
other->monsterinfo.aiflags &= ~AI_COMBAT_POINT;
}
if (self->pathtarget)
{
const char *savetarget;
savetarget = self->target;
self->target = self->pathtarget;
if (other->enemy && other->enemy->client)
activator = other->enemy;
else if (other->oldenemy && other->oldenemy->client)
activator = other->oldenemy;
else if (other->activator && other->activator->client)
activator = other->activator;
else
activator = other;
G_UseTargets(self, activator);
self->target = savetarget;
}
}
void SP_point_combat(edict_t *self)
{
if (deathmatch->integer)
{
G_FreeEdict(self);
return;
}
self->solid = SOLID_TRIGGER;
self->touch = point_combat_touch;
self->mins = { -8, -8, -16 };
self->maxs = { 8, 8, 16 };
self->svflags = SVF_NOCLIENT;
gi.linkentity(self);
}
/*QUAKED info_null (0 0.5 0) (-4 -4 -4) (4 4 4)
Used as a positional target for spotlights, etc.
*/
void SP_info_null(edict_t *self)
{
G_FreeEdict(self);
}
/*QUAKED info_notnull (0 0.5 0) (-4 -4 -4) (4 4 4)
Used as a positional target for lightning.
*/
void SP_info_notnull(edict_t *self)
{
self->absmin = self->s.origin;
self->absmax = self->s.origin;
}
/*QUAKED light (0 1 0) (-8 -8 -8) (8 8 8) START_OFF ALLOW_IN_DM
Non-displayed light.
Default light value is 300.
Default style is 0.
If targeted, will toggle between on and off.
Default _cone value is 10 (used to set size of light for spotlights)
*/
constexpr spawnflags_t SPAWNFLAG_LIGHT_START_OFF = 1_spawnflag;
constexpr spawnflags_t SPAWNFLAG_LIGHT_ALLOW_IN_DM = 2_spawnflag;
USE(light_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
if (self->spawnflags.has(SPAWNFLAG_LIGHT_START_OFF))
{
gi.configstring(CS_LIGHTS + self->style, self->style_on);
self->spawnflags &= ~SPAWNFLAG_LIGHT_START_OFF;
}
else
{
gi.configstring(CS_LIGHTS + self->style, self->style_off);
self->spawnflags |= SPAWNFLAG_LIGHT_START_OFF;
}
}
// ---------------------------------------------------------------------------------
// [Sam-KEX] For keeping track of shadow light parameters and setting them up on
// the server side.
struct shadow_light_info_t
{
int entity_number;
shadow_light_data_t shadowlight;
};
static shadow_light_info_t shadowlightinfo[MAX_SHADOW_LIGHTS];
const shadow_light_data_t *GetShadowLightData(int32_t entity_number)
{
for (int32_t i = 0; i < level.shadow_light_count; i++)
{
if (shadowlightinfo[i].entity_number == entity_number)
return &shadowlightinfo[i].shadowlight;
}
return nullptr;
}
void setup_shadow_lights()
{
for(int i = 0; i < level.shadow_light_count; ++i)
{
edict_t *self = &g_edicts[shadowlightinfo[i].entity_number];
shadowlightinfo[i].shadowlight.lighttype = shadow_light_type_t::point;
shadowlightinfo[i].shadowlight.conedirection = {};
if(self->target)
{
edict_t* target = G_FindByString<&edict_t::targetname>(nullptr, self->target);
if(target)
{
shadowlightinfo[i].shadowlight.conedirection = (target->s.origin - self->s.origin).normalized();
shadowlightinfo[i].shadowlight.lighttype = shadow_light_type_t::cone;
}
}
if (self->itemtarget)
{
edict_t* target = G_FindByString<&edict_t::targetname>(nullptr, self->itemtarget);
if(target)
shadowlightinfo[i].shadowlight.lightstyle = target->style;
}
gi.configstring(CS_SHADOWLIGHTS + i, G_Fmt("{};{};{:1};{};{:1};{:1};{:1};{};{:1};{:1};{:1};{:1}",
self->s.number,
(int)shadowlightinfo[i].shadowlight.lighttype,
shadowlightinfo[i].shadowlight.radius,
shadowlightinfo[i].shadowlight.resolution,
shadowlightinfo[i].shadowlight.intensity,
shadowlightinfo[i].shadowlight.fade_start,
shadowlightinfo[i].shadowlight.fade_end,
shadowlightinfo[i].shadowlight.lightstyle,
shadowlightinfo[i].shadowlight.coneangle,
shadowlightinfo[i].shadowlight.conedirection[0],
shadowlightinfo[i].shadowlight.conedirection[1],
shadowlightinfo[i].shadowlight.conedirection[2]).data());
}
}
// ---------------------------------------------------------------------------------
static void setup_dynamic_light(edict_t* self)
{
// [Sam-KEX] Shadow stuff
if (st.sl.data.radius > 0)
{
self->s.renderfx = RF_CASTSHADOW;
self->itemtarget = st.sl.lightstyletarget;
shadowlightinfo[level.shadow_light_count].entity_number = self->s.number;
shadowlightinfo[level.shadow_light_count].shadowlight = st.sl.data;
level.shadow_light_count++;
self->mins[0] = self->mins[1] = self->mins[2] = 0;
self->maxs[0] = self->maxs[1] = self->maxs[2] = 0;
gi.linkentity(self);
}
}
USE(dynamic_light_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
self->svflags ^= SVF_NOCLIENT;
}
void SP_dynamic_light(edict_t* self)
{
setup_dynamic_light(self);
if (self->targetname)
{
self->use = dynamic_light_use;
}
if (self->spawnflags.has(SPAWNFLAG_LIGHT_START_OFF))
self->svflags ^= SVF_NOCLIENT;
}
void SP_light(edict_t *self)
{
// no targeted lights in deathmatch, because they cause global messages
if((!self->targetname || (deathmatch->integer && !(self->spawnflags.has(SPAWNFLAG_LIGHT_ALLOW_IN_DM)))) && st.sl.data.radius == 0) // [Sam-KEX]
{
G_FreeEdict(self);
return;
}
if (self->style >= 32)
{
self->use = light_use;
if (!self->style_on || !*self->style_on)
self->style_on = "m";
else if (*self->style_on >= '0' && *self->style_on <= '9')
self->style_on = gi.get_configstring(CS_LIGHTS + atoi(self->style_on));
if (!self->style_off || !*self->style_off)
self->style_off = "a";
else if (*self->style_off >= '0' && *self->style_off <= '9')
self->style_off = gi.get_configstring(CS_LIGHTS + atoi(self->style_off));
if (self->spawnflags.has(SPAWNFLAG_LIGHT_START_OFF))
gi.configstring(CS_LIGHTS + self->style, self->style_off);
else
gi.configstring(CS_LIGHTS + self->style, self->style_on);
}
setup_dynamic_light(self);
}
/*QUAKED func_wall (0 .5 .8) ? TRIGGER_SPAWN TOGGLE START_ON ANIMATED ANIMATED_FAST
This is just a solid wall if not inhibited
TRIGGER_SPAWN the wall will not be present until triggered
it will then blink in to existance; it will
kill anything that was in it's way
TOGGLE only valid for TRIGGER_SPAWN walls
this allows the wall to be turned on and off
START_ON only valid for TRIGGER_SPAWN walls
the wall will initially be present
*/
constexpr spawnflags_t SPAWNFLAG_WALL_TRIGGER_SPAWN = 1_spawnflag;
constexpr spawnflags_t SPAWNFLAG_WALL_TOGGLE = 2_spawnflag;
constexpr spawnflags_t SPAWNFLAG_WALL_START_ON = 4_spawnflag;
constexpr spawnflags_t SPAWNFLAG_WALL_ANIMATED = 8_spawnflag;
constexpr spawnflags_t SPAWNFLAG_WALL_ANIMATED_FAST = 16_spawnflag;
USE(func_wall_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
if (self->solid == SOLID_NOT)
{
self->solid = SOLID_BSP;
self->svflags &= ~SVF_NOCLIENT;
gi.linkentity(self);
KillBox(self, false);
}
else
{
self->solid = SOLID_NOT;
self->svflags |= SVF_NOCLIENT;
gi.linkentity(self);
}
if (!self->spawnflags.has(SPAWNFLAG_WALL_TOGGLE))
self->use = nullptr;
}
void SP_func_wall(edict_t *self)
{
self->movetype = MOVETYPE_PUSH;
gi.setmodel(self, self->model);
if (self->spawnflags.has(SPAWNFLAG_WALL_ANIMATED))
self->s.effects |= EF_ANIM_ALL;
if (self->spawnflags.has(SPAWNFLAG_WALL_ANIMATED_FAST))
self->s.effects |= EF_ANIM_ALLFAST;
// just a wall
if (!self->spawnflags.has(SPAWNFLAG_WALL_TRIGGER_SPAWN | SPAWNFLAG_WALL_TOGGLE | SPAWNFLAG_WALL_START_ON))
{
self->solid = SOLID_BSP;
gi.linkentity(self);
return;
}
// it must be TRIGGER_SPAWN
if (!(self->spawnflags & SPAWNFLAG_WALL_TRIGGER_SPAWN))
self->spawnflags |= SPAWNFLAG_WALL_TRIGGER_SPAWN;
// yell if the spawnflags are odd
if (self->spawnflags.has(SPAWNFLAG_WALL_START_ON))
{
if (!self->spawnflags.has(SPAWNFLAG_WALL_TOGGLE))
{
gi.Com_Print("func_wall START_ON without TOGGLE\n");
self->spawnflags |= SPAWNFLAG_WALL_TOGGLE;
}
}
self->use = func_wall_use;
if (self->spawnflags.has(SPAWNFLAG_WALL_START_ON))
{
self->solid = SOLID_BSP;
}
else
{
self->solid = SOLID_NOT;
self->svflags |= SVF_NOCLIENT;
}
gi.linkentity(self);
}
// [Paril-KEX]
/*QUAKED func_animation (0 .5 .8) ? START_ON
Similar to func_wall, but triggering it will toggle animation
state rather than going on/off.
START_ON will start in alterate animation
*/
constexpr spawnflags_t SPAWNFLAG_ANIMATION_START_ON = 1_spawnflag;
USE(func_animation_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
self->bmodel_anim.alternate = !self->bmodel_anim.alternate;
}
void SP_func_animation(edict_t *self)
{
if (!self->bmodel_anim.enabled)
{
gi.Com_PrintFmt("{} has no animation data\n", *self);
G_FreeEdict(self);
return;
}
self->movetype = MOVETYPE_PUSH;
gi.setmodel(self, self->model);
self->solid = SOLID_BSP;
self->use = func_animation_use;
self->bmodel_anim.alternate = self->spawnflags.has(SPAWNFLAG_ANIMATION_START_ON);
if (self->bmodel_anim.alternate)
self->s.frame = self->bmodel_anim.alt_start;
else
self->s.frame = self->bmodel_anim.start;
gi.linkentity(self);
}
/*QUAKED func_object (0 .5 .8) ? TRIGGER_SPAWN ANIMATED ANIMATED_FAST
This is solid bmodel that will fall if it's support it removed.
*/
constexpr spawnflags_t SPAWNFLAGS_OBJECT_TRIGGER_SPAWN = 1_spawnflag;
constexpr spawnflags_t SPAWNFLAGS_OBJECT_ANIMATED = 2_spawnflag;
constexpr spawnflags_t SPAWNFLAGS_OBJECT_ANIMATED_FAST = 4_spawnflag;
TOUCH(func_object_touch) (edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self) -> void
{
// only squash thing we fall on top of
if (other_touching_self)
return;
if (tr.plane.normal[2] < 1.0f)
return;
if (other->takedamage == false)
return;
if (other->damage_debounce_time > level.time)
return;
T_Damage(other, self, self, vec3_origin, closest_point_to_box(other->s.origin, self->absmin, self->absmax), tr.plane.normal, self->dmg, 1, DAMAGE_NONE, MOD_CRUSH);
other->damage_debounce_time = level.time + 10_hz;
}
THINK(func_object_release) (edict_t *self) -> void
{
self->movetype = MOVETYPE_TOSS;
self->touch = func_object_touch;
}
USE(func_object_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
self->solid = SOLID_BSP;
self->svflags &= ~SVF_NOCLIENT;
self->use = nullptr;
func_object_release(self);
KillBox(self, false);
}
void SP_func_object(edict_t *self)
{
gi.setmodel(self, self->model);
self->mins[0] += 1;
self->mins[1] += 1;
self->mins[2] += 1;
self->maxs[0] -= 1;
self->maxs[1] -= 1;
self->maxs[2] -= 1;
if (!self->dmg)
self->dmg = 100;
if (!(self->spawnflags & SPAWNFLAGS_OBJECT_TRIGGER_SPAWN))
{
self->solid = SOLID_BSP;
self->movetype = MOVETYPE_PUSH;
self->think = func_object_release;
self->nextthink = level.time + 20_hz;
}
else
{
self->solid = SOLID_NOT;
self->movetype = MOVETYPE_PUSH;
self->use = func_object_use;
self->svflags |= SVF_NOCLIENT;
}
if (self->spawnflags.has(SPAWNFLAGS_OBJECT_ANIMATED))
self->s.effects |= EF_ANIM_ALL;
if (self->spawnflags.has(SPAWNFLAGS_OBJECT_ANIMATED_FAST))
self->s.effects |= EF_ANIM_ALLFAST;
self->clipmask = MASK_MONSTERSOLID;
self->flags |= FL_NO_STANDING;
gi.linkentity(self);
}
/*QUAKED func_explosive (0 .5 .8) ? Trigger_Spawn ANIMATED ANIMATED_FAST INACTIVE ALWAYS_SHOOTABLE
Any brush that you want to explode or break apart. If you want an
ex0plosion, set dmg and it will do a radius explosion of that amount
at the center of the bursh.
If targeted it will not be shootable.
INACTIVE - specifies that the entity is not explodable until triggered. If you use this you must
target the entity you want to trigger it. This is the only entity approved to activate it.
health defaults to 100.
mass defaults to 75. This determines how much debris is emitted when
it explodes. You get one large chunk per 100 of mass (up to 8) and
one small chunk per 25 of mass (up to 16). So 800 gives the most.
*/
constexpr spawnflags_t SPAWNFLAGS_EXPLOSIVE_TRIGGER_SPAWN = 1_spawnflag;
constexpr spawnflags_t SPAWNFLAGS_EXPLOSIVE_ANIMATED = 2_spawnflag;
constexpr spawnflags_t SPAWNFLAGS_EXPLOSIVE_ANIMATED_FAST = 4_spawnflag;
constexpr spawnflags_t SPAWNFLAGS_EXPLOSIVE_INACTIVE = 8_spawnflag;
constexpr spawnflags_t SPAWNFLAGS_EXPLOSIVE_ALWAYS_SHOOTABLE = 16_spawnflag;
DIE(func_explosive_explode) (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void
{
size_t count;
int mass;
edict_t *master;
bool done = false;
self->takedamage = false;
if (self->dmg)
T_RadiusDamage(self, attacker, (float) self->dmg, nullptr, (float) (self->dmg + 40), DAMAGE_NONE, MOD_EXPLOSIVE);
self->velocity = inflictor->s.origin - self->s.origin;
self->velocity.normalize();
self->velocity *= 150;
mass = self->mass;
if (!mass)
mass = 75;
// big chunks
if (mass >= 100)
{
count = mass / 100;
if (count > 8)
count = 8;
ThrowGibs(self, 1, {
{ count, "models/objects/debris1/tris.md2", GIB_METALLIC | GIB_DEBRIS }
});
}
// small chunks
count = mass / 25;
if (count > 16)
count = 16;
ThrowGibs(self, 2, {
{ count, "models/objects/debris2/tris.md2", GIB_METALLIC | GIB_DEBRIS }
});
// PMM - if we're part of a train, clean ourselves out of it
if (self->flags & FL_TEAMSLAVE)
{
if (self->teammaster)
{
master = self->teammaster;
if (master && master->inuse) // because mappers (other than jim (usually)) are stupid....
{
while (!done)
{
if (master->teamchain == self)
{
master->teamchain = self->teamchain;
done = true;
}
master = master->teamchain;
}
}
}
}
G_UseTargets(self, attacker);
self->s.origin = (self->absmin + self->absmax) * 0.5f;
if (self->noise_index)
gi.positioned_sound(self->s.origin, self, CHAN_AUTO, self->noise_index, 1, ATTN_NORM, 0);
if (self->dmg)
BecomeExplosion1(self);
else
G_FreeEdict(self);
}
USE(func_explosive_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
// Paril: pass activator to explode as attacker. this fixes
// "strike" trying to centerprint to the relay. Should be
// a safe change.
func_explosive_explode(self, self, activator, self->health, vec3_origin, MOD_EXPLOSIVE);
}
// PGM
USE(func_explosive_activate) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
int approved;
approved = 0;
// PMM - looked like target and targetname were flipped here
if (other != nullptr && other->target)
{
if (!strcmp(other->target, self->targetname))
approved = 1;
}
if (!approved && activator != nullptr && activator->target)
{
if (!strcmp(activator->target, self->targetname))
approved = 1;
}
if (!approved)
return;
self->use = func_explosive_use;
if (!self->health)
self->health = 100;
self->die = func_explosive_explode;
self->takedamage = true;
}
// PGM
USE(func_explosive_spawn) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
self->solid = SOLID_BSP;
self->svflags &= ~SVF_NOCLIENT;
self->use = nullptr;
gi.linkentity(self);
KillBox(self, false);
}
void SP_func_explosive(edict_t *self)
{
if (deathmatch->integer)
{ // auto-remove for deathmatch
G_FreeEdict(self);
return;
}
self->movetype = MOVETYPE_PUSH;
gi.modelindex("models/objects/debris1/tris.md2");
gi.modelindex("models/objects/debris2/tris.md2");
gi.setmodel(self, self->model);
if (self->spawnflags.has(SPAWNFLAGS_EXPLOSIVE_TRIGGER_SPAWN))
{
self->svflags |= SVF_NOCLIENT;
self->solid = SOLID_NOT;
self->use = func_explosive_spawn;
}
// PGM
else if (self->spawnflags.has(SPAWNFLAGS_EXPLOSIVE_INACTIVE))
{
self->solid = SOLID_BSP;
if (self->targetname)
self->use = func_explosive_activate;
}
// PGM
else
{
self->solid = SOLID_BSP;
if (self->targetname)
self->use = func_explosive_use;
}
if (self->spawnflags.has(SPAWNFLAGS_EXPLOSIVE_ANIMATED))
self->s.effects |= EF_ANIM_ALL;
if (self->spawnflags.has(SPAWNFLAGS_EXPLOSIVE_ANIMATED_FAST))
self->s.effects |= EF_ANIM_ALLFAST;
// PGM
if (self->spawnflags.has(SPAWNFLAGS_EXPLOSIVE_ALWAYS_SHOOTABLE) || ((self->use != func_explosive_use) && (self->use != func_explosive_activate)))
// PGM
{
if (!self->health)
self->health = 100;
self->die = func_explosive_explode;
self->takedamage = true;
}
if (self->sounds)
{
if (self->sounds == 1)
self->noise_index = gi.soundindex("world/brkglas.wav");
else
gi.Com_PrintFmt("{}: invalid \"sounds\" {}\n", *self, self->sounds);
}
gi.linkentity(self);
}
/*QUAKED misc_explobox (0 .5 .8) (-16 -16 0) (16 16 40)
Large exploding box. You can override its mass (100),
health (80), and dmg (150).
*/
TOUCH(barrel_touch) (edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self) -> void
{
float ratio;
vec3_t v;
if ((!other->groundentity) || (other->groundentity == self))
return;
else if (!other_touching_self)
return;
ratio = (float) other->mass / (float) self->mass;
v = self->s.origin - other->s.origin;
M_walkmove(self, vectoyaw(v), 20 * ratio * gi.frame_time_s);
}
THINK(barrel_explode) (edict_t *self) -> void
{
self->takedamage = false;
T_RadiusDamage(self, self->activator, (float) self->dmg, nullptr, (float) (self->dmg + 40), DAMAGE_NONE, MOD_BARREL);
ThrowGibs(self, (1.5f * self->dmg / 200.f), {
{ 2, "models/objects/debris1/tris.md2", GIB_METALLIC | GIB_DEBRIS },
{ 4, "models/objects/debris3/tris.md2", GIB_METALLIC | GIB_DEBRIS },
{ 8, "models/objects/debris2/tris.md2", GIB_METALLIC | GIB_DEBRIS }
});
if (self->groundentity)
BecomeExplosion2(self);
else
BecomeExplosion1(self);
}
THINK(barrel_burn) (edict_t* self) -> void
{
if (level.time >= self->timestamp)
self->think = barrel_explode;
self->s.effects |= EF_BARREL_EXPLODING;
self->s.sound = gi.soundindex("weapons/bfg__l1a.wav");
self->nextthink = level.time + FRAME_TIME_S;
}
DIE(barrel_delay) (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void
{
// allow "dead" barrels waiting to explode to still receive knockback
if (self->think == barrel_burn || self->think == barrel_explode)
return;
// allow big booms to immediately blow up barrels (rockets, rail, other explosions) because it feels good and powerful
if (damage >= 90)
{
self->think = barrel_explode;
self->activator = attacker;
}
else
{
self->timestamp = level.time + 750_ms;
self->think = barrel_burn;
self->activator = attacker;
}
}
//=========
// PGM - change so barrels will think and hence, blow up
THINK(barrel_think) (edict_t *self) -> void
{
// the think needs to be first since later stuff may override.
self->think = barrel_think;
self->nextthink = level.time + FRAME_TIME_S;
M_CatagorizePosition(self, self->s.origin, self->waterlevel, self->watertype);
self->flags |= FL_IMMUNE_SLIME;
self->air_finished = level.time + 100_sec;
M_WorldEffects(self);
}
THINK(barrel_start) (edict_t *self) -> void
{
M_droptofloor(self);
self->think = barrel_think;
self->nextthink = level.time + FRAME_TIME_S;
}
// PGM
//=========
void SP_misc_explobox(edict_t *self)
{
if (deathmatch->integer)
{ // auto-remove for deathmatch
G_FreeEdict(self);
return;
}
gi.modelindex("models/objects/debris1/tris.md2");
gi.modelindex("models/objects/debris2/tris.md2");
gi.modelindex("models/objects/debris3/tris.md2");
gi.soundindex("weapons/bfg__l1a.wav");
self->solid = SOLID_BBOX;
self->movetype = MOVETYPE_STEP;
self->model = "models/objects/barrels/tris.md2";
self->s.modelindex = gi.modelindex(self->model);
self->mins = { -16, -16, 0 };
self->maxs = { 16, 16, 40 };
if (!self->mass)
self->mass = 50;
if (!self->health)
self->health = 10;
if (!self->dmg)
self->dmg = 150;
self->die = barrel_delay;
self->takedamage = true;
self->flags |= FL_TRAP;
self->touch = barrel_touch;
// PGM - change so barrels will think and hence, blow up
self->think = barrel_start;
self->nextthink = level.time + 20_hz;
// PGM
gi.linkentity(self);
}
//
// miscellaneous specialty items
//
/*QUAKED misc_blackhole (1 .5 0) (-8 -8 -8) (8 8 8) AUTO_NOISE
model="models/objects/black/tris.md2"
*/
constexpr spawnflags_t SPAWNFLAG_BLACKHOLE_AUTO_NOISE = 1_spawnflag;
USE(misc_blackhole_use) (edict_t *ent, edict_t *other, edict_t *activator) -> void
{
/*
gi.WriteByte (svc_temp_entity);
gi.WriteByte (TE_BOSSTPORT);
gi.WritePosition (ent->s.origin);
gi.multicast (ent->s.origin, MULTICAST_PVS);
*/
G_FreeEdict(ent);
}
THINK(misc_blackhole_think) (edict_t *self) -> void
{
if (self->timestamp <= level.time)
{
if (++self->s.frame >= 19)
self->s.frame = 0;
self->timestamp = level.time + 10_hz;
}
if (self->spawnflags.has(SPAWNFLAG_BLACKHOLE_AUTO_NOISE))
{
self->s.angles[0] += 50.0f * gi.frame_time_s;
self->s.angles[1] += 50.0f * gi.frame_time_s;
}
self->nextthink = level.time + FRAME_TIME_MS;
}
void SP_misc_blackhole(edict_t *ent)
{
ent->movetype = MOVETYPE_NONE;
ent->solid = SOLID_NOT;
ent->mins = { -64, -64, 0 };
ent->maxs = { 64, 64, 8 };
ent->s.modelindex = gi.modelindex("models/objects/black/tris.md2");
ent->s.renderfx = RF_TRANSLUCENT;
ent->use = misc_blackhole_use;
ent->think = misc_blackhole_think;
ent->nextthink = level.time + 20_hz;
if (ent->spawnflags.has(SPAWNFLAG_BLACKHOLE_AUTO_NOISE))
{
ent->s.sound = gi.soundindex("world/blackhole.wav");
ent->s.loop_attenuation = ATTN_NORM;
}
gi.linkentity(ent);
}
/*QUAKED misc_eastertank (1 .5 0) (-32 -32 -16) (32 32 32)
*/
THINK(misc_eastertank_think) (edict_t *self) -> void
{
if (++self->s.frame < 293)
self->nextthink = level.time + 10_hz;
else
{
self->s.frame = 254;
self->nextthink = level.time + 10_hz;
}
}
void SP_misc_eastertank(edict_t *ent)
{
ent->movetype = MOVETYPE_NONE;
ent->solid = SOLID_BBOX;
ent->mins = { -32, -32, -16 };
ent->maxs = { 32, 32, 32 };
ent->s.modelindex = gi.modelindex("models/monsters/tank/tris.md2");
ent->s.frame = 254;
ent->think = misc_eastertank_think;
ent->nextthink = level.time + 20_hz;
gi.linkentity(ent);
}
/*QUAKED misc_easterchick (1 .5 0) (-32 -32 0) (32 32 32)
*/
THINK(misc_easterchick_think) (edict_t *self) -> void
{
if (++self->s.frame < 247)
self->nextthink = level.time + 10_hz;
else
{
self->s.frame = 208;
self->nextthink = level.time + 10_hz;
}
}
void SP_misc_easterchick(edict_t *ent)
{
ent->movetype = MOVETYPE_NONE;
ent->solid = SOLID_BBOX;
ent->mins = { -32, -32, 0 };
ent->maxs = { 32, 32, 32 };
ent->s.modelindex = gi.modelindex("models/monsters/bitch/tris.md2");
ent->s.frame = 208;
ent->think = misc_easterchick_think;
ent->nextthink = level.time + 20_hz;
gi.linkentity(ent);
}
/*QUAKED misc_easterchick2 (1 .5 0) (-32 -32 0) (32 32 32)
*/
THINK(misc_easterchick2_think) (edict_t *self) -> void
{
if (++self->s.frame < 287)
self->nextthink = level.time + 10_hz;
else
{
self->s.frame = 248;
self->nextthink = level.time + 10_hz;
}
}
void SP_misc_easterchick2(edict_t *ent)
{
ent->movetype = MOVETYPE_NONE;
ent->solid = SOLID_BBOX;
ent->mins = { -32, -32, 0 };
ent->maxs = { 32, 32, 32 };
ent->s.modelindex = gi.modelindex("models/monsters/bitch/tris.md2");
ent->s.frame = 248;
ent->think = misc_easterchick2_think;
ent->nextthink = level.time + 20_hz;
gi.linkentity(ent);
}
/*QUAKED monster_commander_body (1 .5 0) (-32 -32 0) (32 32 48)
Not really a monster, this is the Tank Commander's decapitated body.
There should be a item_commander_head that has this as it's target.
*/
THINK(commander_body_think) (edict_t *self) -> void
{
if (++self->s.frame < 24)
self->nextthink = level.time + 10_hz;
else
self->nextthink = 0_ms;
if (self->s.frame == 22)
gi.sound(self, CHAN_BODY, gi.soundindex("tank/thud.wav"), 1, ATTN_NORM, 0);
}
USE(commander_body_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
self->think = commander_body_think;
self->nextthink = level.time + 10_hz;
gi.sound(self, CHAN_BODY, gi.soundindex("tank/pain.wav"), 1, ATTN_NORM, 0);
}
THINK(commander_body_drop) (edict_t *self) -> void
{
self->movetype = MOVETYPE_TOSS;
self->s.origin[2] += 2;
}
void SP_monster_commander_body(edict_t *self)
{
self->movetype = MOVETYPE_NONE;
self->solid = SOLID_BBOX;
self->model = "models/monsters/commandr/tris.md2";
self->s.modelindex = gi.modelindex(self->model);
self->mins = { -32, -32, 0 };
self->maxs = { 32, 32, 48 };
self->use = commander_body_use;
self->takedamage = true;
self->flags = FL_GODMODE;
gi.linkentity(self);
gi.soundindex("tank/thud.wav");
gi.soundindex("tank/pain.wav");
self->think = commander_body_drop;
self->nextthink = level.time + 50_hz;
}
/*QUAKED misc_banner (1 .5 0) (-4 -4 -4) (4 4 4)
The origin is the bottom of the banner.
The banner is 128 tall.
model="models/objects/banner/tris.md2"
*/
THINK(misc_banner_think) (edict_t *ent) -> void
{
ent->s.frame = (ent->s.frame + 1) % 16;
ent->nextthink = level.time + 10_hz;
}
void SP_misc_banner(edict_t *ent)
{
ent->movetype = MOVETYPE_NONE;
ent->solid = SOLID_NOT;
ent->s.modelindex = gi.modelindex("models/objects/banner/tris.md2");
ent->s.frame = irandom(16);
gi.linkentity(ent);
ent->think = misc_banner_think;
ent->nextthink = level.time + 10_hz;
}
/*QUAKED misc_deadsoldier (1 .5 0) (-16 -16 0) (16 16 16) ON_BACK ON_STOMACH BACK_DECAP FETAL_POS SIT_DECAP IMPALED
This is the dead player model. Comes in 6 exciting different poses!
*/
constexpr spawnflags_t SPAWNFLAGS_DEADSOLDIER_ON_BACK = 1_spawnflag;
constexpr spawnflags_t SPAWNFLAGS_DEADSOLDIER_ON_STOMACH = 2_spawnflag;
constexpr spawnflags_t SPAWNFLAGS_DEADSOLDIER_BACK_DECAP = 4_spawnflag;
constexpr spawnflags_t SPAWNFLAGS_DEADSOLDIER_FETAL_POS = 8_spawnflag;
constexpr spawnflags_t SPAWNFLAGS_DEADSOLDIER_SIT_DECAP = 16_spawnflag;
constexpr spawnflags_t SPAWNFLAGS_DEADSOLDIER_IMPALED = 32_spawnflag;
DIE(misc_deadsoldier_die) (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void
{
if (self->health > -30)
return;
gi.sound(self, CHAN_BODY, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0);
ThrowGibs(self, damage, {
{ 4, "models/objects/gibs/sm_meat/tris.md2" },
{ "models/objects/gibs/head2/tris.md2", GIB_HEAD }
});
}
void SP_misc_deadsoldier(edict_t *ent)
{
if (deathmatch->integer)
{ // auto-remove for deathmatch
G_FreeEdict(ent);
return;
}
ent->movetype = MOVETYPE_NONE;
ent->solid = SOLID_BBOX;
ent->s.modelindex = gi.modelindex("models/deadbods/dude/tris.md2");
// Defaults to frame 0
if (ent->spawnflags.has(SPAWNFLAGS_DEADSOLDIER_ON_STOMACH))
ent->s.frame = 1;
else if (ent->spawnflags.has(SPAWNFLAGS_DEADSOLDIER_BACK_DECAP))
ent->s.frame = 2;
else if (ent->spawnflags.has(SPAWNFLAGS_DEADSOLDIER_FETAL_POS))
ent->s.frame = 3;
else if (ent->spawnflags.has(SPAWNFLAGS_DEADSOLDIER_SIT_DECAP))
ent->s.frame = 4;
else if (ent->spawnflags.has(SPAWNFLAGS_DEADSOLDIER_IMPALED))
ent->s.frame = 5;
else if (ent->spawnflags.has(SPAWNFLAGS_DEADSOLDIER_ON_BACK))
ent->s.frame = 0;
else
ent->s.frame = 0;
ent->mins = { -16, -16, 0 };
ent->maxs = { 16, 16, 16 };
ent->deadflag = true;
ent->takedamage = true;
// nb: SVF_MONSTER is here so it bleeds
ent->svflags |= SVF_MONSTER | SVF_DEADMONSTER;
ent->die = misc_deadsoldier_die;
ent->monsterinfo.aiflags |= AI_GOOD_GUY | AI_DO_NOT_COUNT;
gi.linkentity(ent);
}
/*QUAKED misc_viper (1 .5 0) (-16 -16 0) (16 16 32)
This is the Viper for the flyby bombing.
It is trigger_spawned, so you must have something use it for it to show up.
There must be a path for it to follow once it is activated.
"speed" How fast the Viper should fly
*/
USE(misc_viper_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
self->svflags &= ~SVF_NOCLIENT;
self->use = train_use;
train_use(self, other, activator);
}
void SP_misc_viper(edict_t *ent)
{
if (!ent->target)
{
gi.Com_PrintFmt("{} without a target\n", *ent);
G_FreeEdict(ent);
return;
}
if (!ent->speed)
ent->speed = 300;
ent->movetype = MOVETYPE_PUSH;
ent->solid = SOLID_NOT;
ent->s.modelindex = gi.modelindex("models/ships/viper/tris.md2");
ent->mins = { -16, -16, 0 };
ent->maxs = { 16, 16, 32 };
ent->think = func_train_find;
ent->nextthink = level.time + 10_hz;
ent->use = misc_viper_use;
ent->svflags |= SVF_NOCLIENT;
ent->moveinfo.accel = ent->moveinfo.decel = ent->moveinfo.speed = ent->speed;
gi.linkentity(ent);
}
/*QUAKED misc_bigviper (1 .5 0) (-176 -120 -24) (176 120 72)
This is a large stationary viper as seen in Paul's intro
*/
void SP_misc_bigviper(edict_t *ent)
{
ent->movetype = MOVETYPE_NONE;
ent->solid = SOLID_BBOX;
ent->mins = { -176, -120, -24 };
ent->maxs = { 176, 120, 72 };
ent->s.modelindex = gi.modelindex("models/ships/bigviper/tris.md2");
gi.linkentity(ent);
}
/*QUAKED misc_viper_bomb (1 0 0) (-8 -8 -8) (8 8 8)
"dmg" how much boom should the bomb make?
*/
TOUCH(misc_viper_bomb_touch) (edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self) -> void
{
G_UseTargets(self, self->activator);
self->s.origin[2] = self->absmin[2] + 1;
T_RadiusDamage(self, self, (float) self->dmg, nullptr, (float) (self->dmg + 40), DAMAGE_NONE, MOD_BOMB);
BecomeExplosion2(self);
}
PRETHINK(misc_viper_bomb_prethink) (edict_t *self) -> void
{
self->groundentity = nullptr;
float diff = (self->timestamp - level.time).seconds();
if (diff < -1.0f)
diff = -1.0f;
vec3_t v = self->moveinfo.dir * (1.0f + diff);
v[2] = diff;
diff = self->s.angles[2];
self->s.angles = vectoangles(v);
self->s.angles[2] = diff + 10;
}
USE(misc_viper_bomb_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
edict_t *viper;
self->solid = SOLID_BBOX;
self->svflags &= ~SVF_NOCLIENT;
self->s.effects |= EF_ROCKET;
self->use = nullptr;
self->movetype = MOVETYPE_TOSS;
self->prethink = misc_viper_bomb_prethink;
self->touch = misc_viper_bomb_touch;
self->activator = activator;
viper = G_FindByString<&edict_t::classname>(nullptr, "misc_viper");
self->velocity = viper->moveinfo.dir * viper->moveinfo.speed;
self->timestamp = level.time;
self->moveinfo.dir = viper->moveinfo.dir;
}
void SP_misc_viper_bomb(edict_t *self)
{
self->movetype = MOVETYPE_NONE;
self->solid = SOLID_NOT;
self->mins = { -8, -8, -8 };
self->maxs = { 8, 8, 8 };
self->s.modelindex = gi.modelindex("models/objects/bomb/tris.md2");
if (!self->dmg)
self->dmg = 1000;
self->use = misc_viper_bomb_use;
self->svflags |= SVF_NOCLIENT;
gi.linkentity(self);
}
/*QUAKED misc_strogg_ship (1 .5 0) (-16 -16 0) (16 16 32)
This is a Storgg ship for the flybys.
It is trigger_spawned, so you must have something use it for it to show up.
There must be a path for it to follow once it is activated.
"speed" How fast it should fly
*/
USE(misc_strogg_ship_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
self->svflags &= ~SVF_NOCLIENT;
self->use = train_use;
train_use(self, other, activator);
}
void SP_misc_strogg_ship(edict_t *ent)
{
if (!ent->target)
{
gi.Com_PrintFmt("{} without a target\n", *ent);
G_FreeEdict(ent);
return;
}
if (!ent->speed)
ent->speed = 300;
ent->movetype = MOVETYPE_PUSH;
ent->solid = SOLID_NOT;
ent->s.modelindex = gi.modelindex("models/ships/strogg1/tris.md2");
ent->mins = { -16, -16, 0 };
ent->maxs = { 16, 16, 32 };
ent->think = func_train_find;
ent->nextthink = level.time + 10_hz;
ent->use = misc_strogg_ship_use;
ent->svflags |= SVF_NOCLIENT;
ent->moveinfo.accel = ent->moveinfo.decel = ent->moveinfo.speed = ent->speed;
gi.linkentity(ent);
}
/*QUAKED misc_satellite_dish (1 .5 0) (-64 -64 0) (64 64 128)
model="models/objects/satellite/tris.md2"
*/
THINK(misc_satellite_dish_think) (edict_t *self) -> void
{
self->s.frame++;
if (self->s.frame < 38)
self->nextthink = level.time + 10_hz;
}
USE(misc_satellite_dish_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
self->s.frame = 0;
self->think = misc_satellite_dish_think;
self->nextthink = level.time + 10_hz;
}
void SP_misc_satellite_dish(edict_t *ent)
{
ent->movetype = MOVETYPE_NONE;
ent->solid = SOLID_BBOX;
ent->mins = { -64, -64, 0 };
ent->maxs = { 64, 64, 128 };
ent->s.modelindex = gi.modelindex("models/objects/satellite/tris.md2");
ent->use = misc_satellite_dish_use;
gi.linkentity(ent);
}
/*QUAKED light_mine1 (0 1 0) (-2 -2 -12) (2 2 12)
*/
void SP_light_mine1(edict_t *ent)
{
ent->movetype = MOVETYPE_NONE;
ent->solid = SOLID_NOT;
ent->svflags = SVF_DEADMONSTER;
ent->s.modelindex = gi.modelindex("models/objects/minelite/light1/tris.md2");
gi.linkentity(ent);
}
/*QUAKED light_mine2 (0 1 0) (-2 -2 -12) (2 2 12)
*/
void SP_light_mine2(edict_t *ent)
{
ent->movetype = MOVETYPE_NONE;
ent->solid = SOLID_NOT;
ent->svflags = SVF_DEADMONSTER;
ent->s.modelindex = gi.modelindex("models/objects/minelite/light2/tris.md2");
gi.linkentity(ent);
}
/*QUAKED misc_gib_arm (1 0 0) (-8 -8 -8) (8 8 8)
Intended for use with the target_spawner
*/
void SP_misc_gib_arm(edict_t *ent)
{
gi.setmodel(ent, "models/objects/gibs/arm/tris.md2");
ent->solid = SOLID_NOT;
ent->s.effects |= EF_GIB;
ent->takedamage = true;
ent->die = gib_die;
ent->movetype = MOVETYPE_TOSS;
ent->deadflag = true;
ent->avelocity[0] = frandom(200);
ent->avelocity[1] = frandom(200);
ent->avelocity[2] = frandom(200);
ent->think = G_FreeEdict;
ent->nextthink = level.time + 10_sec;
gi.linkentity(ent);
}
/*QUAKED misc_gib_leg (1 0 0) (-8 -8 -8) (8 8 8)
Intended for use with the target_spawner
*/
void SP_misc_gib_leg(edict_t *ent)
{
gi.setmodel(ent, "models/objects/gibs/leg/tris.md2");
ent->solid = SOLID_NOT;
ent->s.effects |= EF_GIB;
ent->takedamage = true;
ent->die = gib_die;
ent->movetype = MOVETYPE_TOSS;
ent->deadflag = true;
ent->avelocity[0] = frandom(200);
ent->avelocity[1] = frandom(200);
ent->avelocity[2] = frandom(200);
ent->think = G_FreeEdict;
ent->nextthink = level.time + 10_sec;
gi.linkentity(ent);
}
/*QUAKED misc_gib_head (1 0 0) (-8 -8 -8) (8 8 8)
Intended for use with the target_spawner
*/
void SP_misc_gib_head(edict_t *ent)
{
gi.setmodel(ent, "models/objects/gibs/head/tris.md2");
ent->solid = SOLID_NOT;
ent->s.effects |= EF_GIB;
ent->takedamage = true;
ent->die = gib_die;
ent->movetype = MOVETYPE_TOSS;
ent->deadflag = true;
ent->avelocity[0] = frandom(200);
ent->avelocity[1] = frandom(200);
ent->avelocity[2] = frandom(200);
ent->think = G_FreeEdict;
ent->nextthink = level.time + 10_sec;
gi.linkentity(ent);
}
//=====================================================
/*QUAKED target_character (0 0 1) ?
used with target_string (must be on same "team")
"count" is position in the string (starts at 1)
*/
void SP_target_character(edict_t *self)
{
self->movetype = MOVETYPE_PUSH;
gi.setmodel(self, self->model);
self->solid = SOLID_BSP;
self->s.frame = 12;
gi.linkentity(self);
return;
}
/*QUAKED target_string (0 0 1) (-8 -8 -8) (8 8 8)
*/
USE(target_string_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
edict_t *e;
int n;
size_t l;
char c;
l = strlen(self->message);
for (e = self->teammaster; e; e = e->teamchain)
{
if (!e->count)
continue;
n = e->count - 1;
if (n > l)
{
e->s.frame = 12;
continue;
}
c = self->message[n];
if (c >= '0' && c <= '9')
e->s.frame = c - '0';
else if (c == '-')
e->s.frame = 10;
else if (c == ':')
e->s.frame = 11;
else
e->s.frame = 12;
}
}
void SP_target_string(edict_t *self)
{
if (!self->message)
self->message = "";
self->use = target_string_use;
}
/*QUAKED func_clock (0 0 1) (-8 -8 -8) (8 8 8) TIMER_UP TIMER_DOWN START_OFF MULTI_USE
target a target_string with this
The default is to be a time of day clock
TIMER_UP and TIMER_DOWN run for "count" seconds and then fire "pathtarget"
If START_OFF, this entity must be used before it starts
"style" 0 "xx"
1 "xx:xx"
2 "xx:xx:xx"
*/
constexpr spawnflags_t SPAWNFLAG_TIMER_UP = 1_spawnflag;
constexpr spawnflags_t SPAWNFLAG_TIMER_DOWN = 2_spawnflag;
constexpr spawnflags_t SPAWNFLAG_TIMER_START_OFF = 4_spawnflag;
constexpr spawnflags_t SPAWNFLAG_TIMER_MULTI_USE = 8_spawnflag;
static void func_clock_reset(edict_t *self)
{
self->activator = nullptr;
if (self->spawnflags.has(SPAWNFLAG_TIMER_UP))
{
self->health = 0;
self->wait = (float) self->count;
}
else if (self->spawnflags.has(SPAWNFLAG_TIMER_DOWN))
{
self->health = self->count;
self->wait = 0;
}
}
static void func_clock_format_countdown(edict_t *self)
{
if (self->style == 0)
{
G_FmtTo(self->clock_message, "{:2}", self->health);
return;
}
if (self->style == 1)
{
G_FmtTo(self->clock_message, "{:2}:{:02}", self->health / 60, self->health % 60);
return;
}
if (self->style == 2)
{
G_FmtTo(self->clock_message, "{:2}:{:02}:{:02}", self->health / 3600,
(self->health - (self->health / 3600) * 3600) / 60, self->health % 60);
return;
}
}
THINK(func_clock_think) (edict_t *self) -> void
{
if (!self->enemy)
{
self->enemy = G_FindByString<&edict_t::targetname>(nullptr, self->target);
if (!self->enemy)
return;
}
if (self->spawnflags.has(SPAWNFLAG_TIMER_UP))
{
func_clock_format_countdown(self);
self->health++;
}
else if (self->spawnflags.has(SPAWNFLAG_TIMER_DOWN))
{
func_clock_format_countdown(self);
self->health--;
}
else
{
struct tm *ltime;
time_t gmtime;
time(&gmtime);
ltime = localtime(&gmtime);
G_FmtTo(self->clock_message, "{:2}:{:02}:{:02}", ltime->tm_hour, ltime->tm_min,
ltime->tm_sec);
}
self->enemy->message = self->clock_message;
self->enemy->use(self->enemy, self, self);
if ((self->spawnflags.has(SPAWNFLAG_TIMER_UP) && (self->health > self->wait)) ||
(self->spawnflags.has(SPAWNFLAG_TIMER_DOWN) && (self->health < self->wait)))
{
if (self->pathtarget)
{
const char *savetarget;
savetarget = self->target;
self->target = self->pathtarget;
G_UseTargets(self, self->activator);
self->target = savetarget;
}
if (!self->spawnflags.has(SPAWNFLAG_TIMER_MULTI_USE))
return;
func_clock_reset(self);
if (self->spawnflags.has(SPAWNFLAG_TIMER_START_OFF))
return;
}
self->nextthink = level.time + 1_sec;
}
USE(func_clock_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
if (!self->spawnflags.has(SPAWNFLAG_TIMER_MULTI_USE))
self->use = nullptr;
if (self->activator)
return;
self->activator = activator;
self->think(self);
}
void SP_func_clock(edict_t *self)
{
if (!self->target)
{
gi.Com_PrintFmt("{} with no target\n", *self);
G_FreeEdict(self);
return;
}
if (self->spawnflags.has(SPAWNFLAG_TIMER_DOWN) && !self->count)
{
gi.Com_PrintFmt("{} with no count\n", *self);
G_FreeEdict(self);
return;
}
if (self->spawnflags.has(SPAWNFLAG_TIMER_UP) && (!self->count))
self->count = 60 * 60;
func_clock_reset(self);
self->think = func_clock_think;
if (self->spawnflags.has(SPAWNFLAG_TIMER_START_OFF))
self->use = func_clock_use;
else
self->nextthink = level.time + 1_sec;
}
//=================================================================================
constexpr spawnflags_t SPAWNFLAG_TELEPORTER_NO_SOUND = 1_spawnflag;
constexpr spawnflags_t SPAWNFLAG_TELEPORTER_NO_TELEPORT_EFFECT = 2_spawnflag;
TOUCH(teleporter_touch) (edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self) -> void
{
edict_t *dest;
if (!other->client)
return;
dest = G_FindByString<&edict_t::targetname>(nullptr, self->target);
if (!dest)
{
gi.Com_Print("Couldn't find destination\n");
return;
}
// ZOID
CTFPlayerResetGrapple(other);
// ZOID
// unlink to make sure it can't possibly interfere with KillBox
gi.unlinkentity(other);
other->s.origin = dest->s.origin;
other->s.old_origin = dest->s.origin;
other->s.origin[2] += 10;
// clear the velocity and hold them in place briefly
other->velocity = {};
other->client->ps.pmove.pm_time = 160; // hold time
other->client->ps.pmove.pm_flags |= PMF_TIME_TELEPORT;
// draw the teleport splash at source and on the player
if (!self->spawnflags.has(SPAWNFLAG_TELEPORTER_NO_TELEPORT_EFFECT))
{
self->owner->s.event = EV_PLAYER_TELEPORT;
other->s.event = EV_PLAYER_TELEPORT;
}
else
{
self->owner->s.event = EV_OTHER_TELEPORT;
other->s.event = EV_OTHER_TELEPORT;
}
// set angles
other->client->ps.pmove.delta_angles = dest->s.angles - other->client->resp.cmd_angles;
other->s.angles = {};
other->client->ps.viewangles = {};
other->client->v_angle = {};
AngleVectors(other->client->v_angle, other->client->v_forward, nullptr, nullptr);
gi.linkentity(other);
// kill anything at the destination
KillBox(other, !!other->client);
// [Paril-KEX] move sphere, if we own it
if (other->client->owned_sphere)
{
edict_t *sphere = other->client->owned_sphere;
sphere->s.origin = other->s.origin;
sphere->s.origin[2] = other->absmax[2];
sphere->s.angles[YAW] = other->s.angles[YAW];
gi.linkentity(sphere);
}
}
/*QUAKED misc_teleporter (1 0 0) (-32 -32 -24) (32 32 -16) NO_SOUND NO_TELEPORT_EFFECT N64_EFFECT
Stepping onto this disc will teleport players to the targeted misc_teleporter_dest object.
*/
constexpr spawnflags_t SPAWNFLAG_TEMEPORTER_N64_EFFECT = 4_spawnflag;
void SP_misc_teleporter(edict_t *ent)
{
edict_t *trig;
gi.setmodel(ent, "models/objects/dmspot/tris.md2");
ent->s.skinnum = 1;
if (level.is_n64 || ent->spawnflags.has(SPAWNFLAG_TEMEPORTER_N64_EFFECT))
ent->s.effects = EF_TELEPORTER2;
else
ent->s.effects = EF_TELEPORTER;
if (!(ent->spawnflags & SPAWNFLAG_TELEPORTER_NO_SOUND))
ent->s.sound = gi.soundindex("world/amb10.wav");
ent->solid = SOLID_BBOX;
ent->mins = { -32, -32, -24 };
ent->maxs = { 32, 32, -16 };
gi.linkentity(ent);
// N64 has some of these for visual effects
if (!ent->target)
return;
trig = G_Spawn();
trig->touch = teleporter_touch;
trig->solid = SOLID_TRIGGER;
trig->target = ent->target;
trig->owner = ent;
trig->s.origin = ent->s.origin;
trig->mins = { -8, -8, 8 };
trig->maxs = { 8, 8, 24 };
gi.linkentity(trig);
}
/*QUAKED misc_teleporter_dest (1 0 0) (-32 -32 -24) (32 32 -16)
Point teleporters at these.
*/
void SP_misc_teleporter_dest(edict_t *ent)
{
// Paril-KEX N64 doesn't display these
if (level.is_n64)
return;
gi.setmodel(ent, "models/objects/dmspot/tris.md2");
ent->s.skinnum = 0;
ent->solid = SOLID_BBOX;
// ent->s.effects |= EF_FLIES;
ent->mins = { -32, -32, -24 };
ent->maxs = { 32, 32, -16 };
gi.linkentity(ent);
}
/*QUAKED misc_flare (1.0 1.0 0.0) (-32 -32 -32) (32 32 32) RED GREEN BLUE LOCK_ANGLE
Creates a flare seen in the N64 version.
*/
static constexpr spawnflags_t SPAWNFLAG_FLARE_RED = 1_spawnflag;
static constexpr spawnflags_t SPAWNFLAG_FLARE_GREEN = 2_spawnflag;
static constexpr spawnflags_t SPAWNFLAG_FLARE_BLUE = 4_spawnflag;
static constexpr spawnflags_t SPAWNFLAG_FLARE_LOCK_ANGLE = 8_spawnflag;
USE(misc_flare_use) (edict_t *ent, edict_t *other, edict_t *activator) -> void
{
ent->svflags ^= SVF_NOCLIENT;
gi.linkentity(ent);
}
void SP_misc_flare(edict_t* ent)
{
ent->s.modelindex = 1;
ent->s.renderfx = RF_FLARE;
ent->solid = SOLID_NOT;
ent->s.scale = st.radius;
if (ent->spawnflags.has(SPAWNFLAG_FLARE_RED))
ent->s.renderfx |= RF_SHELL_RED;
if (ent->spawnflags.has(SPAWNFLAG_FLARE_GREEN))
ent->s.renderfx |= RF_SHELL_GREEN;
if (ent->spawnflags.has(SPAWNFLAG_FLARE_BLUE))
ent->s.renderfx |= RF_SHELL_BLUE;
if (ent->spawnflags.has(SPAWNFLAG_FLARE_LOCK_ANGLE))
ent->s.renderfx |= RF_FLARE_LOCK_ANGLE;
if (st.image && *st.image)
{
ent->s.renderfx |= RF_CUSTOMSKIN;
ent->s.frame = gi.imageindex(st.image);
}
ent->mins = { -32, -32, -32 };
ent->maxs = { 32, 32, 32 };
ent->s.modelindex2 = st.fade_start_dist;
ent->s.modelindex3 = st.fade_end_dist;
if (ent->targetname)
ent->use = misc_flare_use;
gi.linkentity(ent);
}
THINK(misc_hologram_think) (edict_t *ent) -> void
{
ent->s.angles[1] += 100 * gi.frame_time_s;
ent->nextthink = level.time + FRAME_TIME_MS;
ent->s.alpha = frandom(0.2f, 0.6f);
}
/*QUAKED misc_hologram (1.0 1.0 0.0) (-16 -16 0) (16 16 32)
Ship hologram seen in the N64 version.
*/
void SP_misc_hologram(edict_t *ent)
{
ent->solid = SOLID_NOT;
ent->s.modelindex = gi.modelindex("models/ships/strogg1/tris.md2");
ent->mins = { -16, -16, 0 };
ent->maxs = { 16, 16, 32 };
ent->s.effects = EF_HOLOGRAM;
ent->think = misc_hologram_think;
ent->nextthink = level.time + FRAME_TIME_MS;
ent->s.alpha = frandom(0.2f, 0.6f);
ent->s.scale = 0.75f;
gi.linkentity(ent);
}
/*QUAKED misc_fireball (0 .5 .8) (-8 -8 -8) (8 8 8) NO_EXPLODE
Lava Balls. Shamelessly copied from Quake 1, like N64 guys
probably did too.
*/
constexpr spawnflags_t SPAWNFLAG_LAVABALL_NO_EXPLODE = 1_spawnflag;
TOUCH(fire_touch) (edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self) -> void
{
if (self->spawnflags.has(SPAWNFLAG_LAVABALL_NO_EXPLODE))
{
G_FreeEdict(self);
return;
}
if (other->takedamage)
T_Damage (other, self, self, vec3_origin, self->s.origin, vec3_origin, 20, 0, DAMAGE_NONE, MOD_EXPLOSIVE);
if (gi.pointcontents(self->s.origin) & CONTENTS_LAVA)
G_FreeEdict(self);
else
BecomeExplosion1(self);
}
THINK(fire_fly) (edict_t *self) -> void
{
edict_t *fireball = G_Spawn();
fireball->s.effects = EF_FIREBALL;
fireball->s.renderfx = RF_MINLIGHT;
fireball->solid = SOLID_BBOX;
fireball->movetype = MOVETYPE_TOSS;
fireball->clipmask = MASK_SHOT;
fireball->velocity[0] = crandom() * 50;
fireball->velocity[1] = crandom() * 50;
fireball->avelocity = { crandom() * 360, crandom() * 360, crandom() * 360 };
fireball->velocity[2] = (self->speed * 1.75f) + (frandom() * 200);
fireball->classname = "fireball";
gi.setmodel(fireball, "models/objects/gibs/sm_meat/tris.md2");
fireball->s.origin = self->s.origin;
fireball->nextthink = level.time + 5_sec;
fireball->think = G_FreeEdict;
fireball->touch = fire_touch;
fireball->spawnflags = self->spawnflags;
gi.linkentity(fireball);
self->nextthink = level.time + random_time(5_sec);
}
void SP_misc_lavaball(edict_t *self)
{
self->classname = "fireball";
self->nextthink = level.time + random_time(5_sec);
self->think = fire_fly;
if (!self->speed)
self->speed = 185;
}
void SP_info_landmark(edict_t* self)
{
self->absmin = self->s.origin;
self->absmax = self->s.origin;
}
constexpr spawnflags_t SPAWNFLAG_WORLD_TEXT_START_OFF = 1_spawnflag;
constexpr spawnflags_t SPAWNFLAG_WORLD_TEXT_TRIGGER_ONCE = 2_spawnflag;
constexpr spawnflags_t SPAWNFLAG_WORLD_TEXT_REMOVE_ON_TRIGGER = 4_spawnflag;
USE( info_world_text_use ) ( edict_t * self, edict_t * other, edict_t * activator ) -> void {
if ( self->activator == nullptr ) {
self->activator = activator;
self->think( self );
} else {
self->nextthink = 0_ms;
self->activator = nullptr;
}
if (self->spawnflags.has(SPAWNFLAG_WORLD_TEXT_TRIGGER_ONCE)) {
self->use = nullptr;
}
if ( self->target != nullptr ) {
edict_t * target = G_PickTarget( self->target );
if ( target != nullptr && target->inuse ) {
if ( target->use ) {
target->use( target, self, self );
}
}
}
if (self->spawnflags.has(SPAWNFLAG_WORLD_TEXT_REMOVE_ON_TRIGGER)) {
G_FreeEdict( self );
}
}
THINK( info_world_text_think ) ( edict_t * self ) -> void {
rgba_t color = rgba_white;
switch ( self->sounds ) {
case 0:
color = rgba_white;
break;
case 1:
color = rgba_red;
break;
case 2:
color = rgba_blue;
break;
case 3:
color = rgba_green;
break;
case 4:
color = rgba_yellow;
break;
case 5:
color = rgba_black;
break;
case 6:
color = rgba_cyan;
break;
case 7:
color = rgba_orange;
break;
default:
color = rgba_white;
gi.Com_PrintFmt( "{}: invalid color\n", *self);
break;
}
if ( self->s.angles[ YAW ] == -3.0f ) {
gi.Draw_OrientedWorldText( self->s.origin, self->message, color, self->size[ 2 ], FRAME_TIME_MS.seconds(), true );
} else {
vec3_t textAngle = { 0.0f, 0.0f, 0.0f };
textAngle[ YAW ] = anglemod( self->s.angles[ YAW ] ) + 180;
if ( textAngle[ YAW ] > 360.0f ) {
textAngle[ YAW ] -= 360.0f;
}
gi.Draw_StaticWorldText( self->s.origin, textAngle, self->message, color, self->size[2], FRAME_TIME_MS.seconds(), true );
}
self->nextthink = level.time + FRAME_TIME_MS;
}
/*QUAKED info_world_text (1.0 1.0 0.0) (-16 -16 0) (16 16 32)
designer placed in world text for debugging.
*/
void SP_info_world_text( edict_t * self ) {
if ( self->message == nullptr ) {
gi.Com_PrintFmt( "{}: no message\n", *self);
G_FreeEdict( self );
return;
} // not much point without something to print...
self->think = info_world_text_think;
self->use = info_world_text_use;
self->size[ 2 ] = st.radius ? st.radius : 0.2f;
if ( !self->spawnflags.has( SPAWNFLAG_WORLD_TEXT_START_OFF ) ) {
self->nextthink = level.time + FRAME_TIME_MS;
self->activator = self;
}
}
#include "m_player.h"
USE( misc_player_mannequin_use ) ( edict_t * self, edict_t * other, edict_t * activator ) -> void {
self->monsterinfo.aiflags |= AI_TARGET_ANGER;
self->enemy = activator;
switch ( self->count ) {
case GESTURE_FLIP_OFF:
self->s.frame = FRAME_flip01;
self->monsterinfo.nextframe = FRAME_flip12;
break;
case GESTURE_SALUTE:
self->s.frame = FRAME_salute01;
self->monsterinfo.nextframe = FRAME_salute11;
break;
case GESTURE_TAUNT:
self->s.frame = FRAME_taunt01;
self->monsterinfo.nextframe = FRAME_taunt17;
break;
case GESTURE_WAVE:
self->s.frame = FRAME_wave01;
self->monsterinfo.nextframe = FRAME_wave11;
break;
case GESTURE_POINT:
self->s.frame = FRAME_point01;
self->monsterinfo.nextframe = FRAME_point12;
break;
}
}
THINK( misc_player_mannequin_think ) ( edict_t * self ) -> void {
if ( self->teleport_time <= level.time ) {
self->s.frame++;
if ( ( self->monsterinfo.aiflags & AI_TARGET_ANGER ) == 0 ) {
if ( self->s.frame > FRAME_stand40 ) {
self->s.frame = FRAME_stand01;
}
} else {
if ( self->s.frame > self->monsterinfo.nextframe ) {
self->s.frame = FRAME_stand01;
self->monsterinfo.aiflags &= ~AI_TARGET_ANGER;
self->enemy = nullptr;
}
}
self->teleport_time = level.time + 10_hz;
}
if ( self->enemy != nullptr ) {
const vec3_t vec = ( self->enemy->s.origin - self->s.origin );
self->ideal_yaw = vectoyaw( vec );
M_ChangeYaw( self );
}
self->nextthink = level.time + FRAME_TIME_MS;
}
void SetupMannequinModel( edict_t * self, const int32_t modelType, const char * weapon, const char * skin ) {
const char * modelName = nullptr;
const char * defaultSkin = nullptr;
switch ( modelType ) {
case 1: {
self->s.skinnum = ( MAX_CLIENTS - 1 );
modelName = "female";
defaultSkin = "venus";
break;
}
case 2: {
self->s.skinnum = ( MAX_CLIENTS - 2 );
modelName = "male";
defaultSkin = "rampage";
break;
}
case 3: {
self->s.skinnum = ( MAX_CLIENTS - 3 );
modelName = "cyborg";
defaultSkin = "oni911";
break;
}
default: {
self->s.skinnum = ( MAX_CLIENTS - 1 );
modelName = "female";
defaultSkin = "venus";
break;
}
}
if ( modelName != nullptr ) {
self->model = G_Fmt( "players/{}/tris.md2", modelName ).data();
const char * weaponName = nullptr;
if ( weapon != nullptr ) {
weaponName = G_Fmt( "players/{}/{}.md2", modelName, weapon ).data();
} else {
weaponName = G_Fmt( "players/{}/{}.md2", modelName, "w_hyperblaster" ).data();
}
self->s.modelindex2 = gi.modelindex( weaponName );
const char * skinName = nullptr;
if ( skin != nullptr ) {
skinName = G_Fmt( "mannequin\\{}/{}", modelName, skin ).data();
} else {
skinName = G_Fmt( "mannequin\\{}/{}", modelName, defaultSkin ).data();
}
gi.configstring( CS_PLAYERSKINS + self->s.skinnum, skinName );
}
}
/*QUAKED misc_player_mannequin (1.0 1.0 0.0) (-32 -32 -32) (32 32 32)
Creates a player mannequin that stands around.
NOTE: this is currently very limited, and only allows one unique model
from each of the three player model types.
"distance" - Sets the type of gesture mannequin when use when triggered
"height" - Sets the type of model to use ( valid numbers: 1 - 3 )
"goals" - Name of the weapon to use.
"image" - Name of the player skin to use.
"radius" - How much to scale the model in-game
*/
void SP_misc_player_mannequin( edict_t * self ) {
self->movetype = MOVETYPE_NONE;
self->solid = SOLID_BBOX;
if (!st.was_key_specified("effects"))
self->s.effects = EF_NONE;
if (!st.was_key_specified("renderfx"))
self->s.renderfx = RF_MINLIGHT;
self->mins = { -16, -16, -24 };
self->maxs = { 16, 16, 32 };
self->yaw_speed = 30;
self->ideal_yaw = 0;
self->teleport_time = level.time + 10_hz;
self->s.modelindex = MODELINDEX_PLAYER;
self->count = st.distance;
SetupMannequinModel( self, st.height, st.goals, st.image );
self->s.scale = 1.0f;
if ( ai_model_scale->value > 0.0f ) {
self->s.scale = ai_model_scale->value;
} else if ( st.radius > 0.0f ) {
self->s.scale = st.radius;
}
self->mins *= self->s.scale;
self->maxs *= self->s.scale;
self->think = misc_player_mannequin_think;
self->nextthink = level.time + FRAME_TIME_MS;
if ( self->targetname ) {
self->use = misc_player_mannequin_use;
}
gi.linkentity( self );
}
/*QUAKED misc_model (1 0 0) (-8 -8 -8) (8 8 8)
*/
void SP_misc_model(edict_t *ent)
{
gi.setmodel(ent, ent->model);
gi.linkentity(ent);
}