595 lines
16 KiB
C++
595 lines
16 KiB
C++
/*
|
|
================================================================
|
|
MORTICIAN
|
|
================================================================
|
|
|
|
Copyright (C) 1998 by 2015, Inc.
|
|
All rights reserved.
|
|
|
|
This source is may not be distributed and/or modified without
|
|
expressly written permission by 2015, Inc.
|
|
*/
|
|
|
|
#include "g_local.h"
|
|
#include "actor.h"
|
|
#include "mortician.h"
|
|
#include "behavior.h"
|
|
#include "path.h"
|
|
#include "explosion.h"
|
|
|
|
|
|
CLASS_DECLARATION( Actor, Mortician, "monster_mortician" );
|
|
|
|
Event EV_Mortician_MJumpTo( "mjumpto" );
|
|
Event EV_Mortician_Flash( "doflash" );
|
|
|
|
ResponseDef Mortician::Responses[] =
|
|
{
|
|
{ &EV_Mortician_MJumpTo, ( Response )Mortician::JumpToEvent },
|
|
{ &EV_Mortician_Flash, ( Response )Mortician::FlashEvent },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
void Mortician::FlashEvent (Event *ev)
|
|
{
|
|
FlashPlayers(worldorigin, 1, 1, 1, 0.5, 300);
|
|
}
|
|
|
|
void Mortician::JumpToEvent (Event *ev)
|
|
{
|
|
Event *e;
|
|
int i;
|
|
int n;
|
|
|
|
if ( ( deadflag ) && ( actortype != IS_INANIMATE ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
e = new Event( EV_Behavior_Args );
|
|
e->SetSource( EV_FROM_SCRIPT );
|
|
e->SetThread( ev->GetThread() );
|
|
e->SetLineNumber( ev->GetLineNumber() );
|
|
|
|
e->AddEntity( this );
|
|
|
|
n = ev->NumArgs();
|
|
|
|
e->AddString( "mjump" );
|
|
for( i = 1; i <= n; i++ )
|
|
{
|
|
e->AddToken( ev->GetToken( i ) );
|
|
}
|
|
|
|
SetBehavior( new MJump, e, ev->GetThread() );
|
|
}
|
|
|
|
|
|
// This takes a target vector, and then sets
|
|
// the entity's horizontal and vertical velocity so that
|
|
// it will jump gracefully to its target, and returns
|
|
// the estimated travel time
|
|
|
|
// It is a copy of Actor::JumpTo, but I had to rewrite it because jumpTo
|
|
// spins the actor around to face its target, which is not something I
|
|
// want the mortician to do.
|
|
float Mortician::JumpTo (Vector targ)
|
|
{
|
|
float traveltime;
|
|
float vertical_speed;
|
|
float speed;
|
|
Vector target;
|
|
Vector dir;
|
|
Vector xydir;
|
|
CheckWater();
|
|
//
|
|
// if we got a jump, go into that mode
|
|
//
|
|
traveltime = 0;
|
|
target = targ;
|
|
dir = target - worldorigin;
|
|
xydir = dir;
|
|
xydir.z = 0;
|
|
|
|
speed = movespeed * 1.7 * gravity;
|
|
speed += (xydir.length()/ 2.5) * gravity;
|
|
|
|
|
|
//setAngles( xydir.toAngles() );
|
|
traveltime = xydir.length() / speed;
|
|
//
|
|
// we add 16 to allow for a little bit higher
|
|
//
|
|
if ( waterlevel > 2 )
|
|
{
|
|
vertical_speed = ( ( dir.z + 16 ) / traveltime ) + ( 0.5f * gravity * 60 * traveltime );
|
|
}
|
|
else
|
|
{
|
|
vertical_speed = ( ( dir.z + 16 ) / traveltime ) + ( 0.5f * gravity * sv_gravity->value * traveltime );
|
|
}
|
|
xydir.normalize();
|
|
|
|
velocity = speed * xydir;
|
|
velocity.z = vertical_speed;
|
|
|
|
return traveltime;
|
|
}
|
|
|
|
|
|
|
|
Mortician::Mortician()
|
|
{
|
|
}
|
|
|
|
|
|
|
|
/****************************************************************************
|
|
|
|
MJump Class Definition
|
|
|
|
Based of jump behaviour, used for the mortician, so he can look better
|
|
when he jumps.
|
|
****************************************************************************/
|
|
|
|
CLASS_DECLARATION( Behavior, MJump, NULL );
|
|
|
|
ResponseDef MJump::Responses[] =
|
|
{
|
|
{ &EV_Behavior_Args, ( Response )MJump::SetArgs },
|
|
{ &EV_Behavior_AnimDone, ( Response )MJump::AnimDone },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
MJump::MJump()
|
|
{
|
|
endtime = 0;
|
|
state = 0;
|
|
animdone = true;
|
|
target = NULL;
|
|
jumpok = true;
|
|
heightneeded = 140;
|
|
heightwanted = 400;
|
|
jumpbegun = false;
|
|
}
|
|
|
|
void MJump::AnimDone (Event *ev)
|
|
{
|
|
animdone = true;
|
|
}
|
|
|
|
void MJump::SetArgs (Event *ev)
|
|
{
|
|
//
|
|
// see if it is an entity first
|
|
//
|
|
movegoal = AI_FindNode( ev->GetString( 2 ) );
|
|
if ( movegoal )
|
|
{
|
|
goal = movegoal->worldorigin;
|
|
}
|
|
else
|
|
{
|
|
Entity *ent;
|
|
|
|
ent = ev->GetEntity( 2 );
|
|
if ( ent )
|
|
{
|
|
goal = ent->worldorigin;
|
|
target = ent; //We need to set target seperate because goal may be changed soon
|
|
}
|
|
}
|
|
|
|
if ( ev->NumArgs() >= 3 )
|
|
accurate = ev->GetInteger( 3 );
|
|
else
|
|
accurate = false;
|
|
}
|
|
|
|
void MJump::ShowInfo (Actor &self)
|
|
{
|
|
Behavior::ShowInfo( self );
|
|
}
|
|
|
|
///////////////////////
|
|
// Find close sight node to
|
|
///////////////////////
|
|
// This function returns a pathnode which is near the target, visible from the target and from
|
|
// self's current position, and is as high or higher than the target (since that usually conveys
|
|
// a sense of tactical advantage). The node must also be more than a minimum distance from
|
|
// self's current position
|
|
Vector MJump::FindCloseSightNodeTo
|
|
(
|
|
Actor &self,
|
|
Vector pos,
|
|
float maxdist // This one is not squared yet!
|
|
)
|
|
{
|
|
int i;
|
|
PathNode *bestnode;
|
|
float bestdist;
|
|
PathNode *node;
|
|
Vector delta;
|
|
float dist;
|
|
trace_t trace1;
|
|
trace_t trace2;
|
|
Vector selfpos;
|
|
Vector temppos;
|
|
// How do I declare these as constants?
|
|
float maxjumpdist = 900*900; // Maximum distance he is capable of jumping (squared)
|
|
float minjumpdist = 150*150; // Absolute minimum distance he will jump
|
|
float jumpdist = 300*300; // Distance he likes to keep from his enemy
|
|
|
|
// reduce maxjumpdist if we need to
|
|
if ((maxdist*maxdist) < maxjumpdist)
|
|
maxjumpdist = maxdist*maxdist;
|
|
|
|
// Raise our endpoints up for a variety of reasons
|
|
selfpos = self.worldorigin;
|
|
selfpos.z += 100;
|
|
temppos = pos;
|
|
temppos.z += 100;
|
|
bestnode = NULL;
|
|
bestdist = 999999999; // greater than ( 8192 * sqr(2) ) ^ 2 -- maximum squared distance
|
|
|
|
delta = selfpos - temppos;
|
|
dist = delta * delta;
|
|
if (dist > jumpdist) // We are a long way away so jump jump near
|
|
{
|
|
for( i = rand()%4; i <= ai_maxnode; i+=4 ) //Check every fourth node since we don't want do be too accurate
|
|
{
|
|
node = AI_GetNode( i );
|
|
if ( node && ( node->occupiedTime <= level.time ) )
|
|
{
|
|
delta = node->worldorigin - self.worldorigin;
|
|
dist = delta * delta;
|
|
if (( node->worldorigin.z > (pos.z-20)) && ( dist > minjumpdist ) && (dist < maxjumpdist))
|
|
{
|
|
// get the distance squared (faster than getting real distance)
|
|
delta = node->worldorigin - pos;
|
|
dist = delta * delta;
|
|
trace1 = G_Trace(temppos, Vector(0, 0, 0), Vector(0, 0, 0), node->worldorigin, &self, MASK_SHOT, "MJump::FindCloseSightNodeTo");
|
|
trace2 = G_Trace(selfpos, Vector(0, 0, 0), Vector(0, 0, 0), node->worldorigin, &self, MASK_SHOT, "MJump::FindCloseSightNodeTo");
|
|
// Now if this one can be seen from both ends, is closest, and is also higher than the target, we want it.
|
|
if ((trace1.fraction >= 1) && (trace2.fraction >= 1) && (dist < bestdist) )
|
|
{
|
|
bestnode = node;
|
|
bestdist = dist;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else // We are close so try to jump over our enemy
|
|
{
|
|
for( i = rand()%4; i <= ai_maxnode; i+=4 ) //Check every fourth node since we don't want do be too accurate
|
|
{
|
|
node = AI_GetNode( i );
|
|
if ( node && ( node->occupiedTime <= level.time ) )
|
|
{
|
|
delta = node->worldorigin - self.worldorigin;
|
|
// get the distance squared (faster than getting real distance)
|
|
dist = delta * delta;
|
|
|
|
delta = node->worldorigin - pos;
|
|
jumpdist = delta * delta; // Jumpdist is now the distance from the test node to the target
|
|
|
|
if (( node->worldorigin.z > (pos.z-10)) && ( dist > minjumpdist ) && (dist > jumpdist) && (dist < maxjumpdist)) //This is the line that makes him try to jump over
|
|
{
|
|
dist = jumpdist;
|
|
trace1 = G_Trace(temppos, Vector(0, 0, 0), Vector(0, 0, 0), node->worldorigin, &self, MASK_SHOT, "MJump::FindCloseSightNodeTo");
|
|
trace2 = G_Trace(selfpos, Vector(0, 0, 0), Vector(0, 0, 0), node->worldorigin, &self, MASK_SHOT, "MJump::FindCloseSightNodeTo");
|
|
// Now if this one can be seen from both ends, is closest, and is also higher than the target, we want it.
|
|
if ((trace1.fraction >= 1) && (trace2.fraction >= 1) && (dist < bestdist))
|
|
{
|
|
bestnode = node;
|
|
bestdist = dist;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} //end else
|
|
// Oh, shit, we didn't find anywhere we can jump to, let's look harder this time
|
|
if ( bestnode == NULL )
|
|
{
|
|
for( i = 0; (i <= ai_maxnode && bestnode == NULL); i++ ) //Check every node this time since we're getting desperate
|
|
{
|
|
node = AI_GetNode( i );
|
|
// if we don't have a node value, carry on
|
|
if(!node)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
delta = node->worldorigin - self.worldorigin; //Make sure it isn't too far away
|
|
dist = delta * delta;
|
|
if ( node && ( node->occupiedTime <= level.time ) && (dist > minjumpdist) && (dist < maxjumpdist) && (node->worldorigin.z > pos.z-500) )
|
|
{
|
|
// get the distance squared (faster than getting real distance)
|
|
delta = node->worldorigin - pos;
|
|
dist = delta * delta;
|
|
trace1 = G_Trace(temppos, Vector(0, 0, 0), Vector(0, 0, 0), node->worldorigin, &self, MASK_SHOT, "MJump::FindCloseSightNodeTo");
|
|
trace2 = G_Trace(selfpos, Vector(0, 0, 0), Vector(0, 0, 0), node->worldorigin, &self, MASK_SHOT, "MJump::FindCloseSightNodeTo");
|
|
if ((trace1.fraction >= 1) && (trace2.fraction >= 1) && (dist < bestdist))
|
|
{
|
|
bestnode = node;
|
|
bestdist = dist;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( bestnode == NULL ) // Revert back to original target if we didn't find anywhere near it
|
|
return pos;
|
|
else
|
|
return bestnode->worldorigin;
|
|
}
|
|
|
|
|
|
void MJump::Begin (Actor &self)
|
|
{
|
|
trace_t trace;
|
|
Vector upcheck;
|
|
float direction; //Used for testing of direction of movement relative to orientation
|
|
float maxjumpdistance = 1000; // Clips the max jump distance if we have a low ceiling
|
|
|
|
// This bit aborts the jump if the ceiling is too low, and culls it if it is a little too low
|
|
|
|
upcheck = self.worldorigin;
|
|
upcheck.z += heightneeded;
|
|
trace = G_Trace(self.worldorigin, Vector(-22, -22, 0), Vector(22, 22, 0), upcheck, &self, MASK_MONSTERSOLID, "MJumpTo::Begin");
|
|
if (trace.fraction >= 1)
|
|
{
|
|
|
|
state = 0; // I shouldn't have to do this here, but I tried it anyhow and it worked...
|
|
|
|
upcheck = self.worldorigin;
|
|
upcheck.z += heightwanted;
|
|
trace = G_Trace(self.worldorigin, Vector(-22, -22, 0), Vector(22, 22, 0), upcheck, &self, MASK_MONSTERSOLID, "MJumpTo::Begin");
|
|
if (trace.fraction < 1)
|
|
// In this piece, we assume that we can jump thrice as far as the roof is high,
|
|
// which is a reasonable (if not entirely accurate) assumption, I think.
|
|
maxjumpdistance = heightwanted * trace.fraction * 3;
|
|
|
|
if ( !accurate )
|
|
goal = FindCloseSightNodeTo ( self, goal, maxjumpdistance );
|
|
|
|
direction = Vector(self.orientation[0]).toYaw() - (goal-self.worldorigin).toYaw();
|
|
direction = fmod (direction, 360.0);
|
|
if (direction < 0)
|
|
direction += 360;
|
|
if (( direction >= 45 ) && ( direction < 135 ))
|
|
jumpdir = "right";
|
|
else if (( direction >= 135 ) && ( direction < 225 ))
|
|
jumpdir = "back";
|
|
else if (( direction >= 225 ) && ( direction < 315 ))
|
|
jumpdir = "left";
|
|
else
|
|
jumpdir = "forward";
|
|
|
|
anim = "jump_" + jumpdir;
|
|
|
|
if ( anim.length() )
|
|
{
|
|
animdone=false;
|
|
self.SetAnim( anim, EV_Actor_NotifyBehavior );
|
|
}
|
|
} //end if (ceiling is high enough)
|
|
else
|
|
{
|
|
jumpok = false;
|
|
}
|
|
}
|
|
|
|
qboolean MJump::Evaluate (Actor &self)
|
|
{
|
|
Vector landpos;
|
|
trace_t trace;
|
|
float direction; //Used for testing of direction of movement relative to orientation
|
|
Mortician *mself;
|
|
Vector targetdir;
|
|
|
|
// Do a quick check so we don't cause major problems if we accidentally pass
|
|
// a non-mortician to this function.
|
|
if (!self.isSubclassOf( Mortician ))
|
|
return false;
|
|
|
|
// Do a check to see if we have any ceiling height
|
|
if (!jumpok)
|
|
return false;
|
|
|
|
if(!target)
|
|
target = world;
|
|
|
|
// OK, now get on with it
|
|
mself = (Mortician *) (&self);
|
|
|
|
upspeed = (self.worldorigin.z - oldworldheight)/FRAMETIME;
|
|
|
|
switch( state )
|
|
{
|
|
case 0:
|
|
float traveltime;
|
|
|
|
if ( animdone )
|
|
{
|
|
animdone = false;
|
|
anim = "jump_" + jumpdir + "_inair";
|
|
self.SetAnim( anim, EV_Actor_NotifyBehavior );
|
|
// Use mself here instead of self so we call the correct jumping function
|
|
traveltime = mself->JumpTo( goal );
|
|
endtime = traveltime + level.time;
|
|
|
|
self.last_jump_time = endtime;
|
|
|
|
state = 1;
|
|
}
|
|
break;
|
|
|
|
case 1:
|
|
// Now, watch the player as we move upwards
|
|
if (animdone)
|
|
{
|
|
|
|
///////////////
|
|
// Turn to face the player
|
|
if ( target == NULL )
|
|
{
|
|
targetdir = (goal - self.worldorigin).toAngles();
|
|
targetdir.setPitch(0);
|
|
}
|
|
else
|
|
{
|
|
targetdir = (target->worldorigin - self.worldorigin).toAngles();
|
|
// Invert pitch since setAngles is screwy, and divide by 2
|
|
if (targetdir.pitch() > 180)
|
|
targetdir.setPitch(targetdir.pitch() - 360);
|
|
targetdir.setPitch(-targetdir.pitch()/2);
|
|
}
|
|
self.setAngles( targetdir );
|
|
|
|
/////////////////
|
|
// Check to see if we will be moving downwards next frame
|
|
if (upspeed <= 0)
|
|
{
|
|
state = 2;
|
|
}
|
|
else
|
|
{
|
|
// Take a shot from in the air
|
|
|
|
if ( ((endtime-level.time)>0.5) &&
|
|
animdone &&
|
|
self.currentEnemy &&
|
|
self.CanSee(self.currentEnemy)) // make sure can actually hit
|
|
{
|
|
animdone = false;
|
|
anim = "air_shoot";
|
|
self.SetAnim( anim, EV_Actor_NotifyBehavior );
|
|
}
|
|
else if (animdone)
|
|
{
|
|
self.SetAnim("air_idle");
|
|
}
|
|
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 2:
|
|
//
|
|
// Moving downwards, waiting to hit the ground...
|
|
//
|
|
|
|
if (animdone)
|
|
self.SetAnim("air_idle");
|
|
|
|
///////////////
|
|
// Turn to face the player
|
|
if ( target == NULL )
|
|
{
|
|
targetdir = (goal - self.worldorigin).toAngles();
|
|
targetdir.setPitch(0);
|
|
}
|
|
else
|
|
{
|
|
targetdir = (target->worldorigin - self.worldorigin).toAngles();
|
|
// Invert pitch since setAngles is screwy, and divide by 2
|
|
if (targetdir.pitch() > 180)
|
|
targetdir.setPitch(targetdir.pitch() - 360);
|
|
targetdir.setPitch(-targetdir.pitch()/2);
|
|
}
|
|
self.setAngles( targetdir );
|
|
landpos = self.worldorigin + ( ((upspeed - self.gravity*sv_gravity->value)*Vector(0, 0, 1)) * FRAMETIME );
|
|
trace = G_Trace(self.worldorigin, Vector(-22, -22, 0), Vector(22, 22, 0), landpos, &self, MASK_MONSTERSOLID, "MJumpTo::Evaluate");
|
|
|
|
if ( (trace.fraction < 1) || ( self.groundentity ) )
|
|
{
|
|
state = 3;
|
|
|
|
// Make sure we're not tipped over any more
|
|
//
|
|
targetdir = (target->worldorigin - self.worldorigin).toAngles();
|
|
targetdir.setPitch(0);
|
|
self.setAngles( targetdir );
|
|
|
|
//
|
|
// Set landing animation depending on velocity relative to orientation
|
|
//
|
|
|
|
// We have to use toYaw here as opposed to yaw because
|
|
// this is a velocity vector rather than an angle vector
|
|
direction = Vector(self.orientation[0]).toYaw() - self.velocity.toYaw();
|
|
direction = fmod (direction, 360.0f);
|
|
if (direction < 0)
|
|
direction += 360;
|
|
temp_vel = self.velocity;
|
|
temp_vel.z = 0;
|
|
if ( (temp_vel.length()/(-upspeed) < 0.4 ) || (temp_vel.length() < 60) )
|
|
jumpdir = "down";
|
|
else if (( direction >= 45 ) && ( direction < 135 ))
|
|
jumpdir = "right";
|
|
else if (( direction >= 135 ) && ( direction < 225 ))
|
|
jumpdir = "back";
|
|
else if (( direction >= 225 ) && ( direction < 315 ))
|
|
jumpdir = "left";
|
|
else
|
|
jumpdir = "forward";
|
|
|
|
anim = "land_" + jumpdir;
|
|
|
|
//
|
|
// if we have an anim, we go to state 4
|
|
//
|
|
if ( self.HasAnim( anim.c_str() ) )
|
|
{
|
|
animdone = false;
|
|
self.SetAnim( anim, EV_Actor_NotifyBehavior );
|
|
state = 4;
|
|
}
|
|
else
|
|
{
|
|
self.SetAnim( "jump_idle", EV_Actor_NotifyBehavior );
|
|
state = 3;
|
|
}
|
|
}
|
|
// Record downward velocity for comparison with horizontal velocity
|
|
// later, to determine which landing animation should be played.
|
|
|
|
break;
|
|
|
|
case 3:
|
|
//
|
|
// we are on the ground and waiting to timeout
|
|
//
|
|
if ( (level.time > endtime) && (self.groundentity) )
|
|
return false;
|
|
break;
|
|
case 4:
|
|
//
|
|
// we are on the ground and waiting for our landing animation to finish
|
|
//
|
|
if (animdone)
|
|
{
|
|
if (self.groundentity)
|
|
return false;
|
|
else
|
|
self.SetAnim( "idle" );
|
|
}
|
|
break;
|
|
}
|
|
|
|
//Save off origin for velocity calcs
|
|
oldworldheight = self.worldorigin.z;
|
|
|
|
return true;
|
|
}
|
|
|
|
void MJump::End (Actor &self)
|
|
{
|
|
Vector targetdir = (target->worldorigin - self.worldorigin).toAngles();
|
|
targetdir.setPitch(0);
|
|
self.setAngles( targetdir );
|
|
|
|
self.SetAnim( "idle" );
|
|
}
|