// 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()) 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; }