// Copyright (c) ZeniMax Media Inc. // Licensed under the GNU General Public License 2.0. /* ============================================================================== black widow, part 2 ============================================================================== */ // timestamp used to prevent rapid fire of melee attack #include "../g_local.h" #include "m_rogue_widow2.h" #include "../m_flash.h" static int sound_pain1; static int sound_pain2; static int sound_pain3; static int sound_death; static int sound_search1; static int sound_tentacles_retract; // sqrt(64*64*2) + sqrt(28*28*2) => 130.1 constexpr vec3_t spawnpoints[] = { { 30, 135, 0 }, { 30, -135, 0 } }; constexpr float sweep_angles[] = { -40.0, -32.0, -24.0, -16.0, -8.0, 0.0, 8.0, 16.0, 24.0, 32.0, 40.0 }; constexpr vec3_t stalker_mins = { -28, -28, -18 }; constexpr vec3_t stalker_maxs = { 28, 28, 18 }; bool infront(edict_t *self, edict_t *other); void WidowCalcSlots(edict_t *self); void WidowPowerups(edict_t *self); void widow2_run(edict_t *self); void widow2_dead(edict_t *self); void widow2_attack_beam(edict_t *self); void widow2_reattack_beam(edict_t *self); void widow_start_spawn(edict_t *self); void widow_done_spawn(edict_t *self); void widow2_spawn_check(edict_t *self); void Widow2SaveBeamTarget(edict_t *self); // death stuff void WidowExplode(edict_t *self); void ThrowWidowGibReal(edict_t *self, const char *gibname, int damage, gib_type_t type, const vec3_t *startpos, bool large, int hitsound, bool fade); void ThrowWidowGibSized(edict_t *self, const char *gibname, int damage, gib_type_t type, const vec3_t *startpos, int hitsound, bool fade); void ThrowWidowGibLoc(edict_t *self, const char *gibname, int damage, gib_type_t type, const vec3_t *startpos, bool fade); void WidowExplosion1(edict_t *self); void WidowExplosion2(edict_t *self); void WidowExplosion3(edict_t *self); void WidowExplosion4(edict_t *self); void WidowExplosion5(edict_t *self); void WidowExplosion6(edict_t *self); void WidowExplosion7(edict_t *self); void WidowExplosionLeg(edict_t *self); void ThrowArm1(edict_t *self); void ThrowArm2(edict_t *self); void ClipGibVelocity(edict_t *ent); // end of death stuff // these offsets used by the tongue constexpr vec3_t offsets[] = { { 17.48f, 0.10f, 68.92f }, { 17.47f, 0.29f, 68.91f }, { 17.45f, 0.53f, 68.87f }, { 17.42f, 0.78f, 68.81f }, { 17.39f, 1.02f, 68.75f }, { 17.37f, 1.20f, 68.70f }, { 17.36f, 1.24f, 68.71f }, { 17.37f, 1.21f, 68.72f }, }; void showme(edict_t *self); void pauseme(edict_t *self) { self->monsterinfo.aiflags |= AI_HOLD_FRAME; } MONSTERINFO_SEARCH(widow2_search) (edict_t *self) -> void { if (frandom() < 0.5f) gi.sound(self, CHAN_VOICE, sound_search1, 1, ATTN_NONE, 0); } void Widow2Beam(edict_t *self) { vec3_t forward, right, target; vec3_t start, targ_angles, vec; monster_muzzleflash_id_t flashnum; if ((!self->enemy) || (!self->enemy->inuse)) return; AngleVectors(self->s.angles, forward, right, nullptr); if ((self->s.frame >= FRAME_fireb05) && (self->s.frame <= FRAME_fireb09)) { // regular beam attack Widow2SaveBeamTarget(self); flashnum = static_cast(MZ2_WIDOW2_BEAMER_1 + self->s.frame - FRAME_fireb05); start = G_ProjectSource(self->s.origin, monster_flash_offset[flashnum], forward, right); target = self->pos2; target[2] += self->enemy->viewheight - 10; forward = target - start; forward.normalize(); monster_fire_heatbeam(self, start, forward, vec3_origin, 10, 50, flashnum); } else if ((self->s.frame >= FRAME_spawn04) && (self->s.frame <= FRAME_spawn14)) { // sweep flashnum = static_cast(MZ2_WIDOW2_BEAM_SWEEP_1 + self->s.frame - FRAME_spawn04); start = G_ProjectSource(self->s.origin, monster_flash_offset[flashnum], forward, right); target = self->enemy->s.origin - start; targ_angles = vectoangles(target); vec = self->s.angles; vec[PITCH] += targ_angles[PITCH]; vec[YAW] -= sweep_angles[flashnum - MZ2_WIDOW2_BEAM_SWEEP_1]; AngleVectors(vec, forward, nullptr, nullptr); monster_fire_heatbeam(self, start, forward, vec3_origin, 10, 50, flashnum); } else { Widow2SaveBeamTarget(self); start = G_ProjectSource(self->s.origin, monster_flash_offset[MZ2_WIDOW2_BEAMER_1], forward, right); target = self->pos2; target[2] += self->enemy->viewheight - 10; forward = target - start; forward.normalize(); monster_fire_heatbeam(self, start, forward, vec3_origin, 10, 50, MZ2_WIDOW2_BEAM_SWEEP_1); } } void Widow2Spawn(edict_t *self) { vec3_t f, r, u, offset, startpoint, spawnpoint; edict_t *ent, *designated_enemy; int i; AngleVectors(self->s.angles, f, r, u); for (i = 0; i < 2; i++) { offset = spawnpoints[i]; startpoint = G_ProjectSource2(self->s.origin, offset, f, r, u); if (FindSpawnPoint(startpoint, stalker_mins, stalker_maxs, spawnpoint, 64)) { ent = CreateGroundMonster(spawnpoint, self->s.angles, stalker_mins, stalker_maxs, "monster_stalker", 256); if (!ent) continue; self->monsterinfo.monster_used++; ent->monsterinfo.commander = self; ent->nextthink = level.time; ent->think(ent); ent->monsterinfo.aiflags |= AI_SPAWNED_WIDOW | AI_DO_NOT_COUNT | AI_IGNORE_SHOTS; if (!coop->integer) { designated_enemy = self->enemy; } else { designated_enemy = PickCoopTarget(ent); if (designated_enemy) { // try to avoid using my enemy if (designated_enemy == self->enemy) { designated_enemy = PickCoopTarget(ent); if (!designated_enemy) designated_enemy = self->enemy; } } else designated_enemy = self->enemy; } if ((designated_enemy->inuse) && (designated_enemy->health > 0)) { ent->enemy = designated_enemy; FoundTarget(ent); ent->monsterinfo.attack(ent); } } } } void widow2_spawn_check(edict_t *self) { Widow2Beam(self); Widow2Spawn(self); } void widow2_ready_spawn(edict_t *self) { vec3_t f, r, u, offset, startpoint, spawnpoint; int i; Widow2Beam(self); AngleVectors(self->s.angles, f, r, u); for (i = 0; i < 2; i++) { offset = spawnpoints[i]; startpoint = G_ProjectSource2(self->s.origin, offset, f, r, u); if (FindSpawnPoint(startpoint, stalker_mins, stalker_maxs, spawnpoint, 64)) { float radius = (stalker_maxs - stalker_mins).length() * 0.5f; SpawnGrow_Spawn(spawnpoint + (stalker_mins + stalker_maxs), radius, radius * 2.f); } } } void widow2_step(edict_t *self) { gi.sound(self, CHAN_BODY, gi.soundindex("widow/bwstep1.wav"), 1, ATTN_NORM, 0); } mframe_t widow2_frames_stand[] = { { ai_stand } }; MMOVE_T(widow2_move_stand) = { FRAME_blackwidow3, FRAME_blackwidow3, widow2_frames_stand, nullptr }; mframe_t widow2_frames_walk[] = { { ai_walk, 9.01f, widow2_step }, { ai_walk, 7.55f }, { ai_walk, 7.01f }, { ai_walk, 6.66f }, { ai_walk, 6.20f }, { ai_walk, 5.78f, widow2_step }, { ai_walk, 7.25f }, { ai_walk, 8.37f }, { ai_walk, 10.41f } }; MMOVE_T(widow2_move_walk) = { FRAME_walk01, FRAME_walk09, widow2_frames_walk, nullptr }; mframe_t widow2_frames_run[] = { { ai_run, 9.01f, widow2_step }, { ai_run, 7.55f }, { ai_run, 7.01f }, { ai_run, 6.66f }, { ai_run, 6.20f }, { ai_run, 5.78f, widow2_step }, { ai_run, 7.25f }, { ai_run, 8.37f }, { ai_run, 10.41f } }; MMOVE_T(widow2_move_run) = { FRAME_walk01, FRAME_walk09, widow2_frames_run, nullptr }; mframe_t widow2_frames_attack_pre_beam[] = { { ai_charge, 4 }, { ai_charge, 4, widow2_step }, { ai_charge, 4 }, { ai_charge, 4, widow2_attack_beam } }; MMOVE_T(widow2_move_attack_pre_beam) = { FRAME_fireb01, FRAME_fireb04, widow2_frames_attack_pre_beam, nullptr }; // Loop this mframe_t widow2_frames_attack_beam[] = { { ai_charge, 0, Widow2Beam }, { ai_charge, 0, Widow2Beam }, { ai_charge, 0, Widow2Beam }, { ai_charge, 0, Widow2Beam }, { ai_charge, 0, widow2_reattack_beam } }; MMOVE_T(widow2_move_attack_beam) = { FRAME_fireb05, FRAME_fireb09, widow2_frames_attack_beam, nullptr }; mframe_t widow2_frames_attack_post_beam[] = { { ai_charge, 4 }, { ai_charge, 4 } }; MMOVE_T(widow2_move_attack_post_beam) = { FRAME_fireb06, FRAME_fireb07, widow2_frames_attack_post_beam, widow2_run }; void WidowDisrupt(edict_t *self) { vec3_t start; vec3_t dir; vec3_t forward, right; float len; AngleVectors(self->s.angles, forward, right, nullptr); start = G_ProjectSource(self->s.origin, monster_flash_offset[MZ2_WIDOW_DISRUPTOR], forward, right); dir = self->pos1 - self->enemy->s.origin; len = dir.length(); if (len < 30) { // calc direction to where we targeted dir = self->pos1 - start; dir.normalize(); monster_fire_tracker(self, start, dir, 20, 500, self->enemy, MZ2_WIDOW_DISRUPTOR); } else { PredictAim(self, self->enemy, start, 1200, true, 0, &dir, nullptr); monster_fire_tracker(self, start, dir, 20, 1200, nullptr, MZ2_WIDOW_DISRUPTOR); } widow2_step(self); } void Widow2SaveDisruptLoc(edict_t *self) { if (self->enemy && self->enemy->inuse) { self->pos1 = self->enemy->s.origin; // save for aiming the shot self->pos1[2] += self->enemy->viewheight; } else self->pos1 = {}; } void widow2_disrupt_reattack(edict_t *self) { float luck = frandom(); if (luck < (0.25f + (skill->integer * 0.15f))) self->monsterinfo.nextframe = FRAME_firea01; } mframe_t widow2_frames_attack_disrupt[] = { { ai_charge, 2 }, { ai_charge, 2 }, { ai_charge, 2, Widow2SaveDisruptLoc }, { ai_charge, -20, WidowDisrupt }, { ai_charge, 2 }, { ai_charge, 2 }, { ai_charge, 2, widow2_disrupt_reattack } }; MMOVE_T(widow2_move_attack_disrupt) = { FRAME_firea01, FRAME_firea07, widow2_frames_attack_disrupt, widow2_run }; void Widow2SaveBeamTarget(edict_t *self) { if (self->enemy && self->enemy->inuse) { self->pos2 = self->pos1; self->pos1 = self->enemy->s.origin; // save for aiming the shot } else { self->pos1 = {}; self->pos2 = {}; } } void Widow2BeamTargetRemove(edict_t *self) { self->pos1 = {}; self->pos2 = {}; } void Widow2StartSweep(edict_t *self) { Widow2SaveBeamTarget(self); } mframe_t widow2_frames_spawn[] = { { ai_charge }, { ai_charge }, { ai_charge, 0, [](edict_t *self) { widow_start_spawn(self); widow2_step(self); } }, { ai_charge, 0, Widow2Beam }, { ai_charge, 0, Widow2Beam }, // 5 { ai_charge, 0, Widow2Beam }, { ai_charge, 0, Widow2Beam }, { ai_charge, 0, Widow2Beam }, { ai_charge, 0, Widow2Beam }, { ai_charge, 0, widow2_ready_spawn }, // 10 { ai_charge, 0, Widow2Beam }, { ai_charge, 0, Widow2Beam }, { ai_charge, 0, Widow2Beam }, { ai_charge, 0, widow2_spawn_check }, { ai_charge }, // 15 { ai_charge }, { ai_charge }, { ai_charge, 0, widow2_reattack_beam } }; MMOVE_T(widow2_move_spawn) = { FRAME_spawn01, FRAME_spawn18, widow2_frames_spawn, nullptr }; static bool widow2_tongue_attack_ok(const vec3_t &start, const vec3_t &end, float range) { vec3_t dir, angles; // check for max distance dir = start - end; if (dir.length() > range) return false; // check for min/max pitch angles = vectoangles(dir); if (angles[0] < -180) angles[0] += 360; if (fabsf(angles[0]) > 30) return false; return true; } void Widow2Tongue(edict_t *self) { vec3_t f, r, u; vec3_t start, end, dir; trace_t tr; AngleVectors(self->s.angles, f, r, u); start = G_ProjectSource2(self->s.origin, offsets[self->s.frame - FRAME_tongs01], f, r, u); end = self->enemy->s.origin; if (!widow2_tongue_attack_ok(start, end, 256)) { end[2] = self->enemy->s.origin[2] + self->enemy->maxs[2] - 8; if (!widow2_tongue_attack_ok(start, end, 256)) { end[2] = self->enemy->s.origin[2] + self->enemy->mins[2] + 8; if (!widow2_tongue_attack_ok(start, end, 256)) return; } } end = self->enemy->s.origin; tr = gi.traceline(start, end, self, MASK_PROJECTILE); if (tr.ent != self->enemy) return; gi.sound(self, CHAN_WEAPON, sound_tentacles_retract, 1, ATTN_NORM, 0); gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_PARASITE_ATTACK); gi.WriteEntity(self); gi.WritePosition(start); gi.WritePosition(end); gi.multicast(self->s.origin, MULTICAST_PVS, false); dir = start - end; T_Damage(self->enemy, self, self, dir, self->enemy->s.origin, vec3_origin, 2, 0, DAMAGE_NO_KNOCKBACK, MOD_UNKNOWN); } void Widow2TonguePull(edict_t *self) { vec3_t vec; vec3_t f, r, u; vec3_t start, end; if ((!self->enemy) || (!self->enemy->inuse)) { self->monsterinfo.run(self); return; } AngleVectors(self->s.angles, f, r, u); start = G_ProjectSource2(self->s.origin, offsets[self->s.frame - FRAME_tongs01], f, r, u); end = self->enemy->s.origin; if (!widow2_tongue_attack_ok(start, end, 256)) return; if (self->enemy->groundentity) { self->enemy->s.origin[2] += 1; self->enemy->groundentity = nullptr; // interesting, you don't have to relink the player } vec = self->s.origin - self->enemy->s.origin; if (self->enemy->client) { vec.normalize(); self->enemy->velocity += (vec * 1000); } else { self->enemy->ideal_yaw = vectoyaw(vec); M_ChangeYaw(self->enemy); self->enemy->velocity = f * 1000; } } void Widow2Crunch(edict_t *self) { vec3_t aim; if ((!self->enemy) || (!self->enemy->inuse)) { self->monsterinfo.run(self); return; } Widow2TonguePull(self); // 70 + 32 aim = { 150, 0, 4 }; if (self->s.frame != FRAME_tongs07) fire_hit(self, aim, irandom(20, 26), 0); else if (self->enemy->groundentity) fire_hit(self, aim, irandom(20, 26), 500); else // not as much kick if they're in the air .. makes it harder to land on her head fire_hit(self, aim, irandom(20, 26), 250); } void Widow2Toss(edict_t *self) { self->timestamp = level.time + 3_sec; } mframe_t widow2_frames_tongs[] = { { ai_charge, 0, Widow2Tongue }, { ai_charge, 0, Widow2Tongue }, { ai_charge, 0, Widow2Tongue }, { ai_charge, 0, Widow2TonguePull }, { ai_charge, 0, Widow2TonguePull }, // 5 { ai_charge, 0, Widow2TonguePull }, { ai_charge, 0, Widow2Crunch }, { ai_charge, 0, Widow2Toss } }; MMOVE_T(widow2_move_tongs) = { FRAME_tongs01, FRAME_tongs08, widow2_frames_tongs, widow2_run }; mframe_t widow2_frames_pain[] = { { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move } }; MMOVE_T(widow2_move_pain) = { FRAME_pain01, FRAME_pain05, widow2_frames_pain, widow2_run }; mframe_t widow2_frames_death[] = { { ai_move }, { ai_move }, { ai_move, 0, WidowExplosion1 }, // 3 boom { ai_move }, { ai_move }, // 5 { ai_move, 0, WidowExplosion2 }, // 6 boom { ai_move }, { ai_move }, { ai_move }, { ai_move }, // 10 { ai_move }, { ai_move }, // 12 { ai_move }, { ai_move }, { ai_move }, // 15 { ai_move }, { ai_move }, { ai_move, 0, WidowExplosion3 }, // 18 { ai_move }, // 19 { ai_move }, // 20 { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move, 0, WidowExplosion4 }, // 25 { ai_move }, // 26 { ai_move }, { ai_move }, { ai_move, 0, WidowExplosion5 }, { ai_move, 0, WidowExplosionLeg }, // 30 { ai_move }, { ai_move }, { ai_move }, { ai_move, 0, WidowExplosion6 }, { ai_move }, // 35 { ai_move }, { ai_move }, { ai_move, 0, WidowExplosion7 }, { ai_move }, { ai_move }, // 40 { ai_move }, { ai_move }, { ai_move }, { ai_move, 0, WidowExplode } // 44 }; MMOVE_T(widow2_move_death) = { FRAME_death01, FRAME_death44, widow2_frames_death, nullptr }; void widow2_start_searching(edict_t *self); void widow2_keep_searching(edict_t *self); void widow2_finaldeath(edict_t *self); mframe_t widow2_frames_dead[] = { { ai_move, 0, widow2_start_searching }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move, 0, widow2_keep_searching } }; MMOVE_T(widow2_move_dead) = { FRAME_dthsrh01, FRAME_dthsrh15, widow2_frames_dead, nullptr }; mframe_t widow2_frames_really_dead[] = { { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move, 0, widow2_finaldeath } }; MMOVE_T(widow2_move_really_dead) = { FRAME_dthsrh16, FRAME_dthsrh22, widow2_frames_really_dead, nullptr }; void widow2_start_searching(edict_t *self) { self->count = 0; } void widow2_keep_searching(edict_t *self) { if (self->count <= 2) { M_SetAnimation(self, &widow2_move_dead); self->s.frame = FRAME_dthsrh01; self->count++; return; } M_SetAnimation(self, &widow2_move_really_dead); } void widow2_finaldeath(edict_t *self) { self->mins = { -70, -70, 0 }; self->maxs = { 70, 70, 80 }; self->movetype = MOVETYPE_TOSS; self->takedamage = true; self->nextthink = 0_ms; gi.linkentity(self); } MONSTERINFO_STAND(widow2_stand) (edict_t *self) -> void { M_SetAnimation(self, &widow2_move_stand); } MONSTERINFO_RUN(widow2_run) (edict_t *self) -> void { self->monsterinfo.aiflags &= ~AI_HOLD_FRAME; if (self->monsterinfo.aiflags & AI_STAND_GROUND) M_SetAnimation(self, &widow2_move_stand); else M_SetAnimation(self, &widow2_move_run); } MONSTERINFO_WALK(widow2_walk) (edict_t *self) -> void { M_SetAnimation(self, &widow2_move_walk); } MONSTERINFO_MELEE(widow2_melee) (edict_t *self) -> void { M_SetAnimation(self, &widow2_move_tongs); } MONSTERINFO_ATTACK(widow2_attack) (edict_t *self) -> void { float range, luck; bool blocked = false; if (self->monsterinfo.aiflags & AI_BLOCKED) { blocked = true; self->monsterinfo.aiflags &= ~AI_BLOCKED; } if (!self->enemy) return; if (self->bad_area) { if ((frandom() < 0.75f) || (level.time < self->monsterinfo.attack_finished)) M_SetAnimation(self, &widow2_move_attack_pre_beam); else { M_SetAnimation(self, &widow2_move_attack_disrupt); } return; } WidowCalcSlots(self); // if we can't see the target, spawn stuff if ((self->monsterinfo.attack_state == AS_BLIND) && (M_SlotsLeft(self) >= 2)) { M_SetAnimation(self, &widow2_move_spawn); return; } // accept bias towards spawning if (blocked && (M_SlotsLeft(self) >= 2)) { M_SetAnimation(self, &widow2_move_spawn); return; } range = realrange(self, self->enemy); if (range < 600) { luck = frandom(); if (M_SlotsLeft(self) >= 2) { if (luck <= 0.40f) M_SetAnimation(self, &widow2_move_attack_pre_beam); else if ((luck <= 0.7f) && !(level.time < self->monsterinfo.attack_finished)) { M_SetAnimation(self, &widow2_move_attack_disrupt); } else M_SetAnimation(self, &widow2_move_spawn); } else { if ((luck <= 0.50f) || (level.time < self->monsterinfo.attack_finished)) M_SetAnimation(self, &widow2_move_attack_pre_beam); else { M_SetAnimation(self, &widow2_move_attack_disrupt); } } } else { luck = frandom(); if (M_SlotsLeft(self) >= 2) { if (luck < 0.3f) M_SetAnimation(self, &widow2_move_attack_pre_beam); else if ((luck < 0.65f) || (level.time < self->monsterinfo.attack_finished)) M_SetAnimation(self, &widow2_move_spawn); else { M_SetAnimation(self, &widow2_move_attack_disrupt); } } else { if ((luck < 0.45f) || (level.time < self->monsterinfo.attack_finished)) M_SetAnimation(self, &widow2_move_attack_pre_beam); else { M_SetAnimation(self, &widow2_move_attack_disrupt); } } } } void widow2_attack_beam(edict_t *self) { M_SetAnimation(self, &widow2_move_attack_beam); widow2_step(self); } void widow2_reattack_beam(edict_t *self) { self->monsterinfo.aiflags &= ~AI_MANUAL_STEERING; if (infront(self, self->enemy)) if (frandom() <= 0.5f) if ((frandom() < 0.7f) || (M_SlotsLeft(self) < 2)) M_SetAnimation(self, &widow2_move_attack_beam); else M_SetAnimation(self, &widow2_move_spawn); else M_SetAnimation(self, &widow2_move_attack_post_beam); else M_SetAnimation(self, &widow2_move_attack_post_beam); } PAIN(widow2_pain) (edict_t *self, edict_t *other, float kick, int damage, const mod_t &mod) -> void { if (level.time < self->pain_debounce_time) return; self->pain_debounce_time = level.time + 5_sec; if (damage < 15) gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NONE, 0); else if (damage < 75) gi.sound(self, CHAN_VOICE, sound_pain2, 1, ATTN_NONE, 0); else gi.sound(self, CHAN_VOICE, sound_pain3, 1, ATTN_NONE, 0); if (!M_ShouldReactToPain(self, mod)) return; // no pain anims in nightmare if (damage >= 15) { if (damage < 75) { if ((skill->integer < 3) && (frandom() < (0.6f - (0.2f * skill->integer)))) { self->monsterinfo.aiflags &= ~AI_MANUAL_STEERING; M_SetAnimation(self, &widow2_move_pain); } } else { if ((skill->integer < 3) && (frandom() < (0.75f - (0.1f * skill->integer)))) { self->monsterinfo.aiflags &= ~AI_MANUAL_STEERING; M_SetAnimation(self, &widow2_move_pain); } } } } MONSTERINFO_SETSKIN(widow2_setskin) (edict_t *self) -> void { if (self->health < (self->max_health / 2)) self->s.skinnum = 1; else self->s.skinnum = 0; } void widow2_dead(edict_t *self) { } void KillChildren(edict_t *self) { edict_t *ent = nullptr; while (1) { ent = G_FindByString<&edict_t::classname>(ent, "monster_stalker"); if (!ent) return; // FIXME - may need to stagger if ((ent->inuse) && (ent->health > 0)) T_Damage(ent, self, self, vec3_origin, self->enemy->s.origin, vec3_origin, (ent->health + 1), 0, DAMAGE_NO_KNOCKBACK, MOD_UNKNOWN); } } DIE(widow2_die) (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void { int n; int clipped; // check for gib if (self->deadflag && M_CheckGib(self, mod)) { clipped = min(damage, 100); gi.sound(self, CHAN_VOICE, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0); for (n = 0; n < 2; n++) ThrowWidowGibLoc(self, "models/objects/gibs/bone/tris.md2", clipped, GIB_NONE, nullptr, false); for (n = 0; n < 3; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_meat/tris.md2", clipped, GIB_NONE, nullptr, false); for (n = 0; n < 3; n++) { ThrowWidowGibSized(self, "models/monsters/blackwidow2/gib1/tris.md2", clipped, GIB_METALLIC, nullptr, 0, false); ThrowWidowGibSized(self, "models/monsters/blackwidow2/gib2/tris.md2", clipped, GIB_METALLIC, nullptr, gi.soundindex("misc/fhit3.wav"), false); } for (n = 0; n < 2; n++) { ThrowWidowGibSized(self, "models/monsters/blackwidow2/gib3/tris.md2", clipped, GIB_METALLIC, nullptr, 0, false); ThrowWidowGibSized(self, "models/monsters/blackwidow/gib3/tris.md2", clipped, GIB_METALLIC, nullptr, 0, false); } ThrowGibs(self, damage, { { "models/objects/gibs/chest/tris.md2" }, { "models/objects/gibs/head2/tris.md2", GIB_HEAD } }); return; } if (self->deadflag) return; gi.sound(self, CHAN_VOICE, sound_death, 1, ATTN_NONE, 0); self->deadflag = true; self->takedamage = false; self->count = 0; KillChildren(self); self->monsterinfo.quad_time = 0_ms; self->monsterinfo.double_time = 0_ms; self->monsterinfo.invincible_time = 0_ms; M_SetAnimation(self, &widow2_move_death); } MONSTERINFO_CHECKATTACK(Widow2_CheckAttack) (edict_t *self) -> bool { vec3_t spot1, spot2; vec3_t temp; float chance; trace_t tr; float enemy_yaw; float real_enemy_range; vec3_t f, r, u; if (!self->enemy) return false; WidowPowerups(self); if ((frandom() < 0.8f) && (M_SlotsLeft(self) >= 2) && (realrange(self, self->enemy) > 150)) { self->monsterinfo.aiflags |= AI_BLOCKED; self->monsterinfo.attack_state = AS_MISSILE; return true; } if (self->enemy->health > 0) { // see if any entities are in the way of the shot spot1 = self->s.origin; spot1[2] += self->viewheight; spot2 = self->enemy->s.origin; spot2[2] += self->enemy->viewheight; tr = gi.traceline(spot1, spot2, self, CONTENTS_SOLID | CONTENTS_MONSTER | CONTENTS_PLAYER | CONTENTS_SLIME | CONTENTS_LAVA); // do we have a clear shot? if (tr.ent != self->enemy && !(tr.ent->svflags & SVF_PLAYER)) { // go ahead and spawn stuff if we're mad a a client if (self->enemy->client && M_SlotsLeft(self) >= 2) { self->monsterinfo.attack_state = AS_BLIND; return true; } // PGM - we want them to go ahead and shoot at info_notnulls if they can. if (self->enemy->solid != SOLID_NOT || tr.fraction < 1.0f) // PGM return false; } } float enemy_range = range_to(self, self->enemy); temp = self->enemy->s.origin - self->s.origin; enemy_yaw = vectoyaw(temp); self->ideal_yaw = enemy_yaw; // melee attack if (self->timestamp < level.time) { real_enemy_range = realrange(self, self->enemy); if (real_enemy_range < 300) { AngleVectors(self->s.angles, f, r, u); spot1 = G_ProjectSource2(self->s.origin, offsets[0], f, r, u); spot2 = self->enemy->s.origin; if (widow2_tongue_attack_ok(spot1, spot2, 256)) { // melee attack ok // be nice in easy mode if (skill->integer == 0 && irandom(4)) return false; if (self->monsterinfo.melee) self->monsterinfo.attack_state = AS_MELEE; else self->monsterinfo.attack_state = AS_MISSILE; return true; } } } if (level.time < self->monsterinfo.attack_finished) return false; if (self->monsterinfo.aiflags & AI_STAND_GROUND) { chance = 0.4f; } else if (enemy_range <= RANGE_NEAR) { chance = 0.8f; } else if (enemy_range <= RANGE_MID) { chance = 0.8f; } else { chance = 0.5f; } // PGM - go ahead and shoot every time if it's a info_notnull if ((frandom() < chance) || (self->enemy->solid == SOLID_NOT)) { self->monsterinfo.attack_state = AS_MISSILE; return true; } return false; } void Widow2Precache() { // cache in all of the stalker stuff, widow stuff, spawngro stuff, gibs gi.soundindex("parasite/parpain1.wav"); gi.soundindex("parasite/parpain2.wav"); gi.soundindex("parasite/pardeth1.wav"); gi.soundindex("parasite/paratck1.wav"); gi.soundindex("parasite/parsght1.wav"); gi.soundindex("infantry/melee2.wav"); gi.soundindex("misc/fhit3.wav"); gi.soundindex("tank/tnkatck3.wav"); gi.soundindex("weapons/disrupt.wav"); gi.soundindex("weapons/disint2.wav"); gi.modelindex("models/monsters/stalker/tris.md2"); gi.modelindex("models/items/spawngro3/tris.md2"); gi.modelindex("models/objects/gibs/sm_metal/tris.md2"); gi.modelindex("models/objects/laser/tris.md2"); gi.modelindex("models/proj/disintegrator/tris.md2"); gi.modelindex("models/monsters/blackwidow/gib1/tris.md2"); gi.modelindex("models/monsters/blackwidow/gib2/tris.md2"); gi.modelindex("models/monsters/blackwidow/gib3/tris.md2"); gi.modelindex("models/monsters/blackwidow/gib4/tris.md2"); gi.modelindex("models/monsters/blackwidow2/gib1/tris.md2"); gi.modelindex("models/monsters/blackwidow2/gib2/tris.md2"); gi.modelindex("models/monsters/blackwidow2/gib3/tris.md2"); gi.modelindex("models/monsters/blackwidow2/gib4/tris.md2"); } /*QUAKED monster_widow2 (1 .5 0) (-70 -70 0) (70 70 144) Ambush Trigger_Spawn Sight */ void SP_monster_widow2(edict_t *self) { if ( !M_AllowSpawn( self ) ) { G_FreeEdict( self ); return; } sound_pain1 = gi.soundindex("widow/bw2pain1.wav"); sound_pain2 = gi.soundindex("widow/bw2pain2.wav"); sound_pain3 = gi.soundindex("widow/bw2pain3.wav"); sound_death = gi.soundindex("widow/death.wav"); sound_search1 = gi.soundindex("bosshovr/bhvunqv1.wav"); sound_tentacles_retract = gi.soundindex("brain/brnatck3.wav"); // self->s.sound = gi.soundindex ("bosshovr/bhvengn1.wav"); self->movetype = MOVETYPE_STEP; self->solid = SOLID_BBOX; self->s.modelindex = gi.modelindex("models/monsters/blackwidow2/tris.md2"); self->mins = { -70, -70, 0 }; self->maxs = { 70, 70, 144 }; self->health = (2000 + 800 + 1000 * skill->integer) * st.health_multiplier; if (coop->integer) self->health += 500 * skill->integer; // self->health = 1; self->gib_health = -900; self->mass = 2500; /* if (skill->integer == 2) { self->monsterinfo.power_armor_type = IT_ITEM_POWER_SHIELD; self->monsterinfo.power_armor_power = 500; } else */ if (skill->integer == 3) { if (!st.was_key_specified("power_armor_type")) self->monsterinfo.power_armor_type = IT_ITEM_POWER_SHIELD; if (!st.was_key_specified("power_armor_power")) self->monsterinfo.power_armor_power = 750; } self->yaw_speed = 30; self->flags |= FL_IMMUNE_LASER; self->monsterinfo.aiflags |= AI_IGNORE_SHOTS; self->pain = widow2_pain; self->die = widow2_die; self->monsterinfo.melee = widow2_melee; self->monsterinfo.stand = widow2_stand; self->monsterinfo.walk = widow2_walk; self->monsterinfo.run = widow2_run; self->monsterinfo.attack = widow2_attack; self->monsterinfo.search = widow2_search; self->monsterinfo.checkattack = Widow2_CheckAttack; self->monsterinfo.setskin = widow2_setskin; gi.linkentity(self); M_SetAnimation(self, &widow2_move_stand); self->monsterinfo.scale = MODEL_SCALE; Widow2Precache(); WidowCalcSlots(self); walkmonster_start(self); } // // Death sequence stuff // void WidowVelocityForDamage(int damage, vec3_t &v) { v[0] = damage * crandom(); v[1] = damage * crandom(); v[2] = damage * crandom() + 200.0f; } TOUCH(widow_gib_touch) (edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self) -> void { self->solid = SOLID_NOT; self->touch = nullptr; self->s.angles[PITCH] = 0; self->s.angles[ROLL] = 0; self->avelocity = {}; if (self->style) gi.sound(self, CHAN_VOICE, self->style, 1, ATTN_NORM, 0); } void ThrowWidowGib(edict_t *self, const char *gibname, int damage, gib_type_t type) { ThrowWidowGibReal(self, gibname, damage, type, nullptr, false, 0, true); } void ThrowWidowGibLoc(edict_t *self, const char *gibname, int damage, gib_type_t type, const vec3_t *startpos, bool fade) { ThrowWidowGibReal(self, gibname, damage, type, startpos, false, 0, fade); } void ThrowWidowGibSized(edict_t *self, const char *gibname, int damage, gib_type_t type, const vec3_t *startpos, int hitsound, bool fade) { ThrowWidowGibReal(self, gibname, damage, type, startpos, true, hitsound, fade); } void ThrowWidowGibReal(edict_t *self, const char *gibname, int damage, gib_type_t type, const vec3_t *startpos, bool sized, int hitsound, bool fade) { edict_t *gib; vec3_t vd; vec3_t origin; vec3_t size; float vscale; if (!gibname) return; gib = G_Spawn(); if (startpos) gib->s.origin = *startpos; else { origin = (self->absmin + self->absmax) * 0.5f; gib->s.origin[0] = origin[0] + crandom() * size[0]; gib->s.origin[1] = origin[1] + crandom() * size[1]; gib->s.origin[2] = origin[2] + crandom() * size[2]; } gib->solid = SOLID_NOT; gib->s.effects |= EF_GIB; gib->flags |= FL_NO_KNOCKBACK; gib->takedamage = true; gib->die = gib_die; gib->s.renderfx |= RF_IR_VISIBLE; gib->s.renderfx &= ~RF_DOT_SHADOW; if (fade) { gib->think = G_FreeEdict; // sized gibs last longer if (sized) gib->nextthink = level.time + random_time(20_sec, 35_sec); else gib->nextthink = level.time + random_time(5_sec, 15_sec); } else { gib->think = G_FreeEdict; // sized gibs last longer if (sized) gib->nextthink = level.time + random_time(60_sec, 75_sec); else gib->nextthink = level.time + random_time(25_sec, 35_sec); } if (!(type & GIB_METALLIC)) { gib->movetype = MOVETYPE_TOSS; vscale = 0.5; } else { gib->movetype = MOVETYPE_BOUNCE; vscale = 1.0; } WidowVelocityForDamage(damage, vd); gib->velocity = self->velocity + (vd * vscale); ClipGibVelocity(gib); gi.setmodel(gib, gibname); if (sized) { gib->style = hitsound; gib->solid = SOLID_BBOX; gib->avelocity[0] = frandom(400); gib->avelocity[1] = frandom(400); gib->avelocity[2] = frandom(400); if (gib->velocity[2] < 0) gib->velocity[2] *= -1; gib->velocity[0] *= 2; gib->velocity[1] *= 2; ClipGibVelocity(gib); gib->velocity[2] = max(frandom(350.f, 450.f), gib->velocity[2]); gib->gravity = 0.25; gib->touch = widow_gib_touch; gib->owner = self; if (gib->s.modelindex == gi.modelindex("models/monsters/blackwidow2/gib2/tris.md2")) { gib->mins = { -10, -10, 0 }; gib->maxs = { 10, 10, 10 }; } else { gib->mins = { -5, -5, 0 }; gib->maxs = { 5, 5, 5 }; } } else { gib->velocity[0] *= 2; gib->velocity[1] *= 2; gib->avelocity[0] = frandom(600); gib->avelocity[1] = frandom(600); gib->avelocity[2] = frandom(600); } gi.linkentity(gib); } void ThrowSmallStuff(edict_t *self, const vec3_t &point) { int n; for (n = 0; n < 2; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_meat/tris.md2", 300, GIB_NONE, &point, false); ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 300, GIB_METALLIC, &point, false); ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 100, GIB_METALLIC, &point, false); } void ThrowMoreStuff(edict_t *self, const vec3_t &point) { int n; if (coop->integer) { ThrowSmallStuff(self, point); return; } for (n = 0; n < 1; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_meat/tris.md2", 300, GIB_NONE, &point, false); for (n = 0; n < 2; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 300, GIB_METALLIC, &point, false); for (n = 0; n < 3; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 100, GIB_METALLIC, &point, false); } THINK(WidowExplode) (edict_t *self) -> void { vec3_t org; int n; self->think = WidowExplode; // redo: org = self->s.origin; org[2] += irandom(24, 40); if (self->count < 8) org[2] += irandom(24, 56); switch (self->count) { case 0: org[0] -= 24; org[1] -= 24; break; case 1: org[0] += 24; org[1] += 24; ThrowSmallStuff(self, org); break; case 2: org[0] += 24; org[1] -= 24; break; case 3: org[0] -= 24; org[1] += 24; ThrowMoreStuff(self, org); break; case 4: org[0] -= 48; org[1] -= 48; break; case 5: org[0] += 48; org[1] += 48; ThrowArm1(self); break; case 6: org[0] -= 48; org[1] += 48; ThrowArm2(self); break; case 7: org[0] += 48; org[1] -= 48; ThrowSmallStuff(self, org); break; case 8: org[0] += 18; org[1] += 18; org[2] = self->s.origin[2] + 48; ThrowMoreStuff(self, org); break; case 9: org[0] -= 18; org[1] += 18; org[2] = self->s.origin[2] + 48; break; case 10: org[0] += 18; org[1] -= 18; org[2] = self->s.origin[2] + 48; break; case 11: org[0] -= 18; org[1] -= 18; org[2] = self->s.origin[2] + 48; break; case 12: self->s.sound = 0; for (n = 0; n < 1; n++) ThrowWidowGib(self, "models/objects/gibs/sm_meat/tris.md2", 400, GIB_NONE); for (n = 0; n < 2; n++) ThrowWidowGib(self, "models/objects/gibs/sm_metal/tris.md2", 100, GIB_METALLIC); for (n = 0; n < 2; n++) ThrowWidowGib(self, "models/objects/gibs/sm_metal/tris.md2", 400, GIB_METALLIC); self->deadflag = true; self->think = monster_think; self->nextthink = level.time + 10_hz; M_SetAnimation(self, &widow2_move_dead); return; } self->count++; if (self->count >= 9 && self->count <= 12) { gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_EXPLOSION1_BIG); gi.WritePosition(org); gi.multicast(self->s.origin, MULTICAST_ALL, false); // goto redo; } else { // else gi.WriteByte(svc_temp_entity); if (self->count % 2) gi.WriteByte(TE_EXPLOSION1); else gi.WriteByte(TE_EXPLOSION1_NP); gi.WritePosition(org); gi.multicast(self->s.origin, MULTICAST_ALL, false); } self->nextthink = level.time + 10_hz; } void WidowExplosion1(edict_t *self) { int n; vec3_t f, r, u, startpoint; vec3_t offset = { 23.74f, -37.67f, 76.96f }; AngleVectors(self->s.angles, f, r, u); startpoint = G_ProjectSource2(self->s.origin, offset, f, r, u); gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_EXPLOSION1); gi.WritePosition(startpoint); gi.multicast(self->s.origin, MULTICAST_ALL, false); for (n = 0; n < 1; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_meat/tris.md2", 300, GIB_NONE, &startpoint, false); for (n = 0; n < 1; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 100, GIB_METALLIC, &startpoint, false); for (n = 0; n < 2; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 300, GIB_METALLIC, &startpoint, false); } void WidowExplosion2(edict_t *self) { int n; vec3_t f, r, u, startpoint; vec3_t offset = { -20.49f, 36.92f, 73.52f }; AngleVectors(self->s.angles, f, r, u); startpoint = G_ProjectSource2(self->s.origin, offset, f, r, u); gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_EXPLOSION1); gi.WritePosition(startpoint); gi.multicast(self->s.origin, MULTICAST_ALL, false); for (n = 0; n < 1; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_meat/tris.md2", 300, GIB_NONE, &startpoint, false); for (n = 0; n < 1; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 100, GIB_METALLIC, &startpoint, false); for (n = 0; n < 2; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 300, GIB_METALLIC, &startpoint, false); } void WidowExplosion3(edict_t *self) { int n; vec3_t f, r, u, startpoint; vec3_t offset = { 2.11f, 0.05f, 92.20f }; AngleVectors(self->s.angles, f, r, u); startpoint = G_ProjectSource2(self->s.origin, offset, f, r, u); gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_EXPLOSION1); gi.WritePosition(startpoint); gi.multicast(self->s.origin, MULTICAST_ALL, false); for (n = 0; n < 1; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_meat/tris.md2", 300, GIB_NONE, &startpoint, false); for (n = 0; n < 1; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 100, GIB_METALLIC, &startpoint, false); for (n = 0; n < 2; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 300, GIB_METALLIC, &startpoint, false); } void WidowExplosion4(edict_t *self) { int n; vec3_t f, r, u, startpoint; vec3_t offset = { -28.04f, -35.57f, -77.56f }; AngleVectors(self->s.angles, f, r, u); startpoint = G_ProjectSource2(self->s.origin, offset, f, r, u); gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_EXPLOSION1); gi.WritePosition(startpoint); gi.multicast(self->s.origin, MULTICAST_ALL, false); for (n = 0; n < 1; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_meat/tris.md2", 300, GIB_NONE, &startpoint, false); for (n = 0; n < 1; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 100, GIB_METALLIC, &startpoint, false); for (n = 0; n < 2; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 300, GIB_METALLIC, &startpoint, false); } void WidowExplosion5(edict_t *self) { int n; vec3_t f, r, u, startpoint; vec3_t offset = { -20.11f, -1.11f, 40.76f }; AngleVectors(self->s.angles, f, r, u); startpoint = G_ProjectSource2(self->s.origin, offset, f, r, u); gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_EXPLOSION1); gi.WritePosition(startpoint); gi.multicast(self->s.origin, MULTICAST_ALL, false); for (n = 0; n < 1; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_meat/tris.md2", 300, GIB_NONE, &startpoint, false); for (n = 0; n < 1; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 100, GIB_METALLIC, &startpoint, false); for (n = 0; n < 2; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 300, GIB_METALLIC, &startpoint, false); } void WidowExplosion6(edict_t *self) { int n; vec3_t f, r, u, startpoint; vec3_t offset = { -20.11f, -1.11f, 40.76f }; AngleVectors(self->s.angles, f, r, u); startpoint = G_ProjectSource2(self->s.origin, offset, f, r, u); gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_EXPLOSION1); gi.WritePosition(startpoint); gi.multicast(self->s.origin, MULTICAST_ALL, false); for (n = 0; n < 1; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_meat/tris.md2", 300, GIB_NONE, &startpoint, false); for (n = 0; n < 1; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 100, GIB_METALLIC, &startpoint, false); for (n = 0; n < 2; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 300, GIB_METALLIC, &startpoint, false); } void WidowExplosion7(edict_t *self) { int n; vec3_t f, r, u, startpoint; vec3_t offset = { -20.11f, -1.11f, 40.76f }; AngleVectors(self->s.angles, f, r, u); startpoint = G_ProjectSource2(self->s.origin, offset, f, r, u); gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_EXPLOSION1); gi.WritePosition(startpoint); gi.multicast(self->s.origin, MULTICAST_ALL, false); for (n = 0; n < 1; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_meat/tris.md2", 300, GIB_NONE, &startpoint, false); for (n = 0; n < 1; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 100, GIB_METALLIC, &startpoint, false); for (n = 0; n < 2; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 300, GIB_METALLIC, &startpoint, false); } void WidowExplosionLeg(edict_t *self) { vec3_t f, r, u, startpoint; vec3_t offset1 = { -31.89f, -47.86f, 67.02f }; vec3_t offset2 = { -44.9f, -82.14f, 54.72f }; AngleVectors(self->s.angles, f, r, u); startpoint = G_ProjectSource2(self->s.origin, offset1, f, r, u); gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_EXPLOSION1_BIG); gi.WritePosition(startpoint); gi.multicast(self->s.origin, MULTICAST_ALL, false); ThrowWidowGibSized(self, "models/monsters/blackwidow2/gib2/tris.md2", 200, GIB_METALLIC, &startpoint, gi.soundindex("misc/fhit3.wav"), false); ThrowWidowGibLoc(self, "models/objects/gibs/sm_meat/tris.md2", 300, GIB_NONE, &startpoint, false); ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 100, GIB_METALLIC, &startpoint, false); startpoint = G_ProjectSource2(self->s.origin, offset2, f, r, u); gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_EXPLOSION1); gi.WritePosition(startpoint); gi.multicast(self->s.origin, MULTICAST_ALL, false); ThrowWidowGibSized(self, "models/monsters/blackwidow2/gib1/tris.md2", 300, GIB_METALLIC, &startpoint, gi.soundindex("misc/fhit3.wav"), false); ThrowWidowGibLoc(self, "models/objects/gibs/sm_meat/tris.md2", 300, GIB_NONE, &startpoint, false); ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 100, GIB_METALLIC, &startpoint, false); } void ThrowArm1(edict_t *self) { int n; vec3_t f, r, u, startpoint; vec3_t offset1 = { 65.76f, 17.52f, 7.56f }; AngleVectors(self->s.angles, f, r, u); startpoint = G_ProjectSource2(self->s.origin, offset1, f, r, u); gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_EXPLOSION1_BIG); gi.WritePosition(startpoint); gi.multicast(self->s.origin, MULTICAST_ALL, false); for (n = 0; n < 2; n++) ThrowWidowGibLoc(self, "models/objects/gibs/sm_metal/tris.md2", 100, GIB_METALLIC, &startpoint, false); } void ThrowArm2(edict_t *self) { vec3_t f, r, u, startpoint; vec3_t offset1 = { 65.76f, 17.52f, 7.56f }; AngleVectors(self->s.angles, f, r, u); startpoint = G_ProjectSource2(self->s.origin, offset1, f, r, u); ThrowWidowGibSized(self, "models/monsters/blackwidow2/gib4/tris.md2", 200, GIB_METALLIC, &startpoint, gi.soundindex("misc/fhit3.wav"), false); ThrowWidowGibLoc(self, "models/objects/gibs/sm_meat/tris.md2", 300, GIB_NONE, &startpoint, false); }