thirtyflightsofloving/game/m_medic.c
Knightmare66 d16b46e3cf Added re-implementation of func_plat2 and func_door2 from rogue to default Lazarus DLL.
Overhauled child entity movement in default Lazarus DLL.
Added bbox versions of various triggers to default Lazarus DLL.
Added level.maptype field to default Lazarus DLL.
Added entity class IDs to default Lazarus DLL.
Incremented savegame version for default Lazarus DLL.
2020-10-27 02:00:05 -04:00

1321 lines
33 KiB
C

/*
===========================================================================
Copyright (C) 1997-2001 Id Software, Inc.
Copyright (C) 2000-2002 Mr. Hyde and Mad Dog
This file is part of Lazarus Quake 2 Mod source code.
Lazarus Quake 2 Mod source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.
Lazarus Quake 2 Mod source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Lazarus Quake 2 Mod source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===========================================================================
*/
/*
==============================================================================
MEDIC
==============================================================================
*/
#include "g_local.h"
#include "m_medic.h"
qboolean visible (edict_t *self, edict_t *other);
int medic_test=0;
static int sound_idle1;
static int sound_pain1;
static int sound_pain2;
static int sound_die;
static int sound_sight;
static int sound_search;
static int sound_hook_launch;
static int sound_hook_hit;
static int sound_hook_heal;
static int sound_hook_retract;
void M_SetEffects (edict_t *ent);
void medic_deadmonster_think (edict_t *self)
{
// Lazarus. This turns off the "owner" value of a dead monster
// that medic previously aborted on after 2 seconds. If the delay
// was NOT used, medic might just rotate around on top of monster
// continuously trying to revive him unsuccessfully. And if owner
// isn't turned off, medic would never try to revive the monster
// once an abortHeal was called.
if (self->target_ent && self->target_ent->inuse)
{
edict_t *deadmonster = self->target_ent;
if (deadmonster->monsterinfo.healer)
{
edict_t *medic = deadmonster->monsterinfo.healer;
if (medic->inuse)
{
vec3_t dir;
VectorSubtract(medic->s.origin,deadmonster->s.origin,dir);
if (VectorLength(dir) < 64)
{
self->nextthink = level.time + 1.0;
return;
}
}
else
deadmonster->monsterinfo.healer = NULL;
}
deadmonster->owner = NULL;
}
G_FreeEdict(self);
}
void cleanupHeal (edict_t *self, qboolean change_frame)
{
// clean up target, if we have one and it's legit
if (self->enemy && self->enemy->inuse)
{
edict_t *temp; // Lazarus
//self->enemy->monsterinfo.healer = NULL;
self->enemy->monsterinfo.aiflags &= ~AI_RESURRECTING;
self->enemy->takedamage = DAMAGE_AIM;
// Lazarus
temp = G_Spawn();
temp->svflags = SVF_NOCLIENT;
temp->target_ent = self->enemy;
temp->think = medic_deadmonster_think;
temp->nextthink = level.time + 2.0;
gi.linkentity(temp);
M_SetEffects (self->enemy);
}
if (change_frame)
self->monsterinfo.nextframe = FRAME_attack52;
}
void DeleteBadMedic(edict_t *self)
{
edict_t *monster;
monster = self->activator;
if (monster)
{
if (self->monsterinfo.badMedic1)
monster->monsterinfo.badMedic1 = NULL;
if (self->monsterinfo.badMedic2)
monster->monsterinfo.badMedic2 = NULL;
}
G_FreeEdict(self);
}
void abortHeal (edict_t *self, qboolean mark)
{
edict_t *temp;
// clean up target
cleanupHeal (self, true);
if ((mark) && (self->enemy) && (self->enemy->inuse))
{
if ((self->enemy->monsterinfo.badMedic1) && (self->enemy->monsterinfo.badMedic1->inuse)
&& (!strncmp(self->enemy->monsterinfo.badMedic1->classname, "monster_medic", 13)) )
self->enemy->monsterinfo.badMedic2 = self;
else
self->enemy->monsterinfo.badMedic1 = self;
temp = G_Spawn();
temp->activator = self->enemy;
if (self == self->enemy->monsterinfo.badMedic1)
temp->monsterinfo.badMedic1 = self;
else
temp->monsterinfo.badMedic2 = self;
temp->think = DeleteBadMedic;
temp->nextthink = level.time + 60;
}
// clean up self
self->monsterinfo.aiflags &= ~AI_MEDIC;
if ((self->oldenemy) && (self->oldenemy->inuse))
self->enemy = self->oldenemy;
else
self->enemy = NULL;
self->monsterinfo.medicTries = 0;
}
// Lazarus: embedded returns true if argument entity's bounding box intersects
// a solid.
qboolean embedded (edict_t *ent)
{
trace_t tr;
tr = gi.trace(ent->s.origin,ent->mins,ent->maxs,ent->s.origin,ent,MASK_MONSTERSOLID);
if (tr.startsolid)
return true;
else
return false;
}
edict_t *medic_FindDeadMonster (edict_t *self)
{
edict_t *ent = NULL;
edict_t *best = NULL;
while ((ent = findradius(ent, self->s.origin, 1024)) != NULL)
{
if (ent == self)
continue;
if (!(ent->svflags & SVF_MONSTER))
continue;
if (ent->monsterinfo.aiflags & AI_GOOD_GUY)
continue;
if (ent->owner)
continue;
if (ent->health > 0)
continue;
if (ent->nextthink && (ent->think != M_FliesOff) && (ent->think != M_FliesOn))
continue;
// check to make sure we haven't bailed on this guy already
if ((ent->monsterinfo.badMedic1 == self) || (ent->monsterinfo.badMedic2 == self))
continue;
if (!visible(self, ent))
continue;
if (embedded(ent))
continue;
if (!canReach(self,ent))
continue;
if (!best)
{
best = ent;
continue;
}
if (ent->max_health <= best->max_health)
continue;
best = ent;
}
if (best)
{
self->oldenemy = self->enemy;
self->enemy = best;
self->enemy->owner = best;
self->monsterinfo.aiflags |= AI_MEDIC;
self->monsterinfo.aiflags &= ~AI_MEDIC_PATROL;
self->monsterinfo.medicTries = 0;
self->movetarget = self->goalentity = NULL;
self->enemy->monsterinfo.healer = self;
self->timestamp = level.time + MEDIC_TRY_TIME;
FoundTarget (self);
if (developer->value)
gi.dprintf("medic found dead monster: %s at %s\n",
best->classname,vtos(best->s.origin));
}
return best;
}
void medic_StopPatrolling (edict_t *self)
{
self->goalentity = NULL;
self->movetarget = NULL;
self->monsterinfo.aiflags &= ~AI_MEDIC_PATROL;
if (!(self->monsterinfo.aiflags & AI_MEDIC))
{
if (medic_FindDeadMonster(self))
return;
}
if (has_valid_enemy(self))
{
if (visible(self, self->enemy))
{
FoundTarget (self);
return;
}
HuntTarget (self);
return;
}
if (self->monsterinfo.aiflags & AI_MEDIC)
abortHeal(self,false);
}
void medic_NextPatrolPoint (edict_t *self, edict_t *hint)
{
edict_t *next=NULL;
edict_t *e;
vec3_t dir;
qboolean switch_paths=false;
self->monsterinfo.aiflags &= ~AI_MEDIC_PATROL;
// if (self->monsterinfo.aiflags & AI_MEDIC)
// return;
if (self->goalentity == hint)
self->goalentity = NULL;
if (self->movetarget == hint)
self->movetarget = NULL;
if (!(self->monsterinfo.aiflags & AI_MEDIC))
{
if (medic_FindDeadMonster(self))
return;
}
if (self->monsterinfo.pathdir == 1)
{
if (hint->hint_chain)
next = hint->hint_chain;
else
{
self->monsterinfo.pathdir = -1;
switch_paths = true;
}
}
if (self->monsterinfo.pathdir == -1)
{
e = hint_chain_starts[hint->hint_chain_id];
while(e)
{
if (e->hint_chain == hint)
{
next = e;
break;
}
e = e->hint_chain;
}
}
if (!next)
{
self->monsterinfo.pathdir = 1;
next = hint->hint_chain;
switch_paths = true;
}
// If switch_paths is true, we reached the end of a hint_chain. Just for grins,
// search for *another* visible hint_path chain and use it if it's reasonably close
if (switch_paths && hint_chain_count > 1)
{
edict_t *e;
edict_t *alternate=NULL;
float dist;
vec3_t dir;
int i;
float bestdistance=512;
for(i=game.maxclients+1; i<globals.num_edicts; i++)
{
e = &g_edicts[i];
if (!e->inuse)
continue;
if (Q_stricmp(e->classname,"hint_path"))
continue;
if (next && (e->hint_chain_id == next->hint_chain_id))
continue;
if (!visible(self,e))
continue;
if (!canReach(self,e))
continue;
VectorSubtract(e->s.origin,self->s.origin,dir);
dist = VectorLength(dir);
if (dist < bestdistance)
{
alternate = e;
bestdistance = dist;
}
}
if (alternate)
next = alternate;
}
if (next)
{
self->hint_chain_id = next->hint_chain_id;
VectorSubtract(next->s.origin, self->s.origin, dir);
self->ideal_yaw = vectoyaw(dir);
self->goalentity = self->movetarget = next;
self->monsterinfo.pausetime = 0;
self->monsterinfo.aiflags |= AI_MEDIC_PATROL;
self->monsterinfo.aiflags &= ~(AI_SOUND_TARGET | AI_PURSUIT_LAST_SEEN | AI_PURSUE_NEXT | AI_PURSUE_TEMP);
// run for it
self->monsterinfo.run (self);
}
else
{
self->monsterinfo.pausetime = level.time + 100000000;
self->monsterinfo.stand (self);
}
}
void medic_idle (edict_t *self)
{
if (!(self->spawnflags & SF_MONSTER_AMBUSH))
gi.sound (self, CHAN_VOICE, sound_idle1, 1, ATTN_IDLE, 0);
if (self->monsterinfo.aiflags & AI_MEDIC)
{
// Then we must have reached this point after losing sight
// of our patient.
abortHeal(self,false);
}
if (medic_FindDeadMonster(self))
return;
// If the map has hint_paths, AND the medic isn't at a HOLD point_combat,
// AND the medic has previously called FoundTarget (trail_time set to
// level.time), then look for hint_path chain and follow it, hopefully
// to find monsters to resurrect
if (self->monsterinfo.aiflags & AI_HINT_TEST)
return;
if (hint_chains_exist && !(self->monsterinfo.aiflags & AI_STAND_GROUND)
&& ((self->monsterinfo.trail_time > 0) || medic_test) )
{
edict_t *e;
edict_t *hint=NULL;
float dist;
vec3_t dir;
int i;
float bestdistance=99999;
for(i=game.maxclients+1; i<globals.num_edicts; i++)
{
e = &g_edicts[i];
if (!e->inuse)
continue;
if (Q_stricmp(e->classname,"hint_path"))
continue;
if (!visible(self,e))
continue;
if (!canReach(self,e))
continue;
VectorSubtract(e->s.origin,self->s.origin,dir);
dist = VectorLength(dir);
if (dist < bestdistance)
{
hint = e;
bestdistance = dist;
}
}
if (hint)
{
self->hint_chain_id = hint->hint_chain_id;
if (!self->monsterinfo.pathdir)
self->monsterinfo.pathdir = 1;
VectorSubtract(hint->s.origin, self->s.origin, dir);
self->ideal_yaw = vectoyaw(dir);
self->goalentity = self->movetarget = hint;
self->monsterinfo.pausetime = 0;
self->monsterinfo.aiflags |= AI_MEDIC_PATROL;
self->monsterinfo.aiflags &= ~(AI_SOUND_TARGET | AI_PURSUIT_LAST_SEEN | AI_PURSUE_NEXT | AI_PURSUE_TEMP);
// run for it
self->monsterinfo.run (self);
}
}
}
void medic_search (edict_t *self)
{
gi.sound (self, CHAN_VOICE, sound_search, 1, ATTN_IDLE, 0);
if (!self->oldenemy)
medic_FindDeadMonster(self);
}
void medic_sight (edict_t *self, edict_t *other)
{
gi.sound (self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0);
}
mframe_t medic_frames_stand [] =
{
ai_stand, 0, medic_idle,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
ai_stand, 0, NULL,
};
mmove_t medic_move_stand = {FRAME_wait1, FRAME_wait90, medic_frames_stand, NULL};
void medic_stand (edict_t *self)
{
self->monsterinfo.currentmove = &medic_move_stand;
}
mframe_t medic_frames_walk [] =
{
ai_walk, 6.2, NULL,
ai_walk, 18.1, NULL,
ai_walk, 1, NULL,
ai_walk, 9, NULL,
ai_walk, 10, NULL,
ai_walk, 9, NULL,
ai_walk, 11, NULL,
ai_walk, 11.6, NULL,
ai_walk, 2, NULL,
ai_walk, 9.9, NULL,
ai_walk, 14, NULL,
ai_walk, 9.3, NULL
};
mmove_t medic_move_walk = {FRAME_walk1, FRAME_walk12, medic_frames_walk, NULL};
void medic_walk (edict_t *self)
{
self->monsterinfo.currentmove = &medic_move_walk;
}
mframe_t medic_frames_run [] =
{
ai_run, 18, NULL,
ai_run, 22.5, NULL,
ai_run, 25.4, NULL,
ai_run, 23.4, NULL,
ai_run, 24, NULL,
ai_run, 35.6, NULL
};
mmove_t medic_move_run = {FRAME_run1, FRAME_run6, medic_frames_run, NULL};
void medic_run (edict_t *self)
{
if (!(self->monsterinfo.aiflags & AI_MEDIC))
{
if (medic_FindDeadMonster(self))
return;
}
if (self->monsterinfo.aiflags & AI_STAND_GROUND)
self->monsterinfo.currentmove = &medic_move_stand;
else
self->monsterinfo.currentmove = &medic_move_run;
}
mframe_t medic_frames_pain1 [] =
{
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL
};
mmove_t medic_move_pain1 = {FRAME_paina1, FRAME_paina8, medic_frames_pain1, medic_run};
mframe_t medic_frames_pain2 [] =
{
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL
};
mmove_t medic_move_pain2 = {FRAME_painb1, FRAME_painb15, medic_frames_pain2, medic_run};
void medic_pain (edict_t *self, edict_t *other, float kick, int damage)
{
if (self->health < (self->max_health / 2))
self->s.skinnum |= 1;
if (level.time < self->pain_debounce_time)
return;
self->pain_debounce_time = level.time + 3;
if (skill->value == 3)
return; // no pain anims in nightmare
if (random() < 0.5)
{
self->monsterinfo.currentmove = &medic_move_pain1;
gi.sound (self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0);
}
else
{
self->monsterinfo.currentmove = &medic_move_pain2;
gi.sound (self, CHAN_VOICE, sound_pain2, 1, ATTN_NORM, 0);
}
}
void medic_fire_blaster (edict_t *self)
{
vec3_t start;
vec3_t forward, right;
vec3_t end;
vec3_t dir;
int effect;
if ((self->s.frame == FRAME_attack9) || (self->s.frame == FRAME_attack12))
effect = EF_BLASTER;
else if ((self->s.frame == FRAME_attack19) || (self->s.frame == FRAME_attack22) || (self->s.frame == FRAME_attack25) || (self->s.frame == FRAME_attack28))
effect = EF_HYPERBLASTER;
else
effect = 0;
AngleVectors (self->s.angles, forward, right, NULL);
G_ProjectSource (self->s.origin, monster_flash_offset[MZ2_MEDIC_BLASTER_1], forward, right, start);
VectorCopy (self->enemy->s.origin, end);
end[2] += self->enemy->viewheight;
// Lazarus fog reduction of accuracy
if (self->monsterinfo.visibility < FOG_CANSEEGOOD)
{
end[0] += crandom() * 640 * (FOG_CANSEEGOOD - self->monsterinfo.visibility);
end[1] += crandom() * 640 * (FOG_CANSEEGOOD - self->monsterinfo.visibility);
end[2] += crandom() * 320 * (FOG_CANSEEGOOD - self->monsterinfo.visibility);
}
VectorSubtract (end, start, dir);
monster_fire_blaster (self, start, dir, 2, 1000, MZ2_MEDIC_BLASTER_1, effect, BLASTER_ORANGE);
}
void medic_dead (edict_t *self)
{
VectorSet (self->mins, -16, -16, -24);
VectorSet (self->maxs, 16, 16, -8);
self->movetype = MOVETYPE_TOSS;
self->svflags |= SVF_DEADMONSTER;
self->nextthink = 0;
gi.linkentity (self);
M_FlyCheck (self);
// Lazarus monster fade
if (world->effects & FX_WORLDSPAWN_CORPSEFADE)
{
self->think=FadeDieSink;
self->nextthink=level.time+corpse_fadetime->value;
}
}
mframe_t medic_frames_death [] =
{
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL,
ai_move, 0, NULL
};
mmove_t medic_move_death = {FRAME_death1, FRAME_death30, medic_frames_death, medic_dead};
void medic_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point)
{
int n;
self->s.skinnum |= 1;
self->monsterinfo.power_armor_type = POWER_ARMOR_NONE;
// if we had a pending patient, free him up for another medic
if ((self->enemy) && (self->enemy->owner == self))
self->enemy->owner = NULL;
// check for gib
if (self->health <= self->gib_health && !(self->spawnflags & SF_MONSTER_NOGIB))
{
gi.sound (self, CHAN_VOICE, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0);
for (n= 0; n < 2; n++)
ThrowGib (self, "models/objects/gibs/bone/tris.md2", damage, GIB_ORGANIC);
for (n= 0; n < 4; n++)
ThrowGib (self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC);
ThrowHead (self, "models/objects/gibs/head2/tris.md2", damage, GIB_ORGANIC);
self->deadflag = DEAD_DEAD;
return;
}
if (self->deadflag == DEAD_DEAD)
return;
// regular death
gi.sound (self, CHAN_VOICE, sound_die, 1, ATTN_NORM, 0);
self->deadflag = DEAD_DEAD;
self->takedamage = DAMAGE_YES;
self->monsterinfo.currentmove = &medic_move_death;
}
void medic_duck_down (edict_t *self)
{
if (self->monsterinfo.aiflags & AI_DUCKED)
return;
self->monsterinfo.aiflags |= AI_DUCKED;
self->maxs[2] -= 32;
self->takedamage = DAMAGE_YES;
self->monsterinfo.pausetime = level.time + 1;
gi.linkentity (self);
}
void medic_duck_hold (edict_t *self)
{
if (level.time >= self->monsterinfo.pausetime)
self->monsterinfo.aiflags &= ~AI_HOLD_FRAME;
else
self->monsterinfo.aiflags |= AI_HOLD_FRAME;
}
void medic_duck_up (edict_t *self)
{
self->monsterinfo.aiflags &= ~AI_DUCKED;
self->maxs[2] += 32;
self->takedamage = DAMAGE_AIM;
gi.linkentity (self);
}
mframe_t medic_frames_duck [] =
{
ai_move, -1, NULL,
ai_move, -1, NULL,
ai_move, -1, medic_duck_down,
ai_move, -1, medic_duck_hold,
ai_move, -1, NULL,
ai_move, -1, NULL,
ai_move, -1, medic_duck_up,
ai_move, -1, NULL,
ai_move, -1, NULL,
ai_move, -1, NULL,
ai_move, -1, NULL,
ai_move, -1, NULL,
ai_move, -1, NULL,
ai_move, -1, NULL,
ai_move, -1, NULL,
ai_move, -1, NULL
};
mmove_t medic_move_duck = {FRAME_duck1, FRAME_duck16, medic_frames_duck, medic_run};
void medic_dodge (edict_t *self, edict_t *attacker, float eta)
{
if (random() > 0.25)
return;
if (!self->enemy)
self->enemy = attacker;
self->monsterinfo.currentmove = &medic_move_duck;
}
mframe_t medic_frames_attackHyperBlaster [] =
{
ai_charge, 0, NULL,
ai_charge, 0, NULL,
ai_charge, 0, NULL,
ai_charge, 0, NULL,
ai_charge, 0, medic_fire_blaster,
ai_charge, 0, medic_fire_blaster,
ai_charge, 0, medic_fire_blaster,
ai_charge, 0, medic_fire_blaster,
ai_charge, 0, medic_fire_blaster,
ai_charge, 0, medic_fire_blaster,
ai_charge, 0, medic_fire_blaster,
ai_charge, 0, medic_fire_blaster,
ai_charge, 0, medic_fire_blaster,
ai_charge, 0, medic_fire_blaster,
ai_charge, 0, medic_fire_blaster,
ai_charge, 0, medic_fire_blaster
};
mmove_t medic_move_attackHyperBlaster = {FRAME_attack15, FRAME_attack30, medic_frames_attackHyperBlaster, medic_run};
void medic_continue (edict_t *self)
{
if (visible (self, self->enemy) )
if (random() <= 0.95)
self->monsterinfo.currentmove = &medic_move_attackHyperBlaster;
}
mframe_t medic_frames_attackBlaster [] =
{
ai_charge, 0, NULL,
ai_charge, 5, NULL,
ai_charge, 5, NULL,
ai_charge, 3, NULL,
ai_charge, 2, NULL,
ai_charge, 0, NULL,
ai_charge, 0, NULL,
ai_charge, 0, NULL,
ai_charge, 0, medic_fire_blaster,
ai_charge, 0, NULL,
ai_charge, 0, NULL,
ai_charge, 0, medic_fire_blaster,
ai_charge, 0, NULL,
ai_charge, 0, medic_continue // Change to medic_continue... Else, go to frame 32
};
mmove_t medic_move_attackBlaster = {FRAME_attack1, FRAME_attack14, medic_frames_attackBlaster, medic_run};
void medic_hook_launch (edict_t *self)
{
gi.sound (self, CHAN_WEAPON, sound_hook_launch, 1, ATTN_NORM, 0);
}
void ED_CallSpawn (edict_t *ent);
static vec3_t medic_cable_offsets[] =
{
45.0, -9.2, 15.5,
48.4, -9.7, 15.2,
47.8, -9.8, 15.8,
47.3, -9.3, 14.3,
45.4, -10.1, 13.1,
41.9, -12.7, 12.0,
37.8, -15.8, 11.2,
34.3, -18.4, 10.7,
32.7, -19.7, 10.4,
32.7, -19.7, 10.4
};
void medic_cable_attack (edict_t *self)
{
vec3_t offset, start, end, f, r;
trace_t tr;
vec3_t dir;
float distance;
if ((!self->enemy) || (!self->enemy->inuse) || (self->enemy->svflags & SVF_GIB))
{
//gi.dprintf ("medic_cable_attack: aborting heal due to target being removed or gibbed\n");
abortHeal (self,false);
return;
}
//Knightmare- don't heal insanes or actors or critters
if (!strcmp (self->enemy->classname, "misc_insane") || !strcmp (self->enemy->classname, "misc_actor")
|| !strcmp (self->enemy->classname, "monster_mutant") || !strcmp (self->enemy->classname, "monster_flipper"))
{
//gi.dprintf ("medic_cable_attack: not healing insane or actor or critter\n");
abortHeal (self, true);
return;
}
// Lazarus: check embeddment
if (embedded(self->enemy))
{
//gi.dprintf ("medic_cable_attack: dead monster embedded in solid, aborting heal\n");
abortHeal (self,false);
return;
}
// see if our enemy has changed to a client, or our target has more than 0 health,
// abort it .. we got switched to someone else due to damage
if ((self->enemy->client) || (self->enemy->health > 0))
{
//gi.dprintf ("medic_cable_attack: aborting heal due to target health > 0 or client\n");
abortHeal (self,false);
return;
}
AngleVectors (self->s.angles, f, r, NULL);
VectorCopy (medic_cable_offsets[self->s.frame - FRAME_attack42], offset);
G_ProjectSource (self->s.origin, offset, f, r, start);
// check for max distance
// Lazarus: Not needed, done in checkattack
// check for min distance
VectorSubtract (self->enemy->s.origin, start, dir);
distance = VectorLength(dir);
if (distance < MEDIC_MIN_DISTANCE)
{
//gi.dprintf("medic_cable_attack: MEDIC_MIN_DISTANCE\n");
abortHeal (self,false);
return;
}
// Lazarus: Check for enemy behind muzzle... don't do these guys, 'cause usually this
// results in monster entanglement
VectorNormalize(dir);
if (DotProduct(dir,f) < 0.)
{
//gi.dprintf ("medic_cable_attack: aborting heal due to possible entanglment\n");
abortHeal (self,false);
return;
}
// check for min/max pitch
// Rogue takes this out... makes medic more likely to heal and
// comments say "doesn't look bad when it fails"... we'll see
/* vectoangles (dir, angles);
if (angles[0] < -180)
angles[0] += 360;
if (fabs(angles[0]) > 45)
return; */
tr = gi.trace (start, NULL, NULL, self->enemy->s.origin, self, MASK_SHOT);
if (tr.fraction != 1.0 && tr.ent != self->enemy)
{
if (tr.ent == world)
{
// give up on second try
if (self->monsterinfo.medicTries > 1)
{
abortHeal (self,true);
return;
}
self->monsterinfo.medicTries++;
cleanupHeal (self, 1);
return;
}
abortHeal (self,false);
return;
}
if (self->s.frame == FRAME_attack43)
{
gi.sound (self->enemy, CHAN_AUTO, sound_hook_hit, 1, ATTN_NORM, 0);
self->enemy->monsterinfo.aiflags |= AI_RESURRECTING;
M_SetEffects(self->enemy);
}
else if (self->s.frame == FRAME_attack50)
{
self->enemy->spawnflags &= SF_MONSTER_NOGIB;
self->enemy->monsterinfo.aiflags = 0;
self->enemy->target = NULL;
self->enemy->targetname = NULL;
self->enemy->combattarget = NULL;
self->enemy->deathtarget = NULL;
self->enemy->owner = self;
// Lazarus: reset initially dead monsters to use the INVERSE of their
// initial health, and force gib_health to default value
if (self->enemy->max_health < 0)
{
self->enemy->max_health = -self->enemy->max_health;
self->enemy->gib_health = 0;
}
self->enemy->health = self->enemy->max_health;
self->enemy->takedamage = DAMAGE_AIM;
self->enemy->flags &= ~FL_NO_KNOCKBACK;
self->enemy->pain_debounce_time = 0;
self->enemy->damage_debounce_time = 0;
self->enemy->deadflag = DEAD_NO;
if (self->enemy->s.effects & EF_FLIES)
M_FliesOff(self->enemy);
ED_CallSpawn (self->enemy);
self->enemy->monsterinfo.healer = NULL;
self->enemy->owner = NULL;
// Knightmare- disable deadmonster_think
if (self->enemy->postthink)
self->enemy->postthink = NULL;
if (self->enemy->think)
{
self->enemy->nextthink = level.time;
self->enemy->think (self->enemy);
}
self->enemy->monsterinfo.aiflags &= ~AI_RESURRECTING;
M_SetEffects(self->enemy);
if (self->oldenemy && self->oldenemy->client)
{
self->enemy->enemy = self->oldenemy;
FoundTarget (self->enemy);
}
else
{
// Lazarus: this should make oblivious monsters
// find player again
self->enemy->enemy = NULL;
}
}
else
{
if (self->s.frame == FRAME_attack44)
gi.sound (self, CHAN_WEAPON, sound_hook_heal, 1, ATTN_NORM, 0);
}
// adjust start for beam origin being in middle of a segment
// Lazarus: This isn't right... this causes cable start point to be well above muzzle
// when target is closeby. f should be vector from muzzle to target, not
// the forward viewing direction. PLUS... 8 isn't right... the model is
// actually 32 units long, so use 16.. fixed below.
// VectorMA (start, 8, f, start);
//Knightmare- if enemy went away, like after returning from another level, return
if (!self->enemy)
return;
// adjust end z for end spot since the monster is currently dead
VectorCopy (self->enemy->s.origin, end);
end[2] = self->enemy->absmin[2] + self->enemy->size[2] / 2;
// Lazarus fix
VectorSubtract(end,start,f);
VectorNormalize(f);
VectorMA(start,16,f,start);
gi.WriteByte (svc_temp_entity);
gi.WriteByte (TE_MEDIC_CABLE_ATTACK);
gi.WriteShort (self - g_edicts);
gi.WritePosition (start);
gi.WritePosition (end);
gi.multicast (self->s.origin, MULTICAST_PVS);
}
void medic_hook_retract (edict_t *self)
{
gi.sound (self, CHAN_WEAPON, sound_hook_retract, 1, ATTN_NORM, 0);
if (self->enemy)
{
self->enemy->monsterinfo.aiflags &= ~AI_RESURRECTING;
M_SetEffects (self->enemy);
}
}
mframe_t medic_frames_attackCable [] =
{
ai_move, 2, NULL,
ai_move, 3, NULL,
ai_move, 5, NULL,
ai_move, 4.4, NULL,
ai_charge, 4.7, NULL,
ai_charge, 5, NULL,
ai_charge, 6, NULL,
ai_charge, 4, NULL,
ai_charge, 0, NULL,
ai_move, 0, medic_hook_launch,
ai_move, 0, medic_cable_attack,
ai_move, 0, medic_cable_attack,
ai_move, 0, medic_cable_attack,
ai_move, 0, medic_cable_attack,
ai_move, 0, medic_cable_attack,
ai_move, 0, medic_cable_attack,
ai_move, 0, medic_cable_attack,
ai_move, 0, medic_cable_attack,
ai_move, 0, medic_cable_attack,
ai_move, -15, medic_hook_retract,
ai_move, -1.5, NULL,
ai_move, -1.2, NULL,
ai_move, -3, NULL,
ai_move, -2, NULL,
ai_move, 0.3, NULL,
ai_move, 0.7, NULL,
ai_move, 1.2, NULL,
ai_move, 1.3, NULL
};
mmove_t medic_move_attackCable = {FRAME_attack33, FRAME_attack60, medic_frames_attackCable, medic_run};
void medic_attack(edict_t *self)
{
if (self->monsterinfo.aiflags & AI_MEDIC)
self->monsterinfo.currentmove = &medic_move_attackCable;
else
self->monsterinfo.currentmove = &medic_move_attackBlaster;
}
qboolean medic_checkattack (edict_t *self)
{
if (!(self->monsterinfo.aiflags & AI_MEDIC))
{
if ( medic_FindDeadMonster(self) )
return false;
}
if (self->monsterinfo.aiflags & AI_MEDIC)
{
float r;
vec3_t forward, right, offset, start;
trace_t tr;
// if we have 5 seconds or less before a timeout,
// look for a hint_path to the target
if ( (self->timestamp < level.time + 5) &&
(self->monsterinfo.last_hint_time + 5 < level.time) )
{
// check for hint_paths.
self->monsterinfo.last_hint_time = level.time;
if (hintcheck_monsterlost(self))
{
if (developer->value)
gi.dprintf("medic at %s using hint_paths to find %s\n",
vtos(self->s.origin), self->enemy->classname);
self->timestamp = level.time + MEDIC_TRY_TIME;
return false;
}
}
// if we ran out of time, give up
if (self->timestamp < level.time)
{
//if (developer->value)
// gi.dprintf("medic at %s timed out, abort heal\n",vtos(self->s.origin));
abortHeal (self, true);
self->timestamp = 0;
return false;
}
// if our target went away
if ((!self->enemy) || (!self->enemy->inuse)) {
abortHeal (self,false);
return false;
}
// if target is embedded in a solid
if (embedded(self->enemy))
{
abortHeal (self,false);
return false;
}
r = realrange(self,self->enemy);
if (r > MEDIC_MAX_HEAL_DISTANCE+10) {
self->monsterinfo.attack_state = AS_STRAIGHT;
//abortHeal(self,false);
return false;
} else if (r < MEDIC_MIN_DISTANCE) {
abortHeal(self,false);
return false;
}
// Lazarus 1.6.2.3: if point-to-point vector from cable to
// target is blocked by a solid
AngleVectors (self->s.angles, forward, right, NULL);
// Offset [8] has the largest displacement to the left... not a sure
// thing but this one should be the most severe test.
VectorCopy (medic_cable_offsets[8], offset);
G_ProjectSource (self->s.origin, offset, forward, right, start);
tr = gi.trace(start,NULL,NULL,self->enemy->s.origin,self,MASK_SHOT|MASK_WATER);
if (tr.fraction < 1.0 && tr.ent != self->enemy)
return false;
medic_attack(self);
return true;
}
// Lazarus: NEVER attack other monsters
if ((self->enemy) && (self->enemy->svflags & SVF_MONSTER))
{
self->enemy = self->oldenemy;
self->oldenemy = NULL;
if (self->enemy && self->enemy->inuse)
{
if (visible(self,self->enemy))
FoundTarget(self);
else
HuntTarget(self);
}
return false;
}
return M_CheckAttack (self);
}
/*QUAKED monster_medic (1 .5 0) (-16 -16 -24) (16 16 32) Ambush Trigger_Spawn Sight
*/
void SP_monster_medic (edict_t *self)
{
if (deathmatch->value)
{
G_FreeEdict (self);
return;
}
if (world->effects & FX_WORLDSPAWN_CORPSEFADE)
{
G_FreeEdict (self);
return;
}
sound_idle1 = gi.soundindex ("medic/idle.wav");
sound_pain1 = gi.soundindex ("medic/medpain1.wav");
sound_pain2 = gi.soundindex ("medic/medpain2.wav");
sound_die = gi.soundindex ("medic/meddeth1.wav");
sound_sight = gi.soundindex ("medic/medsght1.wav");
sound_search = gi.soundindex ("medic/medsrch1.wav");
sound_hook_launch = gi.soundindex ("medic/medatck2.wav");
sound_hook_hit = gi.soundindex ("medic/medatck3.wav");
sound_hook_heal = gi.soundindex ("medic/medatck4.wav");
sound_hook_retract = gi.soundindex ("medic/medatck5.wav");
gi.soundindex ("medic/medatck1.wav");
self->movetype = MOVETYPE_STEP;
self->solid = SOLID_BBOX;
// Lazarus: special purpose skins
if ( self->style )
{
PatchMonsterModel("models/monsters/medic/tris.md2");
self->s.skinnum = self->style * 2;
}
self->s.modelindex = gi.modelindex ("models/monsters/medic/tris.md2");
VectorSet (self->mins, -24, -24, -24);
VectorSet (self->maxs, 24, 24, 32);
// Lazarus: mapper-configurable health
if (!self->health)
self->health = 300;
if (!self->gib_health)
self->gib_health = -130;
if (!self->mass)
self->mass = 400;
self->pain = medic_pain;
self->die = medic_die;
self->monsterinfo.stand = medic_stand;
self->monsterinfo.walk = medic_walk;
self->monsterinfo.run = medic_run;
self->monsterinfo.dodge = medic_dodge;
self->monsterinfo.attack = medic_attack;
self->monsterinfo.melee = NULL;
self->monsterinfo.sight = medic_sight;
self->monsterinfo.idle = medic_idle;
self->monsterinfo.search = medic_search;
self->monsterinfo.checkattack = medic_checkattack;
// Knightmare- added sparks and blood type
if (!self->blood_type)
self->blood_type = 3; //sparks and blood
// Lazarus
if (self->powerarmor) {
self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD;
self->monsterinfo.power_armor_power = self->powerarmor;
}
if (!self->monsterinfo.flies)
self->monsterinfo.flies = 0.15;
gi.linkentity (self);
self->monsterinfo.currentmove = &medic_move_stand;
if (self->health < 0)
{
mmove_t *deathmoves[] = {&medic_move_death,
NULL};
M_SetDeath(self,(mmove_t **)&deathmoves);
}
self->common_name = "Medic";
self->class_id = ENTITY_MONSTER_MEDIC;
self->monsterinfo.scale = MODEL_SCALE;
walkmonster_start (self);
}