quake2-rerelease-dll/rerelease/m_shambler.cpp
2023-10-03 14:43:06 -04:00

598 lines
14 KiB
C++

// Copyright (c) ZeniMax Media Inc.
// Licensed under the GNU General Public License 2.0.
/*
==============================================================================
SHAMBLER
==============================================================================
*/
#include "g_local.h"
#include "m_shambler.h"
#include "m_flash.h"
static cached_soundindex sound_pain;
static cached_soundindex sound_idle;
static cached_soundindex sound_die;
static cached_soundindex sound_sight;
static cached_soundindex sound_windup;
static cached_soundindex sound_melee1;
static cached_soundindex sound_melee2;
static cached_soundindex sound_smack;
static cached_soundindex sound_boom;
//
// misc
//
MONSTERINFO_SIGHT(shambler_sight) (edict_t* self, edict_t* other) -> void
{
gi.sound(self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0);
}
constexpr vec3_t lightning_left_hand[] = {
{ 44, 36, 25 },
{ 10, 44, 57 },
{ -1, 40, 70 },
{ -10, 34, 75 },
{ 7.4f, 24, 89 }
};
constexpr vec3_t lightning_right_hand[] = {
{ 28, -38, 25 },
{ 31, -7, 70 },
{ 20, 0, 80 },
{ 16, 1.2f, 81 },
{ 27, -11, 83 }
};
static void shambler_lightning_update(edict_t *self)
{
edict_t *lightning = self->beam;
if (self->s.frame >= FRAME_magic01 + q_countof(lightning_left_hand))
{
G_FreeEdict(lightning);
self->beam = nullptr;
return;
}
vec3_t f, r;
AngleVectors(self->s.angles, f, r, nullptr);
lightning->s.origin = M_ProjectFlashSource(self, lightning_left_hand[self->s.frame - FRAME_magic01], f, r);
lightning->s.old_origin = M_ProjectFlashSource(self, lightning_right_hand[self->s.frame - FRAME_magic01], f, r);
gi.linkentity(lightning);
}
void shambler_windup(edict_t* self)
{
gi.sound(self, CHAN_WEAPON, sound_windup, 1, ATTN_NORM, 0);
edict_t *lightning = self->beam = G_Spawn();
lightning->s.modelindex = gi.modelindex("models/proj/lightning/tris.md2");
lightning->s.renderfx |= RF_BEAM;
lightning->owner = self;
shambler_lightning_update(self);
}
MONSTERINFO_IDLE(shambler_idle) (edict_t* self) -> void
{
gi.sound(self, CHAN_VOICE, sound_idle, 1, ATTN_IDLE, 0);
}
void shambler_maybe_idle(edict_t* self)
{
if (frandom() > 0.8)
gi.sound(self, CHAN_VOICE, sound_idle, 1, ATTN_IDLE, 0);
}
//
// stand
//
mframe_t shambler_frames_stand[] = {
{ ai_stand },
{ ai_stand },
{ ai_stand },
{ ai_stand },
{ ai_stand },
{ ai_stand },
{ ai_stand },
{ ai_stand },
{ ai_stand },
{ ai_stand },
{ ai_stand },
{ ai_stand },
{ ai_stand },
{ ai_stand },
{ ai_stand },
{ ai_stand },
{ ai_stand }
};
MMOVE_T(shambler_move_stand) = { FRAME_stand01, FRAME_stand17, shambler_frames_stand, nullptr };
MONSTERINFO_STAND(shambler_stand) (edict_t* self) -> void
{
M_SetAnimation(self, &shambler_move_stand);
}
//
// walk
//
void shambler_walk(edict_t* self);
mframe_t shambler_frames_walk[] = {
{ ai_walk, 10 }, // FIXME: add footsteps?
{ ai_walk, 9 },
{ ai_walk, 9 },
{ ai_walk, 5 },
{ ai_walk, 6 },
{ ai_walk, 12 },
{ ai_walk, 8 },
{ ai_walk, 3 },
{ ai_walk, 13 },
{ ai_walk, 9 },
{ ai_walk, 7, shambler_maybe_idle },
{ ai_walk, 5 },
};
MMOVE_T(shambler_move_walk) = { FRAME_walk01, FRAME_walk12, shambler_frames_walk, nullptr };
MONSTERINFO_WALK(shambler_walk) (edict_t* self) -> void
{
M_SetAnimation(self, &shambler_move_walk);
}
//
// run
//
void shambler_run(edict_t* self);
mframe_t shambler_frames_run[] = {
{ ai_run, 20 }, // FIXME: add footsteps?
{ ai_run, 24 },
{ ai_run, 20 },
{ ai_run, 20 },
{ ai_run, 24 },
{ ai_run, 20, shambler_maybe_idle },
};
MMOVE_T(shambler_move_run) = { FRAME_run01, FRAME_run06, shambler_frames_run, nullptr };
MONSTERINFO_RUN(shambler_run) (edict_t* self) -> void
{
if (self->enemy && self->enemy->client)
self->monsterinfo.aiflags |= AI_BRUTAL;
else
self->monsterinfo.aiflags &= ~AI_BRUTAL;
if (self->monsterinfo.aiflags & AI_STAND_GROUND)
{
M_SetAnimation(self, &shambler_move_stand);
return;
}
M_SetAnimation(self, &shambler_move_run);
}
//
// pain
//
// FIXME: needs halved explosion damage
mframe_t shambler_frames_pain[] = {
{ ai_move },
{ ai_move },
{ ai_move },
{ ai_move },
{ ai_move },
{ ai_move },
};
MMOVE_T(shambler_move_pain) = { FRAME_pain01, FRAME_pain06, shambler_frames_pain, shambler_run };
PAIN(shambler_pain) (edict_t* self, edict_t* other, float kick, int damage, const mod_t &mod) -> void
{
if (level.time < self->timestamp)
return;
self->timestamp = level.time + 1_ms;
gi.sound(self, CHAN_AUTO, sound_pain, 1, ATTN_NORM, 0);
if (mod.id != MOD_CHAINFIST && damage <= 30 && frandom() > 0.2f)
return;
// If hard or nightmare, don't go into pain while attacking
if (skill->integer >= 2)
{
if ((self->s.frame >= FRAME_smash01) && (self->s.frame <= FRAME_smash12))
return;
if ((self->s.frame >= FRAME_swingl01) && (self->s.frame <= FRAME_swingl09))
return;
if ((self->s.frame >= FRAME_swingr01) && (self->s.frame <= FRAME_swingr09))
return;
}
if (!M_ShouldReactToPain(self, mod))
return; // no pain anims in nightmare
if (level.time < self->pain_debounce_time)
return;
self->pain_debounce_time = level.time + 2_sec;
M_SetAnimation(self, &shambler_move_pain);
}
MONSTERINFO_SETSKIN(shambler_setskin) (edict_t* self) -> void
{
// FIXME: create pain skin?
//if (self->health < (self->max_health / 2))
// self->s.skinnum |= 1;
//else
// self->s.skinnum &= ~1;
}
//
// attacks
//
/*
void() sham_magic3 =[ $magic3, sham_magic4 ] {
ai_face();
self.nextthink = self.nextthink + 0.2;
local entity o;
self.effects = self.effects | EF_MUZZLEFLASH;
ai_face();
self.owner = spawn();
o = self.owner;
setmodel (o, "progs/s_light.mdl");
setorigin (o, self.origin);
o.angles = self.angles;
o.nextthink = time + 0.7;
o.think = SUB_Remove;
};
*/
void ShamblerSaveLoc(edict_t* self)
{
self->pos1 = self->enemy->s.origin; // save for aiming the shot
self->pos1[2] += self->enemy->viewheight;
self->monsterinfo.nextframe = FRAME_magic09;
gi.sound(self, CHAN_WEAPON, sound_boom, 1, ATTN_NORM, 0);
shambler_lightning_update(self);
}
constexpr spawnflags_t SPAWNFLAG_SHAMBLER_PRECISE = 1_spawnflag;
vec3_t FindShamblerOffset(edict_t *self)
{
vec3_t offset = { 0, 0, 48.f };
for (int i = 0; i < 8; i++)
{
if (M_CheckClearShot(self, offset))
return offset;
offset.z -= 4.f;
}
return { 0, 0, 48.f };
}
void ShamblerCastLightning(edict_t* self)
{
if (!self->enemy)
return;
vec3_t start;
vec3_t dir;
vec3_t forward, right;
vec3_t offset = FindShamblerOffset(self);
AngleVectors(self->s.angles, forward, right, nullptr);
start = M_ProjectFlashSource(self, offset, forward, right);
// calc direction to where we targted
PredictAim(self, self->enemy, start, 0, false, self->spawnflags.has(SPAWNFLAG_SHAMBLER_PRECISE) ? 0.f : 0.1f, &dir, nullptr);
vec3_t end = start + (dir * 8192);
trace_t tr = gi.traceline(start, end, self, MASK_PROJECTILE | CONTENTS_SLIME | CONTENTS_LAVA);
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_LIGHTNING);
gi.WriteEntity(self); // source entity
gi.WriteEntity(world); // destination entity
gi.WritePosition(start);
gi.WritePosition(tr.endpos);
gi.multicast(start, MULTICAST_PVS, false);
fire_bullet(self, start, dir, irandom(8, 12), 15, 0, 0, MOD_TESLA);
}
mframe_t shambler_frames_magic[] = {
{ ai_charge, 0, shambler_windup },
{ ai_charge, 0, shambler_lightning_update },
{ ai_charge, 0, shambler_lightning_update },
{ ai_move, 0, shambler_lightning_update },
{ ai_move, 0, shambler_lightning_update },
{ ai_move, 0, ShamblerSaveLoc},
{ ai_move },
{ ai_charge },
{ ai_move, 0, ShamblerCastLightning },
{ ai_move, 0, ShamblerCastLightning },
{ ai_move, 0, ShamblerCastLightning },
{ ai_move },
};
MMOVE_T(shambler_attack_magic) = { FRAME_magic01, FRAME_magic12, shambler_frames_magic, shambler_run };
MONSTERINFO_ATTACK(shambler_attack) (edict_t* self) -> void
{
M_SetAnimation(self, &shambler_attack_magic);
}
//
// melee
//
void shambler_melee1(edict_t* self)
{
gi.sound(self, CHAN_WEAPON, sound_melee1, 1, ATTN_NORM, 0);
}
void shambler_melee2(edict_t* self)
{
gi.sound(self, CHAN_WEAPON, sound_melee2, 1, ATTN_NORM, 0);
}
void sham_swingl9(edict_t* self);
void sham_swingr9(edict_t* self);
void sham_smash10(edict_t* self)
{
if (!self->enemy)
return;
ai_charge(self, 0);
if (!CanDamage(self->enemy, self))
return;
vec3_t aim = { MELEE_DISTANCE, self->mins[0], -4 };
bool hit = fire_hit(self, aim, irandom(110, 120), 120); // Slower attack
if (hit)
gi.sound(self, CHAN_WEAPON, sound_smack, 1, ATTN_NORM, 0);
// SpawnMeatSpray(self.origin + v_forward * 16, crandom() * 100 * v_right);
// SpawnMeatSpray(self.origin + v_forward * 16, crandom() * 100 * v_right);
};
void ShamClaw(edict_t* self)
{
if (!self->enemy)
return;
ai_charge(self, 10);
if (!CanDamage(self->enemy, self))
return;
vec3_t aim = { MELEE_DISTANCE, self->mins[0], -4 };
bool hit = fire_hit(self, aim, irandom(70, 80), 80); // Slower attack
if (hit)
gi.sound(self, CHAN_WEAPON, sound_smack, 1, ATTN_NORM, 0);
// 250 if left, -250 if right
/*
if (side)
{
makevectorsfixed(self.angles);
SpawnMeatSpray(self.origin + v_forward * 16, side * v_right);
}
*/
};
mframe_t shambler_frames_smash[] = {
{ ai_charge, 2, shambler_melee1 },
{ ai_charge, 6 },
{ ai_charge, 6 },
{ ai_charge, 5 },
{ ai_charge, 4 },
{ ai_charge, 1 },
{ ai_charge, 0 },
{ ai_charge, 0 },
{ ai_charge, 0 },
{ ai_charge, 0, sham_smash10 },
{ ai_charge, 5 },
{ ai_charge, 4 },
};
MMOVE_T(shambler_attack_smash) = { FRAME_smash01, FRAME_smash12, shambler_frames_smash, shambler_run };
mframe_t shambler_frames_swingl[] = {
{ ai_charge, 5, shambler_melee1 },
{ ai_charge, 3 },
{ ai_charge, 7 },
{ ai_charge, 3 },
{ ai_charge, 7 },
{ ai_charge, 9 },
{ ai_charge, 5, ShamClaw },
{ ai_charge, 4 },
{ ai_charge, 8, sham_swingl9 },
};
MMOVE_T(shambler_attack_swingl) = { FRAME_swingl01, FRAME_swingl09, shambler_frames_swingl, shambler_run };
mframe_t shambler_frames_swingr[] = {
{ ai_charge, 1, shambler_melee2 },
{ ai_charge, 8 },
{ ai_charge, 14 },
{ ai_charge, 7 },
{ ai_charge, 3 },
{ ai_charge, 6 },
{ ai_charge, 6, ShamClaw },
{ ai_charge, 3 },
{ ai_charge, 8, sham_swingr9 },
};
MMOVE_T(shambler_attack_swingr) = { FRAME_swingr01, FRAME_swingr09, shambler_frames_swingr, shambler_run };
void sham_swingl9(edict_t* self)
{
ai_charge(self, 8);
if (brandom() && self->enemy && range_to(self, self->enemy) < MELEE_DISTANCE)
M_SetAnimation(self, &shambler_attack_swingr);
}
void sham_swingr9(edict_t* self)
{
ai_charge(self, 1);
ai_charge(self, 10);
if (brandom() && self->enemy && range_to(self, self->enemy) < MELEE_DISTANCE)
M_SetAnimation(self, &shambler_attack_swingl);
}
MONSTERINFO_MELEE(shambler_melee) (edict_t* self) -> void
{
float chance = frandom();
if (chance > 0.6 || self->health == 600)
M_SetAnimation(self, &shambler_attack_smash);
else if (chance > 0.3)
M_SetAnimation(self, &shambler_attack_swingl);
else
M_SetAnimation(self, &shambler_attack_swingr);
}
//
// death
//
void shambler_dead(edict_t* self)
{
self->mins = { -16, -16, -24 };
self->maxs = { 16, 16, -0 };
monster_dead(self);
}
static void shambler_shrink(edict_t* self)
{
self->maxs[2] = 0;
self->svflags |= SVF_DEADMONSTER;
gi.linkentity(self);
}
mframe_t shambler_frames_death[] = {
{ ai_move, 0 },
{ ai_move, 0 },
{ ai_move, 0, shambler_shrink },
{ ai_move, 0 },
{ ai_move, 0 },
{ ai_move, 0 },
{ ai_move, 0 },
{ ai_move, 0 },
{ ai_move, 0 },
{ ai_move, 0 },
{ ai_move, 0 }, // FIXME: thud?
};
MMOVE_T(shambler_move_death) = { FRAME_death01, FRAME_death11, shambler_frames_death, shambler_dead };
DIE(shambler_die) (edict_t* self, edict_t* inflictor, edict_t* attacker, int damage, const vec3_t& point, const mod_t &mod) -> void
{
if (self->beam)
{
G_FreeEdict(self->beam);
self->beam = nullptr;
}
if (self->beam2)
{
G_FreeEdict(self->beam2);
self->beam2 = nullptr;
}
// check for gib
if (M_CheckGib(self, mod))
{
gi.sound(self, CHAN_VOICE, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0);
// FIXME: better gibs for shambler, shambler head
ThrowGibs(self, damage, {
{ "models/objects/gibs/sm_meat/tris.md2" },
{ "models/objects/gibs/chest/tris.md2" },
{ "models/objects/gibs/head2/tris.md2", GIB_HEAD }
});
self->deadflag = true;
return;
}
if (self->deadflag)
return;
// regular death
gi.sound(self, CHAN_VOICE, sound_die, 1, ATTN_NORM, 0);
self->deadflag = true;
self->takedamage = true;
M_SetAnimation(self, &shambler_move_death);
}
void SP_monster_shambler(edict_t* self)
{
if ( !M_AllowSpawn( self ) ) {
G_FreeEdict( self );
return;
}
self->s.modelindex = gi.modelindex("models/monsters/shambler/tris.md2");
self->mins = { -32, -32, -24 };
self->maxs = { 32, 32, 64 };
self->movetype = MOVETYPE_STEP;
self->solid = SOLID_BBOX;
gi.modelindex("models/proj/lightning/tris.md2");
sound_pain.assign("shambler/shurt2.wav");
sound_idle.assign("shambler/sidle.wav");
sound_die.assign("shambler/sdeath.wav");
sound_windup.assign("shambler/sattck1.wav");
sound_melee1.assign("shambler/melee1.wav");
sound_melee2.assign("shambler/melee2.wav");
sound_sight.assign("shambler/ssight.wav");
sound_smack.assign("shambler/smack.wav");
sound_boom.assign("shambler/sboom.wav");
self->health = 600 * st.health_multiplier;
self->gib_health = -60;
self->mass = 500;
self->pain = shambler_pain;
self->die = shambler_die;
self->monsterinfo.stand = shambler_stand;
self->monsterinfo.walk = shambler_walk;
self->monsterinfo.run = shambler_run;
self->monsterinfo.dodge = nullptr;
self->monsterinfo.attack = shambler_attack;
self->monsterinfo.melee = shambler_melee;
self->monsterinfo.sight = shambler_sight;
self->monsterinfo.idle = shambler_idle;
self->monsterinfo.blocked = nullptr;
self->monsterinfo.setskin = shambler_setskin;
gi.linkentity(self);
if (self->spawnflags.has(SPAWNFLAG_SHAMBLER_PRECISE))
self->monsterinfo.aiflags |= AI_IGNORE_SHOTS;
M_SetAnimation(self, &shambler_move_stand);
self->monsterinfo.scale = MODEL_SCALE;
walkmonster_start(self);
}