heretic2-sdk/Toolkit/Programming/GameCode/game/m_assassin.c
1998-11-24 00:00:00 +00:00

2958 lines
80 KiB
C

//==============================================================================
//
// m_assassin.c
//
// Heretic II
// Copyright 1998 Raven Software
//
//
// AI :
//
// STAND1 : Looking straight ahead
//
// WALK1 : a normal straight line
// WALK2 : another normal straight line
//
// MELEE1 : Attack
// MELEE2 : Attack
//
// RUNATTACK : Running and swinging
// RUN1 : chasing an enemy straight ahead
// SHAKE : stand and spaz
// DIE1 : Fall back dead
// LEAN1 : lean agains the wall
// FIST1 : Beat against the wall in rage and desperation
//
//
//==============================================================================
#include "g_local.h"
#include "Utilities.h"
#include "g_DefaultMessageHandler.h"
#include "g_monster.h"
#include "fx.h"
#include "random.h"
#include "buoy.h"
#include "vector.h"
#include "g_playstats.h"
#include "m_assassin.h"
#include "m_assassin_anim.h"
#include "g_HitLocation.h"
#include "g_misc.h"
#include "angles.h"
#include "c_ai.h"
#include "p_anim_branch2.h"
#include "p_anims2.h"
#include "m_stats.h"
qboolean clear_visible (edict_t *self, edict_t *other);
void extrapolateFiredir (edict_t *self,vec3_t p1,float pspeed,edict_t *targ,float accept,vec3_t vec2);
void create_assassin_dagger(edict_t *Arrow);
void assassinInitDeCloak (edict_t *self);
H2COMMON_API void KnockDownPlayer(playerinfo_t *playerinfo);
/*----------------------------------------------------------------------
assassin Base Info
-----------------------------------------------------------------------*/
static animmove_t *animations[ NUM_ANIMS] =
{
&assassin_move_daggerl,// = {14, assassin_frames_daggerl, assassin_pause},
&assassin_move_daggerr,//= {15, assassin_frames_daggerr, assassin_pause},
&assassin_move_daggerb,// = {15, assassin_frames_daggerb, assassin_pause},
&assassin_move_daggerc,// = {11, assassin_frames_daggerc, assassin_pause},
&assassin_move_newdagger,
&assassin_move_newdaggerb,
&assassin_move_backflip,// = {16, assassin_frames_backflip, assassin_pause},
&assassin_move_frontflip,// = {16, assassin_frames_frontflip, assassin_pause},
&assassin_move_dodge_right,// = {10, assassin_frames_dodge_right, assassin_pause},
&assassin_move_dodge_left,// = {10, assassin_frames_dodge_left, assassin_pause},
&assassin_move_deatha,// = {14, assassin_frames_deatha, assassin_dead},
&assassin_move_deathb,// = {14, assassin_frames_deathb, assassin_dead},
&assassin_move_jump,// = {17, assassin_frames_jump, assassin_pause},
&assassin_move_run,// = {10, assassin_frames_run, assassin_pause},
&assassin_move_pain1,// = {5, assassin_frames_pain1, assassin_pause},
&assassin_move_pain2,// = {4, assassin_frames_pain2, assassin_pause},
&assassin_move_delay,// = {1, assassin_frames_delay, assassin_pause},
&assassin_move_stand,
&assassin_move_crouch,
&assassin_move_uncrouch,
&assassin_move_evade_jump,
&assassin_move_evade_backflip,
&assassin_move_evade_frontflip,
&assassin_move_inair,
&assassin_move_land,
&assassin_move_forcedjump,
&assassin_move_fjump,
&assassin_move_bfinair,
&assassin_move_bfland,
&assassin_move_ffinair,
&assassin_move_ffland,
&assassin_move_evinair,
&assassin_move_teleport,
&assassin_move_cloak,
&assassin_move_walk,
&assassin_move_walk_loop,
&assassin_move_backspring,
//crouches
&assassin_move_crouch_trans,
&assassin_move_crouch_idle,
&assassin_move_crouch_look_right,
&assassin_move_crouch_look_right_idle,
&assassin_move_crouch_look_l2r,
&assassin_move_crouch_look_left,
&assassin_move_crouch_look_left_idle,
&assassin_move_crouch_look_r2l,
&assassin_move_crouch_look_r2c,
&assassin_move_crouch_look_l2c,
&assassin_move_crouch_poke,
&assassin_move_crouch_end,
// Cinematic Anims
&assassin_move_c_idle1,
&assassin_move_c_run1,
&assassin_move_c_attack1,
&assassin_move_c_attack2,
};
static int Sounds[NUM_SOUNDS];
static ClassResourceInfo_t resInfo;
/*-------------------------------------------------------------------------
assassin_c_anims
-------------------------------------------------------------------------*/
void assassin_c_anims(edict_t *self, G_Message_t *msg)
{
int int_msg;
int curr_anim;
ai_c_readmessage(self, msg);
int_msg = (int) msg->ID;
self->monsterinfo.c_anim_flag = 0;
switch(int_msg)
{
case MSG_C_ATTACK1:
self->monsterinfo.c_anim_flag |= C_ANIM_REPEAT;
curr_anim = ANIM_C_ATTACK1;
break;
case MSG_C_ATTACK2:
self->monsterinfo.c_anim_flag |= C_ANIM_REPEAT;
curr_anim = ANIM_C_ATTACK2;
break;
case MSG_C_IDLE1:
self->monsterinfo.c_anim_flag |= C_ANIM_REPEAT;
curr_anim = ANIM_C_IDLE1;
break;
case MSG_C_RUN1:
self->monsterinfo.c_anim_flag |= C_ANIM_MOVE;
curr_anim = ANIM_C_RUN1;
break;
default:
self->monsterinfo.c_anim_flag |= C_ANIM_REPEAT;
curr_anim = ANIM_C_IDLE1;
break;
}
SetAnim(self, curr_anim);
}
/*----------------------------------------------------------------------
Action Functions for the monster
-----------------------------------------------------------------------*/
void assassinApplyJump (edict_t *self)
{
self->jump_time = level.time + 2;
VectorCopy(self->movedir, self->velocity);
VectorNormalize(self->movedir);
// gi.dprintf("Jump velocity will be: %4.2f %4.2f %4.2f\n", self->velocity[0], self->velocity[1], self->velocity[2]);
self->monsterinfo.aiflags &= ~AI_OVERRIDE_GUIDE;
}
void assassin_jump(edict_t *self, G_Message_t *msg)
{
// gi.dprintf("Assassin Jumping from AI RUN!\n");
SetAnim(self, ANIM_FORCED_JUMP);
self->monsterinfo.aiflags |= AI_OVERRIDE_GUIDE;
}
void bounce_arrow(edict_t *self)
{
edict_t *Arrow;
Arrow = G_Spawn();
Arrow->owner = self->owner;
Arrow->touch = self->touch;
Arrow->s.modelindex = self->s.modelindex;
VectorCopy(self->s.origin, Arrow->s.origin);
Arrow->owner = self->owner;
Arrow->enemy = self->enemy;
Arrow->nextthink=self->nextthink;
create_assassin_dagger(Arrow);
VectorScale(self->avelocity, -0.5, Arrow->avelocity);
Create_rand_relect_vect(self->velocity, Arrow->velocity);
vectoangles(Arrow->velocity, Arrow->s.angles);
Vec3ScaleAssign(ASSASSIN_DAGGER_SPEED / 2,Arrow->velocity);
Arrow->reflect_debounce_time = self->reflect_debounce_time -1;
gi.CreateEffect(&Arrow->s,
FX_M_EFFECTS,
0,
Arrow->avelocity,
"bv",
FX_ASS_DAGGER,
Arrow->velocity);
G_LinkMissile(Arrow);
}
// The blocked function isn't currently used.
/*
void assassinDaggerBlocked (edict_t *self, trace_t *trace)
{
float damage;
vec3_t hitangles;
edict_t *Other;
Other = trace->ent;
if(Other==self->owner||Other->owner == self->owner)
{
return;
}
// are we reflecting ?
if(EntReflecting(trace->ent, true, true))
{
bounce_arrow(self);
G_SetToFree(self);
return;
}
if(trace->surface&&(trace->surface->flags&SURF_SKY))
{
SkyFly(self);
return;
}
//take into account if angle is within 45 of 0?
if(Other->takedamage)
{
if(Other->materialtype == MAT_FLESH||Other->client)
gi.sound(self,CHAN_AUTO,Sounds[SND_DAGHITF],1,ATTN_NORM,0);
else
gi.sound(self,CHAN_AUTO,Sounds[SND_DAGHITW],1,ATTN_NORM,0);
if(skill->value < 2)
damage = flrand(ASSASSIN_MIN_DAMAGE * 0.5, ASSASSIN_MAX_DAMAGE * 0.5);
else
{
damage = flrand(ASSASSIN_MIN_DAMAGE,ASSASSIN_MAX_DAMAGE);
if(Q_fabs(self->s.angles[PITCH])<45)//up to extra 10 pts damage if pointed correctly
damage += 45/(45 - Q_fabs(self->s.angles[PITCH])) * 10;
}
T_Damage(Other,self,self->owner,self->movedir,self->s.origin,trace->plane.normal, damage, 0, 0,MOD_DIED);
}
else//spark
{
if(Vec3NotZero(trace->plane.normal))
vectoangles(trace->plane.normal, hitangles);
else
VectorSet(hitangles, 0, 0, 90);
gi.CreateEffect(NULL, FX_SPARKS, 0, self->s.origin, "d", hitangles);
gi.sound(self,CHAN_AUTO,Sounds[SND_DAGHITW],1,ATTN_NORM,0);
}
G_FreeEdict(self);
}
*/
void assassinDaggerTouch (edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surface)
{
float damage;
vec3_t hitangles, normal;
if(other==self->owner||other->owner == self->owner)
{
return;
}
// are we reflecting ?
if(EntReflecting(other, true, true) && self->reflect_debounce_time)
{
bounce_arrow(self);
G_SetToFree(self);
return;
}
if(surface&&(surface->flags&SURF_SKY))
{
SkyFly(self);
return;
}
//take into account if angle is within 45 of 0?
VectorSet(normal, 0, 0, 1);
if(plane)
{
if(plane->normal)
{
VectorCopy(plane->normal, normal);
}
}
if(other->takedamage)
{
if(other->materialtype == MAT_FLESH||other->client)
gi.sound(self,CHAN_AUTO,Sounds[SND_DAGHITF],1,ATTN_NORM,0);
else
gi.sound(self,CHAN_AUTO,Sounds[SND_DAGHITW],1,ATTN_NORM,0);
damage = flrand(ASSASSIN_MIN_DAMAGE,ASSASSIN_MAX_DAMAGE);
if(skill->value >= 2 && Q_fabs(self->s.angles[PITCH])<45)//up to extra 10 pts damage if pointed correctly AND on hard skill
damage += 45/(45 - Q_fabs(self->s.angles[PITCH])) * 10;
T_Damage(other,self,self->owner,self->movedir,self->s.origin,normal, damage, 0, 0,MOD_DIED);
}
else//spark
{
if(Vec3NotZero(normal))
vectoangles(normal, hitangles);
else
VectorSet(hitangles, 0, 0, 90);
gi.CreateEffect(NULL, FX_SPARKS, 0, self->s.origin, "d", hitangles);
gi.sound(self,CHAN_AUTO,Sounds[SND_DAGHITW],1,ATTN_NORM,0);
}
G_FreeEdict(self);
}
// create the guts of the dagger
void create_assassin_dagger(edict_t *Arrow)
{
Arrow->movetype=MOVETYPE_FLYMISSILE;
Arrow->solid=SOLID_BBOX;
Arrow->classname="Assassin_Dagger";
Arrow->touch=assassinDaggerTouch;
Arrow->gravity = 0.0f;
Arrow->clipmask=MASK_SHOT;
Arrow->s.effects |= EF_CAMERA_NO_CLIP;
Arrow->svflags |= SVF_ALWAYS_SEND;
Arrow->s.scale = 0.5;
Arrow->think=G_FreeEdict;//ssithraArrowThink;
VectorSet(Arrow->mins, -1.0, -1.0, -1.0);
VectorSet(Arrow->maxs, 1.0, 1.0, 1.0);
}
void assassinThrowDagger(edict_t *self, float right_ofs)
{//fixme; adjust for up/down
vec3_t Forward,check_lead, right, enemy_pos, enemy_dir;//, up;
edict_t *Arrow;
float enemy_dist, eta;//, spoo_arc;
// if(self->s.fmnodeinfo[MESH__RIGHTARM].flags&FMNI_NO_DRAW)
// return;
// gi.sound(self,CHAN_WEAPON,Sounds[SND_ARROW1],1,ATTN_NORM,0);
self->monsterinfo.attack_finished = level.time + 0.4;
Arrow = G_Spawn();
create_assassin_dagger(Arrow);
Arrow->reflect_debounce_time = MAX_REFLECT;
Arrow->nextthink=level.time+3;
Arrow->enemy=self->enemy;
Arrow->owner=self;
VectorCopy(self->enemy->s.origin, enemy_pos);
enemy_pos[2] += self->enemy->viewheight;
AngleVectors (self->s.angles, Forward, right, NULL);
VectorCopy (self->s.origin, Arrow->s.origin);
Arrow->s.origin[2] += 8;
VectorMA (Arrow->s.origin, 8, Forward, Arrow->s.origin);
VectorMA (Arrow->s.origin, right_ofs, right, Arrow->s.origin);
VectorCopy (self->movedir, Arrow->movedir);
vectoangles (Forward, Arrow->s.angles);
extrapolateFiredir (self, Arrow->s.origin, ASSASSIN_DAGGER_SPEED, self->enemy, 0.3, check_lead);
VectorSubtract(enemy_pos, Arrow->s.origin, enemy_dir);
enemy_dist = VectorNormalize(enemy_dir);
if(Vec3IsZero(check_lead))
{
if(DotProduct(enemy_dir, Forward)>0.3)
VectorScale(enemy_dir, ASSASSIN_DAGGER_SPEED, Arrow->velocity);
else
VectorScale(Forward, ASSASSIN_DAGGER_SPEED, Arrow->velocity);
}
else
{
VectorScale(check_lead, ASSASSIN_DAGGER_SPEED, Arrow->velocity);
}
VectorCopy(Arrow->velocity, Arrow->movedir);
VectorNormalize(Arrow->movedir);
vectoangles(Arrow->movedir, Arrow->s.angles);
Arrow->s.angles[PITCH] = -90;
eta = enemy_dist / ASSASSIN_DAGGER_SPEED;//eta
// gi.dprintf("ETA: %f\n", eta);
//ideally, spin @1110 degrees in 1 sec
Arrow->avelocity[PITCH] = -1/eta * (360*3 +30 + flrand(-10,10));
// gi.dprintf("avel: %f\n", Arrow->avelocity[PITCH]);
// gi.dprintf("final rotation: %f\n", Arrow->avelocity[PITCH]*eta);
// gi.dprintf("final angle: %f\n", anglemod(Arrow->s.angles[PITCH]+Arrow->avelocity[PITCH]*eta));
/*
//doesn't make desired effect
if(right_ofs>0)
Arrow->s.angles[ROLL] = flrand(0, 35);
else if(right_ofs<0)
Arrow->s.angles[ROLL] = flrand(-35, 0);
*/
gi.CreateEffect(&Arrow->s,
FX_M_EFFECTS,
0,
Arrow->avelocity,
"bv",
FX_ASS_DAGGER,
Arrow->velocity);
G_LinkMissile(Arrow);
}
/*-------------------------------------------------------------------------
assassin dagger
-------------------------------------------------------------------------*/
void assassindagger (edict_t *self, float right_ofs)
{
vec3_t v, off, dir, org, ang;
float len;
int damage;
int thrownum = 0;
if(!self->enemy)
{
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
return;
}
if(self->enemy->health<0)
{
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
return;
}
VectorSubtract (self->s.origin, self->enemy->s.origin, v);
len = VectorLength (v);
if (len <= (self->maxs[0] + self->enemy->maxs[0] + 56) ) // A hit
{
if (infront(self, self->enemy))
{
if (1)//one or two hands?
gi.sound (self, CHAN_WEAPON, Sounds[SND_SLASH1], 1, ATTN_NORM, 0);
else
gi.sound (self, CHAN_WEAPON, Sounds[SND_SLASH2], 1, ATTN_NORM, 0);
VectorSet(off, 35.0, 0.0, 32.0);
VectorGetOffsetOrigin(off, self->s.origin, self->s.angles[YAW], org);
VectorCopy(self->s.angles, ang);
ang[YAW] += DEGREE_90;
AngleVectors(ang, dir, NULL, NULL);
//4 to 8
damage = irand(ASSASSIN_MIN_DAMAGE, ASSASSIN_MAX_DAMAGE);
if(skill->value >= 2)
{
if(!infront(self->enemy, self))
{//backstab!
damage = self->enemy->health + irand(-20, 10);
if(damage<ASSASSIN_MAX_DAMAGE)
damage = ASSASSIN_MAX_DAMAGE;
}
}
/* if(self->s.fmnodeinfo[MESH__HANDLE].flags & FMNI_NO_DRAW)
damage-=5;
else if(self->s.fmnodeinfo[MESH__HOE].flags & FMNI_NO_DRAW)
damage+=5;
else if(self->s.fmnodeinfo[MESH__GAFF].flags & FMNI_NO_DRAW)
damage+=7;
else if(self->s.fmnodeinfo[MESH__HAMMER].flags & FMNI_NO_DRAW)
damage+=10;*/
T_Damage (self->enemy, self, self, dir, org, vec3_origin, damage, 0, 0,MOD_DIED);
}
}
else // A misssss
{
if((int)right_ofs & BIT_RKNIFE)
{//turn off dagger
thrownum++;
if(!(self->s.fmnodeinfo[MESH__RKNIFE].flags & FMNI_NO_DRAW))
{
self->s.fmnodeinfo[MESH__RKNIFE].flags |= FMNI_NO_DRAW;
assassinThrowDagger(self, 12);
}
}
if((int)right_ofs & BIT_LKNIFE)
{
thrownum++;
if(!(self->s.fmnodeinfo[MESH__LKNIFE].flags & FMNI_NO_DRAW))
{
self->s.fmnodeinfo[MESH__LKNIFE].flags |= FMNI_NO_DRAW;
assassinThrowDagger(self, -12);
}
}
if(thrownum>1)
gi.sound (self, CHAN_WEAPON, Sounds[SND_THROW2], 1, ATTN_NORM, 0);
else if(thrownum>0)
gi.sound (self, CHAN_WEAPON, Sounds[SND_THROW1], 1, ATTN_NORM, 0);
}
}
void assassin_Touch(edict_t *self, trace_t *trace)
{
vec3_t dir;
float strength;
edict_t *other;
other = trace->ent;
if(self->health <= 0)
return;
if(!trace)
return;
if((!self->groundentity||self->groundentity==other) && Vec3NotZero(self->velocity))
{
strength = VectorLength(self->velocity);
if(strength > 50)
{
if(movable(other) && (other->svflags&SVF_MONSTER || other->client))
{
VectorCopy(self->velocity, dir);
if(dir[2] < 0)
dir[2] = 0;
VectorNormalize(dir);
// VectorAdd(other->velocity, dir, other->knockbackvel);
if(self->s.origin[2]+self->mins[2] > other->s.origin[2] + other->maxs[2] * 0.8)
{
if(other->client)
KnockDownPlayer(&other->client->playerinfo);
gi.sound(self, CHAN_BODY, Sounds[SND_LANDF], 1, ATTN_NORM, 0);
/*
VectorMA(other->velocity, strength, dir, other->velocity);
other->groundentity = NULL;
other->velocity[2] = 101;
*/
if(other->takedamage)
{
if(strength>5)
strength = 5;
T_Damage(other, self, self, dir, trace->endpos, dir, strength, strength*4, 0,MOD_DIED);
}
SetAnim(self, ANIM_EVFRONTFLIP);
}
}
}
/*
//backflip off walls! Too late to implement
else if(trace->plane)
{
if(Vec3NotZero(trace->plane.normal))
{
if(trace->plane.normal[2] < 0.75)
{
VectorCopy(self->velocity, dir);
VectorNormalize(dir);
if(DotProduct(dir, trace->plane.normal) < -0.3)
{
SetAnim(self, ANIM_EVBACKFLIP);
}
}
}
}
*/
}
}
/*-------------------------------------------------------------------------
assassin_dead
-------------------------------------------------------------------------*/
void assassin_dead(edict_t *self)
{
self->msgHandler = DeadMsgHandler;
self->deadState = DEAD_DEAD;
M_EndDeath(self);
}
/*-------------------------------------------------------------------------
assassin_death
-------------------------------------------------------------------------*/
void assassin_random_death_sound (edict_t *self)
{
gi.sound(self, CHAN_VOICE, Sounds[SND_DIE1], 1, ATTN_NORM, 0);
}
void assassin_death(edict_t *self, G_Message_t *msg)
{
int chance;
if(self->monsterinfo.aiflags&AI_DONT_THINK)
{
if (irand(0,10) < 5) // Big enough death to be thrown back
SetAnim(self, ANIM_DEATHB);//gib?
else
SetAnim(self, ANIM_DEATHA);
return;
}
// gi.dprintf("Dead\n");
self->msgHandler = DeadMsgHandler;
if(self->deadflag == DEAD_DEAD) //Dead but still being hit
return;
self->isBlocked = self->bounced = NULL;
self->deadflag = DEAD_DEAD;
assassin_dropweapon (self, BIT_LKNIFE|BIT_RKNIFE);
if(self->health <= -80) //gib death
{
gi.sound(self, CHAN_BODY, Sounds[SND_GIB], 1, ATTN_NORM, 0);
if(irand(0,10)<5)
{
self->s.fmnodeinfo[MESH__TORSOFT].flags |= FMNI_NO_DRAW;
self->s.fmnodeinfo[MESH__TORSOBK].flags |= FMNI_NO_DRAW;
self->s.fmnodeinfo[MESH__HEAD].flags |= FMNI_NO_DRAW;
self->s.fmnodeinfo[MESH__R4ARM].flags |= FMNI_NO_DRAW;
self->s.fmnodeinfo[MESH__L4ARM].flags |= FMNI_NO_DRAW;
self->s.fmnodeinfo[MESH__KNIFES].flags |= FMNI_NO_DRAW;
self->s.fmnodeinfo[MESH__LUPARM].flags |= FMNI_NO_DRAW;
self->s.fmnodeinfo[MESH__RUPARM].flags |= FMNI_NO_DRAW;
self->s.fmnodeinfo[MESH__LKNIFE].flags |= FMNI_NO_DRAW;
self->s.fmnodeinfo[MESH__RKNIFE].flags |= FMNI_NO_DRAW;
SprayDebris(self, self->s.origin, 12, 100);
}
else
{
self->think = BecomeDebris;
self->nextthink = level.time + FRAMETIME;
return;
}
}
else
{
assassin_random_death_sound(self);
self->msgHandler = DyingMsgHandler;
}
chance = irand(0,10);
if(chance < 5 && self->health <= -80)
SetAnim(self, ANIM_DEATHA);
else
SetAnim(self, ANIM_DEATHB);
self->pre_think = NULL;
self->next_pre_think = -1;
if(self->s.renderfx & RF_ALPHA_TEXTURE)
if(self->pre_think != assassinDeCloak)
assassinInitDeCloak(self);
}
/*-------------------------------------------------------------------------
assassingrowl
-------------------------------------------------------------------------*/
void assassingrowl (edict_t *self)
{
int chance;
if (!irand(0, 20))
{
chance = irand(0, 9);
if (chance < 3)
gi.sound (self, CHAN_AUTO, Sounds[SND_GROWL1], 1, ATTN_IDLE, 0);
else if (chance < 6)
gi.sound(self, CHAN_AUTO, Sounds[SND_GROWL2], 1, ATTN_IDLE, 0);
else
gi.sound(self, CHAN_AUTO, Sounds[SND_GROWL3], 1, ATTN_IDLE, 0);
}
}
void assassin_random_attack(edict_t *self)
{
int chance;
chance = irand(0,3);
if((chance < 1&&!(self->s.fmnodeinfo[MESH__L4ARM].flags&FMNI_NO_DRAW)) ||
(self->s.fmnodeinfo[MESH__R4ARM].flags&FMNI_NO_DRAW&&!(self->s.fmnodeinfo[MESH__L4ARM].flags&FMNI_NO_DRAW)) )
{
SetAnim(self, ANIM_DAGGERL);
}
else if((chance < 2&&!(self->s.fmnodeinfo[MESH__R4ARM].flags&FMNI_NO_DRAW)) ||
(!(self->s.fmnodeinfo[MESH__R4ARM].flags&FMNI_NO_DRAW)&&self->s.fmnodeinfo[MESH__L4ARM].flags&FMNI_NO_DRAW) )
{
if(irand(0, 1))
SetAnim(self, ANIM_DAGGERR);
else
SetAnim(self, ANIM_NEWDAGGER);
}
else if(!(self->s.fmnodeinfo[MESH__R4ARM].flags&FMNI_NO_DRAW)&&
!(self->s.fmnodeinfo[MESH__L4ARM].flags&FMNI_NO_DRAW))
{
if(irand(0, 1))
SetAnim(self, ANIM_DAGGERB);
else
SetAnim(self, ANIM_NEWDAGGERB);
}
else
{
// gi.dprintf("Can't attack! Run away!\n");
self->monsterinfo.aiflags |= AI_COWARD;
SetAnim(self, ANIM_RUN);
}
}
/*-------------------------------------------------------------------------
assassin_melee
-------------------------------------------------------------------------*/
void assassin_melee(edict_t *self, G_Message_t *msg)
{
if (M_ValidTarget(self, self->enemy))
{
if(!irand(0,7))
{
/*if(self->s.renderfx & RF_ALPHA_TEXTURE)
{
if(self->pre_think != assassinDeCloak)
{
gi.sound(self,CHAN_AUTO,Sounds[SND_DECLOAK],1,ATTN_NORM,0);
self->pre_think = assassinDeCloak;
self->next_pre_think = level.time + FRAMETIME;
assassin_pause(self);
return;
}
}*/
if(assassinCheckTeleport(self, ASS_TP_OFF))
{
// gi.dprintf("melee->teleport\n");
return;//try to get away
}
}
assassin_random_attack(self);
}
else
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
}
void assassin_missile(edict_t *self, G_Message_t *msg)
{
if (M_ValidTarget(self, self->enemy))
{
if(!irand(0, 10))
{//10% chance try special action
if(!irand(0, 2))
{//25% chance teleport
if(assassinCheckTeleport(self, ASS_TP_OFF))
{
// gi.dprintf("missile->teleport\n");
return;
}
}
else if(!(self->s.renderfx & RF_ALPHA_TEXTURE))
{//75% cloak
if(!(self->spawnflags&MSF_ASS_NOSHADOW))
{
SetAnim(self, ANIM_CLOAK);
return;
}
}//else uncloak - unncloak when die
}
//need to check for if behind player- diff behaviour- get close and backstab?
assassin_random_attack(self);
}
else
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
}
/*-------------------------------------------------------------------------
assassin_pain
-------------------------------------------------------------------------*/
void assassin_post_pain (edict_t *self)
{
/*if(self->s.renderfx & RF_ALPHA_TEXTURE)
{
if(!irand(0,4))
{
if(self->pre_think != assassinDeCloak)
{
gi.sound(self,CHAN_AUTO,Sounds[SND_DECLOAK],1,ATTN_NORM,0);
self->pre_think = assassinDeCloak;
self->next_pre_think = level.time + FRAMETIME;
assassin_pause(self);
return;
}
}
}*/
if(self->fire_damage_time < level.time)//don't teleport if burning
{
if(assassinCheckTeleport(self, ASS_TP_ANY))
{
// gi.dprintf("pain->teleport\n");
return;
}
}
assassin_pause(self);
}
int Bit_for_MeshNode_as [16] =
{
BIT_DADDYNULL,
BIT_TORSOFT,
BIT_TORSOBK,
BIT_HEAD,
BIT_LKNIFE,
BIT_RKNIFE,
BIT_R4ARM,
BIT_L4ARM,
BIT_HIPS,
BIT_LCALF,
BIT_RCALF,
BIT_RTHIGH,
BIT_LTHIGH,
BIT_KNIFES,
BIT_LUPARM,
BIT_RUPARM
};
qboolean canthrownode_as (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_as[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 assassin_dropweapon (edict_t *self, int whichknives)
{//NO PART FLY FRAME!
vec3_t handspot, right;
AngleVectors(self->s.angles,NULL,right,NULL);
if(!(self->s.fmnodeinfo[MESH__LKNIFE].flags & FMNI_NO_DRAW)&&whichknives & BIT_LKNIFE)
{
VectorClear(handspot);
VectorMA(handspot, -12, right, handspot);
ThrowWeapon(self, &handspot, BIT_LKNIFE, 0, FRAME_prtfly);//FRAME_atakc3);
self->s.fmnodeinfo[MESH__LKNIFE].flags |= FMNI_NO_DRAW;
}
if(!(self->s.fmnodeinfo[MESH__RKNIFE].flags & FMNI_NO_DRAW)&&whichknives & BIT_RKNIFE)
{
VectorClear(handspot);
VectorMA(handspot, 12, right, handspot);
ThrowWeapon(self, &handspot, BIT_RKNIFE, 0, FRAME_prtfly);//FRAME_atakc3);
self->s.fmnodeinfo[MESH__RKNIFE].flags |= FMNI_NO_DRAW;
}
}
int assassin_convert_hitloc_dead(int hl)
{
switch(hl)
{
case hl_Head:
return hl_TorsoFront;
break;
case hl_TorsoFront://split in half?
if(!irand(0,1))
return hl_LegUpperRight;
else
return hl_LegUpperLeft;
break;
case hl_TorsoBack://split in half?
return hl_Head;
break;
case hl_ArmUpperLeft:
return hl_ArmLowerLeft;
break;
case hl_ArmLowerLeft://left arm
return hl_ArmUpperLeft;
break;
case hl_ArmUpperRight:
return hl_ArmLowerRight;
break;
case hl_ArmLowerRight://right arm
return hl_ArmUpperRight;
break;
case hl_LegUpperLeft:
return hl_LegLowerLeft;
break;
case hl_LegLowerLeft://left leg
return hl_LegUpperLeft;
break;
case hl_LegUpperRight:
return hl_LegLowerRight;
break;
case hl_LegLowerRight://right leg
return hl_LegUpperRight;
break;
default:
return irand(hl_Head, hl_LegLowerRight);
break;
}
}
void assassin_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;
// gi.dprintf("HL: %d",HitLocation);
if(self->health>0)
{
if(self->curAnimID==ANIM_DAGGERL||(self->curAnimID==ANIM_DAGGERB&&irand(0,2)<1))
{//Hit chest during melee, may have hit arms
if(HitLocation == hl_TorsoFront&&irand(0,10)<4)
{
if(irand(0,10)<7)
HitLocation = hl_ArmLowerRight;
else
HitLocation = hl_ArmLowerLeft;
}
}
if(self->curAnimID==ANIM_DAGGERR||self->curAnimID==ANIM_DAGGERC||(self->curAnimID==ANIM_DAGGERB&&irand(0,2)<1))
{//Hit chest during melee, may have hit arms
if(HitLocation == hl_TorsoFront&&irand(0,10)<4)
{
if(irand(0,10)<7)
HitLocation = hl_ArmLowerRight;
else
HitLocation = hl_ArmLowerLeft;
}
}
if(
(HitLocation == hl_ArmUpperLeft&& self->s.fmnodeinfo[MESH__LUPARM].flags & FMNI_NO_DRAW) ||
(HitLocation == hl_ArmUpperRight&& self->s.fmnodeinfo[MESH__RUPARM].flags & FMNI_NO_DRAW)||
(
(HitLocation == hl_TorsoFront|| HitLocation == hl_TorsoBack) &&
self->s.fmnodeinfo[MESH__LUPARM].flags & FMNI_NO_DRAW &&
self->s.fmnodeinfo[MESH__RUPARM].flags & FMNI_NO_DRAW &&
irand(0,10)<4)
)
HitLocation = hl_Head;//Decap
}
else
HitLocation = assassin_convert_hitloc_dead(HitLocation);
VectorClear(gore_spot);
switch(HitLocation)
{
case hl_Head:
if(self->s.fmnodeinfo[MESH__HEAD].flags & FMNI_NO_DRAW)
break;
if(self->s.fmnodeinfo[MESH__HEAD].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)
{
canthrownode_as(self, MESH__HEAD,&throw_nodes);
gore_spot[2]+=18;
ThrowBodyPart(self, &gore_spot, throw_nodes, damage, FRAME_prtfly);
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__HEAD].flags |= FMNI_USE_SKIN;
self->s.fmnodeinfo[MESH__HEAD].skin = self->s.skinnum+1;
}
break;
case hl_TorsoFront://split in half?
if(self->s.fmnodeinfo[MESH__TORSOFT].flags & FMNI_NO_DRAW)
break;
if(self->s.fmnodeinfo[MESH__TORSOFT].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)
{
gore_spot[2]+=12;
canthrownode_as(self, MESH__TORSOFT,&throw_nodes);
canthrownode_as(self, MESH__TORSOBK,&throw_nodes);
canthrownode_as(self, MESH__HEAD,&throw_nodes);
canthrownode_as(self, MESH__R4ARM,&throw_nodes);
canthrownode_as(self, MESH__L4ARM,&throw_nodes);
canthrownode_as(self, MESH__KNIFES,&throw_nodes);
canthrownode_as(self, MESH__LUPARM,&throw_nodes);
canthrownode_as(self, MESH__RUPARM,&throw_nodes);
assassin_dropweapon (self, BIT_LKNIFE|BIT_RKNIFE);
ThrowBodyPart(self, &gore_spot, throw_nodes, damage, FRAME_torsofly);
VectorAdd(self->s.origin, gore_spot, gore_spot);
SprayDebris(self,gore_spot,12,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
{
// if(flrand(0,self->health)<damage*0.5)
// assassin_dropweapon (self, (int)damage);
self->s.fmnodeinfo[MESH__TORSOFT].flags |= FMNI_USE_SKIN;
self->s.fmnodeinfo[MESH__TORSOFT].skin = self->s.skinnum+1;
}
break;
case hl_TorsoBack://split in half?
if(self->s.fmnodeinfo[MESH__TORSOBK].flags & FMNI_NO_DRAW)
break;
if(self->s.fmnodeinfo[MESH__TORSOBK].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)
{
gore_spot[2]+=12;
canthrownode_as(self, MESH__TORSOFT,&throw_nodes);
canthrownode_as(self, MESH__TORSOBK,&throw_nodes);
canthrownode_as(self, MESH__HEAD,&throw_nodes);
canthrownode_as(self, MESH__R4ARM,&throw_nodes);
canthrownode_as(self, MESH__L4ARM,&throw_nodes);
canthrownode_as(self, MESH__KNIFES,&throw_nodes);
canthrownode_as(self, MESH__LUPARM,&throw_nodes);
canthrownode_as(self, MESH__RUPARM,&throw_nodes);
assassin_dropweapon (self, BIT_LKNIFE|BIT_RKNIFE);
ThrowBodyPart(self, &gore_spot, throw_nodes, damage, FRAME_torsofly);
VectorAdd(self->s.origin, gore_spot, gore_spot);
SprayDebris(self,gore_spot,12,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
{
// if(flrand(0,self->health)<damage*0.5)
// assassin_dropweapon (self, (int)damage);
self->s.fmnodeinfo[MESH__TORSOBK].flags |= FMNI_USE_SKIN;
self->s.fmnodeinfo[MESH__TORSOBK].skin = self->s.skinnum+1;
}
break;
case hl_ArmUpperLeft:
if(self->s.fmnodeinfo[MESH__LUPARM].flags & FMNI_NO_DRAW)
break;
if(self->s.fmnodeinfo[MESH__LUPARM].flags & FMNI_USE_SKIN)
damage*=1.5;//greater chance to cut off if previously damaged
// if(flrand(0,self->health)<damage*0.4)
// assassin_dropweapon (self, (int)damage);
if(flrand(0,self->health)<damage*0.75&&dismember_ok)
{
canthrownode_as(self, MESH__L4ARM, &throw_nodes);
if(canthrownode_as(self, MESH__LUPARM, &throw_nodes))
{
AngleVectors(self->s.angles,NULL,right,NULL);
gore_spot[2]+=self->maxs[2]*0.3;
VectorMA(gore_spot,-10,right,gore_spot);
assassin_dropweapon (self, BIT_LKNIFE);
ThrowBodyPart(self, &gore_spot, throw_nodes, damage, FRAME_prtfly);
}
}
else
{
self->s.fmnodeinfo[MESH__LUPARM].flags |= FMNI_USE_SKIN;
self->s.fmnodeinfo[MESH__LUPARM].skin = self->s.skinnum+1;
}
break;
case hl_ArmLowerLeft://left arm
if(self->s.fmnodeinfo[MESH__L4ARM].flags & FMNI_NO_DRAW)
break;
if(self->s.fmnodeinfo[MESH__L4ARM].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_as(self, MESH__L4ARM, &throw_nodes))
{
AngleVectors(self->s.angles,NULL,right,NULL);
gore_spot[2]+=self->maxs[2]*0.3;
VectorMA(gore_spot,-10,right,gore_spot);
assassin_dropweapon (self, BIT_LKNIFE);
ThrowBodyPart(self, &gore_spot, throw_nodes, damage, FRAME_prtfly);
}
}
else
{
self->s.fmnodeinfo[MESH__L4ARM].flags |= FMNI_USE_SKIN;
self->s.fmnodeinfo[MESH__L4ARM].skin = self->s.skinnum+1;
}
break;
case hl_ArmUpperRight:
if(self->s.fmnodeinfo[MESH__RUPARM].flags & FMNI_NO_DRAW)
break;
if(flrand(0,self->health)<damage*0.75&&dismember_ok)
{
canthrownode_as(self, MESH__R4ARM, &throw_nodes);
if(canthrownode_as(self, MESH__RUPARM, &throw_nodes))
{
AngleVectors(self->s.angles,NULL,right,NULL);
gore_spot[2]+=self->maxs[2]*0.3;
VectorMA(gore_spot,10,right,gore_spot);
assassin_dropweapon (self, BIT_RKNIFE);
ThrowBodyPart(self, &gore_spot, throw_nodes, damage, FRAME_prtfly);
}
}
else
{
self->s.fmnodeinfo[MESH__RUPARM].flags |= FMNI_USE_SKIN;
self->s.fmnodeinfo[MESH__RUPARM].skin = self->s.skinnum+1;
}
break;
case hl_ArmLowerRight://right arm
if(self->s.fmnodeinfo[MESH__R4ARM].flags & FMNI_NO_DRAW)
break;
if(flrand(0,self->health)<damage*0.75&&dismember_ok)
{
if(canthrownode_as(self, MESH__R4ARM, &throw_nodes))
{
AngleVectors(self->s.angles,NULL,right,NULL);
gore_spot[2]+=self->maxs[2]*0.3;
VectorMA(gore_spot,10,right,gore_spot);
assassin_dropweapon (self, BIT_RKNIFE);
ThrowBodyPart(self, &gore_spot, throw_nodes, damage, FRAME_prtfly);
}
}
else
{
self->s.fmnodeinfo[MESH__R4ARM].flags |= FMNI_USE_SKIN;
self->s.fmnodeinfo[MESH__R4ARM].skin = self->s.skinnum+1;
}
break;
case hl_LegUpperLeft:
if(self->health>0)
{
if(self->s.fmnodeinfo[MESH__LTHIGH].flags & FMNI_USE_SKIN)
break;
self->s.fmnodeinfo[MESH__LTHIGH].flags |= FMNI_USE_SKIN;
self->s.fmnodeinfo[MESH__LTHIGH].skin = self->s.skinnum+1;
break;
}
else
{
if(self->s.fmnodeinfo[MESH__LTHIGH].flags & FMNI_NO_DRAW)
break;
canthrownode_as(self, MESH__LCALF, &throw_nodes);
if(canthrownode_as(self, MESH__LTHIGH, &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, 0);
}
break;
}
case hl_LegLowerLeft://left leg
if(self->health>0)
{
if(self->s.fmnodeinfo[MESH__LCALF].flags & FMNI_USE_SKIN)
break;
self->s.fmnodeinfo[MESH__LCALF].flags |= FMNI_USE_SKIN;
self->s.fmnodeinfo[MESH__LCALF].skin = self->s.skinnum+1;
break;
}
else
{
if(self->s.fmnodeinfo[MESH__LCALF].flags & FMNI_NO_DRAW)
break;
if(canthrownode_as(self, MESH__LCALF, &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, 0);
}
break;
}
case hl_LegUpperRight:
if(self->health>0)
{
if(self->s.fmnodeinfo[MESH__RTHIGH].flags & FMNI_USE_SKIN)
break;
self->s.fmnodeinfo[MESH__RTHIGH].flags |= FMNI_USE_SKIN;
self->s.fmnodeinfo[MESH__RTHIGH].skin = self->s.skinnum+1;
break;
}
else
{
if(self->s.fmnodeinfo[MESH__RTHIGH].flags & FMNI_NO_DRAW)
break;
canthrownode_as(self, MESH__RCALF, &throw_nodes);
if(canthrownode_as(self, MESH__RTHIGH, &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, 0);
}
break;
}
case hl_LegLowerRight://right leg
if(self->health>0)
{
if(self->s.fmnodeinfo[MESH__RCALF].flags & FMNI_USE_SKIN)
break;
self->s.fmnodeinfo[MESH__RCALF].flags |= FMNI_USE_SKIN;
self->s.fmnodeinfo[MESH__RCALF].skin = self->s.skinnum+1;
break;
}
else
{
if(self->s.fmnodeinfo[MESH__RCALF].flags & FMNI_NO_DRAW)
break;
if(canthrownode_as(self, MESH__RCALF, &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, 0);
}
break;
}
default:
if(flrand(0,self->health)<damage*0.25)
assassin_dropweapon (self, (int)damage);
break;
}
if(self->s.fmnodeinfo[MESH__L4ARM].flags&FMNI_NO_DRAW&&
self->s.fmnodeinfo[MESH__R4ARM].flags&FMNI_NO_DRAW)
{
self->monsterinfo.aiflags |= AI_COWARD;
self->monsterinfo.aiflags |= AI_NO_MELEE;
self->monsterinfo.aiflags |= AI_NO_MISSILE;
}
// gi.dprintf(" done\n");
}
void assassin_dismember_msg(edict_t *self, G_Message_t *msg)
{//fixme: throw current weapon
//fixme - make part fly dir the vector from hit loc to sever loc
//remember- turn on caps!
int damage;
HitLocation_t HitLocation;
ParseMsgParms(msg, "ii", &damage, &HitLocation);
assassin_dismember(self, damage, HitLocation);
}
void assassin_dead_pain (edict_t *self, G_Message_t *msg)
{
if(msg)
if(!(self->svflags & SVF_PARTS_GIBBED))
assassin_dismember_msg(self, msg);
}
void assassin_random_pain_sound (edict_t *self)
{
if(irand(0,1))
gi.sound(self, CHAN_VOICE, Sounds[SND_PAIN1], 1, ATTN_NORM, 0);
else
gi.sound(self, CHAN_VOICE, Sounds[SND_PAIN2], 1, ATTN_NORM, 0);
}
void assassin_pain(edict_t *self, G_Message_t *msg)
{
edict_t *attacker, *inflictor;
int damage, temp;
qboolean force_pain;
if(self->curAnimID == ANIM_TELEPORT)
return;
ParseMsgParms(msg, "eeiii", &inflictor, &attacker, &force_pain, &damage, &temp);
if(inflictor == attacker || !stricmp(inflictor->classname, "Spell_RedRain")||!stricmp(inflictor->classname, "Spell_Hellbolt"))
{//melee hit or contant effect, don't stick around!
if(!(self->spawnflags&MSF_ASS_NOTELEPORT)&&
!(self->spawnflags&MSF_FIXED)&&
self->groundentity)
{
if(assassinChooseTeleportDestination(self, ASS_TP_ANY, true, false))
return;
}
}
if(!force_pain)
if(flrand(0,self->health)>damage)
return;
self->monsterinfo.aiflags &= ~AI_OVERRIDE_GUIDE;
if(!self->maxs[2])
assassinUndoCrouched (self);
assassin_random_pain_sound(self);
if (self->pain_debounce_time < level.time||force_pain)
{
self->pain_debounce_time = level.time + skill->value + 2;
SetAnim(self, ANIM_PAIN2);
if(irand(0, 10) > skill->value)
{
self->monsterinfo.misc_debounce_time = level.time + 3;//3 seconds before can re-cloak
assassinInitDeCloak(self);
}
}
}
void assassinSkipFrameSkillCheck (edict_t *self)
{
if(irand(0, 3) < skill->value)
self->s.frame++;
}
/*-------------------------------------------------------------------------
assassin_pause
-------------------------------------------------------------------------*/
void assassin_pause (edict_t *self)
{
vec3_t v;
float len;
//this gets stuck on, sometimes
self->s.fmnodeinfo[MESH__LKNIFE].flags |= FMNI_NO_DRAW;
self->s.fmnodeinfo[MESH__RKNIFE].flags |= FMNI_NO_DRAW;
if(self->monsterinfo.aiflags&AI_OVERRIDE_GUIDE)
return;
if(self->spawnflags & MSF_FIXED && self->curAnimID == ANIM_DELAY && self->enemy)
{
self->monsterinfo.searchType = SEARCH_COMMON;
MG_FaceGoal(self, true);
}
self->mood_think(self);
if (self->ai_mood == AI_MOOD_NORMAL)
{
FindTarget(self);
if(self->enemy)
{
VectorSubtract (self->s.origin, self->enemy->s.origin, v);
len = VectorLength (v);
if ((len > 80) || (self->monsterinfo.aiflags & AI_FLEE)) // Far enough to run after
{
QPostMessage(self, MSG_RUN,PRI_DIRECTIVE, NULL);
}
else // Close enough to Attack
{
QPostMessage(self, MSG_MELEE, PRI_DIRECTIVE, NULL);
}
}
}
else
{
switch (self->ai_mood)
{
case AI_MOOD_ATTACK:
if(self->ai_mood_flags & AI_MOOD_FLAG_MISSILE)
QPostMessage(self, MSG_MISSILE, PRI_DIRECTIVE, NULL);
else
QPostMessage(self, MSG_MELEE, PRI_DIRECTIVE, NULL);
break;
case AI_MOOD_PURSUE:
case AI_MOOD_NAVIGATE:
QPostMessage(self, MSG_RUN, PRI_DIRECTIVE, NULL);
break;
case AI_MOOD_STAND:
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
break;
case AI_MOOD_WALK:
QPostMessage(self, MSG_WALK, PRI_DIRECTIVE, NULL);
break;
case AI_MOOD_DELAY:
SetAnim(self, ANIM_DELAY);
break;
case AI_MOOD_WANDER:
if(self->spawnflags&MSF_FIXED)
{
SetAnim(self, ANIM_DELAY);
return;
}
QPostMessage(self, MSG_WALK, PRI_DIRECTIVE, NULL);
break;
case AI_MOOD_JUMP:
if(self->spawnflags&MSF_FIXED)
SetAnim(self, ANIM_DELAY);
else
SetAnim(self, ANIM_FJUMP);//fjump will apply the movedir when he's ready
break;
default :
#ifdef _DEVEL
gi.dprintf("assassin: Unusable mood %d!\n", self->ai_mood);
#endif
break;
}
}
}
void assassinChooseJumpAmbush(edict_t *self)
{
float dot;
vec3_t forward, enemy_dir;
if(!self->enemy)
{
if(irand(0, 10)<4)
SetAnim(self, ANIM_JUMP);
else
SetAnim(self, ANIM_FRONTFLIP);
return;
}
AngleVectors(self->s.angles, forward, NULL, NULL);
VectorSubtract(self->enemy->s.origin, self->s.origin, enemy_dir);
VectorNormalize(enemy_dir);
dot = DotProduct(forward, enemy_dir);
if(dot<0)
{//behind
SetAnim(self, ANIM_BACKFLIP);
return;
}
if(!irand(0, 3))
SetAnim(self, ANIM_JUMP);
else
SetAnim(self, ANIM_FRONTFLIP);
return;
}
qboolean assassinChooseSideJumpAmbush(edict_t *self)
{//OR: turn and jump?
float dot;
vec3_t right, enemy_dir;
if(!self->enemy)
return false;
AngleVectors(self->s.angles, NULL, right, NULL);
VectorSubtract(self->enemy->s.origin, self->s.origin, enemy_dir);
VectorNormalize(enemy_dir);
dot = DotProduct(right, enemy_dir);
if(dot>0)
VectorScale(right, 300, self->movedir);
else
VectorScale(right, -300, self->movedir);
self->movedir[2] = 200;
SetAnim(self, ANIM_FJUMP);
return true;
}
/*-------------------------------------------------------------------------
assassin_run
-------------------------------------------------------------------------*/
void assassin_run(edict_t *self, G_Message_t *msg)
{
if(!self->enemy&&self->spawnflags&MSF_WANDER)
{
SetAnim(self, ANIM_RUN);
return;
}
if(self->spawnflags&MSF_ASS_STARTSHADOW)//decloak
{//FIXME: should I wait until infront of enemy and visible to him?
self->spawnflags &= ~MSF_ASS_STARTSHADOW;
assassinInitCloak(self);
}
if(!(self->spawnflags&MSF_FIXED))
{
if(self->spawnflags&MSF_ASS_JUMPAMBUSH)//jump out
{
self->spawnflags &= ~MSF_ASS_JUMPAMBUSH;
assassinChooseJumpAmbush(self);
return;
}
if(self->spawnflags&MSF_ASS_SIDEJUMPAMBUSH)//side-jump out
{
self->spawnflags &= ~MSF_ASS_SIDEJUMPAMBUSH;
if(assassinChooseSideJumpAmbush(self))
return;
}
}
if(self->curAnimID >= ANIM_CROUCH_IDLE && self->curAnimID < ANIM_CROUCH_END)
{
SetAnim(self, ANIM_CROUCH_END);
return;
}
if (M_ValidTarget(self, self->enemy))
{
if(!irand(0, 7))
{
if(assassinCheckTeleport(self, ASS_TP_OFF))
{
// gi.dprintf("run->teleport\n");
return;
}
else if(!irand(0, 3))
{
if(!(self->s.renderfx & RF_ALPHA_TEXTURE))
{
if(!(self->spawnflags&MSF_ASS_NOSHADOW))
{
SetAnim(self, ANIM_CLOAK);
return;
}
}//else uncloak - unncloak when die
}
}
if(!(self->spawnflags&MSF_FIXED))
SetAnim(self, ANIM_RUN);
else
SetAnim(self, ANIM_DELAY);
}
else
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
}
void assassin_go_run(edict_t *self, float dist)
{
if(!self->maxs[2])
assassinUndoCrouched (self);
if(self->enemy)
ai_run(self, dist);
else
ai_walk(self, dist);
}
/*----------------------------------------------------------------------
assassin runorder - order the assassin to choose an run animation
-----------------------------------------------------------------------*/
void assassin_runorder(edict_t *self)
{
QPostMessage(self, MSG_RUN, PRI_DIRECTIVE, NULL);
}
/*-------------------------------------------------------------------------
assassinsqueal
-------------------------------------------------------------------------*/
void assassinsqueal (edict_t *self)
{
/*
if(irand(0, 1))
gi.sound(self, CHAN_WEAPON, Sounds[SND_PAIN1], 1, ATTN_NORM, 0);
else
gi.sound(self, CHAN_WEAPON, Sounds[SND_PAIN2], 1, ATTN_NORM, 0);
*/
}
/*-------------------------------------------------------------------------
assassin_stand
-------------------------------------------------------------------------*/
void assassin_stand(edict_t *self, G_Message_t *msg)
{
if (self->ai_mood == AI_MOOD_DELAY)
SetAnim(self, ANIM_DELAY);
else
{
if(self->s.renderfx & RF_ALPHA_TEXTURE)
{
if(self->pre_think != assassinDeCloak)
assassinInitDeCloak(self);
}
SetAnim(self, ANIM_STAND);
}
return;
}
/*-------------------------------------------------------------------------
assassin_walk
-------------------------------------------------------------------------*/
void assassin_crouch_idle_decision (edict_t *self)
{//FIXME: need to uncrouch
int chance;
chance = irand(0, 100);
switch(self->curAnimID)
{
case ANIM_CROUCH_IDLE:
if(chance < 55)
SetAnim(self, ANIM_CROUCH_IDLE);
else if(chance < 75)
SetAnim(self, ANIM_CROUCH_POKE);
else if(chance < 85)
SetAnim(self, ANIM_CROUCH_LOOK_RIGHT);
else if(chance < 95)
SetAnim(self, ANIM_CROUCH_LOOK_LEFT);
else
SetAnim(self, ANIM_CROUCH_END);
break;
case ANIM_CROUCH_LOOK_RIGHT:
case ANIM_CROUCH_LOOK_RIGHT_IDLE:
case ANIM_CROUCH_LOOK_L2R:
if(chance < 60)
SetAnim(self, ANIM_CROUCH_LOOK_RIGHT_IDLE);
else if(chance < 85)
SetAnim(self, ANIM_CROUCH_LOOK_R2C);
else
SetAnim(self, ANIM_CROUCH_LOOK_R2L);
break;
case ANIM_CROUCH_LOOK_LEFT:
case ANIM_CROUCH_LOOK_LEFT_IDLE:
case ANIM_CROUCH_LOOK_R2L:
if(chance < 60)
SetAnim(self, ANIM_CROUCH_LOOK_LEFT_IDLE);
else if(chance < 85)
SetAnim(self, ANIM_CROUCH_LOOK_L2C);
else
SetAnim(self, ANIM_CROUCH_LOOK_L2R);
break;
case ANIM_CROUCH_TRANS:
self->monsterinfo.pausetime = 99999999;
SetAnim(self, ANIM_CROUCH_IDLE);
break;
case ANIM_CROUCH_LOOK_R2C:
case ANIM_CROUCH_LOOK_L2C:
case ANIM_CROUCH_POKE:
SetAnim(self, ANIM_CROUCH_IDLE);
break;
case ANIM_CROUCH_END:
self->damage_debounce_time = level.time + 10;
self->monsterinfo.pausetime = -1;
SetAnim(self, ANIM_STAND);
break;
default:
SetAnim(self, ANIM_CROUCH_END);
break;
}
}
void assassin_ai_walk (edict_t *self, float dist)
{
if(self->damage_debounce_time < level.time)
{
if(self->enemy)
{
if(vhlen(self->s.origin, self->enemy->s.origin) < 48 && infront(self, self->enemy))
{
assassinNodeOn (self, MESH__LKNIFE);
SetAnim(self, ANIM_CROUCH_TRANS);
return;
}
}
else if(self->oldenemy)
{
if(vhlen(self->s.origin, self->oldenemy->s.origin) < 48 && infront(self, self->oldenemy))
{
assassinNodeOn (self, MESH__LKNIFE);
SetAnim(self, ANIM_CROUCH_TRANS);
return;
}
}
}
ai_walk(self, dist);
}
void assassin_walk(edict_t *self, G_Message_t *msg)
{
if(self->spawnflags&MSF_FIXED)
{
SetAnim(self, ANIM_DELAY);
return;
}
if(self->curAnimID == ANIM_WALK_LOOP)
SetAnim(self, ANIM_WALK_LOOP);
else
SetAnim(self, ANIM_WALK);
return;
}
void assasin_walk_loop_go (edict_t *self)
{
SetAnim(self, ANIM_WALK_LOOP);
}
//=============================================================
// EVASION
//=============================================================
void assassinDodgeLeft (edict_t *self)
{
SetAnim(self, ANIM_DODGE_LEFT);
}
void assassinDodgeRight (edict_t *self)
{
SetAnim(self, ANIM_DODGE_RIGHT);
}
void assassinFrontFlip (edict_t *self)
{
SetAnim(self, ANIM_EVFRONTFLIP);
}
void assassinBackFlip (edict_t *self)
{
SetAnim(self, ANIM_EVBACKFLIP);
}
void assassinBackSprings (edict_t *self)
{
SetAnim(self, ANIM_BACKSPRING);
}
void assassinJump (edict_t *self)
{
SetAnim(self, ANIM_EVJUMP);
}
void assassinCrouch (edict_t *self)
{
SetAnim(self, ANIM_CROUCH);
}
void assassinCrouchedAttack (edict_t *self)
{
SetAnim(self, ANIM_DAGGERC);
}
void assassin_evade (edict_t *self, G_Message_t *msg)
{
edict_t *projectile;
HitLocation_t HitLocation;
int duck_chance, dodgeleft_chance, dodgeright_chance, jump_chance, backflip_chance, frontflip_chance;
int chance;
float eta;
if(!self->groundentity)
return;
ParseMsgParms(msg, "eif", &projectile, &HitLocation, &eta);
if(eta < 2)
self->evade_debounce_time = level.time + eta;
else
self->evade_debounce_time = level.time + 2;
if(skill->value || self->spawnflags & MSF_ASS_TELEPORTDODGE)
{//Pussies were complaining about assassins teleporting away from certain death, so don't do that unless in hard
if(!stricmp(projectile->classname, "Spell_PhoenixArrow") ||
!stricmp(projectile->classname, "Spell_FireWall") ||
!stricmp(projectile->classname, "Spell_SphereOfAnnihilation") ||
!stricmp(projectile->classname, "Spell_Maceball"))
{
if(assassinChooseTeleportDestination(self, ASS_TP_OFF, true, true))
return;
}
}
switch(HitLocation)
{
case hl_Head:
duck_chance = 95;
dodgeleft_chance = 50;
dodgeright_chance = 50;
jump_chance = 0;
backflip_chance = 20;
frontflip_chance = 20;
break;
case hl_TorsoFront://split in half?
duck_chance = 85;
dodgeleft_chance = 40;
dodgeright_chance = 40;
jump_chance = 0;
backflip_chance = 60;
frontflip_chance = 0;
break;
case hl_TorsoBack://split in half?
duck_chance = 80;
dodgeleft_chance = 40;
dodgeright_chance = 40;
jump_chance = 0;
backflip_chance = 0;
frontflip_chance = 60;
break;
case hl_ArmUpperLeft:
duck_chance = 75;
dodgeleft_chance = 0;
dodgeright_chance = 90;
jump_chance = 0;
backflip_chance = 20;
frontflip_chance = 20;
break;
case hl_ArmLowerLeft://left arm
duck_chance = 75;
dodgeleft_chance = 0;
dodgeright_chance = 80;
jump_chance = 30;
backflip_chance = 20;
frontflip_chance = 20;
break;
case hl_ArmUpperRight:
duck_chance = 60;
dodgeleft_chance = 90;
dodgeright_chance = 0;
jump_chance = 0;
backflip_chance = 20;
frontflip_chance = 20;
break;
case hl_ArmLowerRight://right arm
duck_chance = 20;
dodgeleft_chance = 80;
dodgeright_chance = 0;
jump_chance = 30;
backflip_chance = 20;
frontflip_chance = 20;
break;
case hl_LegUpperLeft:
duck_chance = 0;
dodgeleft_chance = 0;
dodgeright_chance = 60;
jump_chance = 50;
backflip_chance = 30;
frontflip_chance = 30;
break;
case hl_LegLowerLeft://left leg
duck_chance = 0;
dodgeleft_chance = 0;
dodgeright_chance = 30;
jump_chance = 80;
backflip_chance = 40;
frontflip_chance = 40;
break;
case hl_LegUpperRight:
duck_chance = 0;
dodgeleft_chance = 60;
dodgeright_chance = 0;
jump_chance = 50;
backflip_chance = 30;
frontflip_chance = 30;
break;
case hl_LegLowerRight://right leg
duck_chance = 0;
dodgeleft_chance = 30;
dodgeright_chance = 0;
jump_chance = 80;
backflip_chance = 40;
frontflip_chance = 40;
break;
default:
duck_chance = 20;
dodgeleft_chance = 10;
dodgeright_chance = 10;
jump_chance = 10;
backflip_chance = 10;
frontflip_chance = 10;
break;
}
if(irand(0, 100) < skill->value * 10)
{
if(self->pre_think != assassinCloak)
assassinInitCloak(self);
}
chance = irand(0, 10);
if(skill->value || self->spawnflags & MSF_ASS_TELEPORTDODGE)
{//Pussies were complaining about assassins teleporting away from certain death, so don't do that unless in hard
if(chance > 8 && !(self->spawnflags&MSF_ASS_NOTELEPORT))
if(assassinChooseTeleportDestination(self, ASS_TP_DEF, false, false))
{
// gi.dprintf("Assassin teleport evade\n");
return;
}
}
chance = irand(0, 100);
if(chance < frontflip_chance)
{
// gi.dprintf("Assassin fflip evade\n");
assassinFrontFlip(self);
return;
}
chance = irand(0, 100);
if(chance < backflip_chance)
{
if(self->curAnimID == ANIM_RUN && irand(0, 3))//running, do the front flip
{
// gi.dprintf("Assassin fflip evade\n");
assassinFrontFlip(self);
}
else
{
if(irand(0, 1))
{
// gi.dprintf("Assassin bspring evade\n");
assassinBackSprings(self);
}
else
{
// gi.dprintf("Assassin bflip evade\n");
assassinBackFlip(self);
}
}
return;
}
chance = irand(0, 100);
if(chance < duck_chance)
{
self->evade_debounce_time = level.time + eta + 2 - skill->value;
// gi.dprintf("Assassin crouch evade\n");
assassinCrouch(self);
return;
}
chance = irand(0, 100);
if(chance < dodgeleft_chance)
{
// gi.dprintf("Assassin dleft evade\n");
assassinDodgeLeft(self);
return;
}
chance = irand(0, 100);
if(chance < dodgeright_chance)
{
// gi.dprintf("Assassin dright evade\n");
assassinDodgeRight(self);
return;
}
chance = irand(0, 100);
if(chance < jump_chance)
{
if(self->curAnimID == ANIM_RUN && irand(0, 4))//running, do the front flip
{
// gi.dprintf("Assassin fflip evade\n");
assassinFrontFlip(self);
}
else
{
// gi.dprintf("Assassin jump evade\n");
assassinJump(self);
}
return;
}
if(skill->value || self->spawnflags & MSF_ASS_TELEPORTDODGE)
{//Pussies were complaining about assassins teleporting away from certain death, so don't do that unless in hard
if(!(self->spawnflags&MSF_ASS_NOTELEPORT))
if(assassinChooseTeleportDestination(self, ASS_TP_DEF, false, false))
{
// gi.dprintf("Assassin tport(desperate) evade\n");
return;
}
}
self->evade_debounce_time = 0;
// gi.dprintf("Assassin failed to evade\n");
}
void assassinCrouchedCheckAttack (edict_t *self, float attack)
{
if(!clear_visible(self, self->enemy))
return;
if(!infront(self, self->enemy))
return;
if(irand(0,10)<5)
return;
if(attack == true)
assassindagger(self, BIT_RKNIFE);
else if(attack == 2)//start crouched attack anim
SetAnim(self, ANIM_DAGGERC);
else//loop back inside that anim
self->monsterinfo.currframeindex = 2;
}
void assassinNodeOn (edict_t *self, float node)
{
self->s.fmnodeinfo[(int)node].flags &= ~FMNI_NO_DRAW;
}
void assassinStop (edict_t *self)
{
if(self->evade_debounce_time - level.time> 0.1f)
self->nextthink = level.time + (self->evade_debounce_time - level.time);
}
void assassinSetCrouched (edict_t *self)
{
VectorSet (self->maxs, 16, 16, 0);
self->viewheight = 0;
}
void assassinUndoCrouched (edict_t *self)
{
VectorSet (self->maxs, 16, 16, 48);
self->viewheight = 40;
}
void assassin_sound(edict_t *self, float channel, float soundnum, float attn)
{
gi.sound (self, (int)(channel), Sounds[(int)(soundnum)], 1, (int)(attn), 0);
}
void assassinGoJump (edict_t *self, float fwdspd,float upspd,float rtspd)
{//fixme: do checks and traces first
vec3_t up, forward, right;
self->monsterinfo.aiflags &= ~AI_OVERRIDE_GUIDE;
assassin_sound(self, CHAN_VOICE, SND_JUMP, ATTN_NORM);
AngleVectors(self->s.angles, forward, right, up);
VectorMA(self->velocity, upspd, up, self->velocity);
VectorMA(self->velocity, fwdspd, forward, self->velocity);
VectorMA(self->velocity, rtspd, right, self->velocity);
}
void assassin_go_inair(edict_t *self)
{
SetAnim(self, ANIM_INAIR);
}
void assassin_go_evinair(edict_t *self)
{
SetAnim(self, ANIM_EVINAIR);
}
void assassin_go_ffinair(edict_t *self)
{
SetAnim(self, ANIM_FFINAIR);
}
void assassin_go_bfinair(edict_t *self)
{
SetAnim(self, ANIM_BFINAIR);
}
void assassinCheckLoop (edict_t *self, float frame)
{//see if should fire again
vec3_t v;
float len, melee_range, min_seperation, jump_range;
if(!self->enemy)
return;
ai_charge2(self, 0);
if(!clear_visible(self, self->enemy))
return;
if(!infront(self, self->enemy))
return;
if(irand(0, 100) < self->bypass_missile_chance)
{
self->monsterinfo.attack_finished = level.time + 3 - skill->value;
return;
}
if(self->ai_mood_flags&AI_MOOD_FLAG_BACKSTAB)
return;
VectorSubtract (self->s.origin, self->enemy->s.origin, v);
len = VectorLength (v);
melee_range = 64;
jump_range = 128;
min_seperation = self->maxs[0] + self->enemy->maxs[0];
if (infront(self, self->enemy))
{//don't loop if enemy close enough
if (len < min_seperation + melee_range)
return;
else if (len < min_seperation + jump_range && irand(0,10)<3)
return;
}
self->monsterinfo.currframeindex = (int)frame;
}
void assassinSmoke(edict_t *self)
{
vec3_t pos;
VectorCopy(self->s.origin, pos);
pos[2]+=self->mins[2];
gi.CreateEffect(NULL, FX_TPORTSMOKE, 0, pos, "");//, "db", hitangles, 5);
//gi.CreateEffect(&self->s, FX_DUST_PUFF, CEF_OWNERS_ORIGIN, self->s.origin, NULL);
}
void assassinGone(edict_t *self)
{
vec3_t enemy_dir;
if(self->placeholder)
G_FreeEdict(self->placeholder);
VectorCopy(self->pos2, self->s.origin);
if(self->enemy)
{//face enemy
VectorSubtract(self->enemy->s.origin, self->s.origin, enemy_dir);
self->s.angles[YAW] = anglemod(vectoyaw(enemy_dir));
}
assassinSmoke(self);
VectorCopy(self->pos2, enemy_dir);//reuse
enemy_dir[2] += 100;
if(gi.pointcontents(enemy_dir) == CONTENTS_EMPTY&&!irand(0,3))
assassinFrontFlip(self);
else
SetAnim(self, ANIM_UNCROUCH);
self->monsterinfo.aiflags &= ~AI_OVERRIDE_GUIDE;
self->svflags &= ~SVF_NO_AUTOTARGET;
//dumbed down
self->touch_debounce_time = level.time + (10 - skill->value*3);
//Should we clear velocity too?
//VectorClear(self->velocity);
gi.linkentity(self);
}
void assassinPrepareTeleportDest(edict_t *self, vec3_t spot, qboolean instant)
{
if(self->s.renderfx & RF_ALPHA_TEXTURE)
{
if(self->pre_think != assassinDeCloak)
{
assassinInitDeCloak(self);
self->monsterinfo.misc_debounce_time = level.time + 3;
}
}
VectorCopy(spot, self->pos2);
self->placeholder = G_Spawn();
VectorCopy(self->pos2, self->placeholder->s.origin);
self->placeholder->solid = SOLID_BBOX;
VectorCopy(self->mins, self->placeholder->mins);
VectorCopy(self->maxs, self->placeholder->mins);
self->placeholder->think = G_FreeEdict;
self->placeholder->nextthink = level.time + 2;//just in case
//dumbed down
if(instant && skill->value > 1)
{
assassinReadyTeleport(self);
assassinGone(self);
}
else
{
SetAnim(self, ANIM_TELEPORT);
}
}
qboolean assassinChooseTeleportDestination(edict_t *self, int type, qboolean imperative, qboolean instant)
{//FIXME: don't teleport into area with red rain or ripper balls!
vec3_t teleport_angles, forward, endpos, startpos;
trace_t trace;
int chance, num_tries, i;
edict_t *noblockent;
float tracedist;
//Instead of chance, do around self if evade, around other if ambush
if(!self->enemy)//fixme- choose my spot?
return false;
if(self->spawnflags&MSF_FIXED)
return false;
if(imperative)
num_tries = (skill->value + 1) * 10;
else
num_tries = 1;
for(i = 0; i < num_tries; i++)
{
switch(type)
{
case ASS_TP_OFF:
chance = irand(0, 66);
break;
case ASS_TP_ANY:
chance = irand(0, 100);
break;
case ASS_TP_DEF:
chance = irand(33, 100);
break;
}
if(chance<33)
{//ANY, OFF to behind enemy
VectorSet(teleport_angles, 0, anglemod(self->enemy->s.angles[YAW] + flrand(-90, 90)), 0);
AngleVectors(teleport_angles, forward, NULL, NULL);
VectorCopy(self->enemy->s.origin, startpos);
startpos[2]+=self->enemy->mins[2];
startpos[2]-=self->mins[2];
tracedist = irand(self->min_missile_range, self->missile_range);
VectorMA(startpos, -tracedist, forward, endpos);
noblockent = self->enemy;
}
else if(chance<66)
{//ANY to anywhere around enemy
VectorSet(teleport_angles, 0, anglemod(flrand(0, 360)), 0);
AngleVectors(teleport_angles, forward, NULL, NULL);
VectorCopy(self->enemy->s.origin, startpos);
startpos[2]+=self->enemy->mins[2];
startpos[2]-=self->mins[2];
tracedist = irand(self->min_missile_range, self->missile_range);
VectorMA(startpos, -tracedist, forward, endpos);
noblockent = self->enemy;
}
else
{//ANY, DEF to anywhere around me
VectorSet(teleport_angles, 0, anglemod(flrand(0, 360)), 0);
AngleVectors(teleport_angles, forward, NULL, NULL);
VectorCopy(self->s.origin, startpos);
tracedist = irand(self->min_missile_range, self->missile_range/2);
VectorMA(startpos, -tracedist, forward, endpos);
noblockent = self;
}
gi.trace(startpos, self->mins, self->maxs, endpos, noblockent, MASK_MONSTERSOLID,&trace);
if(trace.fraction*tracedist < 100)//min origin lerp dist
continue;
if(trace.allsolid || trace.startsolid)
continue;
if(vhlen(trace.endpos, self->enemy->s.origin)>=self->min_missile_range)
{
VectorCopy(trace.endpos, startpos);
VectorCopy(trace.endpos, endpos);
endpos[2] -=64;
gi.trace(startpos, self->mins, self->maxs, endpos, noblockent, MASK_MONSTERSOLID,&trace);
if(trace.fraction<1.0 && !trace.allsolid && !trace.startsolid)//the last two should be false if trace.fraction is < 1.0 but doesn't hurt to check
{
assassinPrepareTeleportDest(self, trace.endpos, instant);
return true;
}
}
}
return false;
}
void assassinReadyTeleport (edict_t *self)
{
assassinSmoke(self);
self->svflags |= SVF_NO_AUTOTARGET;
}
qboolean assassinCheckTeleport (edict_t *self, int type)
{
if(self->spawnflags&MSF_ASS_NOTELEPORT)
return false;
if(self->spawnflags&MSF_FIXED)
return false;
if(!self->groundentity)
return false;
if(!M_ValidTarget(self, self->enemy))
return false;
/* if(!infront(self->enemy, self))
return false;
if(!visible(self->enemy, self))
return false;*/
return assassinChooseTeleportDestination(self, type, false, false);
}
void assassinUnCrouch (edict_t *self)
{
SetAnim(self, ANIM_UNCROUCH);
}
qboolean assassinCheckCloak (edict_t *self)
{
int chance = 0;
if(!self->monsterinfo.awake)
return (false);
if(self->monsterinfo.misc_debounce_time > level.time)//cloak debounce time
return (false);
if(self->spawnflags & MSF_ASS_NOSHADOW)
return (false);
if(self->ai_mood == AI_MOOD_FLEE)
return (true);
if(!self->enemy)
return (false);
if(infront(self->enemy, self))
chance = -3;
if(irand(0, 10 - skill->value + chance) <= 0)
return (true);
return (false);
}
qboolean assassinCheckDeCloak (edict_t *self)
{
float dist;
int chance = 0;
if(!self->monsterinfo.awake)
return (false);
if(self->monsterinfo.misc_debounce_time > level.time)//cloak debounce time
return (false);
if(!self->enemy)
{
if(!(self->spawnflags & MSF_ASS_STARTSHADOW))
return (true);
return (false);
}
dist = M_DistanceToTarget(self, self->enemy);
if(dist<ASSASSIN_MIN_CLOAK_RANGE)
return (true);
if(!infront(self->enemy, self))
chance = -3;
if(irand(0, 10 + skill->value * 2 + chance) <= 0)
return (true);
return (false);
}
void assassinCloakThink (edict_t *self)
{
edict_t *found = NULL;
int lowerseq, upperseq;
vec3_t mins, maxs, startpos, endpos, tport_dest;
trace_t trace;
self->pre_think = assassinCloakThink;
self->next_pre_think = level.time + FRAMETIME;
//check cloak or decloak
if(!(self->s.renderfx & RF_ALPHA_TEXTURE))
{//not cloaked
if(assassinCheckCloak(self))
{
self->monsterinfo.misc_debounce_time = level.time + 7;//10 seconds before will willingly uncloak
assassinInitCloak(self);
}
}
else
{//cloaked
if(assassinCheckDeCloak(self))
assassinInitDeCloak(self);
}
//check to teleport
//dumbed down
if(!skill->value)//was < 2
{
if(self->touch_debounce_time > level.time)
{
return;
}
}
if(self->waterlevel == 3 && self->air_finished <= level.time)//going to drown!
{//pick either last buoy or my startspot
VectorCopy(self->pos1, tport_dest);
if(self->lastbuoy>NULL_BUOY)
{
if(!(gi.pointcontents(level.buoy_list[self->lastbuoy].origin) & MASK_WATER))
VectorCopy(level.buoy_list[self->lastbuoy].origin, tport_dest);
}
VectorCopy(tport_dest, startpos);
VectorCopy(self->mins, mins);
mins[2] = 0;
VectorCopy(self->maxs, maxs);
maxs[2] = 1;
startpos[2] -= self->size[2];
gi.trace(startpos, mins, maxs, endpos, self, MASK_MONSTERSOLID,&trace);
if(!trace.allsolid && !trace.startsolid)
{
VectorCopy(trace.endpos, startpos);
VectorCopy(trace.endpos, endpos);
startpos[2] +=self->size[2];
gi.trace(startpos, self->mins, self->maxs, endpos, self, MASK_MONSTERSOLID,&trace);
if(trace.fraction == 1.0 && !trace.allsolid && !trace.startsolid)
{
assassinPrepareTeleportDest(self, trace.endpos, false);
return;
}
}
}
if(skill->value || self->spawnflags & MSF_ASS_TELEPORTDODGE)
{//Pussies were complaining about assassins teleporting away from certain death, so don't do that unless in hard
if(!(self->spawnflags & MSF_ASS_NOTELEPORT) && !(self->spawnflags&MSF_FIXED) && self->groundentity)
{
if(irand(0, 4 - skill->value) <= 0)
{//easy is 40% chance per second, hard is 60% chance to check per second
while(found = findradius(found, self->s.origin, 200 + skill->value * 50))
{
if(!stricmp(found->classname, "Spell_Maceball"))
{
if(!self->enemy)
{
if(found->owner)
{
self->enemy = found->owner;
FoundTarget(self, false);
}
}
if(assassinChooseTeleportDestination(self, ASS_TP_OFF, true, true))
return;
}
if(!stricmp(found->classname, "Spell_RedRain") ||
!stricmp(found->classname, "Spell_PhoenixArrow") ||
!stricmp(found->classname, "Spell_FireWall") ||
!stricmp(found->classname, "Spell_SphereOfAnnihilation"))
{
if(!self->enemy)
{
if(found->owner)
{
self->enemy = found->owner;
FoundTarget(self, false);
}
}
if(assassinChooseTeleportDestination(self, ASS_TP_ANY, true, false))
return;
}
if(found==self->enemy && found->client)
{
if(M_DistanceToTarget(self, self->enemy) < 128)
{
if(infront(self->enemy, self))
{
//is he using his staff or jumping into me?
lowerseq = found->client->playerinfo.lowerseq;
switch(lowerseq)
{
case ASEQ_WSWORD_SPIN:
case ASEQ_WSWORD_SPIN2:
case ASEQ_WSWORD_STEP2:
case ASEQ_WSWORD_STEP:
case ASEQ_POLEVAULT2:
case ASEQ_POLEVAULT1_W:
case ASEQ_POLEVAULT1_R:
if(assassinChooseTeleportDestination(self, ASS_TP_ANY, true, true))
return;
break;
default:
break;
}
upperseq = found->client->playerinfo.upperseq;
switch(upperseq)
{
case ASEQ_WSWORD_SPIN:
case ASEQ_WSWORD_SPIN2:
case ASEQ_WSWORD_STEP2:
case ASEQ_WSWORD_STEP:
case ASEQ_POLEVAULT2:
case ASEQ_POLEVAULT1_W:
case ASEQ_POLEVAULT1_R:
if(assassinChooseTeleportDestination(self, ASS_TP_ANY, true, true))
return;
break;
default:
break;
}
}
if(found->client->playerinfo.shield_timer > level.time)
{
if(assassinChooseTeleportDestination(self, ASS_TP_OFF, true, true))
return;
}
}
}
}
}
}
}
if(self->evade_debounce_time < level.time)
MG_CheckEvade(self);
}
void assassinCloak (edict_t *self)
{
int interval = 15;
if(self->s.color.r > 50)
self->s.color.r += irand(-interval*3, 0);
if(self->s.color.g > 50)
self->s.color.g += irand(-interval*3, 0);
if(self->s.color.b > 50)
self->s.color.b += irand(-interval*3, 0);
if(self->s.color.a > 150)
self->s.color.a += irand(-interval, 0);
if(self->s.color.r > 50||
self->s.color.g > 50||
self->s.color.b > 50||
self->s.color.a > 150)
{
self->pre_think = assassinCloak;
self->next_pre_think = level.time + FRAMETIME;
}
else
{
self->pre_think = assassinCloakThink;
self->next_pre_think = level.time + FRAMETIME;
}
if(self->evade_debounce_time < level.time)
MG_CheckEvade(self);
}
void assassinDeCloak (edict_t *self)
{
if(!(self->s.renderfx & RF_ALPHA_TEXTURE))
return;
if(self->s.color.r<255 - 10)
self->s.color.r += 10;
else
self->s.color.r = 255;
if(self->s.color.g<255 - 10)
self->s.color.g += 10;
else
self->s.color.g = 255;
if(self->s.color.b<255 - 10)
self->s.color.b += 10;
else
self->s.color.b = 255;
if(self->s.color.a<255 - 5)
self->s.color.a += 5;
else
self->s.color.a = 255;
if(self->s.color.r == 255&&
self->s.color.g == 255&&
self->s.color.b == 255&&
self->s.color.a == 255)
{
self->svflags &= ~SVF_NO_AUTOTARGET;
self->s.renderfx &= ~RF_ALPHA_TEXTURE;
if(self->health > 0)
{
self->pre_think = assassinCloakThink;
self->next_pre_think = level.time + FRAMETIME;
}
else
{
self->pre_think = NULL;
self->next_pre_think = -1;
}
}
else
{
self->pre_think = assassinDeCloak;
self->next_pre_think = level.time + FRAMETIME;
}
if(self->evade_debounce_time < level.time)
MG_CheckEvade(self);
}
void assassinInitDeCloak (edict_t *self)
{
gi.sound(self,CHAN_AUTO,Sounds[SND_DECLOAK],1,ATTN_NORM,0);
self->pre_think = assassinDeCloak;
self->next_pre_think = level.time + FRAMETIME;
}
void assassinInitCloak (edict_t *self)
{
self->s.renderfx |= RF_ALPHA_TEXTURE;
self->svflags |= SVF_NO_AUTOTARGET;
gi.sound(self,CHAN_AUTO,Sounds[SND_CLOAK],1,ATTN_NORM,0);
self->s.color.r = 255;
self->s.color.g = 255;
self->s.color.b = 255;
self->s.color.a = 255;
self->pre_think = assassinCloak;
self->next_pre_think = level.time + FRAMETIME;
}
void assassin_check_mood (edict_t *self, G_Message_t *msg)
{
ParseMsgParms(msg, "i", &self->ai_mood);
assassin_pause(self);
}
//=============================================================
/*-------------------------------------------------------------------------
AssassinStaticsInit
-------------------------------------------------------------------------*/
void AssassinStaticsInit()
{
classStatics[CID_ASSASSIN].msgReceivers[MSG_STAND] = assassin_stand;
classStatics[CID_ASSASSIN].msgReceivers[MSG_WALK] = assassin_walk;
classStatics[CID_ASSASSIN].msgReceivers[MSG_RUN] = assassin_run;
classStatics[CID_ASSASSIN].msgReceivers[MSG_MELEE] = assassin_melee;
classStatics[CID_ASSASSIN].msgReceivers[MSG_MISSILE] = assassin_missile;
classStatics[CID_ASSASSIN].msgReceivers[MSG_PAIN] = assassin_pain;
classStatics[CID_ASSASSIN].msgReceivers[MSG_DEATH] = assassin_death;
classStatics[CID_ASSASSIN].msgReceivers[MSG_DISMEMBER] = MG_parse_dismember_msg;
classStatics[CID_ASSASSIN].msgReceivers[MSG_JUMP] = assassin_jump;
classStatics[CID_ASSASSIN].msgReceivers[MSG_EVADE] = assassin_evade;
classStatics[CID_ASSASSIN].msgReceivers[MSG_DEATH_PAIN] = assassin_dead_pain;
classStatics[CID_ASSASSIN].msgReceivers[MSG_CHECK_MOOD] = assassin_check_mood;
classStatics[CID_ASSASSIN].msgReceivers[MSG_C_IDLE1] = assassin_c_anims;
classStatics[CID_ASSASSIN].msgReceivers[MSG_C_RUN1] = assassin_c_anims;
classStatics[CID_ASSASSIN].msgReceivers[MSG_C_ATTACK1] = assassin_c_anims;
classStatics[CID_ASSASSIN].msgReceivers[MSG_C_ATTACK2] = assassin_c_anims;
resInfo.numAnims = NUM_ANIMS;
resInfo.animations = animations;
//note that the name is different in the path
resInfo.modelIndex = gi.modelindex("models/monsters/assassin/tris.fm");
Sounds[SND_PAIN1]=gi.soundindex("monsters/assassin/pain1.wav");
Sounds[SND_PAIN2]=gi.soundindex("monsters/assassin/pain2.wav");
Sounds[SND_DIE1]=gi.soundindex("monsters/assassin/death1.wav");
Sounds[SND_GIB]=gi.soundindex("monsters/assassin/gib.wav");
Sounds[SND_THROW1]=gi.soundindex("monsters/assassin/throw1.wav");
Sounds[SND_THROW2]=gi.soundindex("monsters/assassin/throw2.wav");
Sounds[SND_DAGHITF]=gi.soundindex("monsters/assassin/daghitf.wav");
Sounds[SND_DAGHITW]=gi.soundindex("monsters/assassin/daghitw.wav");
Sounds[SND_JUMP]=gi.soundindex("monsters/assassin/jump.wav");
Sounds[SND_FLIP]=gi.soundindex("monsters/assassin/flip.wav");
Sounds[SND_LAND]=gi.soundindex("monsters/assassin/land.wav");
Sounds[SND_LANDF]=gi.soundindex("monsters/assassin/landf.wav");
Sounds[SND_SLIDE]=gi.soundindex("monsters/assassin/slide.wav");
Sounds[SND_SLASH1]=gi.soundindex("monsters/assassin/slash1.wav");
Sounds[SND_SLASH2]=gi.soundindex("monsters/assassin/slash2.wav");
Sounds[SND_GROWL1] = gi.soundindex ("monsters/assassin/growl1.wav");
Sounds[SND_GROWL2]=gi.soundindex("monsters/assassin/growl2.wav");
Sounds[SND_GROWL3] = gi.soundindex ("monsters/assassin/growl3.wav");
Sounds[SND_CLOAK]=gi.soundindex("monsters/assassin/cloak.wav");
Sounds[SND_DECLOAK] = gi.soundindex ("monsters/assassin/decloak.wav");
resInfo.numSounds = NUM_SOUNDS;
resInfo.sounds = Sounds;
classStatics[CID_ASSASSIN].resInfo = &resInfo;
}
void assassinCheckDefense(edict_t *self, float enemydist, qboolean enemyvis, qboolean enemyinfront)
{
if(!enemyinfront&&enemyvis&&enemydist<self->melee_range)
{
#ifdef _DEVEL
if(assassinCheckTeleport(self, ASS_TP_DEF))
gi.dprintf("defense->teleport\n");
#else
assassinCheckTeleport(self, ASS_TP_DEF);
#endif
}
else if(!enemyvis && self->monsterinfo.last_successful_enemy_tracking_time + 6 - skill->value < level.time)
{
if(irand(0, 10) > 10 - (3 * (skill->value + 1)))//hard = 90%, med is 40%, easy is 30%
{
#ifdef _DEVEL
gi.dprintf("Assassin trying to teleport to %s since can't find them...\n", self->classname, self->enemy->classname);
#endif
assassinCheckTeleport(self, ASS_TP_OFF);
}
}
}
/*QUAKED monster_assassin (1 .5 0) (-16 -16 -32) (16 16 48) AMBUSH ASLEEP WALKING FwdJumpAmbush NoCloak NoTeleport CINEMATIC FIXED WANDER MELEE_LEAD STALK COWARD TeleportAmbush CloakAmbush SideJumpAmbush TeleportDodge
The assassin
SPAWNFLAGS:
AMBUSH - Will not be woken up by other monsters or shots from player
ASLEEP - will not appear until triggered
WALKING - use WANDER instead
FwdJumpAmbush - will jump out front or back when triggered (depending on whether player is in front or behind him)
NoCloak - can't turn into a shadow
NoTeleport - can't use smoke grenades to trick player and teleport
CINEMATIC - puts monster into cinematic mode for scripting
FIXED - Will stand in place and attack from afar. Never moves.
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
TeleportAmbush - Will teleport into his origin when triggered (before triggered, is not anywhere at all, like "ASLEEP")
CloakAmbush - Start as a shadow and decloak when wakes up
SideJumpAmbush - Will jump out to left or right (depending on which side of the assassin the player is)
TeleportDodge - Can use teleporting to dodge attacks
FIELDS:
"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 = 64
melee_range = 48
missile_range = 1024
min_missile_range = 64
bypass_missile_chance = 10
jump_chance = 100
wakeup_distance = 1024
NOTE: A value of zero will result in defaults, if you actually want zero as the value, use -1
*/
/*-------------------------------------------------------------------------
SP_monster_assassin
-------------------------------------------------------------------------*/
void SP_monster_assassin (edict_t *self)
{
if(self->spawnflags & MSF_WALKING)
{
self->spawnflags |= MSF_WANDER;
self->spawnflags &= ~MSF_WALKING;
}
if(self->spawnflags&MSF_ASS_JUMPAMBUSH||
self->spawnflags&MSF_ASS_SIDEJUMPAMBUSH||
self->spawnflags&MSF_ASS_STARTSHADOW)
self->spawnflags |= MSF_AMBUSH;
if(self->spawnflags & MSF_ASS_TPORTAMBUSH)
self->spawnflags|=MSF_ASLEEP;
if (!walkmonster_start(self)) // Unsuccessful initialization.
return;
self->msgHandler = DefaultMsgHandler;
self->classID = CID_ASSASSIN;
self->monsterinfo.dismember = assassin_dismember;
if(!self->health)
self->health = ASSASSIN_HEALTH * (skill->value + 1)/3;
self->mass = ASSASSIN_MASS;
self->yaw_speed = 20;
self->movetype = PHYSICSTYPE_STEP;
VectorClear(self->knockbackvel);
self->solid=SOLID_BBOX;
VectorCopy(STDMinsForClass[self->classID], self->mins);
VectorCopy(STDMaxsForClass[self->classID], self->maxs);
self->viewheight = 40;
self->isBlocked = self->bounced = assassin_Touch;
self->s.modelindex = classStatics[CID_ASSASSIN].resInfo->modelIndex;
self->materialtype = MAT_FLESH;
//FIXME (somewhere: otherenemy should be more than just *one* kind
self->monsterinfo.otherenemyname = "monster_rat";
//set up my mood function
MG_InitMoods(self);
if(!irand(0,2))
self->ai_mood_flags |= AI_MOOD_FLAG_PREDICT;
self->cant_attack_think = assassinCheckDefense;
self->monsterinfo.aiflags |= AI_NIGHTVISION;
if(self->spawnflags & MSF_WANDER)
{
QPostMessage(self, MSG_WALK, PRI_DIRECTIVE, NULL);
}
else if(self->spawnflags & MSF_ASS_CINEMATIC)
{
self->monsterinfo.c_mode = 1;
QPostMessage(self, MSG_C_IDLE1, PRI_DIRECTIVE, "iiige",0,0,0,NULL,NULL);
}
else
{
if(self->spawnflags&MSF_ASS_STARTSHADOW)
assassinInitCloak (self);
self->pre_think = assassinCloakThink;
self->next_pre_think = level.time + FRAMETIME;
QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL);
}
self->svflags |= SVF_WAIT_NOTSOLID;
self->s.fmnodeinfo[MESH__KNIFES].flags |= FMNI_NO_DRAW;
VectorCopy(self->s.origin, self->pos1);
}