1107 lines
29 KiB
C
1107 lines
29 KiB
C
//==============================================================================
|
|
//
|
|
// m_seraph.c
|
|
//
|
|
// Heretic II
|
|
// Copyright 1998 Raven Software
|
|
//
|
|
// jweier
|
|
//==============================================================================
|
|
|
|
#include "g_local.h"
|
|
#include "m_seraph.h"
|
|
#include "m_seraph_anim.h"
|
|
#include "Utilities.h"
|
|
#include "g_DefaultMessageHandler.h"
|
|
#include "g_monster.h"
|
|
#include "Random.h"
|
|
#include "vector.h"
|
|
#include "fx.h"
|
|
#include "g_HitLocation.h"
|
|
#include "g_misc.h"
|
|
|
|
#include "m_stats.h"
|
|
|
|
void MG_InitMoods(edict_t *self);
|
|
//Seraphs need knowledge of the ogle's animations
|
|
//Any changes in m_ogle.h must be mirrored here
|
|
enum
|
|
{
|
|
OGLE_ANIM_WALK1,
|
|
OGLE_ANIM_PUSH1,
|
|
OGLE_ANIM_PUSH2,
|
|
OGLE_ANIM_PUSH3,
|
|
OGLE_ANIM_STAND1,
|
|
OGLE_ANIM_WORK1,
|
|
OGLE_ANIM_WORK2,
|
|
OGLE_ANIM_WORK3,
|
|
OGLE_ANIM_WORK4,
|
|
OGLE_ANIM_WORK5,
|
|
OGLE_ANIM_PAIN1,
|
|
OGLE_ANIM_PAIN2,
|
|
OGLE_ANIM_PAIN3,
|
|
OGLE_ANIM_REST1_TRANS,
|
|
OGLE_ANIM_REST1_WIPE,
|
|
OGLE_ANIM_REST1,
|
|
OGLE_ANIM_REST2_WIPE,
|
|
OGLE_ANIM_REST3_WIPE,
|
|
OGLE_ANIM_REST4_TRANS,
|
|
OGLE_ANIM_REST4_TRANS2,
|
|
OGLE_ANIM_REST4,
|
|
OGLE_ANIM_CELEBRATE1,
|
|
OGLE_ANIM_CELEBRATE2,
|
|
OGLE_ANIM_CELEBRATE3_TRANS,
|
|
OGLE_ANIM_CELEBRATE3,
|
|
OGLE_ANIM_CELEBRATE4_TRANS,
|
|
OGLE_ANIM_CELEBRATE4,
|
|
OGLE_ANIM_CELEBRATE5_TRANS,
|
|
OGLE_ANIM_CELEBRATE5,
|
|
OGLE_ANIM_CHARGE1,
|
|
OGLE_ANIM_CHARGE2,
|
|
OGLE_ANIM_CHARGE3,
|
|
OGLE_ANIM_CHARGE4,
|
|
OGLE_ANIM_CHARGE5,
|
|
OGLE_ANIM_ATTACK1,
|
|
OGLE_ANIM_ATTACK2,
|
|
OGLE_ANIM_ATTACK3,
|
|
OGLE_ANIM_DEATH1,
|
|
OGLE_ANIM_DEATH2,
|
|
NUM_OGLE_ANIMS
|
|
};
|
|
|
|
static animmove_t *animations[NUM_ANIMS] =
|
|
{
|
|
&seraph_move_walk1,
|
|
&seraph_move_walk2,
|
|
&seraph_move_whip1,
|
|
&seraph_move_whip1_loop,
|
|
&seraph_move_whip1_end,
|
|
&seraph_move_stand1,
|
|
&seraph_move_stand1_tr,
|
|
&seraph_move_stand1_r,
|
|
&seraph_move_stand1_trc,
|
|
&seraph_move_stand1_tl,
|
|
&seraph_move_stand1_l,
|
|
&seraph_move_stand1_tlc,
|
|
&seraph_move_point1,
|
|
&seraph_move_run1,
|
|
&seraph_move_fjump,
|
|
&seraph_move_run1_whip,
|
|
&seraph_move_pain,
|
|
&seraph_move_swipe,
|
|
&seraph_move_get2work,
|
|
&seraph_move_get2work2,
|
|
&seraph_move_startle,
|
|
&seraph_move_ready2idle,
|
|
&seraph_move_backup,
|
|
&seraph_move_death1,
|
|
&seraph_move_death2_go,
|
|
&seraph_move_death2_loop,
|
|
&seraph_move_death2_end,
|
|
&seraph_move_backup2,
|
|
};
|
|
|
|
static int sounds[NUM_SOUNDS];
|
|
static ClassResourceInfo_t resInfo;
|
|
|
|
#define OVERLORD_RADIUS 1000 //FIXME: Tweak out
|
|
|
|
/*
|
|
==========================================================
|
|
|
|
Seraph Helper functions
|
|
|
|
==========================================================
|
|
*/
|
|
|
|
void seraphApplyJump (edict_t *self)
|
|
{
|
|
self->jump_time = level.time + 0.75;
|
|
VectorCopy(self->movedir, self->velocity);
|
|
VectorNormalize(self->movedir);
|
|
}
|
|
|
|
void seraph_dead ( edict_t *self )
|
|
{
|
|
self->health = 0;
|
|
self->solid = SOLID_NOT;
|
|
|
|
M_EndDeath ( self );
|
|
}
|
|
|
|
void seraph_death_loop ( edict_t *self )
|
|
{
|
|
SetAnim(self, ANIM_DEATH2_LOOP);
|
|
}
|
|
|
|
void seraph_check_land ( edict_t *self )
|
|
{
|
|
vec3_t endpos;
|
|
trace_t trace;
|
|
|
|
M_ChangeYaw(self);
|
|
|
|
VectorCopy(self->s.origin, endpos);
|
|
endpos[2] -= 48;
|
|
|
|
gi.trace(self->s.origin, self->mins, self->maxs, endpos, self, MASK_MONSTERSOLID,&trace);
|
|
|
|
if ( (trace.fraction < 1 || trace.allsolid || trace.startsolid ) && self->curAnimID != ANIM_DEATH2_END && self->curAnimID != ANIM_DEATH2_GO)
|
|
{
|
|
self->elasticity = 1.25;
|
|
self->friction = 0.5;
|
|
SetAnim(self, ANIM_DEATH2_END);
|
|
}
|
|
}
|
|
|
|
void seraph_sound_startle(edict_t *self)
|
|
{
|
|
gi.sound (self, CHAN_VOICE, sounds[SND_STARTLE], 1, ATTN_NORM, 0);
|
|
}
|
|
|
|
void seraph_sound_slap(edict_t *self)
|
|
{
|
|
gi.sound (self, CHAN_WEAPON, sounds[SND_SLAP], 1, ATTN_NORM, 0);
|
|
}
|
|
|
|
void seraph_sound_scold(edict_t *self)
|
|
{//no talking!
|
|
gi.sound (self, CHAN_VOICE, sounds[SND_SCOLD3], 1, ATTN_NORM, 0);
|
|
}
|
|
|
|
void seraph_sound_scold2(edict_t *self)
|
|
{
|
|
if(irand(0, 1))//back to work!
|
|
gi.sound(self, CHAN_VOICE, sounds[SND_SCOLD1], 1, ATTN_NORM, 0);
|
|
else
|
|
gi.sound(self, CHAN_VOICE, sounds[SND_SCOLD2], 1, ATTN_NORM, 0);
|
|
}
|
|
|
|
void seraph_sound_yell(edict_t *self)
|
|
{
|
|
gi.sound (self, CHAN_VOICE, sounds[SND_SCARE], 1, ATTN_NORM, 0);
|
|
}
|
|
|
|
void seraph_sound_whip(edict_t *self)
|
|
{
|
|
gi.sound (self, CHAN_WEAPON, sounds[SND_ATTACK], 1, ATTN_NORM, 0);
|
|
}
|
|
|
|
//Become startled and look around
|
|
void seraph_startle(edict_t *self)
|
|
{
|
|
SetAnim(self, ANIM_STARTLE);
|
|
}
|
|
|
|
//Seraph has finished his startle, either track down the enemy, or go back to normal
|
|
void seraph_done_startle(edict_t *self)
|
|
{
|
|
if (!FindTarget(self))
|
|
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
|
|
}
|
|
|
|
//Seraph has finished ANIM_GET2WORK, and must reset its enemy
|
|
void seraph_done_get2work(edict_t *self)
|
|
{
|
|
if(self->enemy)
|
|
self->enemy->targeted = 0;
|
|
self->enemy = NULL;
|
|
self->ai_mood = AI_MOOD_STAND;
|
|
self->ai_mood_flags &= ~AI_MOOD_FLAG_IGNORE;
|
|
seraph_pause(self);
|
|
}
|
|
|
|
//Special walk code for going to yell at an ogle
|
|
void seraph_ai_walk(edict_t *self, float dist)
|
|
{
|
|
qboolean MG_MoveToGoal (edict_t *self, float dist);
|
|
|
|
if (self->enemy && M_DistanceToTarget(self, self->enemy) < 72)
|
|
{
|
|
self->ai_mood = AI_MOOD_STAND;
|
|
if (irand(0,1))
|
|
SetAnim(self, ANIM_GET2WORK);
|
|
else
|
|
SetAnim(self, ANIM_GET2WORK2);
|
|
}
|
|
else
|
|
{
|
|
MG_MoveToGoal(self, dist);
|
|
}
|
|
}
|
|
|
|
void seraph2idle (edict_t *self)
|
|
{
|
|
SetAnim(self, ANIM_STAND1);
|
|
}
|
|
|
|
//Upper level AI interfacing
|
|
void seraph_pause(edict_t *self)
|
|
{
|
|
self->mood_think(self);
|
|
|
|
if(self->enemy)
|
|
if ((self->ai_mood_flags & AI_MOOD_FLAG_IGNORE)&& self->enemy->classID == CID_OGLE)
|
|
return;
|
|
|
|
switch (self->ai_mood)
|
|
{
|
|
case AI_MOOD_ATTACK:
|
|
QPostMessage(self, MSG_MELEE, PRI_DIRECTIVE, NULL);
|
|
break;
|
|
case AI_MOOD_PURSUE:
|
|
case AI_MOOD_NAVIGATE:
|
|
case AI_MOOD_WALK:
|
|
QPostMessage(self, MSG_RUN, PRI_DIRECTIVE, NULL);
|
|
break;
|
|
case AI_MOOD_STAND:
|
|
if (!self->enemy)
|
|
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
|
|
break;//else what?
|
|
|
|
case AI_MOOD_DELAY:
|
|
break;
|
|
|
|
case AI_MOOD_WANDER:
|
|
SetAnim(self, ANIM_WALK1);
|
|
break;
|
|
|
|
case AI_MOOD_POINT_NAVIGATE:
|
|
SetAnim(self, ANIM_WALK2);
|
|
break;
|
|
|
|
case AI_MOOD_JUMP:
|
|
SetAnim(self, ANIM_FJUMP);
|
|
break;
|
|
|
|
default :
|
|
#ifdef _DEVEL
|
|
gi.dprintf("seraph guard: Unusable mood %d!\n", self->ai_mood);
|
|
#endif
|
|
break;
|
|
}
|
|
}
|
|
|
|
void seraph_check_mood (edict_t *self, G_Message_t *msg)
|
|
{
|
|
ParseMsgParms(msg, "i", &self->ai_mood);
|
|
|
|
seraph_pause(self);
|
|
}
|
|
|
|
//Targets a specific ogle and puts it back to work
|
|
void seraph_enforce_ogle(edict_t *self)
|
|
{
|
|
if(!self->enemy)
|
|
return;
|
|
|
|
self->enemy->use(self->enemy, self, self);
|
|
|
|
if(irand(0, 1))//back to work!
|
|
gi.sound(self, CHAN_VOICE, sounds[SND_SCOLD1], 1, ATTN_NORM, 0);
|
|
else
|
|
gi.sound(self, CHAN_VOICE, sounds[SND_SCOLD2], 1, ATTN_NORM, 0);
|
|
}
|
|
|
|
//Targets all idle ogles and puts them back to work
|
|
void seraph_enforce(edict_t *self)
|
|
{
|
|
edict_t *ogle;
|
|
|
|
ogle = NULL;
|
|
|
|
while((ogle = findradius(ogle, self->s.origin, OVERLORD_RADIUS)) != NULL)
|
|
{
|
|
if (ogle->classID != CID_OGLE)
|
|
continue;
|
|
|
|
if (ogle->ai_mood != AI_MOOD_REST)
|
|
continue;
|
|
|
|
if (ogle->targetEnt != self)
|
|
continue;
|
|
|
|
//Setup within the ogle code
|
|
ogle->use(ogle, self, self);
|
|
}
|
|
|
|
}
|
|
|
|
//Check to see if you can make it to an idle ogle and scare them
|
|
qboolean seraph_checkscare(edict_t *self, edict_t *ogle)
|
|
{//FIX? refers to goalentity
|
|
trace_t trace;
|
|
vec3_t sf, of, mins;
|
|
float dot;
|
|
|
|
AngleVectors(self->s.angles, sf, NULL, NULL);
|
|
AngleVectors(ogle->s.angles, of, NULL, NULL);
|
|
|
|
dot = DotProduct(sf, of);
|
|
|
|
//Only do it if you're in back of them, and they're facing away
|
|
if (dot)
|
|
{
|
|
VectorCopy(self->mins, mins);
|
|
mins[2] += 18; //Account for step ability
|
|
gi.trace(self->s.origin, mins, self->maxs, ogle->s.origin, self, MASK_MONSTERSOLID,&trace);
|
|
|
|
if (trace.ent == ogle)
|
|
{
|
|
//Necessary info for the AI_MOOD_POINT_NAVIGATE stuff
|
|
VectorCopy(self->s.origin, self->monsterinfo.nav_goal);
|
|
self->old_yaw = self->s.angles[YAW];
|
|
self->ai_mood = AI_MOOD_POINT_NAVIGATE;
|
|
self->movetarget = self->goalentity = self->enemy = ogle;
|
|
|
|
//Tells the other Seraphs not to try and get this one too
|
|
ogle->targeted = 1;
|
|
|
|
self->ai_mood_flags |= AI_MOOD_FLAG_IGNORE;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//Check the ogles and make sure their noses are to the grind stone
|
|
void seraph_oversee(edict_t *self)
|
|
{
|
|
edict_t *ogle;
|
|
ogle=NULL;
|
|
|
|
while((ogle = findradius(ogle, self->s.origin, OVERLORD_RADIUS)) != NULL)
|
|
{
|
|
if (ogle->ai_mood != AI_MOOD_REST)
|
|
continue;
|
|
|
|
if (ogle->classID != CID_OGLE)
|
|
continue;
|
|
|
|
if (ogle->targeted)
|
|
return;
|
|
|
|
if (ogle->targetEnt != self)
|
|
return;
|
|
|
|
//See if we can scare this one
|
|
if (!seraph_checkscare(self, ogle))
|
|
{
|
|
self->ai_mood = AI_MOOD_ATTACK;
|
|
self->ai_mood_flags |= AI_MOOD_FLAG_WHIP;
|
|
}
|
|
else //If we can scare it, stop and do it
|
|
return;
|
|
}
|
|
}
|
|
|
|
//Cycles through the various idle animations
|
|
void seraph_idle(edict_t *self)
|
|
{
|
|
int chance = irand(0,100);
|
|
|
|
seraph_oversee(self);
|
|
|
|
//Check to see if we were supposed to point at an ogle
|
|
if ( (self->ai_mood == AI_MOOD_ATTACK) && (self->ai_mood_flags & AI_MOOD_FLAG_WHIP) && (self->curAnimID != ANIM_POINT1) )
|
|
{
|
|
SetAnim(self, ANIM_POINT1);
|
|
self->ai_mood = AI_MOOD_STAND;
|
|
self->ai_mood_flags &= ~AI_MOOD_FLAG_WHIP;
|
|
return;
|
|
}
|
|
|
|
//See if we're going to go scare an ogle
|
|
if ( (self->ai_mood == AI_MOOD_POINT_NAVIGATE) )
|
|
{
|
|
SetAnim(self, ANIM_WALK2);
|
|
return;
|
|
}
|
|
|
|
switch( self->curAnimID )
|
|
{
|
|
case ANIM_STAND1:
|
|
if (chance < 20)
|
|
SetAnim(self, ANIM_STAND1_TR);
|
|
else if (chance < 40)
|
|
SetAnim(self, ANIM_STAND1_TL);
|
|
break;
|
|
|
|
//Right
|
|
case ANIM_POINT1:
|
|
case ANIM_STAND1_TR:
|
|
SetAnim(self, ANIM_STAND1_R);
|
|
break;
|
|
|
|
case ANIM_STAND1_R:
|
|
if (chance < 50)
|
|
SetAnim(self, ANIM_STAND1_TRC);
|
|
break;
|
|
|
|
case ANIM_STAND1_TLC:
|
|
case ANIM_STAND1_TRC:
|
|
SetAnim(self, ANIM_STAND1);
|
|
break;
|
|
|
|
//Left
|
|
case ANIM_STAND1_TL:
|
|
SetAnim(self, ANIM_STAND1_L);
|
|
break;
|
|
|
|
case ANIM_STAND1_L:
|
|
if (chance < 50)
|
|
SetAnim(self, ANIM_STAND1_TLC);
|
|
break;
|
|
}
|
|
}
|
|
|
|
//Check to do damage with a whip
|
|
void seraph_strike(edict_t *self, float damage, float a, float b)
|
|
{
|
|
trace_t trace;
|
|
edict_t *victim;
|
|
vec3_t soff, eoff, mins, maxs, bloodDir, direction;
|
|
|
|
VectorSet(soff, 16, 16, 32);
|
|
VectorSet(eoff, 80, 16, 8);
|
|
|
|
VectorSet(mins, -2, -2, -2);
|
|
VectorSet(maxs, 2, 2, 2);
|
|
|
|
VectorSubtract(soff, eoff, bloodDir);
|
|
VectorNormalize(bloodDir);
|
|
|
|
victim = M_CheckMeleeLineHit(self, soff, eoff, mins, maxs, &trace, direction);
|
|
|
|
if (victim)
|
|
{
|
|
if (victim == self)
|
|
{
|
|
//Create a puff effect
|
|
//gi.CreateEffect(NULL, FX_SPARKS, 0, hitPos, "db", vec3_origin, irand(1,3));
|
|
gi.sound (self, CHAN_WEAPON, sounds[SND_ATTACK], 1, ATTN_NORM, 0);
|
|
}
|
|
else
|
|
{
|
|
//Hurt whatever we were whacking away at
|
|
T_Damage(victim, self, self, direction, trace.endpos, bloodDir,
|
|
damage, damage, DAMAGE_EXTRA_BLOOD|DAMAGE_EXTRA_KNOCKBACK,MOD_DIED);
|
|
gi.sound (self, CHAN_WEAPON, sounds[SND_ATTACK], 1, ATTN_NORM, 0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//Play swoosh sound?
|
|
gi.sound (self, CHAN_WEAPON, sounds[SND_ATTACK], 1, ATTN_NORM, 0);
|
|
}
|
|
}
|
|
|
|
/*
|
|
==========================================================
|
|
|
|
Seraph Message functions
|
|
|
|
==========================================================
|
|
*/
|
|
|
|
//Seraph has died
|
|
void seraph_death_pain(edict_t *self, G_Message_t *msg)
|
|
{
|
|
if (self->health < -80)
|
|
{
|
|
BecomeDebris(self);
|
|
return;
|
|
}
|
|
|
|
}
|
|
|
|
void seraph_dropweapon (edict_t *self);
|
|
void seraph_death(edict_t *self, G_Message_t *msg)
|
|
{
|
|
edict_t *targ, *inflictor, *attacker;
|
|
vec3_t dVel, vf, yf;
|
|
float damage;
|
|
int soundID;
|
|
|
|
ParseMsgParms(msg, "eeei", &targ, &inflictor, &attacker, &damage);
|
|
|
|
M_StartDeath(self, ANIM_DEATH1);
|
|
|
|
if (!stricmp(attacker->classname, "monster_ogle"))
|
|
{
|
|
self->health = 1;
|
|
self->takedamage = DAMAGE_NO;
|
|
self->solid = SOLID_BBOX;
|
|
|
|
SetAnim(self, ANIM_DEATH1);
|
|
|
|
soundID = irand(SND_DEATH1, SND_DEATH4);
|
|
gi.sound (self, CHAN_BODY, sounds[soundID], 1, ATTN_NORM, 0);
|
|
|
|
return;
|
|
}
|
|
|
|
if (self->health < -80)
|
|
{
|
|
return;
|
|
}
|
|
else if (self->health < -10)
|
|
{
|
|
seraph_dropweapon (self);
|
|
|
|
SetAnim(self, ANIM_DEATH2_GO);
|
|
|
|
VectorCopy(targ->velocity, vf);
|
|
VectorNormalize(vf);
|
|
|
|
VectorScale(vf, -1, yf);
|
|
|
|
self->ideal_yaw = vectoyaw( yf );
|
|
self->yaw_speed = 24;
|
|
|
|
self->elasticity = 1.2;
|
|
self->friction = 0.8;
|
|
|
|
VectorScale(vf, 300, dVel);
|
|
dVel[2] = irand(150,200);
|
|
|
|
VectorCopy(dVel, self->velocity);
|
|
}
|
|
else
|
|
{
|
|
SetAnim(self, ANIM_DEATH1);
|
|
}
|
|
|
|
soundID = irand(SND_DEATH1, SND_DEATH4);
|
|
gi.sound (self, CHAN_BODY, sounds[soundID], 1, ATTN_NORM, 0);
|
|
}
|
|
|
|
//Check to see if the Seraph is already standing, if not, transition into it
|
|
void seraph_stand(edict_t *self, G_Message_t *msg)
|
|
{
|
|
if (self->curAnimID == ANIM_READY2IDLE)
|
|
SetAnim(self, ANIM_STAND1);
|
|
else
|
|
SetAnim(self, ANIM_READY2IDLE);
|
|
}
|
|
|
|
//Classic run-attack function
|
|
void seraph_run(edict_t *self, G_Message_t *msg)
|
|
{
|
|
if (M_ValidTarget(self, self->enemy))
|
|
{
|
|
SetAnim(self, ANIM_RUN1);
|
|
return;
|
|
}
|
|
|
|
//If our enemy is dead, we need to stand
|
|
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
|
|
}
|
|
|
|
//Classic melee attack function
|
|
void seraph_melee(edict_t *self, G_Message_t *msg)
|
|
{
|
|
vec3_t attackVel, vf;
|
|
float dist;
|
|
int ret;
|
|
|
|
//Don't interrupt a current animation with another melee call inside of it
|
|
if (self->curAnimID == ANIM_ATTACK1_LOOP)
|
|
return;
|
|
|
|
if(self->enemy)
|
|
if ( (self->ai_mood_flags & AI_MOOD_FLAG_IGNORE) && (!stricmp(self->enemy->classname, "monster_ogle")) )
|
|
return;
|
|
|
|
if (M_ValidTarget(self, self->enemy))
|
|
{
|
|
if(self->ai_mood == AI_MOOD_FLEE)
|
|
{
|
|
SetAnim(self, ANIM_BACKUP2);
|
|
return;
|
|
}
|
|
//Set this for any uses below
|
|
AngleVectors(self->s.angles, vf, NULL, NULL);
|
|
|
|
dist = M_DistanceToTarget(self, self->enemy);
|
|
|
|
if (dist < 100)
|
|
{
|
|
VectorMA(vf, 0, vf, attackVel);
|
|
ret = M_PredictTargetEvasion( self, self->enemy, attackVel, self->enemy->velocity, self->melee_range, 5 );
|
|
|
|
if (ret)
|
|
SetAnim(self, ANIM_ATTACK1_LOOP);
|
|
else
|
|
SetAnim(self, ANIM_RUN1_WHIP);
|
|
}
|
|
else
|
|
SetAnim(self, ANIM_RUN1);
|
|
|
|
return;
|
|
}
|
|
|
|
//If our enemy is dead, we need to stand
|
|
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
|
|
}
|
|
|
|
//Take pain
|
|
void seraph_pain(edict_t *self, G_Message_t *msg)
|
|
{
|
|
edict_t *attacker;
|
|
int temp, damage, soundID;
|
|
int force_pain;
|
|
|
|
ParseMsgParms(msg, "eeiii", &temp, &attacker, &force_pain, &damage, &temp);
|
|
|
|
if(self->monsterinfo.awake)
|
|
self->ai_mood_flags &= ~AI_MOOD_FLAG_IGNORE;
|
|
|
|
//Weighted random based on health compared to the maximum it was at
|
|
if (force_pain || ((irand(0, self->max_health+50) > self->health) && !irand(0,2)))
|
|
{
|
|
soundID = irand(SND_PAIN1, SND_PAIN4);
|
|
|
|
gi.sound (self, CHAN_BODY, sounds[soundID], 1, ATTN_NORM, 0);
|
|
|
|
SetAnim(self, ANIM_PAIN);
|
|
}
|
|
|
|
if(attacker)
|
|
{
|
|
if(attacker!=self->enemy)
|
|
{
|
|
if(!self->enemy)
|
|
self->enemy = attacker;
|
|
else if(M_DistanceToTarget(self, self->enemy) > self->melee_range)
|
|
{
|
|
if(self->enemy->client)
|
|
self->oldenemy = self->enemy;
|
|
self->enemy = attacker;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
qboolean seraphAlerted (edict_t *self, alertent_t *alerter, edict_t *enemy)
|
|
{
|
|
if(self->alert_time < level.time)
|
|
{//not startled already
|
|
if(!(alerter->alert_svflags&SVF_ALERT_NO_SHADE) && skill->value < 3.0 && !(self->monsterinfo.aiflags & AI_NIGHTVISION))
|
|
{
|
|
if(enemy->light_level < flrand(6, 77))
|
|
{//too dark, can't see enemy
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if(!infront(self,enemy))
|
|
{
|
|
if(alerter->lifetime < level.time + 2)
|
|
self->alert_time = level.time + 2;//be ready for 2 seconds to wake up if alerted again
|
|
else
|
|
self->alert_time = alerter->lifetime;//be alert as long as the alert sticks around
|
|
seraph_startle(self);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if(enemy->svflags&SVF_MONSTER)
|
|
self->enemy = alerter->enemy;
|
|
else
|
|
self->enemy = enemy;
|
|
|
|
FoundTarget(self, true);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
int Bit_for_MeshNode_so [NUM_MESH_NODES] =
|
|
{
|
|
BIT_BASEBIN,
|
|
BIT_PITHEAD,//overlord head
|
|
BIT_SHOULDPAD,
|
|
BIT_GUARDHEAD,//guard head
|
|
BIT_LHANDGRD,//left hand guard
|
|
BIT_LHANDBOSS,//left hand overlord
|
|
BIT_RHAND,//right hand
|
|
BIT_FRTORSO,
|
|
BIT_ARMSPIKES,
|
|
BIT_LFTUPARM,
|
|
BIT_RTLEG,
|
|
BIT_RTARM,
|
|
BIT_LFTLEG,
|
|
BIT_BKTORSO,
|
|
BIT_AXE,//axe
|
|
BIT_WHIP//whip
|
|
};
|
|
|
|
void seraph_back (edict_t *self, float dist)
|
|
{
|
|
if(!MG_BoolWalkMove(self, self->s.angles[YAW] + 180, dist))
|
|
{
|
|
if(!irand(0, 1000))
|
|
{
|
|
self->monsterinfo.aiflags |= AI_COWARD;
|
|
SetAnim(self, ANIM_RUN1);
|
|
}
|
|
}
|
|
}
|
|
|
|
qboolean canthrownode_so (edict_t *self, int BP, int *throw_nodes)
|
|
{//see if it's on, if so, add it to throw_nodes
|
|
//turn it off on thrower
|
|
if(!(self->s.fmnodeinfo[BP].flags & FMNI_NO_DRAW))
|
|
{
|
|
*throw_nodes |= Bit_for_MeshNode_so[BP];
|
|
self->s.fmnodeinfo[BP].flags |= FMNI_NO_DRAW;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//THROWS weapon, turns off those nodes, sets that weapon as gone
|
|
void seraph_dropweapon (edict_t *self)
|
|
{
|
|
vec3_t handspot, forward, right, up;
|
|
|
|
if(!(self->s.fmnodeinfo[MESH__WHIP].flags & FMNI_NO_DRAW))
|
|
{
|
|
VectorClear(handspot);
|
|
AngleVectors(self->s.angles,forward,right,up);
|
|
|
|
VectorMA(handspot,8,forward,handspot);
|
|
VectorMA(handspot,5,right,handspot);
|
|
VectorMA(handspot,12,up,handspot);
|
|
ThrowWeapon(self, &handspot, BIT_WHIP, 0, FRAME_partfly);
|
|
self->s.fmnodeinfo[MESH__WHIP].flags |= FMNI_NO_DRAW;
|
|
return;
|
|
}
|
|
}
|
|
|
|
void seraph_dismember(edict_t *self, int damage, int HitLocation)
|
|
{
|
|
int throw_nodes = 0;
|
|
vec3_t gore_spot, right;
|
|
qboolean dismember_ok = false;
|
|
|
|
if(HitLocation & hl_MeleeHit)
|
|
{
|
|
dismember_ok = true;
|
|
HitLocation &= ~hl_MeleeHit;
|
|
}
|
|
|
|
if(HitLocation<1)
|
|
return;
|
|
|
|
if(HitLocation>hl_Max)
|
|
return;
|
|
|
|
|
|
VectorClear(gore_spot);
|
|
switch(HitLocation)
|
|
{
|
|
case hl_Head:
|
|
if(self->s.fmnodeinfo[MESH__PITHEAD].flags & FMNI_NO_DRAW)
|
|
break;
|
|
if(self->s.fmnodeinfo[MESH__PITHEAD].flags & FMNI_USE_SKIN)
|
|
damage*=1.5;//greater chance to cut off if previously damaged
|
|
if(flrand(0,self->health)<damage*0.3&&dismember_ok)
|
|
{
|
|
seraph_dropweapon(self);
|
|
canthrownode_so(self, MESH__PITHEAD,&throw_nodes);
|
|
|
|
gore_spot[2]+=18;
|
|
ThrowBodyPart(self, &gore_spot, throw_nodes, damage, FRAME_partfly);
|
|
|
|
VectorAdd(self->s.origin, gore_spot, gore_spot);
|
|
SprayDebris(self,gore_spot,8,damage);
|
|
|
|
if(self->health>0)
|
|
{
|
|
self->health = 1;
|
|
T_Damage (self, self, self, vec3_origin, vec3_origin, vec3_origin, 10, 20,0,MOD_DIED);
|
|
}
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
self->s.fmnodeinfo[MESH__PITHEAD].flags |= FMNI_USE_SKIN;
|
|
self->s.fmnodeinfo[MESH__PITHEAD].skin = self->s.skinnum+1;
|
|
}
|
|
break;
|
|
|
|
case hl_TorsoFront://split in half?
|
|
if(self->s.fmnodeinfo[MESH__FRTORSO].flags & FMNI_USE_SKIN)
|
|
damage*=1.5;//greater chance to cut off if previously damaged
|
|
if(flrand(0,self->health)<damage)
|
|
{
|
|
self->s.fmnodeinfo[MESH__FRTORSO].flags |= FMNI_USE_SKIN;
|
|
self->s.fmnodeinfo[MESH__FRTORSO].skin = self->s.skinnum+1;
|
|
}
|
|
break;
|
|
case hl_TorsoBack://split in half?
|
|
if(self->s.fmnodeinfo[MESH__BKTORSO].flags & FMNI_USE_SKIN)
|
|
damage*=1.5;//greater chance to cut off if previously damaged
|
|
if(flrand(0,self->health) < damage)
|
|
{
|
|
self->s.fmnodeinfo[MESH__BKTORSO].flags |= FMNI_USE_SKIN;
|
|
self->s.fmnodeinfo[MESH__BKTORSO].skin = self->s.skinnum+1;
|
|
}
|
|
break;
|
|
|
|
case hl_ArmUpperLeft:
|
|
if(flrand(0,self->health)<damage)
|
|
{
|
|
self->s.fmnodeinfo[MESH__LFTUPARM].flags |= FMNI_USE_SKIN;
|
|
self->s.fmnodeinfo[MESH__LFTUPARM].skin = self->s.skinnum+1;
|
|
}
|
|
break;
|
|
case hl_ArmLowerLeft://left arm
|
|
if(self->s.fmnodeinfo[MESH__LHANDBOSS].flags & FMNI_NO_DRAW)
|
|
break;
|
|
if(self->s.fmnodeinfo[MESH__LHANDBOSS].flags & FMNI_USE_SKIN)
|
|
damage*=1.5;//greater chance to cut off if previously damaged
|
|
if(flrand(0,self->health)<damage*0.75&&dismember_ok)
|
|
{
|
|
if(canthrownode_so(self, MESH__LHANDBOSS, &throw_nodes))
|
|
{
|
|
AngleVectors(self->s.angles,NULL,right,NULL);
|
|
gore_spot[2]+=self->maxs[2]*0.3;
|
|
VectorMA(gore_spot,-10,right,gore_spot);
|
|
ThrowBodyPart(self, &gore_spot, throw_nodes, damage, FRAME_partfly);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self->s.fmnodeinfo[MESH__LHANDBOSS].flags |= FMNI_USE_SKIN;
|
|
self->s.fmnodeinfo[MESH__LHANDBOSS].skin = self->s.skinnum+1;
|
|
}
|
|
break;
|
|
case hl_ArmUpperRight:
|
|
if(flrand(0,self->health)<damage)
|
|
{
|
|
self->s.fmnodeinfo[MESH__RTARM].flags |= FMNI_USE_SKIN;
|
|
self->s.fmnodeinfo[MESH__RTARM].skin = self->s.skinnum+1;
|
|
}
|
|
break;
|
|
case hl_ArmLowerRight://right arm
|
|
if(self->s.fmnodeinfo[MESH__RHAND].flags & FMNI_NO_DRAW)
|
|
break;
|
|
if(flrand(0,self->health)<damage*0.75&&dismember_ok)
|
|
{
|
|
if(canthrownode_so(self, MESH__RHAND, &throw_nodes))
|
|
{
|
|
AngleVectors(self->s.angles,NULL,right,NULL);
|
|
gore_spot[2]+=self->maxs[2]*0.3;
|
|
VectorMA(gore_spot,10,right,gore_spot);
|
|
seraph_dropweapon (self);
|
|
ThrowBodyPart(self, &gore_spot, throw_nodes, damage, FRAME_partfly);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self->s.fmnodeinfo[MESH__RHAND].flags |= FMNI_USE_SKIN;
|
|
self->s.fmnodeinfo[MESH__RHAND].skin = self->s.skinnum+1;
|
|
}
|
|
break;
|
|
|
|
case hl_LegUpperLeft:
|
|
case hl_LegLowerLeft://left leg
|
|
if(self->s.fmnodeinfo[MESH__LFTLEG].flags & FMNI_USE_SKIN)
|
|
break;
|
|
self->s.fmnodeinfo[MESH__LFTLEG].flags |= FMNI_USE_SKIN;
|
|
self->s.fmnodeinfo[MESH__LFTLEG].skin = self->s.skinnum+1;
|
|
break;
|
|
case hl_LegUpperRight:
|
|
case hl_LegLowerRight://right leg
|
|
if(self->s.fmnodeinfo[MESH__RTLEG].flags & FMNI_USE_SKIN)
|
|
break;
|
|
self->s.fmnodeinfo[MESH__RTLEG].flags |= FMNI_USE_SKIN;
|
|
self->s.fmnodeinfo[MESH__RTLEG].skin = self->s.skinnum+1;
|
|
break;
|
|
}
|
|
|
|
if(self->s.fmnodeinfo[MESH__RHAND].flags & FMNI_NO_DRAW)
|
|
self->monsterinfo.aiflags |= AI_NO_MELEE;
|
|
|
|
//FIXME: when get missile anim
|
|
// if(self->s.fmnodeinfo[MESH__LHANDGRD].flags & FMNI_NO_DRAW)
|
|
// self->monsterinfo.aiflags |= AI_NO_MISSILE;
|
|
|
|
if(self->monsterinfo.aiflags & AI_NO_MELEE)//&&self->monsterinfo.aiflags & AI_NO_MISSILE)
|
|
SetAnim(self, ANIM_BACKUP);
|
|
}
|
|
|
|
void ser_ovl_SightSound(edict_t *self, G_Message_t *Msg)
|
|
{
|
|
gi.sound(self, CHAN_VOICE, sounds[irand(SND_SIGHT1, SND_SIGHT3)], 1, ATTN_NORM, 0);
|
|
}
|
|
|
|
/*
|
|
==========================================================
|
|
|
|
Seraph Spawn functions
|
|
|
|
==========================================================
|
|
*/
|
|
|
|
void SeraphOverlordStaticsInit()
|
|
{
|
|
classStatics[CID_SERAPH_OVERLORD].msgReceivers[MSG_STAND] = seraph_stand;
|
|
classStatics[CID_SERAPH_OVERLORD].msgReceivers[MSG_RUN] = seraph_run;
|
|
classStatics[CID_SERAPH_OVERLORD].msgReceivers[MSG_MELEE] = seraph_melee;
|
|
classStatics[CID_SERAPH_OVERLORD].msgReceivers[MSG_PAIN] = seraph_pain;
|
|
classStatics[CID_SERAPH_OVERLORD].msgReceivers[MSG_DEATH] = seraph_death;
|
|
classStatics[CID_SERAPH_OVERLORD].msgReceivers[MSG_DISMEMBER] = MG_parse_dismember_msg;
|
|
classStatics[CID_SERAPH_OVERLORD].msgReceivers[MSG_DEATH_PAIN] = seraph_death_pain;
|
|
classStatics[CID_SERAPH_OVERLORD].msgReceivers[MSG_CHECK_MOOD] = seraph_check_mood;
|
|
classStatics[CID_SERAPH_OVERLORD].msgReceivers[MSG_DISMEMBER] = MG_parse_dismember_msg;
|
|
classStatics[CID_SERAPH_OVERLORD].msgReceivers[MSG_VOICE_SIGHT] = ser_ovl_SightSound;
|
|
|
|
resInfo.numAnims = NUM_ANIMS;
|
|
resInfo.animations = animations;
|
|
resInfo.modelIndex = gi.modelindex("models/monsters/overlord/tris.fm");
|
|
resInfo.numSounds = NUM_SOUNDS;
|
|
resInfo.sounds = sounds;
|
|
|
|
sounds[SND_ATTACK] = gi.soundindex("monsters/seraph/overlord/attack.wav");
|
|
|
|
sounds[SND_SCOLD1] = gi.soundindex("monsters/seraph/overlord/scold1.wav");// Back to work
|
|
sounds[SND_SCOLD2] = gi.soundindex("monsters/seraph/overlord/scold2.wav"); // Get to work
|
|
sounds[SND_SCOLD3] = gi.soundindex("monsters/seraph/overlord/scold3.wav"); // No talking
|
|
|
|
sounds[SND_SCARE] = gi.soundindex("monsters/seraph/overlord/scare.wav");// Hey!
|
|
|
|
sounds[SND_STARTLE] = gi.soundindex("monsters/seraph/overlord/startle.wav");
|
|
sounds[SND_SLAP] = gi.soundindex("monsters/seraph/overlord/slap.wav");
|
|
|
|
sounds[SND_DEATH1] = gi.soundindex("monsters/seraph/death1.wav");
|
|
sounds[SND_DEATH2] = gi.soundindex("monsters/seraph/death2.wav");
|
|
sounds[SND_DEATH3] = gi.soundindex("monsters/seraph/wimpdeath1.wav");
|
|
sounds[SND_DEATH4] = gi.soundindex("monsters/seraph/wimpdeath2.wav");
|
|
|
|
sounds[SND_PAIN1] = gi.soundindex("monsters/seraph/pain1.wav");
|
|
sounds[SND_PAIN2] = gi.soundindex("monsters/seraph/pain2.wav");
|
|
sounds[SND_PAIN3] = gi.soundindex("monsters/seraph/pain3.wav");
|
|
sounds[SND_PAIN4] = gi.soundindex("monsters/seraph/pain4.wav");
|
|
|
|
sounds[SND_SIGHT1] = gi.soundindex("monsters/seraph/overlord/sight1.wav");
|
|
sounds[SND_SIGHT2] = gi.soundindex("monsters/seraph/overlord/sight2.wav");
|
|
sounds[SND_SIGHT3] = gi.soundindex("monsters/seraph/overlord/scare.wav");
|
|
|
|
classStatics[CID_SERAPH_OVERLORD].resInfo = &resInfo;
|
|
}
|
|
|
|
/*QUAKED monster_seraph_overlord(1 .5 0) (-24 -24 -34) (24 24 34) AMBUSH ASLEEP 4 8 16 32 64 FIXED WANDER MELEE_LEAD STALK COWARD EXTRA1 EXTRA2 EXTRA3 EXTRA4
|
|
The big, nasty, tyranical Overlords..
|
|
|
|
AMBUSH - Will not be woken up by other monsters or shots from player
|
|
|
|
ASLEEP - will not appear until triggered
|
|
|
|
WALKING- Use WANDER instead
|
|
|
|
WANDER - Monster will wander around aimlessly (but follows buoys)
|
|
|
|
MELEE_LEAD - Monster will tryto cut you off when you're running and fighting him, works well if there are a few monsters in a group, half doing this, half not
|
|
|
|
STALK - Monster will only approach and attack from behind- if you're facing the monster it will just stand there. Once the monster takes pain, however, it will stop this behaviour and attack normally
|
|
|
|
COWARD - Monster starts off in flee mode- runs away from you when woken up
|
|
|
|
"homebuoy" - monsters will head to this buoy if they don't have an enemy ("homebuoy" should be targetname of the buoy you want them to go to)
|
|
|
|
"wakeup_target" - monsters will fire this target the first time it wakes up (only once)
|
|
|
|
"pain_target" - monsters will fire this target the first time it gets hurt (only once)
|
|
|
|
mintel - monster intelligence- this basically tells a monster how many buoys away an enemy has to be for it to give up.
|
|
|
|
melee_range - How close the player has to be, maximum, for the monster to go into melee. If this is zero, the monster will never melee. If it is negative, the monster will try to keep this distance from the player. If the monster has a backup, he'll use it if too clode, otherwise, a negative value here means the monster will just stop running at the player at this distance.
|
|
Examples:
|
|
melee_range = 60 - monster will start swinging it player is closer than 60
|
|
melee_range = 0 - monster will never do a mele attack
|
|
melee_range = -100 - monster will never do a melee attack and will back away (if it has that ability) when player gets too close
|
|
|
|
missile_range - Maximum distance the player can be from the monster to be allowed to use it's ranged attack.
|
|
|
|
min_missile_range - Minimum distance the player can be from the monster to be allowed to use it's ranged attack.
|
|
|
|
bypass_missile_chance - Chance that a monster will NOT fire it's ranged attack, even when it has a clear shot. This, in effect, will make the monster come in more often than hang back and fire. A percentage (0 = always fire/never close in, 100 = never fire/always close in).- must be whole number
|
|
|
|
jump_chance - every time the monster has the opportunity to jump, what is the chance (out of 100) that he will... (100 = jump every time)- must be whole number
|
|
|
|
wakeup_distance - How far (max) the player can be away from the monster before it wakes up. This just means that if the monster can see the player, at what distance should the monster actually notice him and go for him.
|
|
|
|
DEFAULTS:
|
|
mintel = 24
|
|
melee_range = 100
|
|
missile_range = 0
|
|
min_missile_range = 0
|
|
bypass_missile_chance = 0
|
|
jump_chance = 30
|
|
wakeup_distance = 1024
|
|
|
|
NOTE: A value of zero will result in defaults, if you actually want zero as the value, use -1
|
|
*/
|
|
void SP_monster_seraph_overlord(edict_t *self)
|
|
{
|
|
self->classID = CID_SERAPH_OVERLORD;
|
|
|
|
if (!monster_start(self)) // Failed initialization
|
|
return;
|
|
|
|
self->msgHandler = DefaultMsgHandler;
|
|
self->monsterinfo.alert = seraphAlerted;
|
|
self->think = walkmonster_start_go;
|
|
self->monsterinfo.dismember = seraph_dismember;
|
|
|
|
if (!self->health)
|
|
{
|
|
self->health = SERAPH_HEALTH;
|
|
}
|
|
|
|
//Apply to the end result (whether designer set or not)
|
|
self->max_health = self->health = MonsterHealth(self->health);
|
|
|
|
self->mass = SERAPH_MASS;
|
|
self->yaw_speed = 18;
|
|
|
|
self->movetype = PHYSICSTYPE_STEP;
|
|
self->solid=SOLID_BBOX;
|
|
|
|
VectorCopy(STDMinsForClass[self->classID], self->mins);
|
|
VectorCopy(STDMaxsForClass[self->classID], self->maxs);
|
|
|
|
self->materialtype = MAT_FLESH;
|
|
|
|
self->s.modelindex = classStatics[CID_SERAPH_OVERLORD].resInfo->modelIndex;
|
|
self->s.skinnum=0;
|
|
|
|
self->ai_mood_flags |= AI_MOOD_FLAG_PREDICT;
|
|
|
|
self->monsterinfo.otherenemyname = "monster_rat";
|
|
|
|
if (!self->s.scale)
|
|
{
|
|
self->s.scale = self->monsterinfo.scale = MODEL_SCALE;
|
|
}
|
|
|
|
//Turn off the Guard pieces!
|
|
self->s.fmnodeinfo[MESH__AXE].flags |= FMNI_NO_DRAW;
|
|
self->s.fmnodeinfo[MESH__GUARDHEAD].flags |= FMNI_NO_DRAW;
|
|
self->s.fmnodeinfo[MESH__LHANDGRD].flags |= FMNI_NO_DRAW;
|
|
self->s.fmnodeinfo[MESH__ARMSPIKES].flags |= FMNI_NO_DRAW;
|
|
self->s.fmnodeinfo[MESH__SHOULDPAD].flags |= FMNI_NO_DRAW;
|
|
|
|
MG_InitMoods(self);
|
|
|
|
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
|
|
|
|
self->melee_range *= self->s.scale;
|
|
}
|