//============================================================================== // // m_mssithra.c // // Heretic II // Copyright 1998 Raven Software // //============================================================================== #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 "m_mssithra.h" #include "m_mssithra_anim.h" #include "g_misc.h" #include "g_HitLocation.h" #include "p_anim_branch2.h" #include "m_stats.h" #include "g_playstats.h" void extrapolateFiredir (edict_t *self,vec3_t p1,float pspeed,edict_t *targ,float accept,vec3_t vec2); H2COMMON_API void KnockDownPlayer(playerinfo_t *playerinfo); void create_ssithra_arrow(edict_t *Arrow); #define MSSITHRA_JUMP_VELOCITY 300.0 #define MSSITHRA_HOP_VELOCITY 128.0 #define MSSITHRA_SF_NAMOR 8 #define MSSITHRA_SF_SPIN 16 //======================================== //INITIALIZE //======================================== static animmove_t *animations[NUM_ANIMS] = { &mssithra_move_claw1, &mssithra_move_death1, &mssithra_move_idle1, &mssithra_move_jump1, &mssithra_move_fjump, // &mssithra_move_pain1, &mssithra_move_roar, &mssithra_move_shoota1, &mssithra_move_shootb1, &mssithra_move_walk1, &mssithra_move_backpedal1, &mssithra_move_run1, &mssithra_move_delay, &mssithra_move_shoot1_trans, &mssithra_move_shoot1_loop, &mssithra_move_shoot1_detrans }; static int Sounds[NUM_SOUNDS]; static ClassResourceInfo_t resInfo; /* //======================================== //MOVEMENT //======================================== */ void mssithra_stand(edict_t *self, G_Message_t *msg) { SetAnim(self, ANIM_IDLE1); } void mssithra_decide_stand(edict_t *self) { if(mssithraCheckMood(self)) return; SetAnim(self, ANIM_IDLE1); } void mssithra_pain(edict_t *self, G_Message_t *msg) {//fixme - make part fly dir the vector from hit loc to sever loc int temp, damage; qboolean force_pain; if(self->deadflag == DEAD_DEAD) //Dead but still being hit return; ParseMsgParms(msg, "eeiii", &temp, &temp, &force_pain, &damage, &temp); if(!force_pain) { if(irand(0,10)<5||!self->groundentity) return; if(self->pain_debounce_time > level.time) return; } if(self->curAnimID == ANIM_CLAW1) return; self->pain_debounce_time = level.time + 2; if(irand(0,10)<5) 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); if (self->flags&FL_INWATER) self->flags |= FL_SWIM; else self->flags &= ~FL_SWIM; if(irand(0,10)<1) SetAnim(self, ANIM_ROAR1);//make him tougher? more aggressive? } void mssithra_pain_react (edict_t *self) { if(!self->enemy) { mssithra_decide_stand(self); return; } if(self->enemy->health<=0||self->enemy == self||!self->enemy->takedamage) { self->enemy=NULL; mssithra_decide_stand(self); return; } } //=========================================== //DEATHS //=========================================== void mssithra_death(edict_t *self, G_Message_t *msg) { self->svflags |= SVF_DEADMONSTER; self->msgHandler=DyingMsgHandler; if(self->deadflag == DEAD_DEAD) return; self->deadflag = DEAD_DEAD; gi.sound (self, CHAN_VOICE, Sounds[SND_DIE], 1, ATTN_NORM, 0); if (self->flags&FL_INWATER) self->flags |= FL_SWIM; else self->flags &= ~FL_SWIM; SetAnim(self, ANIM_DEATH1); //Remove the life bar once dead M_ShowLifeMeter( self, 0, 0); self->post_think = NULL; self->next_post_think = -1; } void mssithra_dead(edict_t *self) {//maybe allow dead bodies to be chopped? Make BBOX small? self->msgHandler = DeadMsgHandler; self->deadState = DEAD_DEAD; self->think = NULL; self->nextthink = 0; self->flags |= FL_DONTANIMATE; gi.linkentity(self); } void mssithraKillSelf (edict_t *self) { vec3_t gore_spot; self->svflags &= ~SVF_DEADMONSTER; // now treat as a different content type self->msgHandler = DefaultMsgHandler; self->deadflag = false; VectorCopy(self->s.origin,gore_spot); gore_spot[2]+=12; self->health = 1; T_Damage (self, self, self, vec3_origin, gore_spot, vec3_origin, 10, 0,0,MOD_DIED); self->health = -69; } //=========================================== //SOUNDS //=========================================== void mssithraSound(edict_t *self, float soundnum, float channel, float attenuation) { return; if(!channel) channel = CHAN_AUTO; if(!attenuation) attenuation = ATTN_NORM; else if(attenuation == -1) attenuation = ATTN_NONE; gi.sound(self, (int)channel, Sounds[(int)(soundnum)], 1, (int)attenuation, 0); } //=========================================== //ATTACKS //=========================================== void mssithra_melee(edict_t *self, G_Message_t *msg) { vec3_t v; float len, melee_range, min_seperation, jump_range; if(M_ValidTarget(self, self->enemy)) { 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 (len < min_seperation + melee_range) // A hit { SetAnim(self, ANIM_CLAW1); } } else QPostMessage(self, MSG_STAND,PRI_DIRECTIVE, NULL); } void mssithra_missile(edict_t *self, G_Message_t *msg) {//NEWSTUFF: jump closer to claw, loop shooting anims vec3_t v; float len, min_seperation, jump_range; if(M_ValidTarget(self, self->enemy)) { VectorSubtract (self->s.origin, self->enemy->s.origin, v); len = VectorLength (v); jump_range = 128; min_seperation = self->maxs[0] + self->enemy->maxs[0]; if (irand(0,(skill->value+1)*2)) { SetAnim(self, ANIM_SHOOT_TRANS); } else { SetAnim(self, ANIM_IDLE1); } } else QPostMessage(self, MSG_STAND,PRI_DIRECTIVE, NULL); } void mssithraSwipe (edict_t *self) { vec3_t v, off, dir, org, ang; float len; VectorSubtract (self->s.origin, self->enemy->s.origin, v); len = VectorLength (v); if (len < (self->maxs[0] + self->enemy->maxs[0] + 45) ) // A hit { if (infront(self, self->enemy)) { gi.sound (self, CHAN_WEAPON, Sounds[SND_SWIPEHIT], 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); T_Damage (self->enemy, self, self, dir, org, vec3_origin, MSSITHRA_DMG_SWIPE, 0, DAMAGE_DISMEMBER,MOD_DIED); if(self->enemy->health>0)//else don't gib? { if(!irand(0,5)) { if(!stricmp(self->enemy->classname, "player")) KnockDownPlayer(&self->enemy->client->playerinfo); } } return; } } gi.sound (self, CHAN_WEAPON, Sounds[SND_SWIPE], 1, ATTN_NORM, 0); } void mssithra_missile_explode(edict_t *self) { int damage = irand(8, 16); //TODO: Spawn an explosion effect gi.CreateEffect( NULL, FX_M_EFFECTS, 0, self->s.origin, "bv", FX_MSSITHRA_EXPLODE, self->movedir); T_DamageRadius(self, self->owner, self->owner, 128, damage, damage/2, DAMAGE_ATTACKER_IMMUNE, MOD_DIED); G_FreeEdict(self); } void mssithraAlphaArrowTouch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surface) { edict_t *Arrow; // are we reflecting ? if (self->reflect_debounce_time) { if(EntReflecting(other, true, true)) { Arrow = G_Spawn(); create_ssithra_arrow(Arrow); VectorCopy(self->s.origin, Arrow->s.origin); Arrow->owner = self->owner; Arrow->nextthink=self->nextthink; Arrow->think=G_FreeEdict; Create_rand_relect_vect(self->velocity, Arrow->velocity); vectoangles(Arrow->velocity, Arrow->s.angles); Arrow->s.angles[YAW]+=90; Vec3ScaleAssign(MSSITHRA_ARROW_SPEED/2,Arrow->velocity); Arrow->reflect_debounce_time = self->reflect_debounce_time - 1; G_LinkMissile(Arrow); //gi.CreateEffect(&Arrow->s, FX_WEAPON_REDRAINMISSILE, CEF_OWNERS_ORIGIN|CEF_FLAG8|CEF_FLAG7, NULL, "t", Arrow->velocity); G_SetToFree(self); return; } } if(other->takedamage) { if (plane->normal) VectorCopy(plane->normal, self->movedir); self->dmg = irand(MSSITHRA_DMG_MIN*2, MSSITHRA_DMG_MAX*2); mssithra_missile_explode(self); } else { gi.sound(self, CHAN_WEAPON, Sounds[SND_INWALL], 0.5, ATTN_NORM, 0); self->s.effects |= EF_ALTCLIENTFX; VectorClear(self->velocity); if (plane->normal) VectorCopy(plane->normal, self->movedir); self->dmg = irand(MSSITHRA_DMG_MIN, MSSITHRA_DMG_MAX); self->think = mssithra_missile_explode; self->nextthink = level.time + flrand(0.5, 1.5); } } // create the guts of the ssithra arrow void create_ssithra_arrow(edict_t *Arrow) { Arrow->s.modelindex = gi.modelindex("models/objects/exarrow/tris.fm"); Arrow->movetype = MOVETYPE_FLYMISSILE; Arrow->solid = SOLID_BBOX; Arrow->classname = "mssithra_Arrow"; Arrow->touch = mssithraAlphaArrowTouch; Arrow->clipmask = MASK_SHOT; Arrow->s.effects |= EF_ALWAYS_ADD_EFFECTS | EF_CAMERA_NO_CLIP; Arrow->svflags |= SVF_ALWAYS_SEND; VectorSet(Arrow->mins,-1.0,-1.0,-1.0); VectorSet(Arrow->maxs,1.0,1.0,1.0); Arrow->s.scale = 1.5; } void mssithraArrow(edict_t *self) {//fixme; adjust for up/down vec3_t Forward, targ_pos; vec3_t Right, fire_spot, fire_dir;// , up; edict_t *Arrow; float spread; int num_shots = 3; if (!self->enemy) return; if(self->enemy->health<=0) { self->enemy=NULL; mssithra_decide_stand(self); return; } if(self->monsterinfo.attack_finished>level.time) return; gi.sound(self,CHAN_WEAPON,Sounds[SND_ARROW],1,ATTN_NORM,0); self->monsterinfo.attack_finished = level.time + 0.4; VectorCopy(self->s.origin,fire_spot); fire_spot[2]+=self->maxs[2]*0.5; AngleVectors(self->s.angles,Forward,Right,NULL); VectorMA(fire_spot,72,Forward,fire_spot); VectorMA(fire_spot,16,Right,fire_spot); fire_spot[2] += 24; VectorRandomCopy(self->enemy->s.origin, targ_pos, 16); VectorSubtract(targ_pos, fire_spot, Forward); VectorNormalize2(Forward, fire_dir); while(num_shots) { Arrow = G_Spawn(); create_ssithra_arrow(Arrow); Arrow->reflect_debounce_time = MAX_REFLECT; VectorCopy(fire_spot,Arrow->s.origin); VectorCopy(self->movedir,Arrow->movedir); //Increase the spread for lower levels switch((int)skill->value) { case 0: spread = 0.35; break; case 1: spread = 0.2; break; case 2: default: spread = 0.1; break; } AngleVectors(self->s.angles,NULL,Right,NULL); switch (num_shots) { case 3: VectorScale(Right, spread,Right); break; case 1: VectorScale(Right,-spread,Right); break; case 2: default: VectorClear(Right); break; } VectorAdd(fire_dir, Right, Arrow->movedir); VectorNormalize(Arrow->movedir); VectorScale(Arrow->movedir,MSSITHRA_ARROW_SPEED,Arrow->velocity); vectoangles(Arrow->velocity, Arrow->s.angles); Arrow->s.angles[YAW] += 90; Arrow->owner=self; Arrow->enemy=self->enemy; gi.CreateEffect(&Arrow->s, FX_M_EFFECTS, CEF_OWNERS_ORIGIN | CEF_FLAG6, NULL, "bv", FX_MSSITHRA_ARROW, Arrow->velocity); G_LinkMissile(Arrow); Arrow->nextthink=level.time+5; Arrow->think=G_FreeEdict;//mssithraArrowThink; num_shots--; } } void mssithraCheckLoop (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(!visible(self, self->enemy)) return; if(!infront(self, self->enemy)) return; if(irand(0,10)<5) 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; } /*------------------------------------------------------------------------- mssithra_pause -------------------------------------------------------------------------*/ qboolean mssithraCheckMood (edict_t *self) { if(self->monsterinfo.aiflags&AI_OVERRIDE_GUIDE) return false; self->mood_think(self); if (self->ai_mood == AI_MOOD_NORMAL) return false; 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); return true; break; case AI_MOOD_PURSUE: case AI_MOOD_NAVIGATE: QPostMessage(self, MSG_RUN, PRI_DIRECTIVE, NULL); return true; break; case AI_MOOD_WALK: QPostMessage(self, MSG_WALK, PRI_DIRECTIVE, NULL); return true; break; case AI_MOOD_STAND: QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL); return true; break; } return false; } void mssithra_postthink(edict_t *self) { //Only display a lifemeter if we have an enemy if (self->enemy) { if (self->dmg < self->max_health) { M_ShowLifeMeter( self, self->dmg, self->dmg); self->dmg+=50; } else { M_ShowLifeMeter( self, self->health, self->max_health); } } self->next_post_think = level.time + 0.05; } void mmssithraRandomGrowlSound (edict_t *self) { if(!irand(0,2)) gi.sound(self, CHAN_VOICE, Sounds[SND_GROWL1], 1, ATTN_NORM, 0); else if(!irand(0,1)) gi.sound(self, CHAN_VOICE, Sounds[SND_GROWL2], 1, ATTN_NORM, 0); else gi.sound(self, CHAN_VOICE, Sounds[SND_GROWL3], 1, ATTN_NORM, 0); } void mssithra_ShotLoop(edict_t *self) { SetAnim(self, ANIM_SHOOT_LOOP); } void mssithraCheckShotLoop(edict_t *self) { //TODO: Check to keep shooting or to stop shooting if (irand(0,(skill->value+1)*2)) { //Just keep playing the animation return; } else { SetAnim(self, ANIM_SHOOT_DETRANS); } } void MssithraStaticsInit() { classStatics[CID_MSSITHRA].msgReceivers[MSG_STAND] = mssithra_stand; classStatics[CID_MSSITHRA].msgReceivers[MSG_MISSILE] = mssithra_missile; classStatics[CID_MSSITHRA].msgReceivers[MSG_MELEE] = mssithra_melee; classStatics[CID_MSSITHRA].msgReceivers[MSG_DEATH] = mssithra_death; classStatics[CID_MSSITHRA].msgReceivers[MSG_PAIN] = mssithra_pain; classStatics[CID_MSSITHRA].msgReceivers[MSG_RUN] = mssithra_missile; resInfo.numAnims = NUM_ANIMS; resInfo.animations = animations; resInfo.modelIndex = gi.modelindex("models/monsters/mutantsithra/tris.fm"); Sounds[SND_PAIN1]=gi.soundindex("monsters/mssithra/pain1.wav"); Sounds[SND_PAIN2]=gi.soundindex("monsters/mssithra/pain2.wav"); Sounds[SND_DIE]=gi.soundindex("monsters/mssithra/death1.wav"); Sounds[SND_SWIPE] = gi.soundindex ("monsters/mssithra/swipe.wav"); Sounds[SND_SWIPEHIT]=gi.soundindex("monsters/mssithra/swipehit.wav"); Sounds[SND_ARROW]=gi.soundindex("weapons/RedRainPowerFire.wav"); Sounds[SND_AEXPLODE]=gi.soundindex("weapons/FlyingFistImpact.wav"); Sounds[SND_GROWL1]=gi.soundindex("monsters/mssithra/growl1.wav"); Sounds[SND_GROWL2] = gi.soundindex ("monsters/mssithra/growl2.wav"); Sounds[SND_GROWL3] = gi.soundindex ("monsters/mssithra/growl3.wav"); Sounds[SND_ROAR]=gi.soundindex("monsters/mssithra/roar.wav"); Sounds[SND_INWALL]=gi.soundindex("weapons/staffhitwall.wav"); resInfo.numSounds = NUM_SOUNDS; resInfo.sounds = Sounds; classStatics[CID_MSSITHRA].resInfo = &resInfo; } /*QUAKED monster_mssithra (1 .5 0) (-36 -36 0) (36 36 96) AMBUSH ASLEEP 4 8 16 32 64 FIXED WANDER MELEE_LEAD STALK COWARD EXTRA1 EXTRA2 EXTRA3 EXTRA4 The mssithra AMBUSH - Will not be woken up by other monsters or shots from player ASLEEP - will not appear until triggered WALKING - Use WANDER instead WANDER - Monster will wander around aimlessly (but follows buoys) MELEE_LEAD - Monster will tryto cut you off when you're running and fighting him, works well if there are a few monsters in a group, half doing this, half not STALK - Monster will only approach and attack from behind- if you're facing the monster it will just stand there. Once the monster takes pain, however, it will stop this behaviour and attack normally COWARD - Monster starts off in flee mode- runs away from you when woken up "homebuoy" - monsters will head to this buoy if they don't have an enemy ("homebuoy" should be targetname of the buoy you want them to go to) "wakeup_target" - monsters will fire this target the first time it wakes up (only once) "pain_target" - monsters will fire this target the first time it gets hurt (only once) "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 = 16 melee_range = 100 missile_range = 400 min_missile_range = 100 bypass_missile_chance = 25 jump_chance = 25 wakeup_distance = 1024 NOTE: A value of zero will result in defaults, if you actually want zero as the value, use -1 */ void SP_monster_mssithra (edict_t *self) { // Generic Monster Initialization if (!walkmonster_start (self)) // failed initialization return; self->msgHandler = DefaultMsgHandler; self->classID = CID_MSSITHRA; self->materialtype = MAT_FLESH; self->flags |= FL_IMMUNE_SLIME; if(self->flags&FL_INWATER|| gi.pointcontents(self->s.origin)&CONTENTS_WATER|| self->waterlevel >= 3) self->flags|=FL_SWIM; if(self->health<=0) self->health = MSSITHRA_HEALTH; //Apply to the end result (whether designer set or not) self->max_health = self->health = MonsterHealth(self->health); self->mass = MSSITHRA_MASS; self->yaw_speed = 20; self->viewheight = 88; self->monsterinfo.aiflags |= AI_BRUTAL|AI_AGRESSIVE; self->movetype=PHYSICSTYPE_STEP;//MOVETYPE_STEP VectorClear(self->knockbackvel); self->solid=SOLID_BBOX; VectorCopy(STDMinsForClass[self->classID], self->mins); VectorCopy(STDMaxsForClass[self->classID], self->maxs); self->s.modelindex = classStatics[CID_MSSITHRA].resInfo->modelIndex; self->s.skinnum = 0; if (!self->monsterinfo.scale) { self->s.scale = self->monsterinfo.scale = (MODEL_SCALE + 0.25); } self->monsterinfo.otherenemyname = "obj_barrel"; //set up my mood function MG_InitMoods(self); QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL); self->post_think = mssithra_postthink; self->next_post_think = level.time + 0.1; //Turn the goofy bolts off! self->s.fmnodeinfo[MESH__BOLTS].flags |= FMNI_NO_DRAW; self->dmg = 0; self->svflags|=SVF_BOSS; }