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

1630 lines
No EOL
42 KiB
C++

// Copyright (c) ZeniMax Media Inc.
// Licensed under the GNU General Public License 2.0.
#include "g_local.h"
#include "bots/bot_includes.h"
//
// monster weapons
//
void monster_muzzleflash(edict_t *self, const vec3_t &start, monster_muzzleflash_id_t id)
{
if (id <= 255)
gi.WriteByte(svc_muzzleflash2);
else
gi.WriteByte(svc_muzzleflash3);
gi.WriteEntity(self);
if (id <= 255)
gi.WriteByte(id);
else
gi.WriteShort(id);
gi.multicast(start, MULTICAST_PHS, false);
}
void monster_fire_bullet(edict_t *self, const vec3_t &start, const vec3_t &dir, int damage, int kick, int hspread,
int vspread, monster_muzzleflash_id_t flashtype)
{
fire_bullet(self, start, dir, damage, kick, hspread, vspread, MOD_UNKNOWN);
monster_muzzleflash(self, start, flashtype);
}
void monster_fire_shotgun(edict_t *self, const vec3_t &start, const vec3_t &aimdir, int damage, int kick, int hspread,
int vspread, int count, monster_muzzleflash_id_t flashtype)
{
fire_shotgun(self, start, aimdir, damage, kick, hspread, vspread, count, MOD_UNKNOWN);
monster_muzzleflash(self, start, flashtype);
}
void monster_fire_blaster(edict_t *self, const vec3_t &start, const vec3_t &dir, int damage, int speed,
monster_muzzleflash_id_t flashtype, effects_t effect)
{
fire_blaster(self, start, dir, damage, speed, effect, MOD_BLASTER);
monster_muzzleflash(self, start, flashtype);
}
void monster_fire_flechette(edict_t *self, const vec3_t &start, const vec3_t &dir, int damage, int speed,
monster_muzzleflash_id_t flashtype)
{
fire_flechette(self, start, dir, damage, speed, damage / 2);
monster_muzzleflash(self, start, flashtype);
}
void monster_fire_grenade(edict_t *self, const vec3_t &start, const vec3_t &aimdir, int damage, int speed,
monster_muzzleflash_id_t flashtype, float right_adjust, float up_adjust)
{
fire_grenade(self, start, aimdir, damage, speed, 2.5_sec, damage + 40.f, right_adjust, up_adjust, true);
monster_muzzleflash(self, start, flashtype);
}
void monster_fire_rocket(edict_t *self, const vec3_t &start, const vec3_t &dir, int damage, int speed,
monster_muzzleflash_id_t flashtype)
{
fire_rocket(self, start, dir, damage, speed, (float) damage + 20, damage);
monster_muzzleflash(self, start, flashtype);
}
void monster_fire_railgun(edict_t *self, const vec3_t &start, const vec3_t &aimdir, int damage, int kick,
monster_muzzleflash_id_t flashtype)
{
if (gi.pointcontents(start) & MASK_SOLID)
return;
fire_rail(self, start, aimdir, damage, kick);
monster_muzzleflash(self, start, flashtype);
}
void monster_fire_bfg(edict_t *self, const vec3_t &start, const vec3_t &aimdir, int damage, int speed, int kick,
float damage_radius, monster_muzzleflash_id_t flashtype)
{
fire_bfg(self, start, aimdir, damage, speed, damage_radius);
monster_muzzleflash(self, start, flashtype);
}
// [Paril-KEX]
vec3_t M_ProjectFlashSource(edict_t *self, const vec3_t &offset, const vec3_t &forward, const vec3_t &right)
{
return G_ProjectSource(self->s.origin, self->s.scale ? (offset * self->s.scale) : offset, forward, right);
}
// [Paril-KEX] check if shots fired from the given offset
// might be blocked by something
bool M_CheckClearShot(edict_t *self, const vec3_t &offset, vec3_t &start)
{
// no enemy, just do whatever
if (!self->enemy)
return false;
vec3_t f, r;
vec3_t real_angles = { self->s.angles[0], self->ideal_yaw, 0.f };
AngleVectors(real_angles, f, r, nullptr);
start = M_ProjectFlashSource(self, offset, f, r);
vec3_t target;
bool is_blind = self->monsterinfo.attack_state == AS_BLIND || (self->monsterinfo.aiflags & (AI_MANUAL_STEERING | AI_LOST_SIGHT));
if (is_blind)
target = self->monsterinfo.blind_fire_target;
else
target = self->enemy->s.origin + vec3_t{ 0, 0, (float) self->enemy->viewheight };
trace_t tr = gi.traceline(start, target, self, MASK_PROJECTILE & ~CONTENTS_DEADMONSTER);
if (tr.ent == self->enemy || tr.ent->client || (tr.fraction > 0.8f && !tr.startsolid))
return true;
if (!is_blind)
{
target = self->enemy->s.origin;
trace_t tr = gi.traceline(start, target, self, MASK_PROJECTILE & ~CONTENTS_DEADMONSTER);
if (tr.ent == self->enemy || tr.ent->client || (tr.fraction > 0.8f && !tr.startsolid))
return true;
}
return false;
}
bool M_CheckClearShot(edict_t *self, const vec3_t &offset)
{
vec3_t start;
return M_CheckClearShot(self, offset, start);
}
void M_CheckGround(edict_t *ent, contents_t mask)
{
vec3_t point;
trace_t trace;
if (ent->flags & (FL_SWIM | FL_FLY))
return;
if ((ent->velocity[2] * ent->gravityVector[2]) < -100) // PGM
{
ent->groundentity = nullptr;
return;
}
// if the hull point one-quarter unit down is solid the entity is on ground
point[0] = ent->s.origin[0];
point[1] = ent->s.origin[1];
point[2] = ent->s.origin[2] + (0.25f * ent->gravityVector[2]); // PGM
trace = gi.trace(ent->s.origin, ent->mins, ent->maxs, point, ent, mask);
// check steepness
// PGM
if (ent->gravityVector[2] < 0) // normal gravity
{
if (trace.plane.normal[2] < 0.7f && !trace.startsolid)
{
ent->groundentity = nullptr;
return;
}
}
else // inverted gravity
{
if (trace.plane.normal[2] > -0.7f && !trace.startsolid)
{
ent->groundentity = nullptr;
return;
}
}
// PGM
if (!trace.startsolid && !trace.allsolid)
{
ent->s.origin = trace.endpos;
ent->groundentity = trace.ent;
ent->groundentity_linkcount = trace.ent->linkcount;
ent->velocity[2] = 0;
}
}
void M_CatagorizePosition(edict_t *self, const vec3_t &in_point, water_level_t &waterlevel, contents_t &watertype)
{
vec3_t point;
contents_t cont;
//
// get waterlevel
//
point[0] = in_point[0];
point[1] = in_point[1];
if (self->gravityVector[2] > 0)
point[2] = in_point[2] + self->maxs[2] - 1;
else
point[2] = in_point[2] + self->mins[2] + 1;
cont = gi.pointcontents(point);
if (!(cont & MASK_WATER))
{
waterlevel = WATER_NONE;
watertype = CONTENTS_NONE;
return;
}
watertype = cont;
waterlevel = WATER_FEET;
point[2] += 26;
cont = gi.pointcontents(point);
if (!(cont & MASK_WATER))
return;
waterlevel = WATER_WAIST;
point[2] += 22;
cont = gi.pointcontents(point);
if (cont & MASK_WATER)
waterlevel = WATER_UNDER;
}
bool M_ShouldReactToPain(edict_t *self, const mod_t &mod)
{
if (self->monsterinfo.aiflags & (AI_DUCKED | AI_COMBAT_POINT))
return false;
return mod.id == MOD_CHAINFIST || skill->integer < 3;
}
void M_WorldEffects(edict_t *ent)
{
int dmg;
if (ent->health > 0)
{
if (!(ent->flags & FL_SWIM))
{
if (ent->waterlevel < WATER_UNDER)
{
ent->air_finished = level.time + 12_sec;
}
else if (ent->air_finished < level.time)
{ // drown!
if (ent->pain_debounce_time < level.time)
{
dmg = 2 + (int) (2 * floorf((level.time - ent->air_finished).seconds()));
if (dmg > 15)
dmg = 15;
T_Damage(ent, world, world, vec3_origin, ent->s.origin, vec3_origin, dmg, 0, DAMAGE_NO_ARMOR,
MOD_WATER);
ent->pain_debounce_time = level.time + 1_sec;
}
}
}
else
{
if (ent->waterlevel > WATER_NONE)
{
ent->air_finished = level.time + 9_sec;
}
else if (ent->air_finished < level.time)
{ // suffocate!
if (ent->pain_debounce_time < level.time)
{
dmg = 2 + (int) (2 * floorf((level.time - ent->air_finished).seconds()));
if (dmg > 15)
dmg = 15;
T_Damage(ent, world, world, vec3_origin, ent->s.origin, vec3_origin, dmg, 0, DAMAGE_NO_ARMOR,
MOD_WATER);
ent->pain_debounce_time = level.time + 1_sec;
}
}
}
}
if (ent->waterlevel == WATER_NONE)
{
if (ent->flags & FL_INWATER)
{
gi.sound(ent, CHAN_BODY, gi.soundindex("player/watr_out.wav"), 1, ATTN_NORM, 0);
ent->flags &= ~FL_INWATER;
}
}
else
{
if ((ent->watertype & CONTENTS_LAVA) && !(ent->flags & FL_IMMUNE_LAVA))
{
if (ent->damage_debounce_time < level.time)
{
ent->damage_debounce_time = level.time + 100_ms;
T_Damage(ent, world, world, vec3_origin, ent->s.origin, vec3_origin, 10 * ent->waterlevel, 0, DAMAGE_NONE,
MOD_LAVA);
}
}
if ((ent->watertype & CONTENTS_SLIME) && !(ent->flags & FL_IMMUNE_SLIME))
{
if (ent->damage_debounce_time < level.time)
{
ent->damage_debounce_time = level.time + 100_ms;
T_Damage(ent, world, world, vec3_origin, ent->s.origin, vec3_origin, 4 * ent->waterlevel, 0, DAMAGE_NONE,
MOD_SLIME);
}
}
if (!(ent->flags & FL_INWATER))
{
if (ent->watertype & CONTENTS_LAVA)
{
if ((ent->svflags & SVF_MONSTER) && ent->health > 0)
{
if (frandom() <= 0.5f)
gi.sound(ent, CHAN_BODY, gi.soundindex("player/lava1.wav"), 1, ATTN_NORM, 0);
else
gi.sound(ent, CHAN_BODY, gi.soundindex("player/lava2.wav"), 1, ATTN_NORM, 0);
}
else
gi.sound(ent, CHAN_BODY, gi.soundindex("player/watr_in.wav"), 1, ATTN_NORM, 0);
}
else if (ent->watertype & CONTENTS_SLIME)
gi.sound(ent, CHAN_BODY, gi.soundindex("player/watr_in.wav"), 1, ATTN_NORM, 0);
else if (ent->watertype & CONTENTS_WATER)
gi.sound(ent, CHAN_BODY, gi.soundindex("player/watr_in.wav"), 1, ATTN_NORM, 0);
ent->flags |= FL_INWATER;
ent->damage_debounce_time = 0_ms;
}
}
}
bool M_droptofloor_generic(vec3_t &origin, const vec3_t &mins, const vec3_t &maxs, bool ceiling, edict_t *ignore, contents_t mask, bool allow_partial)
{
vec3_t end;
trace_t trace;
// PGM
if (gi.trace(origin, mins, maxs, origin, ignore, mask).startsolid)
{
if (!ceiling)
origin[2] += 1;
else
origin[2] -= 1;
}
if (!ceiling)
{
end = origin;
end[2] -= 256;
}
else
{
end = origin;
end[2] += 256;
}
// PGM
trace = gi.trace(origin, mins, maxs, end, ignore, mask);
if (trace.fraction == 1 || trace.allsolid || (!allow_partial && trace.startsolid))
return false;
origin = trace.endpos;
return true;
}
bool M_droptofloor(edict_t *ent)
{
contents_t mask = G_GetClipMask(ent);
if (!ent->spawnflags.has(SPAWNFLAG_MONSTER_NO_DROP))
{
if (!M_droptofloor_generic(ent->s.origin, ent->mins, ent->maxs, ent->gravityVector[2] > 0, ent, mask, true))
return false;
}
else
{
if (gi.trace(ent->s.origin, ent->mins, ent->maxs, ent->s.origin, ent, mask).startsolid)
return false;
}
gi.linkentity(ent);
M_CheckGround(ent, mask);
M_CatagorizePosition(ent, ent->s.origin, ent->waterlevel, ent->watertype);
return true;
}
void M_SetEffects(edict_t *ent)
{
ent->s.effects &= ~(EF_COLOR_SHELL | EF_POWERSCREEN | EF_DOUBLE | EF_QUAD | EF_PENT | EF_FLIES);
ent->s.renderfx &= ~(RF_SHELL_RED | RF_SHELL_GREEN | RF_SHELL_BLUE | RF_SHELL_DOUBLE);
ent->s.sound = 0;
ent->s.loop_attenuation = 0;
// we're gibbed
if (ent->s.renderfx & RF_LOW_PRIORITY)
return;
if (ent->monsterinfo.weapon_sound && ent->health > 0)
{
ent->s.sound = ent->monsterinfo.weapon_sound;
ent->s.loop_attenuation = ATTN_NORM;
}
else if (ent->monsterinfo.engine_sound)
ent->s.sound = ent->monsterinfo.engine_sound;
if (ent->monsterinfo.aiflags & AI_RESURRECTING)
{
ent->s.effects |= EF_COLOR_SHELL;
ent->s.renderfx |= RF_SHELL_RED;
}
ent->s.renderfx |= RF_DOT_SHADOW;
// no power armor/powerup effects if we died
if (ent->health <= 0)
return;
if (ent->powerarmor_time > level.time)
{
if (ent->monsterinfo.power_armor_type == IT_ITEM_POWER_SCREEN)
{
ent->s.effects |= EF_POWERSCREEN;
}
else if (ent->monsterinfo.power_armor_type == IT_ITEM_POWER_SHIELD)
{
ent->s.effects |= EF_COLOR_SHELL;
ent->s.renderfx |= RF_SHELL_GREEN;
}
}
// PMM - new monster powerups
if (ent->monsterinfo.quad_time > level.time)
{
if (G_PowerUpExpiring(ent->monsterinfo.quad_time))
ent->s.effects |= EF_QUAD;
}
if (ent->monsterinfo.double_time > level.time)
{
if (G_PowerUpExpiring(ent->monsterinfo.double_time))
ent->s.effects |= EF_DOUBLE;
}
if (ent->monsterinfo.invincible_time > level.time)
{
if (G_PowerUpExpiring(ent->monsterinfo.invincible_time))
ent->s.effects |= EF_PENT;
}
}
bool M_AllowSpawn( edict_t * self ) {
if ( deathmatch->integer && !ai_allow_dm_spawn->integer ) {
return false;
}
return true;
}
void M_SetAnimation(edict_t *self, const save_mmove_t &move, bool instant)
{
// [Paril-KEX] free the beams if we switch animations.
if (self->beam)
{
G_FreeEdict(self->beam);
self->beam = nullptr;
}
if (self->beam2)
{
G_FreeEdict(self->beam2);
self->beam2 = nullptr;
}
// instant switches will cause active_move to change on the next frame
if (instant)
{
self->monsterinfo.active_move = move;
self->monsterinfo.next_move = nullptr;
return;
}
// these wait until the frame is ready to be finished
self->monsterinfo.next_move = move;
}
void M_MoveFrame(edict_t *self)
{
const mmove_t *move = self->monsterinfo.active_move.pointer();
// [Paril-KEX] high tick rate adjustments;
// monsters still only step frames and run thinkfunc's at
// 10hz, but will run aifuncs at full speed with
// distance spread over 10hz
self->nextthink = level.time + FRAME_TIME_S;
// time to run next 10hz move yet?
bool run_frame = self->monsterinfo.next_move_time <= level.time;
// we asked nicely to switch frames when the timer ran up
if (run_frame && self->monsterinfo.next_move.pointer() && self->monsterinfo.active_move != self->monsterinfo.next_move)
{
M_SetAnimation(self, self->monsterinfo.next_move, true);
move = self->monsterinfo.active_move.pointer();
}
if (!move)
return;
// no, but maybe we were explicitly forced into another move (pain,
// death, etc)
if (!run_frame)
run_frame = (self->s.frame < move->firstframe || self->s.frame > move->lastframe);
if (run_frame)
{
// [Paril-KEX] allow next_move and nextframe to work properly after an endfunc
bool explicit_frame = false;
if ((self->monsterinfo.nextframe) && (self->monsterinfo.nextframe >= move->firstframe) &&
(self->monsterinfo.nextframe <= move->lastframe))
{
self->s.frame = self->monsterinfo.nextframe;
self->monsterinfo.nextframe = 0;
}
else
{
if (self->s.frame == move->lastframe)
{
if (move->endfunc)
{
move->endfunc(self);
if (self->monsterinfo.next_move)
{
M_SetAnimation(self, self->monsterinfo.next_move, true);
if (self->monsterinfo.nextframe)
{
self->s.frame = self->monsterinfo.nextframe;
self->monsterinfo.nextframe = 0;
explicit_frame = true;
}
}
// regrab move, endfunc is very likely to change it
move = self->monsterinfo.active_move.pointer();
// check for death
if (self->svflags & SVF_DEADMONSTER)
return;
}
}
if (self->s.frame < move->firstframe || self->s.frame > move->lastframe)
{
self->monsterinfo.aiflags &= ~AI_HOLD_FRAME;
self->s.frame = move->firstframe;
}
else if (!explicit_frame)
{
if (!(self->monsterinfo.aiflags & AI_HOLD_FRAME))
{
self->s.frame++;
if (self->s.frame > move->lastframe)
self->s.frame = move->firstframe;
}
}
}
if (self->monsterinfo.aiflags & AI_HIGH_TICK_RATE)
self->monsterinfo.next_move_time = level.time;
else
self->monsterinfo.next_move_time = level.time + 10_hz;
if ((self->monsterinfo.nextframe) && !((self->monsterinfo.nextframe >= move->firstframe) &&
(self->monsterinfo.nextframe <= move->lastframe)))
self->monsterinfo.nextframe = 0;
}
// NB: frame thinkfunc can be called on the same frame
// as the animation changing
int32_t index = self->s.frame - move->firstframe;
if (move->frame[index].aifunc)
{
if (!(self->monsterinfo.aiflags & AI_HOLD_FRAME))
{
float dist = move->frame[index].dist * self->monsterinfo.scale;
dist /= gi.tick_rate / 10;
move->frame[index].aifunc(self, dist);
}
else
move->frame[index].aifunc(self, 0);
}
if (run_frame && move->frame[index].thinkfunc)
move->frame[index].thinkfunc(self);
if (move->frame[index].lerp_frame != -1)
{
self->s.renderfx |= RF_OLD_FRAME_LERP;
self->s.old_frame = move->frame[index].lerp_frame;
}
}
void G_MonsterKilled(edict_t *self)
{
level.killed_monsters++;
if (coop->integer && self->enemy && self->enemy->client)
self->enemy->client->resp.score++;
if (g_debug_monster_kills->integer)
{
bool found = false;
for (auto &ent : level.monsters_registered)
{
if (ent == self)
{
ent = nullptr;
found = true;
break;
}
}
if (!found)
{
#if defined(_DEBUG) && defined(KEX_PLATFORM_WINPC)
__debugbreak();
#endif
gi.Center_Print(&g_edicts[1], "found missing monster?");
}
if (level.killed_monsters == level.total_monsters)
{
gi.Center_Print(&g_edicts[1], "all monsters dead");
}
}
}
void M_ProcessPain(edict_t *e)
{
if (!e->monsterinfo.damage_blood)
return;
if (e->health <= 0)
{
// ROGUE
if (e->monsterinfo.aiflags & AI_MEDIC)
{
if (e->enemy && e->enemy->inuse && (e->enemy->svflags & SVF_MONSTER)) // god, I hope so
{
cleanupHealTarget(e->enemy);
}
// clean up self
e->monsterinfo.aiflags &= ~AI_MEDIC;
}
// ROGUE
if (!e->deadflag)
{
e->enemy = e->monsterinfo.damage_attacker;
// ROGUE
// ROGUE - free up slot for spawned monster if it's spawned
if (e->monsterinfo.aiflags & AI_SPAWNED_CARRIER)
{
if (e->monsterinfo.commander && e->monsterinfo.commander->inuse &&
!strcmp(e->monsterinfo.commander->classname, "monster_carrier"))
e->monsterinfo.commander->monsterinfo.monster_slots++;
e->monsterinfo.commander = nullptr;
}
if (e->monsterinfo.aiflags & AI_SPAWNED_WIDOW)
{
// need to check this because we can have variable numbers of coop players
if (e->monsterinfo.commander && e->monsterinfo.commander->inuse &&
!strncmp(e->monsterinfo.commander->classname, "monster_widow", 13))
{
if (e->monsterinfo.commander->monsterinfo.monster_used > 0)
e->monsterinfo.commander->monsterinfo.monster_used--;
e->monsterinfo.commander = nullptr;
}
}
if (!(e->monsterinfo.aiflags & AI_DO_NOT_COUNT) && !(e->spawnflags & SPAWNFLAG_MONSTER_DEAD))
G_MonsterKilled(e);
e->touch = nullptr;
monster_death_use(e);
}
e->die(e, e->monsterinfo.damage_inflictor, e->monsterinfo.damage_attacker, e->monsterinfo.damage_blood, e->monsterinfo.damage_from, e->monsterinfo.damage_mod);
// [Paril-KEX] medic commander only gets his slots back after the monster is gibbed, since we can revive them
if (e->health <= e->gib_health)
{
if (e->monsterinfo.aiflags & AI_SPAWNED_MEDIC_C)
{
if (e->monsterinfo.commander && e->monsterinfo.commander->inuse && !strcmp(e->monsterinfo.commander->classname, "monster_medic_commander"))
e->monsterinfo.commander->monsterinfo.monster_used -= e->monsterinfo.monster_slots;
e->monsterinfo.commander = nullptr;
}
}
if (e->inuse && e->health > e->gib_health && e->s.frame == e->monsterinfo.active_move->lastframe)
{
e->s.frame -= irandom(1, 3);
if (e->groundentity && e->movetype == MOVETYPE_TOSS && !(e->flags & FL_STATIONARY))
e->s.angles[1] += brandom() ? 4.5f : -4.5f;
}
}
else
e->pain(e, e->monsterinfo.damage_attacker, (float) e->monsterinfo.damage_knockback, e->monsterinfo.damage_blood, e->monsterinfo.damage_mod);
if (!e->inuse)
return;
if (e->monsterinfo.setskin)
e->monsterinfo.setskin(e);
e->monsterinfo.damage_blood = 0;
e->monsterinfo.damage_knockback = 0;
e->monsterinfo.damage_attacker = e->monsterinfo.damage_inflictor = nullptr;
// [Paril-KEX] fire health target
if (e->healthtarget)
{
const char *target = e->target;
e->target = e->healthtarget;
G_UseTargets(e, e->enemy);
e->target = target;
}
}
//
// Monster utility functions
//
THINK(monster_dead_think) (edict_t *self) -> void
{
// flies
if ((self->monsterinfo.aiflags & AI_STINKY) && !(self->monsterinfo.aiflags & AI_STUNK))
{
if (!self->fly_sound_debounce_time)
self->fly_sound_debounce_time = level.time + random_time(5_sec, 15_sec);
else if (self->fly_sound_debounce_time < level.time)
{
if (!self->s.sound)
{
self->s.effects |= EF_FLIES;
self->s.sound = gi.soundindex("infantry/inflies1.wav");
self->fly_sound_debounce_time = level.time + 60_sec;
}
else
{
self->s.effects &= ~EF_FLIES;
self->s.sound = 0;
self->monsterinfo.aiflags |= AI_STUNK;
}
}
}
if (!self->monsterinfo.damage_blood)
{
if (self->s.frame != self->monsterinfo.active_move->lastframe)
self->s.frame++;
}
self->nextthink = level.time + 10_hz;
}
void monster_dead(edict_t *self)
{
self->think = monster_dead_think;
self->nextthink = level.time + 10_hz;
self->movetype = MOVETYPE_TOSS;
self->svflags |= SVF_DEADMONSTER;
self->monsterinfo.damage_blood = 0;
self->fly_sound_debounce_time = 0_ms;
self->monsterinfo.aiflags &= ~AI_STUNK;
gi.linkentity(self);
}
static BoxEdictsResult_t M_CheckDodge_BoxEdictsFilter(edict_t *ent, void *data)
{
edict_t *self = (edict_t *) data;
// not a valid projectile
if (!(ent->svflags & SVF_PROJECTILE) || !(ent->flags & FL_DODGE))
return BoxEdictsResult_t::Skip;
// not moving
if (ent->velocity.lengthSquared() < 16.f)
return BoxEdictsResult_t::Skip;
// projectile is behind us, we can't see it
if (!infront(self, ent))
return BoxEdictsResult_t::Skip;
// will it hit us within 1 second? gives us enough time to dodge
trace_t tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, ent->s.origin + ent->velocity, ent, ent->clipmask);
if (tr.ent == self)
{
vec3_t v = tr.endpos - ent->s.origin;
gtime_t eta = gtime_t::from_sec(v.length() / ent->velocity.length());
self->monsterinfo.dodge(self, ent->owner, eta, &tr, (ent->movetype == MOVETYPE_BOUNCE || ent->movetype == MOVETYPE_TOSS));
return BoxEdictsResult_t::End;
}
return BoxEdictsResult_t::Skip;
}
// [Paril-KEX] active checking for projectiles to dodge
static void M_CheckDodge(edict_t *self)
{
// we recently made a valid dodge, don't try again for a bit
if (self->monsterinfo.dodge_time > level.time)
return;
gi.BoxEdicts(self->absmin - vec3_t{512, 512, 512}, self->absmax + vec3_t{512, 512, 512}, nullptr, 0, AREA_SOLID, M_CheckDodge_BoxEdictsFilter, self);
}
static bool CheckPathVisibility(const vec3_t &start, const vec3_t &end)
{
trace_t tr = gi.traceline(start, end, nullptr, MASK_SOLID | CONTENTS_PROJECTILECLIP | CONTENTS_MONSTERCLIP | CONTENTS_PLAYERCLIP);
bool valid = tr.fraction == 1.0f;
if (!valid)
{
// try raising some of the points
bool can_raise_start = false, can_raise_end = false;
vec3_t raised_start = start + vec3_t{0.f, 0.f, 16.f};
vec3_t raised_end = end + vec3_t{0.f, 0.f, 16.f};
if (gi.traceline(start, raised_start, nullptr, MASK_SOLID | CONTENTS_PROJECTILECLIP | CONTENTS_MONSTERCLIP | CONTENTS_PLAYERCLIP).fraction == 1.0f)
can_raise_start = true;
if (gi.traceline(end, raised_end, nullptr, MASK_SOLID | CONTENTS_PROJECTILECLIP | CONTENTS_MONSTERCLIP | CONTENTS_PLAYERCLIP).fraction == 1.0f)
can_raise_end = true;
// try raised start -> end
if (can_raise_start)
{
tr = gi.traceline(raised_start, end, nullptr, MASK_SOLID | CONTENTS_PROJECTILECLIP | CONTENTS_MONSTERCLIP | CONTENTS_PLAYERCLIP);
if (tr.fraction == 1.0f)
return true;
}
// try start -> raised end
if (can_raise_end)
{
tr = gi.traceline(start, raised_end, nullptr, MASK_SOLID | CONTENTS_PROJECTILECLIP | CONTENTS_MONSTERCLIP | CONTENTS_PLAYERCLIP);
if (tr.fraction == 1.0f)
return true;
}
// try both raised
if (can_raise_start && can_raise_end)
{
tr = gi.traceline(raised_start, raised_end, nullptr, MASK_SOLID | CONTENTS_PROJECTILECLIP | CONTENTS_MONSTERCLIP | CONTENTS_PLAYERCLIP);
if (tr.fraction == 1.0f)
return true;
}
//gi.Draw_Line(start, end, rgba_red, 0.1f, false);
}
return valid;
}
THINK(monster_think) (edict_t *self) -> void
{
// [Paril-KEX] monster sniff testing; if we can make an unobstructed path to the player, murder ourselves.
if (g_debug_monster_kills->integer)
{
if (g_edicts[1].inuse)
{
trace_t enemy_trace = gi.traceline(self->s.origin, g_edicts[1].s.origin, self, MASK_SHOT);
if (enemy_trace.fraction < 1.0f && enemy_trace.ent == &g_edicts[1])
T_Damage(self, &g_edicts[1], &g_edicts[1], { 0, 0, -1 }, self->s.origin, { 0, 0, -1 }, 9999, 9999, DAMAGE_NO_PROTECTION, MOD_BFG_BLAST);
else
{
static vec3_t points[64];
if (self->disintegrator_time <= level.time)
{
PathRequest request;
request.goal = g_edicts[1].s.origin;
request.moveDist = 4.0f;
request.nodeSearch.ignoreNodeFlags = true;
request.nodeSearch.radius = 9999;
request.pathFlags = PathFlags::All;
request.start = self->s.origin;
request.traversals.dropHeight = 9999;
request.traversals.jumpHeight = 9999;
request.pathPoints.array = points;
request.pathPoints.count = q_countof(points);
PathInfo info;
if (gi.GetPathToGoal(request, info))
{
if (info.returnCode != PathReturnCode::NoStartNode &&
info.returnCode != PathReturnCode::NoGoalNode &&
info.returnCode != PathReturnCode::NoPathFound &&
info.returnCode != PathReturnCode::NoNavAvailable &&
info.numPathPoints < q_countof(points))
{
if (CheckPathVisibility(g_edicts[1].s.origin + vec3_t { 0.f, 0.f, g_edicts[1].mins.z }, points[info.numPathPoints - 1]) &&
CheckPathVisibility(self->s.origin + vec3_t { 0.f, 0.f, self->mins.z }, points[0]))
{
size_t i = 0;
for (; i < info.numPathPoints - 1; i++)
if (!CheckPathVisibility(points[i], points[i + 1]))
break;
if (i == info.numPathPoints - 1)
T_Damage(self, &g_edicts[1], &g_edicts[1], { 0, 0, 1 }, self->s.origin, { 0, 0, 1 }, 9999, 9999, DAMAGE_NO_PROTECTION, MOD_BFG_BLAST);
else
self->disintegrator_time = level.time + 500_ms;
}
else
self->disintegrator_time = level.time + 500_ms;
}
else
{
self->disintegrator_time = level.time + 1_sec;
}
}
else
{
self->disintegrator_time = level.time + 1_sec;
}
}
}
if (!self->deadflag && !(self->monsterinfo.aiflags & AI_DO_NOT_COUNT))
gi.Draw_Bounds(self->absmin, self->absmax, rgba_red, gi.frame_time_s, false);
}
}
self->s.renderfx &= ~(RF_STAIR_STEP | RF_OLD_FRAME_LERP);
M_ProcessPain(self);
// pain/die above freed us
if (!self->inuse || self->think != monster_think)
return;
if (self->hackflags & HACKFLAG_ATTACK_PLAYER)
{
if (!self->enemy && g_edicts[1].inuse)
{
self->enemy = &g_edicts[1];
FoundTarget(self);
}
}
if (self->health > 0 && self->monsterinfo.dodge && !(globals.server_flags & SERVER_FLAG_LOADING))
M_CheckDodge(self);
M_MoveFrame(self);
if (self->linkcount != self->monsterinfo.linkcount)
{
self->monsterinfo.linkcount = self->linkcount;
M_CheckGround(self, G_GetClipMask(self));
}
M_CatagorizePosition(self, self->s.origin, self->waterlevel, self->watertype);
M_WorldEffects(self);
M_SetEffects(self);
}
/*
================
monster_use
Using a monster makes it angry at the current activator
================
*/
USE(monster_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
if (self->enemy)
return;
if (self->health <= 0)
return;
if (!activator)
return;
if (activator->flags & FL_NOTARGET)
return;
if (!(activator->client) && !(activator->monsterinfo.aiflags & AI_GOOD_GUY))
return;
if (activator->flags & FL_DISGUISED) // PGM
return; // PGM
// delay reaction so if the monster is teleported, its sound is still heard
self->enemy = activator;
FoundTarget(self);
}
void monster_start_go(edict_t *self);
THINK(monster_triggered_spawn) (edict_t *self) -> void
{
self->s.origin[2] += 1;
self->solid = SOLID_BBOX;
self->movetype = MOVETYPE_STEP;
self->svflags &= ~SVF_NOCLIENT;
self->air_finished = level.time + 12_sec;
gi.linkentity(self);
KillBox(self, false);
monster_start_go(self);
// RAFAEL
if (strcmp(self->classname, "monster_fixbot") == 0)
{
if (self->spawnflags.has(SPAWNFLAG_FIXBOT_LANDING | SPAWNFLAG_FIXBOT_TAKEOFF | SPAWNFLAG_FIXBOT_FIXIT))
{
self->enemy = nullptr;
return;
}
}
// RAFAEL
if (self->enemy && !(self->spawnflags & SPAWNFLAG_MONSTER_AMBUSH) && !(self->enemy->flags & FL_NOTARGET) && !(self->monsterinfo.aiflags & AI_GOOD_GUY))
{
// ROGUE
if (!(self->enemy->flags & FL_DISGUISED))
// ROGUE
FoundTarget(self);
// ROGUE
else // PMM - just in case, make sure to clear the enemy so FindTarget doesn't get confused
self->enemy = nullptr;
// ROGUE
}
else
{
self->enemy = nullptr;
}
}
USE(monster_triggered_spawn_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
// we have a one frame delay here so we don't telefrag the guy who activated us
self->think = monster_triggered_spawn;
self->nextthink = level.time + FRAME_TIME_S;
if (activator->client && !(self->hackflags & HACKFLAG_END_CUTSCENE))
self->enemy = activator;
self->use = monster_use;
if (self->spawnflags.has(SPAWNFLAG_MONSTER_SCENIC))
{
M_droptofloor(self);
self->nextthink = 0_ms;
self->think(self);
if (self->spawnflags.has(SPAWNFLAG_MONSTER_AMBUSH))
monster_use(self, other, activator);
for (int i = 0; i < 30; i++)
{
self->think(self);
self->monsterinfo.next_move_time = 0_ms;
}
}
}
THINK(monster_triggered_think) (edict_t *self) -> void
{
if (!(self->monsterinfo.aiflags & AI_DO_NOT_COUNT))
gi.Draw_Bounds(self->absmin, self->absmax, rgba_blue, gi.frame_time_s, false);
self->nextthink = level.time + 1_ms;
}
void monster_triggered_start(edict_t *self)
{
self->solid = SOLID_NOT;
self->movetype = MOVETYPE_NONE;
self->svflags |= SVF_NOCLIENT;
self->nextthink = 0_ms;
self->use = monster_triggered_spawn_use;
if (g_debug_monster_kills->integer)
{
self->think = monster_triggered_think;
self->nextthink = level.time + 1_ms;
}
if (!self->targetname ||
(G_FindByString<&edict_t::target>(nullptr, self->targetname) == nullptr &&
G_FindByString<&edict_t::pathtarget>(nullptr, self->targetname) == nullptr &&
G_FindByString<&edict_t::deathtarget>(nullptr, self->targetname) == nullptr &&
G_FindByString<&edict_t::itemtarget>(nullptr, self->targetname) == nullptr &&
G_FindByString<&edict_t::healthtarget>(nullptr, self->targetname) == nullptr &&
G_FindByString<&edict_t::combattarget>(nullptr, self->targetname) == nullptr))
{
gi.Com_PrintFmt("{}: is trigger spawned, but has no targetname or no entity to spawn it\n", *self);
}
}
/*
================
monster_death_use
When a monster dies, it fires all of its targets with the current
enemy as activator.
================
*/
void monster_death_use(edict_t *self)
{
self->flags &= ~(FL_FLY | FL_SWIM);
self->monsterinfo.aiflags &= (AI_DOUBLE_TROUBLE | AI_GOOD_GUY | AI_STINKY | AI_SPAWNED_MASK);
if (self->item)
{
edict_t *dropped = Drop_Item(self, self->item);
if (self->itemtarget)
{
dropped->target = self->itemtarget;
self->itemtarget = nullptr;
}
self->item = nullptr;
}
if (self->deathtarget)
self->target = self->deathtarget;
if (self->target)
G_UseTargets(self, self->enemy);
// [Paril-KEX] fire health target
if (self->healthtarget)
{
self->target = self->healthtarget;
G_UseTargets(self, self->enemy);
}
}
// [Paril-KEX] adjust the monster's health from how
// many active players we have
void G_Monster_ScaleCoopHealth(edict_t *self)
{
// already scaled
if (self->monsterinfo.health_scaling >= level.coop_scale_players)
return;
// this is just to fix monsters that change health after spawning...
// looking at you, soldiers
if (!self->monsterinfo.base_health)
self->monsterinfo.base_health = self->max_health;
int32_t delta = level.coop_scale_players - self->monsterinfo.health_scaling;
int32_t additional_health = delta * (int32_t) (self->monsterinfo.base_health * level.coop_health_scaling);
self->health = max(1, self->health + additional_health);
self->max_health += additional_health;
self->monsterinfo.health_scaling = level.coop_scale_players;
}
struct monster_filter_t
{
inline bool operator()(edict_t *self) const
{
return self->inuse && (self->flags & FL_COOP_HEALTH_SCALE) && self->health > 0;
}
};
// check all active monsters' scaling
void G_Monster_CheckCoopHealthScaling()
{
for (auto monster : entity_iterable_t<monster_filter_t>())
G_Monster_ScaleCoopHealth(monster);
}
//============================================================================
constexpr spawnflags_t SPAWNFLAG_MONSTER_FUBAR = 4_spawnflag;
bool monster_start(edict_t *self)
{
if ( !M_AllowSpawn( self ) ) {
G_FreeEdict( self );
return false;
}
if (self->spawnflags.has(SPAWNFLAG_MONSTER_SCENIC))
self->monsterinfo.aiflags |= AI_GOOD_GUY;
// [Paril-KEX] n64
if (self->hackflags & (HACKFLAG_END_CUTSCENE | HACKFLAG_ATTACK_PLAYER))
self->monsterinfo.aiflags |= AI_DO_NOT_COUNT;
if (self->spawnflags.has(SPAWNFLAG_MONSTER_FUBAR) && !(self->monsterinfo.aiflags & AI_GOOD_GUY))
{
self->spawnflags &= ~SPAWNFLAG_MONSTER_FUBAR;
self->spawnflags |= SPAWNFLAG_MONSTER_AMBUSH;
}
// [Paril-KEX] simplify other checks
if (self->monsterinfo.aiflags & AI_GOOD_GUY)
self->monsterinfo.aiflags |= AI_DO_NOT_COUNT;
// ROGUE
if (!(self->monsterinfo.aiflags & AI_DO_NOT_COUNT) && !self->spawnflags.has(SPAWNFLAG_MONSTER_DEAD))
{
if (g_debug_monster_kills->integer)
level.monsters_registered[level.total_monsters] = self;
// ROGUE
level.total_monsters++;
}
self->nextthink = level.time + FRAME_TIME_S;
self->svflags |= SVF_MONSTER;
self->takedamage = true;
self->air_finished = level.time + 12_sec;
self->use = monster_use;
self->max_health = self->health;
self->clipmask = MASK_MONSTERSOLID;
self->deadflag = false;
self->svflags &= ~SVF_DEADMONSTER;
self->flags &= ~FL_ALIVE_KNOCKBACK_ONLY;
self->flags |= FL_COOP_HEALTH_SCALE;
self->s.old_origin = self->s.origin;
self->monsterinfo.initial_power_armor_type = self->monsterinfo.power_armor_type;
self->monsterinfo.max_power_armor_power = self->monsterinfo.power_armor_power;
if (!self->monsterinfo.checkattack)
self->monsterinfo.checkattack = M_CheckAttack;
if ( ai_model_scale->value > 0 ) {
self->s.scale = ai_model_scale->value;
}
if (self->s.scale)
{
self->monsterinfo.scale *= self->s.scale;
self->mins *= self->s.scale;
self->maxs *= self->s.scale;
self->mass *= self->s.scale;
}
// set combat style if unset
if (self->monsterinfo.combat_style == COMBAT_UNKNOWN)
{
if (!self->monsterinfo.attack && self->monsterinfo.melee)
self->monsterinfo.combat_style = COMBAT_MELEE;
else
self->monsterinfo.combat_style = COMBAT_MIXED;
}
if (st.item)
{
self->item = FindItemByClassname(st.item);
if (!self->item)
gi.Com_PrintFmt("{}: bad item: {}\n", *self, st.item);
}
// randomize what frame they start on
if (self->monsterinfo.active_move)
self->s.frame =
irandom(self->monsterinfo.active_move->firstframe, self->monsterinfo.active_move->lastframe + 1);
// PMM - get this so I don't have to do it in all of the monsters
self->monsterinfo.base_height = self->maxs[2];
// Paril: monsters' old default viewheight (25)
// is all messed up for certain monsters. Calculate
// from maxs to make a bit more sense.
if (!self->viewheight)
self->viewheight = (int) (self->maxs[2] - 8.f);
// PMM - clear these
self->monsterinfo.quad_time = 0_ms;
self->monsterinfo.double_time = 0_ms;
self->monsterinfo.invincible_time = 0_ms;
// set base health & set base scaling to 1 player
self->monsterinfo.base_health = self->health;
self->monsterinfo.health_scaling = 1;
// [Paril-KEX] co-op health scale
G_Monster_ScaleCoopHealth(self);
return true;
}
stuck_result_t G_FixStuckObject(edict_t *self, vec3_t check)
{
contents_t mask = G_GetClipMask(self);
stuck_result_t result = G_FixStuckObject_Generic(check, self->mins, self->maxs, [self, mask] (const vec3_t &start, const vec3_t &mins, const vec3_t &maxs, const vec3_t &end) {
return gi.trace(start, mins, maxs, end, self, mask);
});
if (result == stuck_result_t::NO_GOOD_POSITION)
return result;
self->s.origin = check;
if (result == stuck_result_t::FIXED)
gi.Com_PrintFmt("fixed stuck {}\n", *self);
return result;
}
void monster_start_go(edict_t *self)
{
// Paril: moved here so this applies to swim/fly monsters too
if (!(self->flags & FL_STATIONARY))
{
const vec3_t check = self->s.origin;
// [Paril-KEX] different nudge method; see if any of the bbox sides are clear,
// if so we can see how much headroom we have in that direction and shift us.
// most of the monsters stuck in solids will only be stuck on one side, which
// conveniently leaves only one side not in a solid; this won't fix monsters
// stuck in a corner though.
bool is_stuck = false;
if ((self->monsterinfo.aiflags & AI_GOOD_GUY) || (self->flags & (FL_FLY | FL_SWIM)))
is_stuck = gi.trace(self->s.origin, self->mins, self->maxs, self->s.origin, self, MASK_MONSTERSOLID).startsolid;
else
is_stuck = !M_droptofloor(self) || !M_walkmove(self, 0, 0);
if (is_stuck)
{
if (G_FixStuckObject(self, check) != stuck_result_t::NO_GOOD_POSITION)
{
if (self->monsterinfo.aiflags & AI_GOOD_GUY)
is_stuck = gi.trace(self->s.origin, self->mins, self->maxs, self->s.origin, self, MASK_MONSTERSOLID).startsolid;
else if (!(self->flags & (FL_FLY | FL_SWIM)))
M_droptofloor(self);
is_stuck = false;
}
}
// last ditch effort: brute force
if (is_stuck)
{
// Paril: try nudging them out. this fixes monsters stuck
// in very shallow slopes.
constexpr const int32_t adjust[] = { 0, -1, 1, -2, 2, -4, 4, -8, 8 };
bool walked = false;
for (int32_t y = 0; !walked && y < 3; y++)
for (int32_t x = 0; !walked && x < 3; x++)
for (int32_t z = 0; !walked && z < 3; z++)
{
self->s.origin[0] = check[0] + adjust[x];
self->s.origin[1] = check[1] + adjust[y];
self->s.origin[2] = check[2] + adjust[z];
if (self->monsterinfo.aiflags & AI_GOOD_GUY)
{
is_stuck = gi.trace(self->s.origin, self->mins, self->maxs, self->s.origin, self, MASK_MONSTERSOLID).startsolid;
if (!is_stuck)
walked = true;
}
else if (!(self->flags & (FL_FLY | FL_SWIM)))
{
M_droptofloor(self);
walked = M_walkmove(self, 0, 0);
}
}
}
if (is_stuck)
gi.Com_PrintFmt("WARNING: {} stuck in solid\n", *self);
}
vec3_t v;
if (self->health <= 0)
return;
self->s.old_origin = self->s.origin;
// check for target to combat_point and change to combattarget
if (self->target)
{
bool notcombat;
bool fixup;
edict_t *target;
target = nullptr;
notcombat = false;
fixup = false;
while ((target = G_FindByString<&edict_t::targetname>(target, self->target)) != nullptr)
{
if (strcmp(target->classname, "point_combat") == 0)
{
self->combattarget = self->target;
fixup = true;
}
else
{
notcombat = true;
}
}
if (notcombat && self->combattarget)
gi.Com_PrintFmt("{}: has target with mixed types\n", *self);
if (fixup)
self->target = nullptr;
}
// validate combattarget
if (self->combattarget)
{
edict_t *target;
target = nullptr;
while ((target = G_FindByString<&edict_t::targetname>(target, self->combattarget)) != nullptr)
{
if (strcmp(target->classname, "point_combat") != 0)
{
gi.Com_PrintFmt("{} has a bad combattarget {} ({})\n", *self, self->combattarget, *target);
}
}
}
// allow spawning dead
bool spawn_dead = self->spawnflags.has(SPAWNFLAG_MONSTER_DEAD);
if (self->target)
{
self->goalentity = self->movetarget = G_PickTarget(self->target);
if (!self->movetarget)
{
gi.Com_PrintFmt("{}: can't find target {}\n", *self, self->target);
self->target = nullptr;
self->monsterinfo.pausetime = HOLD_FOREVER;
if (!spawn_dead)
self->monsterinfo.stand(self);
}
else if (strcmp(self->movetarget->classname, "path_corner") == 0)
{
v = self->goalentity->s.origin - self->s.origin;
self->ideal_yaw = self->s.angles[YAW] = vectoyaw(v);
if (!spawn_dead)
self->monsterinfo.walk(self);
self->target = nullptr;
}
else
{
self->goalentity = self->movetarget = nullptr;
self->monsterinfo.pausetime = HOLD_FOREVER;
if (!spawn_dead)
self->monsterinfo.stand(self);
}
}
else
{
self->monsterinfo.pausetime = HOLD_FOREVER;
if (!spawn_dead)
self->monsterinfo.stand(self);
}
if (spawn_dead)
{
// to spawn dead, we'll mimick them dying naturally
self->health = 0;
vec3_t f = self->s.origin;
if (self->die)
self->die(self, self, self, 0, vec3_origin, MOD_SUICIDE);
if (!self->inuse)
return;
if (self->monsterinfo.setskin)
self->monsterinfo.setskin(self);
self->monsterinfo.aiflags |= AI_SPAWNED_DEAD;
auto move = self->monsterinfo.active_move.pointer();
for (size_t i = move->firstframe; i < move->lastframe; i++)
{
self->s.frame = i;
if (move->frame[i - move->firstframe].thinkfunc)
move->frame[i - move->firstframe].thinkfunc(self);
if (!self->inuse)
return;
}
if (move->endfunc)
move->endfunc(self);
if (!self->inuse)
return;
if (self->monsterinfo.start_frame)
self->s.frame = self->monsterinfo.start_frame;
else
self->s.frame = move->lastframe;
self->s.origin = f;
gi.linkentity(self);
self->monsterinfo.aiflags &= ~AI_SPAWNED_DEAD;
}
else
{
self->think = monster_think;
self->nextthink = level.time + FRAME_TIME_S;
self->monsterinfo.aiflags |= AI_SPAWNED_ALIVE;
}
}
THINK(walkmonster_start_go) (edict_t *self) -> void
{
if (!self->yaw_speed)
self->yaw_speed = 20;
if (self->spawnflags.has(SPAWNFLAG_MONSTER_TRIGGER_SPAWN))
monster_triggered_start(self);
else
monster_start_go(self);
}
void walkmonster_start(edict_t *self)
{
self->think = walkmonster_start_go;
monster_start(self);
}
THINK(flymonster_start_go) (edict_t *self) -> void
{
if (!self->yaw_speed)
self->yaw_speed = 30;
if (self->spawnflags.has(SPAWNFLAG_MONSTER_TRIGGER_SPAWN))
monster_triggered_start(self);
else
monster_start_go(self);
}
void flymonster_start(edict_t *self)
{
self->flags |= FL_FLY;
self->think = flymonster_start_go;
monster_start(self);
}
THINK(swimmonster_start_go) (edict_t *self) -> void
{
if (!self->yaw_speed)
self->yaw_speed = 30;
if (self->spawnflags.has(SPAWNFLAG_MONSTER_TRIGGER_SPAWN))
monster_triggered_start(self);
else
monster_start_go(self);
}
void swimmonster_start(edict_t *self)
{
self->flags |= FL_SWIM;
self->think = swimmonster_start_go;
monster_start(self);
}
USE(trigger_health_relay_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
{
float percent_health = clamp((float) (other->health) / (float) (other->max_health), 0.f, 1.f);
// not ready to trigger yet
if (percent_health > self->speed)
return;
// fire!
G_UseTargets(self, activator);
// kill self
G_FreeEdict(self);
}
/*QUAKED trigger_health_relay (1.0 1.0 0.0) (-8 -8 -8) (8 8 8)
Special type of relay that fires when a linked object is reduced
beyond a certain amount of health.
It will only fire once, and free itself afterwards.
*/
void SP_trigger_health_relay(edict_t *self)
{
if (!self->targetname)
{
gi.Com_PrintFmt("{} missing targetname\n", *self);
G_FreeEdict(self);
return;
}
if (self->speed < 0 || self->speed > 100)
{
gi.Com_PrintFmt("{} has bad \"speed\" (health percentage); must be between 0 and 100, inclusive\n", *self);
G_FreeEdict(self);
return;
}
self->svflags |= SVF_NOCLIENT;
self->use = trigger_health_relay_use;
}