// // Heretic II // Copyright 1998 Raven Software // // spl_morph.c // // Created by John Scott, but written by Jake Simpson and finally rodgered to be work with client // prediction by Marcus Whitlock. // "Rodgered" ??? No wonder he was working late :) #include "g_local.h" #include "g_monster.h" #include "g_Physics.h" #include "g_playstats.h" #include "g_teleport.h" #include "g_Skeletons.h" #include "g_volume_effect.h" #include "m_chicken.h" #include "m_chicken_anim.h" #include "angles.h" #include "fx.h" #include "matrix.h" #include "vector.h" #include "Utilities.h" #include "p_main.h" #include "p_anims.h" #include "random.h" #include "h2common.h" #define ARROW_SPEED 400.0F #define ARROW_RADIUS 2.0F #define ANGLE_INC 360/NUM_OF_OVUMS char chicken_text[] = "monster_chicken"; extern void ED_CallSpawn (edict_t *ent); extern void PlayerKillShrineFX(edict_t *self); extern void SpawnInitialPlayerEffects(edict_t *ent); extern void MorphPlayerToChicken(edict_t *self, edict_t *caster); extern vec3_t mins; extern vec3_t maxs; void create_morph(edict_t *morph); // ************************************************************************************************* // MorphFadeIn // ----------- // Fade in the chicken - for MONSTERS only. // ************************************************************************************************* void MorphFadeIn(edict_t *self) { self->s.color.a += MORPH_TELE_FADE; self->nextthink = level.time + 0.1; if (!(--self->morph_timer)) { self->think = walkmonster_start_go; } } // ************************************************************************************************* // MorphFadeOut // ------------ // Fade out the chicken model till its gone - for MONSTERS only. // ************************************************************************************************* void MorphFadeOut(edict_t *self) { edict_t *newent; self->s.color.a -= MORPH_TELE_FADE; self->nextthink = level.time + 0.1; if (!(--self->morph_timer)) { // create the Chicken object newent = G_Spawn(); newent->classname = chicken_text; VectorCopy(self->s.origin, newent->s.origin); // if we are looking at an original model thats not got an origin at the waist, move us up in the world if (self->mins[2] == 0) newent->s.origin[2] += 16; VectorCopy(self->s.angles, newent->s.angles); newent->enemy = self->enemy; // keep some info around so we can return to our original persona newent->map = self->classname; newent->target = self->target; // time we stay a chicken newent->time = level.time + 20; ED_CallSpawn(newent); newent->s.color.c = 0xffffff; newent->morph_timer = MORPH_TELE_TIME; newent->think = MorphFadeIn; gi.CreateEffect(&newent->s, FX_PLAYER_TELEPORT_IN, CEF_OWNERS_ORIGIN|CEF_FLAG6, NULL, "" ); // do the teleport sound gi.sound(newent,CHAN_WEAPON,gi.soundindex("weapons/teleport.wav"),1,ATTN_NORM,0); G_SetToFree(self); } } // ************************************************************************************************* // CleanUpMorph // ------------ // Done morphing, clean up after ourselves - for PLAYER only. Called from G_ANIMACTOR.C. // ************************************************************************************************* void CleanUpMorph(edict_t *self) { self->client->tele_dest[0] = self->client->tele_dest[1] = self->client->tele_dest[2] = 0; self->client->tele_count = 0; self->client->playerinfo.edictflags &= ~FL_LOCKMOVE; self->client->playerinfo.renderfx &= ~RF_TRANSLUCENT; self->client->playerinfo.flags &=~PLAYER_FLAG_MORPHING; self->client->shrine_framenum = level.time - 1;; self->s.color.a = 255; } // ************************************************************************************************* // reset_morph_to_elf // ------------------ // We are done being a chicken, let's be Corvus again - switch models from chicken back to corvus // and do teleport fade in - for PLAYER only. Called from G_ANIMACTOR.C. // ************************************************************************************************* void reset_morph_to_elf(edict_t *ent) { // we have no damage, and no motion type ent->takedamage = DAMAGE_AIM; ent->movetype = PHYSICSTYPE_STEP; ent->health = ent->max_health; // move the camera back to where it should be, and reset our lungs and stuff ent->viewheight = 0; ent->mass = 200; ent->deadflag = DEAD_NO; ent->air_finished = level.time + HOLD_BREATH_TIME; ent->s.scale = 1.0; // set the model back to corvux #ifdef COMP_FMOD ent->model = "models/player/corvette/tris_c.fm"; #else ent->model = "models/player/corvette/tris.fm"; #endif ent->pain = player_pain; ent->die = player_die; ent->flags &= ~FL_NO_KNOCKBACK; ent->gravity = 1.0; // reset our skins ent->client->playerinfo.effects = 0; ent->client->playerinfo.skinnum = 0; ent->client->playerinfo.clientnum = ent - g_edicts - 1; ent->s.modelindex = 255; // will use the skin specified model ent->client->playerinfo.frame = 0; // turn our skeleton back on ent->s.skeletalType = SKEL_CORVUS; ent->client->playerinfo.effects|=(EF_SWAPFRAME|EF_JOINTED|EF_CAMERA_NO_CLIP|EF_PLAYER); ent->client->playerinfo.effects&=~EF_CHICKEN; ent->client->playerinfo.edictflags &= ~FL_CHICKEN; ent->client->playerinfo.renderfx &= ~RF_IGNORE_REFS; // reset our mins and max's. And then let the physics move us out of anyone elses bounding box VectorCopy (mins, ent->intentMins); VectorCopy (maxs, ent->intentMaxs); ent->physicsFlags |= PF_RESIZE; // reset our thinking ent->think = ent->oldthink; ent->nextthink = level.time + 0.1; // reset our animations P_PlayerBasicAnimReset(&ent->client->playerinfo); ent->client->playerinfo.upperframe = 43; ent->client->playerinfo.lowerframe = 43; P_PlayerUpdateModelAttributes(&ent->client->playerinfo); P_PlayerAnimSetLowerSeq(&ent->client->playerinfo, ASEQ_NONE); P_PlayerAnimSetLowerSeq(&ent->client->playerinfo, ASEQ_IDLE_WIPE_BROW); // re-spawn anything that should be - shrine effects and the like // SpawnInitialPlayerEffects(ent); // draw the teleport splash at the destination gi.CreateEffect(&ent->s, FX_PLAYER_TELEPORT_IN, CEF_BROADCAST|CEF_OWNERS_ORIGIN|CEF_FLAG6, ent->s.origin, ""); // restart the loop and tell us next time we aren't de-materialising ent->client->tele_count = TELE_TIME; ent->client->tele_dest[0] = ent->client->tele_dest[1] = ent->client->tele_dest[2] = -1; } // ************************************************************************************************* // MorphChickenToPlayer // -------------------- // Modify a chicken into a player - first call. Start the teleport effect on the chicken. // For PLAYER only. // ************************************************************************************************* void MorphChickenToPlayer(edict_t *self) { gclient_t *playerinfo; playerinfo = self->client; // if we are teleporting or morphing, forget it if (self->client->playerinfo.flags & (PLAYER_FLAG_TELEPORT | PLAYER_FLAG_MORPHING)) return; // set the player as teleporting self->client->playerinfo.flags |= PLAYER_FLAG_MORPHING; // time taken over dematerialisation self->client->tele_count = TELE_TIME_OUT; // make us invunerable for a couple of seconds self->client->shrine_framenum = level.time + 10; // tell us how we triggered the teleport self->client->tele_type = 1; // clear the velocity and hold them in place briefly VectorClear (self->velocity); self->client->ps.pmove.pm_time = 50; // make the player still self->flags |= FL_LOCKMOVE; // allow the player to fade out self->s.color.a = 255; self->s.color.r = 255; self->s.color.g = 255; self->s.color.b = 255; self->s.renderfx |= RF_TRANSLUCENT; // make us not think at all self->think = NULL; // make it so that the stuff that does the demateriasation in G_ANIM_ACTOR knows we are fading out, not in self->client->tele_dest[0] = self->client->tele_dest[1] = self->client->tele_dest[2] = 0; // draw the teleport splash at the teleport source gi.CreateEffect(&self->s, FX_PLAYER_TELEPORT_OUT, CEF_OWNERS_ORIGIN |CEF_FLAG6, NULL, "" ); // do the teleport sound gi.sound(self,CHAN_WEAPON,gi.soundindex("weapons/teleport.wav"),1,ATTN_NORM,0); } // ************************************************************************************************* // watch_chicken // ------------- // Watch the chicken to see if we should become the elf again. For PLAYER only. // ************************************************************************************************* void watch_chicken(edict_t *self) { // are we done yet ? if (self->morph_timer <= level.time) { MorphChickenToPlayer(self); } self->nextthink = level.time + 0.1; } // ************************************************************************************************* // Perform_Morph // ------------ // Switch the models from player to chicken and then make us re-appear ala teleport. For PLAYER // only. Called from G_ANIMACTOR.C. // ************************************************************************************************* void Perform_Morph(edict_t *self) { qboolean super_chicken = false; trace_t trace; vec3_t mins = { -16, -16, -36}; vec3_t maxs = { 16, 16, 36}; vec3_t pos; int i; // change out our model self->model = "models/monsters/chicken2/tris.fm"; self->s.modelindex = gi.modelindex("models/monsters/chicken2/tris.fm"); self->client->playerinfo.effects &= ~(EF_JOINTED|EF_SWAPFRAME); self->client->playerinfo.effects |= EF_CHICKEN; self->s.skeletalType = SKEL_NULL; self->client->playerinfo.renderfx |= RF_IGNORE_REFS; if (!irand(0,10)) super_chicken = true; if (super_chicken) { VectorCopy(self->s.origin, pos); pos[2] += 2; gi.trace(pos, mins, maxs, pos, self, MASK_PLAYERSOLID,&trace); if (trace.fraction < 1 || trace.startsolid || trace.allsolid) super_chicken = false; } if (super_chicken) { // reset our motion stuff self->health = 999; self->mass = 3000; self->yaw_speed = 30; self->gravity = 1.0; self->monsterinfo.scale = 2.5; self->s.scale = 2.5; VectorSet(self->mins, -16, -16, -48); VectorSet(self->maxs, 16, 16, 64); self->client->playerinfo.edictflags |= FL_SUPER_CHICKEN; } else { self->health = 1; self->mass = 30; self->yaw_speed = 20; self->gravity = 0.6; self->monsterinfo.scale = MODEL_SCALE; // new mins and max's too VectorSet(self->intentMins,-8,-8,-12); VectorSet(self->intentMaxs,8,8,12); self->client->playerinfo.edictflags |= FL_AVERAGE_CHICKEN; } // not being knocked back, and stepping like a chicken self->movetype = PHYSICSTYPE_STEP; VectorClear(self->knockbackvel); // reseting which skin we use, and new scale self->client->playerinfo.skinnum = 0; self->client->playerinfo.clientnum = self - g_edicts - 1; // reset our thinking self->oldthink = self->think; self->think = watch_chicken; self->nextthink = level.time + 0.1; self->physicsFlags |= PF_RESIZE; for (i=0;iclient->playerinfo.fmnodeinfo[i].flags &= ~FMNI_NO_DRAW; // reset our animation P_PlayerAnimSetLowerSeq(&self->client->playerinfo, ASEQ_STAND); // draw the teleport splash at the destination gi.CreateEffect(&self->s, FX_PLAYER_TELEPORT_IN, CEF_BROADCAST|CEF_OWNERS_ORIGIN|CEF_FLAG6, self->s.origin, ""); // restart the loop and tell us next time we aren't de-materialising self->client->tele_count = TELE_TIME; self->client->tele_dest[0] = self->client->tele_dest[1] = self->client->tele_dest[2] = -1; } // ************************************************************************************************* // MorphPlayerToChicken // -------------------- // Modify a player into a chicken - first call. Start the teleport effect on the player. For PLAYER // only. // ************************************************************************************************* void MorphPlayerToChicken(edict_t *self, edict_t *caster) { gclient_t *playerinfo; playerinfo = self->client; // if we are teleporting or morphing, forget it if (self->client->playerinfo.flags & (PLAYER_FLAG_TELEPORT | PLAYER_FLAG_MORPHING)) return; // remove any hand or weapon effects P_TurnOffPlayerEffects(&self->client->playerinfo); // remove any shrine effects he has PlayerKillShrineFX(self); // set the player as teleporting self->client->playerinfo.flags |= PLAYER_FLAG_MORPHING; // time taken over dematerialisation self->client->tele_count = TELE_TIME_OUT; // make us invunerable for a couple of seconds self->client->shrine_framenum = level.time + 10; // tell us how we triggered the teleport self->client->tele_type = 1; // clear the velocity and hold them in place briefly VectorClear (self->velocity); self->client->ps.pmove.pm_time = 50; // make the player still self->client->playerinfo.flags |= FL_LOCKMOVE; // allow the player to fade out self->s.color.a = 255; self->s.color.r = 255; self->s.color.g = 255; self->s.color.b = 255; self->s.renderfx |= RF_TRANSLUCENT; // make it so that the stuff that does the demateriasation in G_ANIM_ACTOR knows we are fading out, not in self->client->tele_dest[0] = self->client->tele_dest[1] = self->client->tele_dest[2] = 0; // tell us how long we have to be a chicken self->morph_timer = level.time + MORPH_DUR; // draw the teleport splash at the teleport source gi.CreateEffect(&self->s, FX_PLAYER_TELEPORT_OUT, CEF_OWNERS_ORIGIN | CEF_FLAG6, NULL, ""); // do the teleport sound gi.sound(self,CHAN_WEAPON,gi.soundindex("weapons/teleport.wav"),1,ATTN_NORM,0); } // ************************************************************************************************* // MorphPlayerToChicken2 // --------------------- // Modify a player into a chicken - first call. Start the teleport effect on the player. For PLAYER // only. Temporary func. See Marcus for explaination. // ************************************************************************************************* void MorphPlayerToChicken2(edict_t *self, edict_t *caster) { gclient_t *playerinfo; playerinfo = self->client; // if we are teleporting or morphing, forget it if (self->client->playerinfo.flags & (PLAYER_FLAG_TELEPORT | PLAYER_FLAG_MORPHING)) return; // remove any hand or weapon effects P_TurnOffPlayerEffects(&self->client->playerinfo); // remove any shrine effects he has PlayerKillShrineFX(self); // set the player as teleporting self->client->playerinfo.flags |= PLAYER_FLAG_MORPHING; // time taken over dematerialisation self->client->tele_count = TELE_TIME_OUT; // make us invunerable for a couple of seconds self->client->shrine_framenum = level.time + 10; // tell us how we triggered the teleport self->client->tele_type = 1; // clear the velocity and hold them in place briefly VectorClear (self->velocity); self->client->ps.pmove.pm_time = 50; // make the player still self->client->playerinfo.flags |= FL_LOCKMOVE; // allow the player to fade out self->s.color.a = 255; self->s.color.r = 255; self->s.color.g = 255; self->s.color.b = 255; self->client->playerinfo.renderfx |= RF_TRANSLUCENT; // make it so that the stuff that does the demateriasation in G_ANIM_ACTOR knows we are fading out, not in self->client->tele_dest[0] = self->client->tele_dest[1] = self->client->tele_dest[2] = 0; // tell us how long we have to be a chicken self->morph_timer = level.time + MORPH_DUR; // draw the teleport splash at the teleport source gi.CreateEffect(&self->s, FX_PLAYER_TELEPORT_OUT, CEF_OWNERS_ORIGIN | CEF_FLAG6, NULL, ""); // do the teleport sound gi.sound(self,CHAN_WEAPON,gi.soundindex("weapons/teleport.wav"),1,ATTN_NORM,0); } edict_t *MorphReflect(edict_t *self, edict_t *other, vec3_t vel) { edict_t *morph; byte yaw, pitch; // create a new missile to replace the old one - this is necessary cos physics will do nasty shit // with the existing one,since we hit something. Hence, we create a new one totally. morph = G_Spawn(); create_morph(morph); morph->reflect_debounce_time = self->reflect_debounce_time -1; morph->reflected_time=self->reflected_time; morph->owner = other; morph->enemy = self->enemy; VectorCopy(self->s.origin, morph->s.origin); VectorCopy(vel, morph->velocity); VectorNormalize2(morph->velocity, morph->movedir); AnglesFromDir(morph->movedir, morph->s.angles); G_LinkMissile(morph); yaw = Q_ftol((morph->s.angles[YAW]/6.2831) * 255.0); pitch = Q_ftol((morph->s.angles[PITCH]/6.2831) * 255.0); gi.CreateEffect(&morph->s, FX_SPELL_MORPHMISSILE, CEF_OWNERS_ORIGIN|CEF_FLAG6, NULL, "bb", yaw,pitch); // kill the existing missile, since its a pain in the ass to modify it so the physics won't screw it. G_SetToFree(self); // Do a nasty looking blast at the impact point gi.CreateEffect(&morph->s, FX_LIGHTNING_HIT, CEF_OWNERS_ORIGIN, NULL, "t", morph->velocity); return(morph); } // **************************************************************************** // MorphMissile touch // **************************************************************************** // This called when missile touches anything (world or edict) void MorphMissileTouch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surface) { // has the target got reflection turned on ? if(EntReflecting(other, true, true) && self->reflect_debounce_time) { Create_rand_relect_vect(self->velocity, self->velocity); Vec3ScaleAssign(ARROW_SPEED/2, self->velocity); MorphReflect(self, other, self->velocity); return; } // Turn target into a chicken if monster or player if(((other->svflags & SVF_MONSTER) && !(other->svflags&SVF_BOSS) && !(other->monsterinfo.c_mode)) || ((other->client)&&(deathmatch->value))) { //Don't turn a super chicken back to a player if ( (other->client) && (other->client->playerinfo.edictflags & FL_SUPER_CHICKEN) ) { // Turn off the client effect gi.sound(other,CHAN_WEAPON,gi.soundindex("misc/null.wav"),1,ATTN_NORM,0); gi.CreateEffect(NULL, FX_SPELL_MORPHEXPLODE, 0, self->s.origin, "d", self->movedir); G_SetToFree(self); return; } // don't target team members in team deathmatching, if they are on the same team, and friendly fire is not enabled. if ((other->client && (int)(dmflags->value) & (DF_MODELTEAMS | DF_SKINTEAMS)) && !((int)dmflags->value & DF_HURT_FRIENDS) && deathmatch->value) { if (OnSameTeam(other, self->owner)) { // Turn off the client effect gi.sound(other,CHAN_WEAPON,gi.soundindex("misc/null.wav"),1,ATTN_NORM,0); gi.CreateEffect(NULL, FX_SPELL_MORPHEXPLODE, 0, self->s.origin, "d", self->movedir); G_SetToFree(self); return; } } if (other->svflags & SVF_MONSTER ) { // deal with the existing bad guy other->think = MorphFadeOut; other->nextthink = level.time + 0.1; other->touch = NULL; other->morph_timer = MORPH_TELE_TIME; other->enemy = self->owner; VectorClear(other->velocity); gi.CreateEffect(&other->s, FX_PLAYER_TELEPORT_OUT, CEF_OWNERS_ORIGIN|CEF_FLAG6, NULL, "" ); } else MorphPlayerToChicken(other, self->owner); if (deathmatch->value) { //There shouldn't be any monsters in deathmatch.. but... assert(other->client); if ( (other->client) && (other->client->playerinfo.edictflags & FL_SUPER_CHICKEN) ) gi.sound(other,CHAN_VOICE,gi.soundindex("weapons/supercrow.wav"),1,ATTN_NONE,0); else gi.sound(other,CHAN_VOICE,gi.soundindex("weapons/crow.wav"),1,ATTN_NONE,0); } else { gi.sound(other,CHAN_VOICE,gi.soundindex("weapons/crow.wav"),1,ATTN_NORM,0); } gi.sound(other,CHAN_WEAPON,gi.soundindex("misc/null.wav"),1,ATTN_NORM,0); gi.CreateEffect(NULL, FX_SPELL_MORPHEXPLODE, 0, self->s.origin, "d", self->movedir); } // else we hit a wall / object else { if(plane && (plane->normal)) // Start the explosion gi.CreateEffect(NULL, FX_SPELL_MORPHEXPLODE, 0, self->s.origin, "d", plane->normal); else gi.CreateEffect(NULL, FX_SPELL_MORPHEXPLODE, 0, self->s.origin, "d", self->movedir); } // Turn off the client effect G_SetToFree(self); // Allow time to get to client } // **************************************************************************** // MorphMissile think // **************************************************************************** void MorphMissileThink(edict_t *self) { self->svflags |= SVF_NOCLIENT; // No messages to client after it has received velocity self->think = NULL; // Not required to think anymore } // create the guts of the morph ovum void create_morph(edict_t *morph) { morph->s.effects |= EF_ALWAYS_ADD_EFFECTS; morph->svflags |= SVF_ALWAYS_SEND; morph->movetype = MOVETYPE_FLYMISSILE; // set up our collision boxes VectorSet(morph->mins, -ARROW_RADIUS, -ARROW_RADIUS, -ARROW_RADIUS); VectorSet(morph->maxs, ARROW_RADIUS, ARROW_RADIUS, ARROW_RADIUS); morph->solid = SOLID_BBOX; morph->clipmask = MASK_MONSTERSOLID; morph->touch = MorphMissileTouch; morph->think = MorphMissileThink; morph->classname = "Spell_MorphArrow"; morph->nextthink = level.time + 0.1; } // **************************************************************************** // SpellCastMorph // **************************************************************************** void SpellCastMorph(edict_t *Caster, vec3_t StartPos, vec3_t AimAngles, vec3_t unused, float value) { edict_t *morph; int i; byte yaw; float current_ang; vec3_t temp_angles; short morpharray[NUM_OF_OVUMS]; // if (!(Caster->client->playerinfo.edictflags & FL_CHICKEN)) // { // MorphPlayerToChicken2(Caster, Caster); // return; // } // first ovum gets sent out along our aiming angle current_ang = AimAngles[YAW]; for (i=0; is.origin); // decide its direction morph->s.angles[YAW] = current_ang; VectorScale(morph->s.angles, ANGLE_TO_RAD, temp_angles); DirFromAngles(temp_angles, morph->velocity); Vec3ScaleAssign(ARROW_SPEED,morph->velocity); create_morph(morph); morph->reflect_debounce_time = MAX_REFLECT; morph->owner = Caster; G_LinkMissile(morph); // if we are the first effect, calculate our yaw if (!i) yaw = Q_ftol((morph->s.angles[YAW]/360.0) * 255.0); // Store the entity numbers for sending with the effect. morpharray[i] = morph->s.number; //increment current angle to get circular radius of ovums current_ang+= ANGLE_INC; } // create the client effect that gets seen on screen gi.CreateEffect(&Caster->s, FX_SPELL_MORPHMISSILE_INITIAL, CEF_OWNERS_ORIGIN, NULL, "bssssss", yaw, morpharray[0], morpharray[1], morpharray[2], morpharray[3], morpharray[4], morpharray[5]); } // end