//============================================================================== // // 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(damages.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)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)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)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)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)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)health)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)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)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)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)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(distenemy, 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&&enemydistmelee_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); }