quake4-sdk/source/mpgame/ai/AI_Move.cpp
2007-06-15 00:00:00 +00:00

4832 lines
129 KiB
C++

/*
===============================================================================
AI_Move.cpp
This file has all movement related functions. It and its sister H file were
split from AI.h and AI.cpp in order to prevent merge conflicts and to make
further changes to the system possible.
===============================================================================
*/
#include "../../idlib/precompiled.h"
#pragma hdrstop
#include "../Game_local.h"
#include "AI.h"
#include "AI_Manager.h"
#include "AI_Util.h"
#include "AAS_Find.h"
/*
===============================================================================
idMoveState
===============================================================================
*/
/*
=====================
idMoveState::idMoveState
=====================
*/
idMoveState::idMoveState() {
moveType = MOVETYPE_ANIM;
moveCommand = MOVE_NONE;
moveStatus = MOVE_STATUS_DONE;
moveDest.Zero();
moveDir.Set( 1.0f, 0.0f, 0.0f );
goalEntity = NULL;
goalEntityOrigin.Zero();
toAreaNum = 0;
startTime = 0;
duration = 0;
speed = 0.0f;
range = 0.0f;
wanderYaw = 0;
nextWanderTime = 0;
blockTime = 0;
obstacle = NULL;
lastMoveOrigin = vec3_origin;
lastMoveTime = 0;
anim = 0;
travelFlags = TFL_WALK|TFL_AIR;
kickForce = 2048.0f;
fl.ignoreObstacles = false;
fl.allowAnimMove = false;
fl.allowPrevAnimMove = false;
fl.allowHiddenMove = false;
blockedRadius = 0.0f;
blockedMoveTime = 750;
blockedAttackTime = 750;
turnRate = 360.0f;
turnVel = 0.0f;
anim_turn_yaw = 0.0f;
anim_turn_amount = 0.0f;
anim_turn_angles = 0.0f;
fly_offset = 0;
fly_seek_scale = 1.0f;
fly_roll_scale = 0.0f;
fly_roll_max = 0.0f;
fly_roll = 0.0f;
fly_pitch_scale = 0.0f;
fly_pitch_max = 0.0f;
fly_pitch = 0.0f;
fly_speed = 0.0f;
fly_bob_strength = 0.0f;
fly_bob_vert = 0.0f;
fly_bob_horz = 0.0f;
currentDirection = MOVEDIR_FORWARD;
idealDirection = MOVEDIR_FORWARD;
current_yaw = 0.0f;
ideal_yaw = 0.0f;
flyTiltJoint = INVALID_JOINT;
addVelocity.Zero();
}
/*
=====================
idMoveState::Spawn
=====================
*/
void idMoveState::Spawn( idDict &spawnArgs ) {
memset ( &fl, 0, sizeof(fl) );
fl.allowAnimMove = true;
fl.allowPrevAnimMove = false;
fl.allowHiddenMove = false;
fl.noRun = spawnArgs.GetBool ( "forceWalk", "0" );
fl.noWalk = spawnArgs.GetBool ( "forceRun", "0" );
fl.noTurn = spawnArgs.GetBool ( "noTurn", "0" );
fl.noGravity = spawnArgs.GetBool ( "animate_z", "0" );
fl.noRangedInterrupt = spawnArgs.GetBool ( "noRangedInterrupt", "0" );
fl.flyTurning = spawnArgs.GetBool ( "flyTurning", "0" );
fl.allowDirectional = spawnArgs.GetBool ( "directionalMovement", "0" );
fl.allowPushMovables = spawnArgs.GetBool( "af_push_moveables", "0" );
fl.ignoreObstacles = spawnArgs.GetBool( "ignore_obstacles", "0" );
fl.disabled = spawnArgs.GetBool ( "noMove", "0" );
walkRange = spawnArgs.GetFloat ( "walkRange", "100" );
walkTurn = spawnArgs.GetFloat ( "walkTurn", "45" );
followRange = spawnArgs.GetVec2 ( "followRange", "70 250" );
searchRange = spawnArgs.GetVec2 ( "searchRange", "0 1024" );
attackPositionRange = spawnArgs.GetFloat ( "attackPositionRange", "0" );
turnDelta = spawnArgs.GetFloat ( "turnDelta", "10" );
blockTime = 0;
spawnArgs.GetInt( "fly_offset", "100", fly_offset );
spawnArgs.GetFloat( "fly_speed", "100", fly_speed );
spawnArgs.GetFloat( "fly_bob_strength", "50", fly_bob_strength );
spawnArgs.GetFloat( "fly_bob_vert", "2", fly_bob_horz );
spawnArgs.GetFloat( "fly_bob_horz", "2.7", fly_bob_vert );
spawnArgs.GetFloat( "fly_seek_scale", "4", fly_seek_scale );
spawnArgs.GetFloat( "fly_roll_scale", "90", fly_roll_scale );
spawnArgs.GetFloat( "fly_roll_max", "60", fly_roll_max );
spawnArgs.GetFloat( "fly_pitch_scale", "45", fly_pitch_scale );
spawnArgs.GetFloat( "fly_pitch_max", "30", fly_pitch_max );
spawnArgs.GetFloat( "turn_rate", "360", turnRate );
spawnArgs.GetFloat( "kick_force", "4096", kickForce );
spawnArgs.GetFloat( "blockedRadius", "-1", blockedRadius );
spawnArgs.GetInt( "blockedMoveTime", "750", blockedMoveTime );
spawnArgs.GetInt( "blockedAttackTime", "750", blockedAttackTime );
}
/*
=====================
idMoveState::Save
=====================
*/
void idMoveState::Save( idSaveGame *savefile ) const {
int i;
savefile->Write ( &fl, sizeof(fl) );
savefile->WriteInt( (int)moveType );
savefile->WriteInt( (int)moveCommand );
savefile->WriteInt( (int)moveStatus );
savefile->WriteVec3( moveDest );
savefile->WriteVec3( moveDir );
savefile->WriteInt( toAreaNum );
savefile->WriteInt( startTime );
savefile->WriteInt( duration );
savefile->WriteFloat( speed );
savefile->WriteFloat( range );
savefile->WriteFloat( wanderYaw );
savefile->WriteInt( nextWanderTime );
savefile->WriteInt( blockTime );
obstacle.Save( savefile );
savefile->WriteVec3( lastMoveOrigin );
savefile->WriteInt( lastMoveTime );
savefile->WriteInt( anim );
savefile->WriteInt( travelFlags );
savefile->WriteFloat ( kickForce );
savefile->WriteFloat ( blockedRadius );
savefile->WriteInt ( blockedMoveTime );
savefile->WriteInt ( blockedAttackTime );
savefile->WriteFloat( ideal_yaw );
savefile->WriteFloat( current_yaw );
savefile->WriteFloat( turnRate );
savefile->WriteFloat( turnVel );
savefile->WriteFloat( anim_turn_yaw );
savefile->WriteFloat( anim_turn_amount );
savefile->WriteFloat( anim_turn_angles );
savefile->WriteJoint( flyTiltJoint );
savefile->WriteFloat( fly_speed );
savefile->WriteFloat( fly_bob_strength );
savefile->WriteFloat( fly_bob_vert );
savefile->WriteFloat( fly_bob_horz );
savefile->WriteInt( fly_offset );
savefile->WriteFloat( fly_seek_scale );
savefile->WriteFloat( fly_roll_scale );
savefile->WriteFloat( fly_roll_max );
savefile->WriteFloat( fly_roll );
savefile->WriteFloat( fly_pitch_scale );
savefile->WriteFloat( fly_pitch_max );
savefile->WriteFloat( fly_pitch );
savefile->WriteInt ( (int) currentDirection );
savefile->WriteInt ( (int) idealDirection );
savefile->WriteFloat( walkRange );
savefile->WriteFloat( walkTurn );
savefile->WriteVec2 ( followRange );
savefile->WriteVec2 ( searchRange );
savefile->WriteFloat( attackPositionRange );
savefile->WriteFloat( turnDelta );
savefile->WriteVec3 ( goalPos );
savefile->WriteInt ( goalArea );
goalEntity.Save( savefile );
savefile->WriteVec3( goalEntityOrigin );
savefile->WriteVec3( myPos ); // cnicholson: Added unsaved var
savefile->WriteInt( myArea ); // cnicholson: Added unsaved var
savefile->WriteVec3 ( seekPos );
savefile->WriteInt( (int) MAX_PATH_LEN ); // cnicholson: Added unsaved vars
for (i=0; i< MAX_PATH_LEN; ++i) {
// TOSAVE: idReachability* reach;
savefile->WriteVec3( path[i].seekPos );
}
savefile->WriteInt( pathLen ); // cnicholson: Added unsaved var
savefile->WriteInt( pathArea ); // cnicholson: Added unsaved var
savefile->WriteInt( pathTime ); // cnicholson: Added unsaved var
savefile->WriteVec3( addVelocity );
}
/*
=====================
idMoveState::Restore
=====================
*/
void idMoveState::Restore( idRestoreGame *savefile ) {
int i, num;
savefile->Read ( &fl, sizeof(fl) );
savefile->ReadInt( (int&)moveType );
savefile->ReadInt( (int&)moveCommand );
savefile->ReadInt( (int&)moveStatus );
savefile->ReadVec3( moveDest );
savefile->ReadVec3( moveDir );
savefile->ReadInt( toAreaNum );
savefile->ReadInt( startTime );
savefile->ReadInt( duration );
savefile->ReadFloat( speed );
savefile->ReadFloat( range );
savefile->ReadFloat( wanderYaw );
savefile->ReadInt( nextWanderTime );
savefile->ReadInt( blockTime );
obstacle.Restore ( savefile );
savefile->ReadVec3( lastMoveOrigin );
savefile->ReadInt( lastMoveTime );
savefile->ReadInt( anim );
savefile->ReadInt( travelFlags );
savefile->ReadFloat ( kickForce );
savefile->ReadFloat ( blockedRadius );
savefile->ReadInt ( blockedMoveTime );
savefile->ReadInt ( blockedAttackTime );
savefile->ReadFloat( ideal_yaw );
savefile->ReadFloat( current_yaw );
savefile->ReadFloat( turnRate );
savefile->ReadFloat( turnVel );
savefile->ReadFloat( anim_turn_yaw );
savefile->ReadFloat( anim_turn_amount );
savefile->ReadFloat( anim_turn_angles );
savefile->ReadJoint( flyTiltJoint );
savefile->ReadFloat( fly_speed );
savefile->ReadFloat( fly_bob_strength );
savefile->ReadFloat( fly_bob_vert );
savefile->ReadFloat( fly_bob_horz );
savefile->ReadInt( fly_offset );
savefile->ReadFloat( fly_seek_scale );
savefile->ReadFloat( fly_roll_scale );
savefile->ReadFloat( fly_roll_max );
savefile->ReadFloat( fly_roll );
savefile->ReadFloat( fly_pitch_scale );
savefile->ReadFloat( fly_pitch_max );
savefile->ReadFloat( fly_pitch );
savefile->ReadInt ( (int&) currentDirection );
savefile->ReadInt ( (int&) idealDirection );
savefile->ReadFloat( walkRange );
savefile->ReadFloat( walkTurn );
savefile->ReadVec2 ( followRange );
savefile->ReadVec2 ( searchRange );
savefile->ReadFloat( attackPositionRange );
savefile->ReadFloat( turnDelta );
savefile->ReadVec3 ( goalPos );
savefile->ReadInt ( goalArea );
goalEntity.Restore ( savefile );
savefile->ReadVec3( goalEntityOrigin );
savefile->ReadVec3( myPos ); // cnicholson: Added unrestored var
savefile->ReadInt( myArea ); // cnicholson: Added unrestored var
savefile->ReadVec3 ( seekPos );
savefile->ReadInt( num );
for (i=0; i< num; ++i) {
// TOSAVE: idReachability* reach;
savefile->ReadVec3( path[i].seekPos );
}
savefile->ReadInt( pathLen ); // cnicholson: Added unrestored var
savefile->ReadInt( pathArea ); // cnicholson: Added unrestored var
savefile->ReadInt( pathTime ); // cnicholson: Added unrestored var
savefile->ReadVec3( addVelocity );
}
/*
============
idAI::SetMoveType
============
*/
void idAI::SetMoveType ( moveType_t moveType ) {
if ( move.moveType == moveType ) {
return;
}
StopSound ( SND_CHANNEL_HEART, false );
switch ( moveType ) {
case MOVETYPE_STATIC:
move.travelFlags = 0;
break;
case MOVETYPE_FLY:
move.travelFlags = TFL_FLY|TFL_WALK|TFL_AIR;
StartSound ( "snd_fly", SND_CHANNEL_HEART, 0, false, NULL );
break;
case MOVETYPE_ANIM:
move.travelFlags = TFL_WALK|TFL_AIR;
break;
case MOVETYPE_CUSTOM:
move.travelFlags = TFL_WALK|TFL_AIR|TFL_WALKOFFLEDGE;
break;
}
move.moveType = moveType;
}
/*
=====================
idAI::Event_SaveMove
=====================
*/
void idAI::Event_SaveMove( void ) {
savedMove = move;
}
/*
=====================
idAI::Event_RestoreMove
=====================
*/
void idAI::Event_RestoreMove( void ) {
idVec3 dest;
switch( savedMove.moveCommand ) {
case MOVE_NONE :
StopMove( savedMove.moveStatus );
break;
case MOVE_FACE_ENEMY :
FaceEnemy();
break;
case MOVE_FACE_ENTITY :
FaceEntity( savedMove.goalEntity.GetEntity() );
break;
case MOVE_TO_ENEMY :
MoveToEnemy();
break;
case MOVE_TO_ENTITY :
MoveToEntity( savedMove.goalEntity.GetEntity(), savedMove.range );
break;
case MOVE_OUT_OF_RANGE :
MoveOutOfRange( savedMove.goalEntity.GetEntity(), savedMove.range );
break;
case MOVE_TO_ATTACK:
MoveToAttack ( savedMove.goalEntity.GetEntity(), savedMove.anim );
break;
case MOVE_TO_COVER :
MoveToCover ( combat.attackRange[0], combat.attackRange[1], AITACTICAL_COVER );
break;
case MOVE_TO_POSITION :
MoveTo ( savedMove.moveDest, savedMove.range );
break;
case MOVE_SLIDE_TO_POSITION :
SlideToPosition( savedMove.moveDest, savedMove.duration );
break;
case MOVE_WANDER :
WanderAround();
break;
}
if ( GetMovePos( move.seekPos ) ) {
CheckObstacleAvoidance( move.seekPos, dest );
}
}
/*
============
idAI::KickObstacles
============
*/
void idAI::KickObstacles( const idVec3 &dir, float force, idEntity *alwaysKick ) {
int i, numListedClipModels;
idBounds clipBounds;
idEntity *obEnt;
idClipModel *clipModel;
idClipModel *clipModelList[ MAX_GENTITIES ];
int clipmask;
idVec3 org;
idVec3 forceVec;
idVec3 delta;
idVec2 perpendicular;
org = physicsObj.GetOrigin();
// find all possible obstacles
clipBounds = physicsObj.GetAbsBounds();
clipBounds.TranslateSelf( dir * 32.0f );
clipBounds.ExpandSelf( 8.0f );
clipBounds.AddPoint( org );
clipmask = physicsObj.GetClipMask();
// RAVEN BEGIN
// ddynerman: multiple clip worlds
numListedClipModels = gameLocal.ClipModelsTouchingBounds( this, clipBounds, clipmask, clipModelList, MAX_GENTITIES );
// RAVEN END
for ( i = 0; i < numListedClipModels; i++ ) {
clipModel = clipModelList[i];
obEnt = clipModel->GetEntity();
if ( obEnt == alwaysKick ) {
// we'll kick this one outside the loop
continue;
}
if ( !clipModel->IsTraceModel() ) {
continue;
}
// RAVEN BEGIN
// jnewquist: Use accessor for static class type
if (( obEnt->IsType( idMoveable::GetClassType() ) || obEnt->IsType( idAFAttachment::GetClassType() )) && obEnt->GetPhysics()->IsPushable() ) {
// RAVEN END
delta = obEnt->GetPhysics()->GetOrigin() - org;
delta.NormalizeFast();
perpendicular.x = -delta.y;
perpendicular.y = delta.x;
delta.z += 0.5f;
delta.ToVec2() += perpendicular * gameLocal.random.CRandomFloat() * 0.5f;
forceVec = delta * force * obEnt->GetPhysics()->GetMass();
obEnt->ApplyImpulse( this, 0, obEnt->GetPhysics()->GetOrigin(), forceVec );
}
}
if ( alwaysKick ) {
delta = alwaysKick->GetPhysics()->GetOrigin() - org;
delta.NormalizeFast();
perpendicular.x = -delta.y;
perpendicular.y = delta.x;
delta.z += 0.5f;
delta.ToVec2() += perpendicular * gameLocal.random.CRandomFloat() * 0.5f;
forceVec = delta * force * alwaysKick->GetPhysics()->GetMass();
alwaysKick->ApplyImpulse( this, 0, alwaysKick->GetPhysics()->GetOrigin(), forceVec );
}
}
/*
============
ValidForBounds
============
*/
bool ValidForBounds( const idAASSettings *settings, const idBounds &bounds ) {
int i;
for ( i = 0; i < 3; i++ ) {
if ( bounds[0][i] < settings->boundingBoxes[0][0][i] ) {
return false;
}
if ( bounds[1][i] > settings->boundingBoxes[0][1][i] ) {
return false;
}
}
return true;
}
/*
=====================
idAI::SetAAS
=====================
*/
void idAI::SetAAS( void ) {
idStr use_aas;
spawnArgs.GetString( "use_aas", NULL, use_aas );
if ( !use_aas || !use_aas[0] ) {
//don't intend to use AAS at all?
//no warning - lack of AAS intentional
aas = NULL;
return;
}
aas = gameLocal.GetAAS( use_aas );
if ( aas ) {
const idAASSettings *settings = aas->GetSettings();
if ( settings ) {
if ( !ValidForBounds( settings, physicsObj.GetBounds() ) ) {
gameLocal.Error( "%s cannot use use_aas %s\n", name.c_str(), use_aas.c_str() );
}
float height = settings->maxStepHeight;
physicsObj.SetMaxStepHeight( height );
return;
} else {
aas = NULL;
}
}
gameLocal.Printf( "WARNING: %s has no AAS file\n", name.c_str() );
}
/*
=====================
idAI::ReachedPos
=====================
*/
bool idAI::ReachedPos( const idVec3 &pos, const aiMoveCommand_t moveCommand, float range ) const {
// When moving towards the enemy just see if our bounding box touches the desination
if ( moveCommand == MOVE_TO_ENEMY ) {
if ( !enemy.ent || physicsObj.GetAbsBounds().IntersectsBounds( enemy.ent->GetPhysics()->GetAbsBounds().Expand( range ) ) ) {
return true;
}
return false;
}
// Dont add vertical bias when using fly move
if ( move.moveType == MOVETYPE_FLY ) {
float offset;
if ( moveCommand == MOVE_TO_ENTITY ) {
offset = 0.0f;
} else {
offset = move.fly_offset;
}
idBounds bnds;
bnds = idBounds ( idVec3(-range,-range,-range), idVec3(range,range,range+offset) );
bnds.TranslateSelf( physicsObj.GetOrigin() );
return bnds.ContainsPoint( pos );
}
if ( moveCommand == MOVE_TO_TETHER )
{//if you're not actually in the tether, we don't want to stop early (for compliance with ai_trigger condition_tether)
if ( !IsWithinTether() )
{
return false;
}
}
// Excluded z height when determining reached
if ( move.toAreaNum > 0 ) {
if ( PointReachableAreaNum( physicsObj.GetOrigin() ) == move.toAreaNum ) {
idBounds bnds;
bnds = idBounds ( idVec3(-range,-range,-4096.0f), idVec3(range,range,4096.0f) );
bnds.TranslateSelf( physicsObj.GetOrigin() );
return bnds.ContainsPoint( pos );
}
}
idBounds bnds;
bnds = idBounds ( idVec3(-range,-range,-16.0f), idVec3(range,range,64.0f) );
bnds.TranslateSelf( physicsObj.GetOrigin() );
return bnds.ContainsPoint( pos );
}
/*
=====================
idAI::PointReachableAreaNum
=====================
*/
int idAI::PointReachableAreaNum( const idVec3 &pos, const float boundsScale ) const {
int areaNum;
idVec3 size;
idBounds bounds;
if ( !aas ) {
return 0;
}
size = aas->GetSettings()->boundingBoxes[0][1] * boundsScale;
bounds[0] = -size;
size.z = 32.0f;
bounds[1] = size;
if ( move.moveType == MOVETYPE_FLY ) {
areaNum = aas->PointReachableAreaNum( pos, bounds, AREA_REACHABLE_WALK | AREA_REACHABLE_FLY );
} else {
areaNum = aas->PointReachableAreaNum( pos, bounds, AREA_REACHABLE_WALK );
}
return areaNum;
}
/*
=====================
idAI::PathToGoal
=====================
*/
bool idAI::PathToGoal( aasPath_t &path, int areaNum, const idVec3 &origin, int goalAreaNum, const idVec3 &goalOrigin ) const {
idVec3 org;
idVec3 goal;
if ( !aas ) {
return false;
}
org = origin;
aas->PushPointIntoAreaNum( areaNum, org );
if ( !areaNum ) {
return false;
}
goal = goalOrigin;
aas->PushPointIntoAreaNum( goalAreaNum, goal );
if ( !goalAreaNum ) {
return false;
}
//push goal back up if flying to a point way above an area
if ( move.moveType == MOVETYPE_FLY ) {
if ( areaNum == goalAreaNum ) {
if ( goalOrigin.z > goal.z ) {
float areaTop = aas->AreaCeiling( goalAreaNum ) - GetPhysics()->GetBounds()[1].z;
if ( goalOrigin.z > areaTop ) {
//crap, cap it...
goal.z = areaTop;
} else {
goal.z = goalOrigin.z;
}
}
}
}
if ( move.moveType == MOVETYPE_FLY ) {
return aas->FlyPathToGoal( path, areaNum, org, goalAreaNum, goal, move.travelFlags );
} else {
return aas->WalkPathToGoal( path, areaNum, org, goalAreaNum, goal, move.travelFlags );
}
}
/*
=====================
idAI::TravelDistance
Returns the approximate travel distance from one position to the goal, or if no AAS, the straight line distance.
This is feakin' slow, so it's not good to do it too many times per frame. It also is slower the further you
are from the goal, so try to break the goals up into shorter distances.
=====================
*/
float idAI::TravelDistance( const idVec3 &start, const idVec3 &end ) const {
int fromArea;
int toArea;
float dist;
idVec2 delta;
aasPath_t path;
if ( !aas ) {
// no aas, so just take the straight line distance
delta = end.ToVec2() - start.ToVec2();
dist = delta.LengthFast();
if ( DebugFilter(ai_debugMove) ) { // Blue = Travel Distance
gameRenderWorld->DebugLine( colorBlue, start, end, gameLocal.msec, false );
gameRenderWorld->DrawText( va( "%d", ( int )dist ), ( start + end ) * 0.5f, 0.1f, colorWhite, gameLocal.GetLocalPlayer()->viewAngles.ToMat3() );
}
return dist;
}
fromArea = PointReachableAreaNum( start );
toArea = PointReachableAreaNum( end );
if ( !fromArea || !toArea ) {
// can't seem to get there
return -1;
}
if ( fromArea == toArea ) {
// same area, so just take the straight line distance
delta = end.ToVec2() - start.ToVec2();
dist = delta.LengthFast();
if ( DebugFilter(ai_debugMove) ) { // Blue = Travel Distance
gameRenderWorld->DebugLine( colorBlue, start, end, gameLocal.msec, false );
gameRenderWorld->DrawText( va( "%d", ( int )dist ), ( start + end ) * 0.5f, 0.1f, colorWhite, gameLocal.GetLocalPlayer()->viewAngles.ToMat3() );
}
return dist;
}
idReachability *reach;
int travelTime;
if ( !aas->RouteToGoalArea( fromArea, start, toArea, move.travelFlags, travelTime, &reach ) ) {
return -1;
}
if ( DebugFilter(ai_debugMove) ) { // Travel Distance, Fly Path & Walk Path
if ( move.moveType == MOVETYPE_FLY ) {
aas->ShowFlyPath( start, toArea, end );
} else {
aas->ShowWalkPath( start, toArea, end );
}
}
return travelTime;
}
float idAI::TravelDistance ( idEntity *ent ) const {
return TravelDistance ( physicsObj.GetOrigin(), ent->GetPhysics()->GetOrigin() );
}
float idAI::TravelDistance( idEntity* start, idEntity* end ) const {
assert( start );
assert( end );
return TravelDistance( start->GetPhysics()->GetOrigin(), end->GetPhysics()->GetOrigin() );
}
float idAI::TravelDistance( const idVec3 &pos ) const {
return TravelDistance( physicsObj.GetOrigin(), pos );
}
/*
============
idAI::ScriptedPlaybackMove
============
*/
void idAI::ScriptedPlaybackMove ( const char* playback, int flags, int numFrames ) {
// Start the scripted sequence
if ( !ScriptedBegin ( false ) ) {
return;
}
// Start the playback
if ( !mPlayback.Start( spawnArgs.GetString ( playback ), this, flags, numFrames ) ) {
ScriptedEnd ( );
return;
}
move.goalEntity = NULL;
move.moveCommand = MOVE_RV_PLAYBACK;
move.moveType = MOVETYPE_PLAYBACK;
move.fl.done = false;
// Wait till its done and mark it finished
SetState ( "State_ScriptedPlaybackMove" );
PostState ( "State_ScriptedStop" );
}
/*
=====================
idAI::FaceEnemy
Continually face the enemy's last known position. MoveDone is always true in this case.
=====================
*/
bool idAI::FaceEnemy( void ) {
idEntity *enemyEnt = enemy.ent;
if ( !enemyEnt ) {
StopMove( MOVE_STATUS_DEST_NOT_FOUND );
return false;
}
TurnToward( enemy.lastKnownPosition );
move.goalEntity = enemyEnt;
move.moveDest = physicsObj.GetOrigin();
move.moveCommand = MOVE_FACE_ENEMY;
move.moveStatus = MOVE_STATUS_WAITING;
move.startTime = gameLocal.time;
move.speed = 0.0f;
move.fl.done = true;
move.fl.moving = false;
move.fl.goalUnreachable = false;
aifl.simpleThink = false;
return true;
}
/*
=====================
idAI::FaceEntity
Continually face the entity position. MoveDone will never be true in this case.
=====================
*/
bool idAI::FaceEntity( idEntity *ent ) {
if ( !ent ) {
StopMove( MOVE_STATUS_DEST_NOT_FOUND );
return false;
}
idVec3 entityOrg = ent->GetPhysics()->GetOrigin();
TurnToward( entityOrg );
move.goalEntity = ent;
move.moveDest = physicsObj.GetOrigin();
move.moveCommand = MOVE_FACE_ENTITY;
move.moveStatus = MOVE_STATUS_WAITING;
move.startTime = gameLocal.time;
move.speed = 0.0f;
move.fl.done = false;
move.fl.moving = false;
move.fl.goalUnreachable = false;
aifl.simpleThink = false;
return true;
}
/*
=====================
idAI::StartMove
Initialize a new movement by setting up the movement structure
=====================
*/
bool idAI::StartMove ( aiMoveCommand_t command, const idVec3& goalOrigin, int goalArea, idEntity* goalEntity, aasFeature_t* feature, float range ) {
// If we are already there then we are done
if ( ReachedPos( goalOrigin, command ) ) {
StopMove( MOVE_STATUS_DONE );
return true;
}
move.lastMoveOrigin = physicsObj.GetOrigin ( );
move.seekPos = move.goalPos = move.moveDest = goalOrigin;
move.toAreaNum = goalArea;
move.goalEntity = goalEntity;
move.moveCommand = command;
move.moveStatus = MOVE_STATUS_MOVING;
move.speed = move.fly_speed;
move.startTime = gameLocal.time;
move.range = range;
move.fl.done = false;
move.fl.goalUnreachable = false;
move.fl.moving = true;
aasSensor->Reserve ( feature );
OnStartMoving ( );
return true;
}
/*
=====================
idAI::StopMove
=====================
*/
void idAI::StopMove( moveStatus_t status ) {
aiMoveCommand_t oldCommand = move.moveCommand;
float saveZ = 0.0f;
move.fl.done = true;
move.fl.moving = false;
move.fl.goalUnreachable = false;
move.fl.obstacleInPath = false;
move.fl.blocked = false;
if ( move.moveType == MOVETYPE_FLY ) {
if ( move.moveCommand == MOVE_TO_ENEMY
|| move.moveCommand == MOVE_TO_ATTACK ) {
saveZ = (move.moveDest.z-physicsObj.GetOrigin().z);
}
}
move.moveCommand = MOVE_NONE;
move.moveStatus = status;
move.toAreaNum = 0;
move.goalEntity = NULL;
move.moveDest = physicsObj.GetOrigin();
move.moveDest.z += saveZ;
move.startTime = gameLocal.time;
move.duration = 0;
move.range = 0.0f;
move.speed = 0.0f;
move.anim = 0;
move.moveDir.Zero();
move.lastMoveOrigin.Zero();
move.lastMoveTime = gameLocal.time;
// Callback for handling stopping
if ( oldCommand != MOVE_NONE ) {
OnStopMoving ( oldCommand );
}
}
/*
=====================
idAI::MoveToTether
=====================
*/
bool idAI::MoveToTether ( rvAITether* tether ) {
aasGoal_t goal;
// find a goal using the currently active tether
if ( !aas || !tether ) {
return false;
}
if ( !tether->FindGoal ( this, goal ) ) {
//This is extremely bad - if 2 guys both try to get to the center of a tether, they get hosed.
return MoveTo ( tether->GetPhysics()->GetOrigin ( ), tether->GetOriginReachedRange() );
}
if ( move.moveType == MOVETYPE_FLY ) {
//Hmm... we shouldn't have to clamp as the area wouldn't have been valid unless the tether was in its bounds and height, but...?
//float areaTop = aas->AreaCeiling( goal.areaNum );
goal.origin.z = tether->GetPhysics()->GetOrigin().z;
}
return StartMove ( MOVE_TO_TETHER, goal.origin, goal.areaNum, NULL, NULL, AI_TETHER_MINRANGE );
}
/*
=====================
idAI::MoveToAttack
=====================
*/
bool idAI::MoveToAttack ( idEntity *ent, int attack_anim ) {
aasObstacle_t obstacle;
aasGoal_t goal;
idBounds bounds;
idVec3 pos;
if ( !aas || !ent ) {
return false;
}
const idVec3 &org = physicsObj.GetOrigin();
obstacle.absBounds = ent->GetPhysics()->GetAbsBounds();
pos = LastKnownPosition ( ent );
// if we havent started a find yet or the current find is something other than a attack find then start a new one
if ( !aasFind || !dynamic_cast<rvAASFindGoalForAttack*>(aasFind) ) {
// Allocate the new aas find
delete aasFind;
aasFind = new rvAASFindGoalForAttack ( this );
// Find a goal that gives us a viable attack position on our enemy
aas->FindNearestGoal( goal, PointReachableAreaNum( org ), org, pos, move.travelFlags, IsTethered()?0:move.searchRange[0], move.searchRange[1], &obstacle, 1, *aasFind );
}
assert ( aasFind );
// Test some more points with the existing find
rvAASFindGoalForAttack* aasFindAttack = static_cast<rvAASFindGoalForAttack*>(aasFind);
if ( !aasFindAttack->TestCachedGoals ( aifl.simpleThink ? 1 : 4, goal ) ) {
delete aasFind;
aasFind = NULL;
return false;
}
// Havent found a goal yet but exhaused our trace count
if ( !goal.areaNum ) {
return false;
}
// Dont need the find anymore
delete aasFind;
aasFind = NULL;
if ( move.moveType == MOVETYPE_FLY ) {
//float up above the ground?
if ( aas && aas->GetFile() ) {
//we have AAS
float areaTop = aas->AreaCeiling(goal.areaNum)-GetPhysics()->GetBounds()[1][2];
if ( pos.z > areaTop ) {
goal.origin.z = areaTop;
} else if ( pos.z > goal.origin.z+1.0f ) {
goal.origin.z = pos.z;
} else if ( move.fly_offset > 0 ) {
if ( (goal.origin.z+move.fly_offset) > areaTop ) {
goal.origin.z = areaTop;
} else {
goal.origin.z += move.fly_offset;
}
}
}
}
return StartMove ( MOVE_TO_ATTACK, goal.origin, goal.areaNum, ent, NULL, move.attackPositionRange?move.attackPositionRange:8.0f );
}
/*
=====================
idAI::MoveToEnemy
=====================
*/
bool idAI::MoveToEnemy( void ) {
int areaNum;
aasPath_t path;
idVec3 pos;
if ( !enemy.ent ) {
return false;
}
// pos = LastKnownPosition ( enemy.ent );
pos = enemy.ent->GetPhysics()->GetOrigin ( );
// If we are already moving to the entity and its position hasnt changed then we are done
if ( move.moveCommand == MOVE_TO_ENEMY && move.goalEntity == enemy.ent && move.goalEntityOrigin == pos ) {
return true;
}
// Early out if we are already there.
if ( ReachedPos( pos, MOVE_TO_ENEMY, 8.0f ) ) {
StopMove( MOVE_STATUS_DONE );
return true;
}
// See if its posible to get where we want to go
areaNum = 0;
if ( aas ) {
areaNum = PointReachableAreaNum( pos );
aas->PushPointIntoAreaNum( areaNum, pos );
if ( !PathToGoal( path, PointReachableAreaNum( physicsObj.GetOrigin() ), physicsObj.GetOrigin(), areaNum, pos ) ) {
return false;
}
}
move.goalEntityOrigin = pos;
if ( move.moveType == MOVETYPE_FLY ) {
//float up above the ground?
if ( aas && aas->GetFile() ) {
//we have AAS
float areaTop = aas->AreaCeiling(areaNum)-GetPhysics()->GetBounds()[1][2];
if ( move.fly_offset > 0 ) {
if ( (pos.z+move.fly_offset) > areaTop ) {
pos.z = areaTop;
move.goalEntityOrigin.z = areaTop;
} else {
pos.z += move.fly_offset;
move.goalEntityOrigin.z += move.fly_offset;
}
}
}
}
// If we are already moving towards the given enemy then we have updated enough
if ( move.moveCommand == MOVE_TO_ENEMY && move.goalEntity == enemy.ent ) {
move.moveDest = pos;
move.toAreaNum = areaNum;
return true;
}
return StartMove ( MOVE_TO_ENEMY, pos, areaNum, enemy.ent, NULL, 8.0f );
}
/*
=====================
idAI::MoveToEntity
=====================
*/
bool idAI::MoveToEntity( idEntity *ent, float range ) {
int areaNum;
aasPath_t path;
idVec3 pos;
if ( !ent ) {
return false;
}
// Where do we want to go?
pos = ent->GetPhysics()->GetOrigin();
// If we are already moving to the entity and its position hasnt changed then we are done
if ( move.moveCommand == MOVE_TO_ENTITY && move.goalEntity == ent && move.goalEntityOrigin == pos ) {
return true;
}
// If we arent flying we should move to a position on the floor
if ( move.moveType != MOVETYPE_FLY ) {
ent->GetFloorPos( 64.0f, pos );
}
// Early out if we are already there.
if ( ReachedPos( pos, MOVE_TO_ENTITY, range ) ) {
StopMove( MOVE_STATUS_DONE );
return true;
}
// See if its posible to get where we want to go
areaNum = 0;
if ( aas ) {
areaNum = PointReachableAreaNum( pos );
aas->PushPointIntoAreaNum( areaNum, pos );
if ( !PathToGoal( path, PointReachableAreaNum( physicsObj.GetOrigin() ), physicsObj.GetOrigin(), areaNum, pos ) ) {
return false;
}
}
move.goalEntityOrigin = ent->GetPhysics()->GetOrigin();
// If we are already moving towards the given enemy then we have updated enough
if ( move.moveCommand == MOVE_TO_ENTITY && move.goalEntity == ent ) {
move.moveDest = pos;
move.range = range <= 0.0f ? 8.0f : range;
move.toAreaNum = areaNum;
return true;
}
return StartMove ( MOVE_TO_ENTITY, pos, areaNum, ent, NULL, range <= 0.0f ? 8.0f : range );
}
/*
=====================
idAI::MoveOutOfRange
=====================
*/
bool idAI::MoveOutOfRange( idEntity *ent, float range, float minRange ) {
aasObstacle_t obstacle;
aasGoal_t goal;
idBounds bounds;
idVec3 pos;
int obstacles;
if ( !aas || !ent ) {
return false;
}
const idVec3 &org = physicsObj.GetOrigin();
// consider the entity the monster is getting close to as an obstacle
if ( ent != this ) {
obstacles = 1;
obstacle.absBounds = ent->GetPhysics()->GetAbsBounds();
} else {
obstacles = 0;
}
pos = LastKnownPosition ( ent );
// Find a goal out of range of where we are
rvAASFindGoalOutOfRange findGoal( this );
if ( !aas->FindNearestGoal( goal, PointReachableAreaNum( org ), org, pos, move.travelFlags, minRange, range, &obstacle, obstacles, findGoal ) ) {
return false;
}
return StartMove ( MOVE_OUT_OF_RANGE, goal.origin, goal.areaNum, ent, NULL, 8.0f );
}
/*
=====================
idAI::MoveTo
=====================
*/
bool idAI::MoveTo ( const idVec3 &pos, float range ) {
idVec3 org;
int areaNum;
aasPath_t path;
if ( !aas ) {
return false;
}
org = pos;
areaNum = PointReachableAreaNum( org );
if ( !areaNum ) {
return false;
}
// Can we get to where we want to go?
aas->PushPointIntoAreaNum( areaNum, org );
if ( !PathToGoal( path, areaNum, physicsObj.GetOrigin(), PointReachableAreaNum( physicsObj.GetOrigin() ), org ) ) {
return false;
}
// Start moving
return StartMove ( MOVE_TO_POSITION, org, areaNum, NULL, NULL, range );
}
/*
=====================
idAI::MoveToCover
=====================
*/
bool idAI::MoveToCover( float minRange, float maxRange, aiTactical_t coverType ) {
idVec3 org;
int areaNum;
aasPath_t path;
aasFeature_t* feature = 0;
idVec3 featureOrigin;
if ( !aas ) {
return false;
}
// Look for nearby cover
switch ( coverType ) {
case AITACTICAL_HIDE: aasSensor->SearchHide(); break;
case AITACTICAL_COVER_FLANK: aasSensor->SearchFlank(); break;
case AITACTICAL_COVER_ADVANCE: aasSensor->SearchAdvance(); break;
case AITACTICAL_COVER_RETREAT: aasSensor->SearchRetreat(); break;
case AITACTICAL_COVER_AMBUSH: aasSensor->SearchAmbush(); break;
case AITACTICAL_COVER:
default:
aasSensor->SearchCover();
break;
}
if ( !aasSensor->FeatureCount ( ) ) {
return false;
}
feature = aasSensor->Feature ( 0 );
featureOrigin = feature->Origin ( );
org = featureOrigin;
areaNum = 0;
// Find the aas area our cover point is in
areaNum = PointReachableAreaNum( org );
if ( !areaNum ) {
return false;
}
// See if there is a path to our goal or not
aas->PushPointIntoAreaNum( areaNum, org );
/*
if ( !PathToGoal( path, PointReachableAreaNum( physicsObj.GetOrigin() ), physicsObj.GetOrigin(), areaNum, org ) ) {
return false;
}
*/
combat.coverValidTime = gameLocal.time;
// Start the move
return StartMove ( MOVE_TO_COVER, org, areaNum, enemy.ent, feature, AI_COVER_MINRANGE / 2.0f );
}
/*
=====================
idAI::MoveToHide
=====================
*/
bool idAI::MoveToHide ( void ) {
aasObstacle_t obstacle;
aasGoal_t goal;
idBounds bounds;
idVec3 pos;
// Need an enemy to hide from
if ( !aas || !enemy.ent ) {
return false;
}
const idVec3& org = physicsObj.GetOrigin();
obstacle.absBounds = enemy.ent->GetPhysics()->GetAbsBounds();
pos = LastKnownPosition ( enemy.ent );
// Search aas for a suitable goal
rvAASFindGoalForHide findGoal( pos );
if ( !aas->FindNearestGoal( goal, PointReachableAreaNum( org ), org, pos, move.travelFlags, 0.0f, 0.0f, &obstacle, 1, findGoal ) ) {
return false;
}
// Start the movement
return StartMove ( MOVE_TO_HIDE, goal.origin, goal.areaNum, enemy.ent, NULL, 0.0f );
}
/*
=====================
idAI::SlideToPosition
=====================
*/
bool idAI::SlideToPosition( const idVec3 &pos, float time ) {
StopMove( MOVE_STATUS_DONE );
move.moveDest = pos;
move.goalEntity = NULL;
move.moveCommand = MOVE_SLIDE_TO_POSITION;
move.moveStatus = MOVE_STATUS_MOVING;
move.startTime = gameLocal.time;
move.duration = idPhysics::SnapTimeToPhysicsFrame( SEC2MS( time ) );
move.fl.done = false;
move.fl.goalUnreachable = false;
move.fl.moving = false;
aifl.simpleThink = false;
move.fl.allowAnimMove = false;
if ( move.duration > 0 ) {
move.moveDir = ( pos - physicsObj.GetOrigin() ) / MS2SEC( move.duration );
if ( move.moveType != MOVETYPE_FLY ) {
move.moveDir.z = 0.0f;
}
move.speed = move.moveDir.LengthFast();
}
return true;
}
/*
=====================
idAI::WanderAround
=====================
*/
bool idAI::WanderAround( void ) {
idVec3 dest;
StopMove( MOVE_STATUS_DONE );
dest = physicsObj.GetOrigin() + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 256.0f;
if ( !NewWanderDir( dest ) ) {
StopMove( MOVE_STATUS_DEST_UNREACHABLE );
move.fl.goalUnreachable = true;
return false;
}
return StartMove ( MOVE_WANDER, dest, 0, NULL, NULL, 0.0f );
}
/*
================
idAI::StepDirection
================
*/
bool idAI::StepDirection( float dir ) {
predictedPath_t path;
idVec3 org;
move.wanderYaw = dir;
move.moveDir = idAngles( 0, move.wanderYaw, 0 ).ToForward();
org = physicsObj.GetOrigin();
idAI::PredictPath( this, aas, org, move.moveDir * 48.0f, 1000, 1000, ( move.moveType == MOVETYPE_FLY ) ? SE_BLOCKED : ( SE_ENTER_OBSTACLE | SE_BLOCKED | SE_ENTER_LEDGE_AREA ), path );
if ( path.blockingEntity && ( ( move.moveCommand == MOVE_TO_ENEMY ) || ( move.moveCommand == MOVE_TO_ENTITY ) ) && ( path.blockingEntity == move.goalEntity.GetEntity() ) ) {
// don't report being blocked if we ran into our goal entity
return true;
}
if ( ( move.moveType == MOVETYPE_FLY ) && ( path.endEvent == SE_BLOCKED ) ) {
float z;
move.moveDir = path.endVelocity * 1.0f / 48.0f;
// trace down to the floor and see if we can go forward
idAI::PredictPath( this, aas, org, idVec3( 0.0f, 0.0f, -1024.0f ), 1000, 1000, SE_BLOCKED, path );
idVec3 floorPos = path.endPos;
idAI::PredictPath( this, aas, floorPos, move.moveDir * 48.0f, 1000, 1000, SE_BLOCKED, path );
if ( !path.endEvent ) {
move.moveDir.z = -1.0f;
return true;
}
// trace up to see if we can go over something and go forward
idAI::PredictPath( this, aas, org, idVec3( 0.0f, 0.0f, 256.0f ), 1000, 1000, SE_BLOCKED, path );
idVec3 ceilingPos = path.endPos;
for( z = org.z; z <= ceilingPos.z + 64.0f; z += 64.0f ) {
idVec3 start;
if ( z <= ceilingPos.z ) {
start.x = org.x;
start.y = org.y;
start.z = z;
} else {
start = ceilingPos;
}
idAI::PredictPath( this, aas, start, move.moveDir * 48.0f, 1000, 1000, SE_BLOCKED, path );
if ( !path.endEvent ) {
move.moveDir.z = 1.0f;
return true;
}
}
return false;
}
return ( path.endEvent == 0 );
}
/*
================
idAI::NewWanderDir
================
*/
bool idAI::NewWanderDir( const idVec3 &dest ) {
float deltax, deltay;
float d[ 3 ];
float tdir, olddir, turnaround;
move.nextWanderTime = gameLocal.time + ( gameLocal.random.RandomFloat() * 500 + 500 );
olddir = idMath::AngleNormalize360( ( int )( move.current_yaw / 45 ) * 45 );
turnaround = idMath::AngleNormalize360( olddir - 180 );
idVec3 org = physicsObj.GetOrigin();
deltax = dest.x - org.x;
deltay = dest.y - org.y;
if ( deltax > 10 ) {
d[ 1 ]= 0;
} else if ( deltax < -10 ) {
d[ 1 ] = 180;
} else {
d[ 1 ] = DI_NODIR;
}
if ( deltay < -10 ) {
d[ 2 ] = 270;
} else if ( deltay > 10 ) {
d[ 2 ] = 90;
} else {
d[ 2 ] = DI_NODIR;
}
// try direct route
if ( d[ 1 ] != DI_NODIR && d[ 2 ] != DI_NODIR ) {
if ( d[ 1 ] == 0 ) {
tdir = d[ 2 ] == 90 ? 45 : 315;
} else {
tdir = d[ 2 ] == 90 ? 135 : 215;
}
if ( tdir != turnaround && StepDirection( tdir ) ) {
return true;
}
}
// try other directions
if ( ( gameLocal.random.RandomInt() & 1 ) || abs( deltay ) > abs( deltax ) ) {
tdir = d[ 1 ];
d[ 1 ] = d[ 2 ];
d[ 2 ] = tdir;
}
if ( d[ 1 ] != DI_NODIR && d[ 1 ] != turnaround && StepDirection( d[1] ) ) {
return true;
}
if ( d[ 2 ] != DI_NODIR && d[ 2 ] != turnaround && StepDirection( d[ 2 ] ) ) {
return true;
}
// there is no direct path to the player, so pick another direction
if ( olddir != DI_NODIR && StepDirection( olddir ) ) {
return true;
}
// randomly determine direction of search
if ( gameLocal.random.RandomInt() & 1 ) {
for( tdir = 0; tdir <= 315; tdir += 45 ) {
if ( tdir != turnaround && StepDirection( tdir ) ) {
return true;
}
}
} else {
for ( tdir = 315; tdir >= 0; tdir -= 45 ) {
if ( tdir != turnaround && StepDirection( tdir ) ) {
return true;
}
}
}
if ( turnaround != DI_NODIR && StepDirection( turnaround ) ) {
return true;
}
// can't move
StopMove( MOVE_STATUS_DEST_UNREACHABLE );
return false;
}
/*
=====================
idAI::GetMovePos
=====================
*/
bool idAI::GetMovePos( idVec3 &seekPos, idReachability** seekReach ) {
int areaNum;
aasPath_t path;
bool result;
idVec3 org;
org = physicsObj.GetOrigin();
seekPos = org;
// RAVEN BEGIN
// cdr: Alternate Routes Bug
if (seekReach) {
(*seekReach) = 0;
}
// RAVEN END
switch( move.moveCommand ) {
case MOVE_NONE :
seekPos = move.moveDest;
return false;
case MOVE_FACE_ENEMY :
case MOVE_FACE_ENTITY :
seekPos = move.moveDest;
return false;
case MOVE_TO_POSITION_DIRECT :
seekPos = move.moveDest;
if ( ReachedPos( move.moveDest, move.moveCommand ) ) {
StopMove( MOVE_STATUS_DONE );
}
return false;
case MOVE_SLIDE_TO_POSITION :
seekPos = org;
return false;
case MOVE_TO_ENTITY:
MoveToEntity( move.goalEntity.GetEntity(), move.range );
break;
case MOVE_TO_ENEMY:
if ( !MoveToEnemy() && combat.tacticalCurrent == AITACTICAL_MELEE ) {
StopMove( MOVE_STATUS_DEST_UNREACHABLE );
return false;
}
break;
}
if ( move.moveType == MOVETYPE_FLY && move.moveCommand >= NUM_NONMOVING_COMMANDS ) {
//flying
if ( DistanceTo( move.moveDest ) < 1024.0f ) {
//less than huge translation dist, which is actually 4096, but, just to be safe...
trace_t moveTrace;
gameLocal.Translation( this, moveTrace, org, move.moveDest, physicsObj.GetClipModel(), mat3_identity, MASK_MONSTERSOLID, this, move.goalEntity.GetEntity() );
if ( moveTrace.fraction >= 1.0f ) {
//can head straight for it, so do it.
seekPos = move.moveDest;
return false;
}
}
}
move.moveStatus = MOVE_STATUS_MOVING;
result = false;
if ( move.moveCommand == MOVE_WANDER ) {
move.moveDest = org + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 256.0f;
} else {
if ( ReachedPos( move.moveDest, move.moveCommand, move.range ) ) {
StopMove( MOVE_STATUS_DONE );
seekPos = org;
return false;
}
}
if ( aas && move.toAreaNum ) {
areaNum = PointReachableAreaNum( org );
if ( PathToGoal( path, areaNum, org, move.toAreaNum, move.moveDest ) ) {
seekPos = path.moveGoal;
if ( aas->GetFile() ) {
//we have AAS
if ( move.fly_offset > 0 ) {
if ( (seekPos-move.moveDest).LengthSqr() > 10.0f ) {
//not heading to final dest, that already has offset in it
float areaTop = aas->AreaCeiling(path.moveAreaNum)-GetPhysics()->GetBounds()[1][2];
if ( (seekPos.z+move.fly_offset) > areaTop ) {
seekPos.z = areaTop;
} else {
seekPos.z += move.fly_offset;
}
}
}
}
// RAVEN BEGIN
// cdr: Alternate Routes Bug
if (seekReach) {
(*seekReach) = (idReachability*)(path.reachability);
}
// RAVEN END
result = true;
move.nextWanderTime = 0;
} else {
move.fl.goalUnreachable = true;
}
}
if ( !result ) {
// wander around
if ( ( gameLocal.time > move.nextWanderTime ) || !StepDirection( move.wanderYaw ) ) {
result = NewWanderDir( move.moveDest );
if ( !result ) {
StopMove( MOVE_STATUS_DEST_UNREACHABLE );
move.fl.goalUnreachable = true;
seekPos = org;
return false;
}
} else {
result = true;
}
seekPos = org + move.moveDir * 2048.0f;
} else {
move.fl.goalUnreachable = false;
}
if ( DebugFilter(ai_debugMove) ) { // YELLOW = seekPos
gameRenderWorld->DebugLine( colorYellow, physicsObj.GetOrigin(), seekPos );
}
return result;
}
/*
=====================
idAI::BlockedFailSafe
=====================
*/
void idAI::BlockedFailSafe( void ) {
/* move.fl.blocked = false;
if ( !ai_blockedFailSafe.GetBool() || move.blockedRadius < 0.0f ) {
return;
}
if ( !physicsObj.OnGround() || enemy.GetEntity() == NULL ||
( physicsObj.GetOrigin() - move.lastMoveOrigin ).LengthSqr() > Square( move.blockedRadius ) ) {
move.lastMoveOrigin = physicsObj.GetOrigin();
move.lastMoveTime = gameLocal.time;
}
if ( move.lastMoveTime < gameLocal.time - move.blockedMoveTime ) {
if ( lastAttackTime < gameLocal.time - move.blockedAttackTime ) {
move.fl.blocked = true;
move.lastMoveTime = gameLocal.time;
}
}*/
}
/***********************************************************************
turning
***********************************************************************/
/*
=====================
idAI::TurnToward
=====================
*/
bool idAI::TurnToward( float yaw ) {
move.ideal_yaw = idMath::AngleNormalize180( yaw );
bool result = FacingIdeal();
return result;
}
/*
=====================
idAI::TurnToward
=====================
*/
bool idAI::TurnToward( const idVec3 &pos ) {
idVec3 dir;
idVec3 local_dir;
float lengthSqr;
dir = pos - physicsObj.GetOrigin();
physicsObj.GetGravityAxis().ProjectVector( dir, local_dir );
local_dir.z = 0.0f;
lengthSqr = local_dir.LengthSqr();
if ( lengthSqr > Square( 2.0f ) || ( lengthSqr > Square( 0.1f ) && enemy.ent == NULL ) ) {
move.ideal_yaw = idMath::AngleNormalize180( local_dir.ToYaw() );
}
return FacingIdeal();
}
/*
=====================
idAI::TurnTowardLeader
=====================
*/
bool idAI::TurnTowardLeader( bool faceLeaderByDefault ) {
if ( !leader.GetEntity() ) {
return false;
}
//see if there's a wall in the direction the player's looking
trace_t tr;
idVec3 leaderLookDest = leader->GetPhysics()->GetOrigin() + (( (leader.GetEntity() && leader.GetEntity()->IsType( idPlayer::GetClassType() )) ? ((idPlayer*)leader.GetEntity())->intentDir:leader->viewAxis[0])*300.0f);
idVec3 myLookDir = leaderLookDest-GetPhysics()->GetOrigin();
myLookDir.Normalize();
idVec3 start = GetPhysics()->GetOrigin();
start.z += EyeHeight();
idVec3 end = start + (myLookDir*128.0f);
end.z = start.z;
idVec3 currentLookDir = viewAxis[0];
currentLookDir.Normalize();
if ( !GetEnemy() && (!leader->IsType(idPlayer::GetClassType()) || !((idPlayer*)leader.GetEntity())->IsFlashlightOn()) ) {
//Not in combat and leader isn't looking around with flashlight
if ( myLookDir*currentLookDir > 0.666f ) {
//new dir isn't different enough from current dir for me to care
return true;
}
}
gameLocal.TracePoint( this, tr, start, end, MASK_OPAQUE, this );
idEntity* traceEnt = gameLocal.entities[ tr.c.entityNum ];
if ( tr.fraction < 1.0f
&& (tr.fraction<0.5f||!traceEnt||!traceEnt->IsType(idDoor::GetClassType())) ) {
//wall there - NOTE: okay to look at doors
if ( faceLeaderByDefault//want to face leader by default
|| leader->viewAxis[0].ToYaw() == move.ideal_yaw ) {//a wall must have moved in front of us?
//face the leader
return TurnToward( leader->GetPhysics()->GetOrigin() );
}
//just keep looking in the last valid dir
if ( FacingIdeal() ) {
//make sure it's still valid
idVec3 end = start + (viewAxis[0]*128.0f);
end.z = start.z;
gameLocal.TracePoint( this, tr, start, end, MASK_OPAQUE, this );
traceEnt = gameLocal.entities[ tr.c.entityNum ];
if ( tr.fraction < 1.0f
&& (tr.fraction<0.5f||!traceEnt||!traceEnt->IsType(idDoor::GetClassType())) ) {
//a wall right in front of us - NOTE: okay to look at doors
//face the leader
return TurnToward( leader->GetPhysics()->GetOrigin() );
}
}
return true;
}
//opening, face leader's dir
return TurnToward( myLookDir.ToYaw() );
}
/*
============
idAI::DirectionalTurnToward
Turn toward the given point using directional movement
============
*/
bool idAI::DirectionalTurnToward ( const idVec3 &pos ) {
static float moveDirOffset [ MOVEDIR_MAX ] = {
0.0f, 180.0f, -90.0f, 90.0f
};
// Issue standard TurnToward if we are not currently eligible for directional movement
if ( combat.tacticalCurrent != AITACTICAL_MOVE_PLAYERPUSH ) {
//always move directionally when getting out of the player's way
if( !combat.fl.aware || focusType < AIFOCUS_USE_DIRECTIONAL_MOVE || !move.fl.moving || move.moveCommand == MOVE_TO_ENEMY || !move.fl.allowDirectional ) {
move.idealDirection = MOVEDIR_FORWARD;
return TurnToward ( pos );
}
}
// Turn towards our desination
float moveYaw;
TurnToward ( pos );
moveYaw = move.ideal_yaw;
// Turn towards the goal entity and determine the angle difference
TurnToward ( currentFocusPos );
// Check for a direction change only when we can no longer see
// where we need to look and where we are looking is greater than 3/4ths the maximum look
if ( (pos - GetPhysics()->GetOrigin()).LengthFast ( ) > 8.0f )
if ( !FacingIdeal ( ) )
if ( fabs ( idMath::AngleNormalize180 ( move.ideal_yaw - move.current_yaw ) ) >= fabs(lookMax[YAW]) ||
fabs ( idMath::AngleNormalize180 ( idMath::AngleNormalize180 ( moveDirOffset[move.idealDirection] + moveYaw ) - move.current_yaw ) ) >= 80.0f ) {
float diffYaw;
diffYaw = idMath::AngleNormalize180 ( move.ideal_yaw - moveYaw );
if ( diffYaw > -45.0f && diffYaw < 45.0f ) {
move.idealDirection = MOVEDIR_FORWARD;
} else if ( diffYaw < -135.0f || diffYaw > 135.0f ) {
move.idealDirection = MOVEDIR_BACKWARD;
} else if ( diffYaw < 0.0f ) {
move.idealDirection = MOVEDIR_LEFT;
} else {
move.idealDirection = MOVEDIR_RIGHT;
}
}
return TurnToward( idMath::AngleNormalize180 ( moveDirOffset[move.idealDirection] + moveYaw ) );
}
/*
=====================
idAI::Turn
=====================
*/
void idAI::Turn( void ) {
float diff;
float diff2;
float turnAmount;
animFlags_t animflags;
// If cant turn or turning is disabled just bail
if ( !CanTurn ( ) ) {
return;
}
// check if the animator has marked this anim as non-turning
if ( !legsAnim.Disabled() && !legsAnim.AnimDone( 0 ) ) {
animflags = legsAnim.GetAnimFlags();
} else {
animflags = torsoAnim.GetAnimFlags();
}
if ( animflags.ai_no_turn ) {
return;
}
if ( move.anim_turn_angles && animflags.anim_turn ) {
idMat3 rotateAxis;
// set the blend between no turn and full turn
float frac = move.anim_turn_amount / move.anim_turn_angles;
animator.CurrentAnim( ANIMCHANNEL_LEGS )->SetSyncedAnimWeight( 0, 1.0f - frac );
animator.CurrentAnim( ANIMCHANNEL_LEGS )->SetSyncedAnimWeight( 1, frac );
animator.CurrentAnim( ANIMCHANNEL_TORSO )->SetSyncedAnimWeight( 0, 1.0f - frac );
animator.CurrentAnim( ANIMCHANNEL_TORSO )->SetSyncedAnimWeight( 1, frac );
// get the total rotation from the start of the anim
animator.GetDeltaRotation( 0, gameLocal.time, rotateAxis );
move.current_yaw = idMath::AngleNormalize180( move.anim_turn_yaw + rotateAxis[ 0 ].ToYaw() );
} else {
diff = idMath::AngleNormalize180( move.ideal_yaw - move.current_yaw );
if ( move.currentDirection != move.idealDirection ) {
move.turnVel += (AI_TURN_SCALE * 2.0f)* diff * MS2SEC( gameLocal.msec );
} else if ( move.fl.running ) {
move.turnVel += (AI_TURN_SCALE * 3.0f)* diff * MS2SEC( gameLocal.msec );
} else if ( move.fl.moving ) {
move.turnVel += (AI_TURN_SCALE * 1.5f)* diff * MS2SEC( gameLocal.msec );
} else {
move.turnVel += AI_TURN_SCALE * diff * MS2SEC( gameLocal.msec );
}
if ( move.turnVel > move.turnRate ) {
move.turnVel = move.turnRate;
} else if ( move.turnVel < -move.turnRate ) {
move.turnVel = -move.turnRate;
}
turnAmount = move.turnVel * MS2SEC( gameLocal.msec );
if ( ( diff >= 0.0f ) && ( turnAmount >= diff ) ) {
move.turnVel = diff / MS2SEC( gameLocal.msec );
turnAmount = diff;
} else if ( ( diff <= 0.0f ) && ( turnAmount <= diff ) ) {
move.turnVel = diff / MS2SEC( gameLocal.msec );
turnAmount = diff;
}
move.current_yaw = idMath::AngleNormalize180( move.current_yaw + turnAmount );
diff2 = idMath::AngleNormalize180( move.ideal_yaw - move.current_yaw );
if ( idMath::Fabs( diff2 ) < 0.1f ) {
move.current_yaw = move.ideal_yaw;
}
}
viewAxis = idAngles( 0, move.current_yaw, 0 ).ToMat3();
// if ( DebugFilter(ai_debugMove) ) { // RED = ideal_yaw, GREEN = current_yaw, YELLOW = current+velocity
// const idVec3 &org = physicsObj.GetOrigin();
// gameRenderWorld->DebugLine( colorRed, org, org + idAngles( 0, move.ideal_yaw, 0 ).ToForward() * 64, gameLocal.msec );
// gameRenderWorld->DebugLine( colorGreen, org, org + idAngles( 0, move.current_yaw, 0 ).ToForward() * 48, gameLocal.msec );
// gameRenderWorld->DebugLine( colorYellow, org, org + idAngles( 0, move.current_yaw + move.turnVel, 0 ).ToForward() * 32, gameLocal.msec );
// if ( move.anim_turn_angles && animflags.anim_turn ) {
// gameRenderWorld->DebugLine( colorOrange, org, org + idAngles( 0, move.anim_turn_yaw, 0 ).ToForward() * 32, gameLocal.msec );
// }
// }
}
/*
=====================
idAI::FacingIdeal
=====================
*/
bool idAI::FacingIdeal( void ) {
float diff;
if ( !move.turnRate ) {
return true;
}
diff = idMath::AngleDelta ( move.current_yaw, move.ideal_yaw );
if ( idMath::Fabs( diff ) < 0.01f ) {
// force it to be exact
move.current_yaw = move.ideal_yaw;
return true;
}
return false;
}
/*
================
idAI::AnimTurn
================
*/
void idAI::AnimTurn ( float angles, bool force ) {
move.turnVel = 0.0f;
move.anim_turn_angles = angles;
if ( angles ) {
move.anim_turn_yaw = move.current_yaw;
if ( force ) {
move.anim_turn_amount = angles;
} else {
move.anim_turn_amount = idMath::Fabs( idMath::AngleNormalize180( move.current_yaw - move.ideal_yaw ) );
if ( move.anim_turn_amount > move.anim_turn_angles ) {
move.anim_turn_amount = move.anim_turn_angles;
}
}
} else {
move.anim_turn_amount = 0.0f;
animator.CurrentAnim( ANIMCHANNEL_LEGS )->SetSyncedAnimWeight( 0, 1.0f );
animator.CurrentAnim( ANIMCHANNEL_LEGS )->SetSyncedAnimWeight( 1, 0.0f );
animator.CurrentAnim( ANIMCHANNEL_TORSO )->SetSyncedAnimWeight( 0, 1.0f );
animator.CurrentAnim( ANIMCHANNEL_TORSO )->SetSyncedAnimWeight( 1, 0.0f );
}
}
/***********************************************************************
Movement Helper Functions
***********************************************************************/
/*
================
idAI::ApplyImpulse
================
*/
void idAI::ApplyImpulse( idEntity *ent, int id, const idVec3 &point, const idVec3 &impulse, bool splash ) {
// FIXME: Jim take a look at this and see if this is a reasonable thing to do
// instead of a spawnArg flag.. Sabaoth is the only slide monster ( and should be the only one for D3 )
// and we don't want him taking physics impulses as it can knock him off the path
if ( move.moveType != MOVETYPE_STATIC && move.moveType != MOVETYPE_SLIDE ) {
idActor::ApplyImpulse( ent, id, point, impulse );
}
}
/*
=====================
idAI::GetAnimMoveDelta
=====================
*/
void idAI::GetAnimMoveDelta( const idMat3 &oldaxis, const idMat3 &axis, idVec3 &delta ) {
idVec3 oldModelOrigin;
idVec3 modelOrigin;
animator.GetDelta( gameLocal.time - gameLocal.msec, gameLocal.time, delta );
delta = axis * delta;
if ( modelOffset != vec3_zero ) {
// the pivot of the monster's model is around its origin, and not around the bounding
// box's origin, so we have to compensate for this when the model is offset so that
// the monster still appears to rotate around it's origin.
oldModelOrigin = modelOffset * oldaxis;
modelOrigin = modelOffset * axis;
delta += oldModelOrigin - modelOrigin;
}
delta *= physicsObj.GetGravityAxis();
}
/*
=====================
TestTeammateCollisions
=====================
*/
void TestTeammateCollisions(idAI* owner) {
idActor* teammate;
idVec3 teammateDirection;
idVec3 teammateVelocity;
float teammateDirectionDot;
float teammateDistance;
float teammateSpeed;
const idVec3& myOrigin = owner->GetPhysics()->GetOrigin();
const idBounds& myBounds = owner->GetPhysics()->GetBounds();
idVec3 myVelocity = owner->GetPhysics()->GetLinearVelocity();
float mySpeed = myVelocity.NormalizeFast();
// Don't Bother, We're Blocked Or Not Moving
//---------------------------------------------
if (owner->move.blockTime>gameLocal.GetTime() || !owner->move.fl.moving || mySpeed<1.0f) {
return;
}
for (teammate = aiManager.GetAllyTeam((aiTeam_t)owner->team); teammate; teammate = teammate->teamNode.Next()) {
if (teammate->fl.hidden || teammate == owner || teammate->health <= 0) {
continue;
}
// If On Same Floor
//------------------
teammateDirection = teammate->GetPhysics()->GetOrigin() - myOrigin;
if (fabsf(teammateDirection[2])>myBounds.Size().z) {
continue;
}
// If Close Enough
//-----------------
teammateDistance = teammateDirection.NormalizeFast();
if (teammateDistance>128.0f) {
continue;
}
// Completely Ignore Dudes Directly Behind Me
//--------------------------------------------
teammateDirectionDot = teammateDirection*myVelocity;
if (teammateDirectionDot<-0.5f) {
continue;
}
// Switch To Walk If I'm Heading For A Teammate
//----------------------------------------------
if (teammateDirectionDot>0.85f) {
owner->move.fl.obstacleInPath = true; // make him slow to a walk
if ( owner->DebugFilter(ai_debugMove) ) { // WHITE = Walk Teammate Near
gameRenderWorld->DebugArrow( colorWhite, myOrigin, teammate->GetPhysics()->GetOrigin(), 4, 250 );
}
}
if (teammateDistance<48.0f) {
teammateVelocity = teammate->GetPhysics()->GetLinearVelocity();
teammateSpeed = teammateVelocity.NormalizeFast();
if (teammateSpeed>50.0f) {
// If I'm Following Him, And I'm RIGHT Behind Him, Stop And Let Him Go A Bit Farther Ahead
//------------------------------------------------------------------------------------------
if (teammateDirectionDot>0.85f && teammateVelocity*myVelocity>0.4f) {
owner->move.blockTime = gameLocal.time + 1000; // Set Blocktime Timer
owner->move.fl.blocked = true;
if ( owner->DebugFilter(ai_debugMove) ) { // RED = Stop Teammate Near
gameRenderWorld->DebugArrow( colorRed, myOrigin, teammate->GetPhysics()->GetOrigin(), 8, 1000);
}
}
// Stop moving if my leader or a guy with a higher entity number is headed straight for me
//-----------------------------------------------------------------------------------------
else if (teammate->entityNumber<owner->entityNumber && teammateDirection*teammateVelocity<0.5f) {
owner->move.blockTime = gameLocal.time + 1500; // Set Blocktime Timer
owner->move.fl.blocked = true;
if ( owner->DebugFilter(ai_debugMove) ) { // RED = Stop Teammate Near
gameRenderWorld->DebugArrow( colorRed, myOrigin, teammate->GetPhysics()->GetOrigin(), 8, 1500);
}
}
}
}
}
}
/*
=====================
idAI::CheckObstacleAvoidance
=====================
*/
void idAI::CheckObstacleAvoidance( const idVec3 &goalPos, idVec3 &seekPos, idReachability* goalReach ) {
move.fl.blocked = false; // Makes Character Stop Moving
move.fl.obstacleInPath = false; // Makes Character Walk
move.obstacle = NULL;
seekPos = goalPos;
if (move.fl.ignoreObstacles) {
return;
}
if ( g_perfTest_aiNoObstacleAvoid.GetBool() ) {
return;
}
// Test For Path Around Obstacles
//--------------------------------
obstaclePath_t path;
move.fl.blocked = !FindPathAroundObstacles( &physicsObj, aas, move.moveCommand == MOVE_TO_ENEMY ? enemy.ent : NULL, physicsObj.GetOrigin(), goalPos, path );
move.fl.obstacleInPath = (path.firstObstacle || path.seekPosObstacle || path.startPosObstacle);
move.obstacle = (path.firstObstacle)?(path.firstObstacle):(path.seekPosObstacle);
seekPos = path.seekPos;
// Don't Worry About Obstacles Out Of Walk Range
//-----------------------------------------------
if (move.obstacle && DistanceTo(move.obstacle)>155.0f) {
move.fl.blocked = false;
move.fl.obstacleInPath = false;
move.obstacle = 0;
seekPos = goalPos;
}
// cdr: Alternate Routes Bug
// If An Obstacle Remains, And The Seek Pos Is Fairly Farr Off Of The Straight Line Path, Then Mark The Reach As Blocked
//-----------------------------------------------------------------------------------------------------------------------
if (move.fl.obstacleInPath && goalReach && !(goalReach->travelType&TFL_INVALID) && seekPos.Dist2XY(goalPos)>100.0f) {
float scale;
float dist;
dist = seekPos.DistToLineSeg(physicsObj.GetOrigin(), goalPos, scale);
if (scale<0.95f && dist>50.0f) {
aiManager.MarkReachBlocked(aas, goalReach, path.allObstacles);
}
}
TestTeammateCollisions(this);
if ( DebugFilter(ai_showObstacleAvoidance) ) {
gameRenderWorld->DebugLine( colorBlue, goalPos + idVec3( 1.0f, 1.0f, 0.0f ), goalPos + idVec3( 1.0f, 1.0f, 64.0f ), gameLocal.msec );
gameRenderWorld->DebugLine( !move.fl.blocked ? colorYellow : colorRed, path.seekPos, path.seekPos + idVec3( 0.0f, 0.0f, 64.0f ), gameLocal.msec );
}
}
/*
============
idAI::TestAnimMove
============
*/
bool idAI::TestAnimMove ( int animNum, idEntity *ignore, idVec3 *pMoveVec ) {
const idAnim* anim;
predictedPath_t path;
idVec3 moveVec;
anim = GetAnimator()->GetAnim ( animNum );
assert ( anim );
// moveVec = anim->TotalMovementDelta() * idAngles( 0.0f, move.ideal_yaw, 0.0f ).ToMat3() * physicsObj.GetGravityAxis();
moveVec = anim->TotalMovementDelta() * idAngles( 0.0f, move.current_yaw, 0.0f ).ToMat3() * physicsObj.GetGravityAxis();
idAI::PredictPath( this, aas, physicsObj.GetOrigin(), moveVec / MS2SEC( anim->Length() ), anim->Length(), 200, SE_BLOCKED | SE_ENTER_LEDGE_AREA, path, ignore );
if ( DebugFilter(ai_debugMove) ) { // TestAnimMove
gameRenderWorld->DebugLine( colorGreen, physicsObj.GetOrigin(), physicsObj.GetOrigin() + moveVec, anim->Length() );
gameRenderWorld->DebugBounds( path.endEvent == 0 ? colorYellow : colorRed, physicsObj.GetBounds(), physicsObj.GetOrigin() + moveVec, anim->Length() );
}
if ( pMoveVec ) {
*pMoveVec = moveVec;
}
return ( path.endEvent == 0 );
}
/*
=====================
Seek
=====================
*/
idVec3 Seek( idVec3 &vel, const idVec3 &org, const idVec3 &goal, float prediction ) {
idVec3 predictedPos;
idVec3 goalDelta;
idVec3 seekVel;
// predict our position
predictedPos = org + vel * prediction;
goalDelta = goal - predictedPos;
// goalDelta = goal - org;
seekVel = goalDelta * MS2SEC( gameLocal.msec );
return seekVel;
}
/***********************************************************************
Movement
***********************************************************************/
/*
=====================
idAI::DeadMove
=====================
*/
void idAI::DeadMove( void ) {
idVec3 delta;
monsterMoveResult_t moveResult;
DeathPush ( );
idVec3 org = physicsObj.GetOrigin();
GetAnimMoveDelta( viewAxis, viewAxis, delta );
physicsObj.SetDelta( delta );
RunPhysics();
moveResult = physicsObj.GetMoveResult();
move.fl.onGround = physicsObj.OnGround();
}
/*
=====================
idAI::AdjustFlyingAngles
=====================
*/
void idAI::AdjustFlyingAngles( void ) {
idVec3 vel;
float speed;
float roll;
float pitch;
vel = physicsObj.GetLinearVelocity();
speed = vel.Length();
if ( speed < 5.0f ) {
roll = 0.0f;
pitch = 0.0f;
} else {
roll = vel * viewAxis[ 1 ] * -move.fly_roll_scale / move.fly_speed;
if ( roll > move.fly_roll_max ) {
roll = move.fly_roll_max;
} else if ( roll < -move.fly_roll_max ) {
roll = -move.fly_roll_max;
}
pitch = vel * viewAxis[ 0 ] * -move.fly_pitch_scale / move.fly_speed;
if ( pitch > move.fly_pitch_max ) {
pitch = move.fly_pitch_max;
} else if ( pitch < -move.fly_pitch_max ) {
pitch = -move.fly_pitch_max;
}
}
move.fly_roll = move.fly_roll * 0.95f + roll * 0.05f;
move.fly_pitch = move.fly_pitch * 0.95f + pitch * 0.05f;
if ( move.flyTiltJoint != INVALID_JOINT ) {
animator.SetJointAxis( move.flyTiltJoint, JOINTMOD_WORLD, idAngles( move.fly_pitch, 0.0f, move.fly_roll ).ToMat3() );
} else {
viewAxis = idAngles( move.fly_pitch, move.current_yaw, move.fly_roll ).ToMat3();
}
}
/*
=====================
idAI::AddFlyBob
=====================
*/
void idAI::AddFlyBob( idVec3 &vel ) {
idVec3 fly_bob_add;
float t;
if ( move.fly_bob_strength ) {
t = MS2SEC( gameLocal.time + entityNumber * 497 );
fly_bob_add = ( viewAxis[ 1 ] * idMath::Sin16( t * move.fly_bob_horz ) + viewAxis[ 2 ] * idMath::Sin16( t * move.fly_bob_vert ) ) * move.fly_bob_strength;
vel += fly_bob_add * MS2SEC( gameLocal.msec );
if ( DebugFilter(ai_debugMove) ) { // FlyBob
const idVec3 &origin = physicsObj.GetOrigin();
gameRenderWorld->DebugArrow( colorOrange, origin, origin + fly_bob_add, 0 );
}
}
}
/*
=====================
idAI::AdjustFlyHeight
=====================
*/
void idAI::AdjustFlyHeight( idVec3 &vel, const idVec3 &goalPos ) {
const idVec3 &origin = physicsObj.GetOrigin();
predictedPath_t path;
idVec3 end;
idVec3 dest;
trace_t trace;
bool goLower;
// make sure we're not flying too high to get through doors
// FIXME: with move to enemy this thinks we're going to hit
// the enemy and then says we need to go lower... but that's not quite right...
// if we're about to hit our enemy, then maintaining our height should be desirable..?
goLower = false;
if ( origin.z > goalPos.z ) {
dest = goalPos;
dest.z = origin.z + 128.0f;
idAI::PredictPath( this, aas, goalPos, dest - origin, 1000, 1000, SE_BLOCKED, path, move.goalEntity.GetEntity() );
if ( path.endPos.z < origin.z ) {
//Hmm, should we make sure the path.endPos is high enough off the ground?
if ( move.fly_offset && (move.moveCommand == MOVE_TO_ENEMY || move.moveCommand == MOVE_TO_ATTACK) ) {
int pathArea = PointReachableAreaNum( path.endPos );
if ( aas && (aas->AreaFlags( pathArea )&AREA_FLOOR) ) {
path.endPos.z = aas->AreaBounds( pathArea )[0][2];
float areaTop = aas->AreaCeiling( pathArea ) - GetPhysics()->GetBounds()[1].z;
if ( path.endPos.z + move.fly_offset > areaTop ) {
path.endPos.z = areaTop;
} else {
path.endPos.z += move.fly_offset;
}
}
}
idVec3 addVel = Seek( vel, origin, path.endPos, AI_SEEK_PREDICTION );
vel.z += addVel.z;
goLower = true;
}
if ( DebugFilter(ai_debugMove) ) { // Fly Height
gameRenderWorld->DebugBounds( goLower ? colorRed : colorGreen, physicsObj.GetBounds(), path.endPos, gameLocal.msec );
}
}
if ( !goLower ) {
// make sure we don't fly too low
end = origin;
if ( move.moveCommand == MOVE_TO_ENEMY ) {
end.z = enemy.lastKnownPosition.z + move.fly_offset;
} else if ( move.moveCommand == MOVE_TO_ENTITY ) {
end.z = goalPos.z;
} else {
end.z = goalPos.z;// + move.fly_offset;
}
// RAVEN BEGIN
// ddynerman: multiple collision world
idVec3 cappedEnd = (end-origin);
if ( cappedEnd.LengthFast() > 1024.0f ) {
//don't do translation predictions over 1024
cappedEnd.Normalize();
cappedEnd *= 1024.0f;
}
cappedEnd += origin;
gameLocal.Translation( this, trace, origin, cappedEnd, physicsObj.GetClipModel(), mat3_identity, MASK_MONSTERSOLID, this );
// RAVEN END
vel += Seek( vel, origin, trace.endpos, AI_SEEK_PREDICTION );
}
}
/*
=====================
idAI::FlySeekGoal
=====================
*/
void idAI::FlySeekGoal( idVec3 &vel, idVec3 &goalPos ) {
idVec3 seekVel;
// seek the goal position
seekVel = Seek( vel, physicsObj.GetOrigin(), goalPos, AI_SEEK_PREDICTION );
seekVel *= move.fly_seek_scale;
//seekVel.Normalize();
//vel = seekVel*move.speed;
vel += seekVel;
}
/*
=====================
idAI::AdjustFlySpeed
=====================
*/
void idAI::AdjustFlySpeed( idVec3 &vel ) {
float goalSpeed;
// Slow down movespeed when we close to goal (this is similar to how AnimMove ai will
// switch to walking when they are close, it allows for more fine control of movement)
if ( move.walkRange > 0.0f ) {
float distSqr;
distSqr = (physicsObj.GetOrigin ( ) - move.moveDest).LengthSqr ( );
goalSpeed = move.speed * idMath::ClampFloat ( 0.1f, 1.0f, distSqr / Square ( move.walkRange ) );
} else {
goalSpeed = move.speed;
}
// apply dampening as long as we arent within the walk range
// if ( goalSpeed != move.speed) {
float speed;
vel -= vel * AI_FLY_DAMPENING * MS2SEC( gameLocal.msec );
// gradually speed up/slow down to desired speed
speed = vel.Normalize();
speed += ( goalSpeed - speed ) * MS2SEC( gameLocal.msec );
if ( speed < 0.0f ) {
speed = 0.0f;
} else if (goalSpeed && ( speed > goalSpeed ) ) {
speed = goalSpeed;
}
vel *= speed;
// } else {
// vel.Normalize ( );
// vel *= goalSpeed;
// }
}
/*
=====================
idAI::FlyTurn
=====================
*/
void idAI::FlyTurn( void ) {
if ( move.moveCommand == MOVE_FACE_ENEMY || ForceFaceEnemy() ) {
TurnToward( enemy.lastKnownPosition );
} else if ( ( move.moveCommand == MOVE_FACE_ENTITY ) && move.goalEntity.GetEntity() ) {
TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() );
} else if ( focusType != AIFOCUS_NONE && move.fl.allowDirectional ) {
DirectionalTurnToward ( currentFocusPos );
} else if ( move.speed > 0.0f ) {
const idVec3 &vel = physicsObj.GetLinearVelocity();
if ( vel.ToVec2().LengthSqr() > 0.1f ) {
TurnToward( vel.ToYaw() );
}
}
Turn();
}
/*
=====================
idAI::FlyMove
=====================
*/
void idAI::FlyMove( void ) {
idVec3 oldorigin;
idVec3 newDest;
move.fl.blocked = false;
if ( ( move.moveCommand >= NUM_NONMOVING_COMMANDS ) && ReachedPos( move.moveDest, move.moveCommand, move.range ) ) {
StopMove( MOVE_STATUS_DONE );
}
if ( DebugFilter(ai_debugMove) ) { // Fly Move
gameLocal.Printf( "%d: %s: %s, vel = %.2f, sp = %.2f, maxsp = %.2f\n", gameLocal.time, name.c_str(), aiMoveCommandString[ move.moveCommand ], physicsObj.GetLinearVelocity().Length(), move.speed, move.fly_speed );
}
// Dont move when movement is disabled
if ( !CanMove() || legsAnim.Disabled ( ) ) {
// Still allow turning though
FlyTurn ( );
if ( (aifl.action || aifl.scripted ) && legsAnim.Disabled () && move.fl.allowAnimMove ) {
idMat3 oldaxis = viewAxis;
idVec3 delta;
GetAnimMoveDelta( oldaxis, viewAxis, delta );
physicsObj.UseFlyMove( false );
if ( spawnArgs.GetBool( "alwaysBob" ) ) {
AddFlyBob( delta );
}
physicsObj.SetDelta( delta );
physicsObj.ForceDeltaMove( true );
RunPhysics();
} else if ( spawnArgs.GetBool( "alwaysBob" ) ) {
idVec3 vel = physicsObj.GetLinearVelocity();
AddFlyBob( vel );
physicsObj.SetLinearVelocity( vel );
// run the physics for this frame
oldorigin = physicsObj.GetOrigin();
physicsObj.UseFlyMove( true );
physicsObj.UseVelocityMove( false );
physicsObj.SetDelta( vec3_zero );
physicsObj.ForceDeltaMove( move.fl.noGravity );
RunPhysics();
}
UpdateAnimationControllers ( );
return;
}
if ( move.moveCommand != MOVE_TO_POSITION_DIRECT ) {
idVec3 vel = physicsObj.GetLinearVelocity();
if ( GetMovePos( move.seekPos ) ) {
CheckObstacleAvoidance( move.seekPos, newDest );
move.seekPos.x = newDest.x;
move.seekPos.y = newDest.y;
}
if ( move.speed ) {
FlySeekGoal( vel, move.seekPos );
}
// add in bobbing
AddFlyBob( vel );
if ( ( move.moveCommand != MOVE_TO_POSITION ) ) {
AdjustFlyHeight( vel, move.seekPos );
}
AdjustFlySpeed( vel );
vel += move.addVelocity;
move.addVelocity.Zero();
physicsObj.SetLinearVelocity( vel );
}
// turn
FlyTurn();
// run the physics for this frame
oldorigin = physicsObj.GetOrigin();
physicsObj.UseFlyMove( true );
physicsObj.UseVelocityMove( false );
physicsObj.SetDelta( vec3_zero );
physicsObj.ForceDeltaMove( move.fl.noGravity );
RunPhysics();
monsterMoveResult_t moveResult = physicsObj.GetMoveResult();
idEntity *blockEnt = physicsObj.GetSlideMoveEntity();
if ( blockEnt && blockEnt->IsType( idMoveable::GetClassType() ) && blockEnt->GetPhysics()->IsPushable() ) {
KickObstacles( viewAxis[ 0 ], move.kickForce, blockEnt );
} else if ( moveResult == MM_BLOCKED ) {
move.blockTime = gameLocal.time + 500;
move.fl.blocked = true;
}
idVec3 org = physicsObj.GetOrigin();
if ( oldorigin != org ) {
TouchTriggers();
}
if ( DebugFilter(ai_debugMove) ) { // Fly Move
gameRenderWorld->DebugLine( colorCyan, oldorigin, physicsObj.GetOrigin(), 4000 );
gameRenderWorld->DebugBounds( colorOrange, physicsObj.GetBounds(), org, gameLocal.msec );
gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), move.moveDest, gameLocal.msec );
gameRenderWorld->DebugLine( colorRed, org, org + physicsObj.GetLinearVelocity(), gameLocal.msec, true );
gameRenderWorld->DebugLine( colorBlue, org, move.seekPos, gameLocal.msec, true );
gameRenderWorld->DebugLine( colorYellow, GetEyePosition ( ), GetEyePosition ( ) + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 16.0f, gameLocal.msec, true );
DrawRoute();
}
}
/*
============
idAI::UpdatePlayback
============
*/
void idAI::UpdatePlayback ( idVec3 &goalPos, idVec3 &delta, idVec3 &oldorigin, idMat3 &oldaxis ) {
rvDeclPlaybackData pbd;
bool atDest;
// New playback stuff
if( !mPlayback.IsActive() ) {
return;
}
atDest = mPlayback.UpdateFrame( this, pbd );
goalPos = pbd.GetPosition();
SetOrigin( goalPos );
viewAxis = pbd.GetAngles().ToMat3();
// Keep the yaw updated
idVec3 local_dir;
physicsObj.GetGravityAxis().ProjectVector( viewAxis[ 0 ], local_dir );
move.current_yaw = local_dir.ToYaw();
move.ideal_yaw = idMath::AngleNormalize180( move.current_yaw );
OnUpdatePlayback ( pbd );
}
/*
============
idAI::
============
*/
void idAI::PlaybackMove( void ){
idVec3 goalPos;
idVec3 delta;
idVec3 goalDelta;
monsterMoveResult_t moveResult;
idVec3 newDest;
idVec3 oldorigin = physicsObj.GetOrigin();
idMat3 oldaxis = viewAxis;
move.fl.blocked = false;
move.obstacle = NULL;
goalPos = oldorigin;
UpdatePlayback( goalPos, delta, oldorigin, oldaxis );
if ( DebugFilter(ai_debugMove) ) { // Playback Move
gameRenderWorld->DebugLine( colorCyan, oldorigin, physicsObj.GetOrigin(), 5000 );
}
physicsObj.UseFlyMove( true );
physicsObj.UseVelocityMove( false );
physicsObj.SetLinearVelocity( vec3_zero );
physicsObj.SetDelta( vec3_zero );
physicsObj.ForceDeltaMove( true );
RunPhysics();
moveResult = physicsObj.GetMoveResult();
idEntity *blockEnt = physicsObj.GetSlideMoveEntity();
// RAVEN BEGIN
// jnewquist: Use accessor for static class type
if ( blockEnt && blockEnt->IsType( idMoveable::GetClassType() ) && blockEnt->GetPhysics()->IsPushable() ) {
// RAVEN END
KickObstacles( viewAxis[ 0 ], move.kickForce, blockEnt );
} else {
move.fl.blocked = true;
}
BlockedFailSafe();
move.fl.onGround = physicsObj.OnGround();
idVec3 org = physicsObj.GetOrigin();
if ( oldorigin != org ) {
TouchTriggers();
}
if ( DebugFilter(ai_debugMove) ) { // Playback Move
gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), org );
gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), move.moveDest );
gameRenderWorld->DebugLine( colorYellow, GetEyePosition(), GetEyePosition() + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 16.0f, gameLocal.msec, true );
DrawRoute();
}
}
/*
=====================
idAI::StaticMove
=====================
*/
void idAI::StaticMove( void ) {
idEntity* enemyEnt = enemy.ent;
if ( aifl.dead ) {
return;
}
if ( ( move.moveCommand == MOVE_FACE_ENEMY || ForceFaceEnemy() ) && enemyEnt ) {
TurnToward( enemy.lastKnownPosition );
} else if ( ( move.moveCommand == MOVE_FACE_ENTITY ) && move.goalEntity.GetEntity() ) {
TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() );
} else if ( move.moveCommand != MOVE_NONE ) {
TurnToward( move.moveDest );
}
Turn();
physicsObj.ForceDeltaMove( true ); // disable gravity
RunPhysics();
move.fl.onGround = false;
if ( DebugFilter(ai_debugMove) ) { // Static Move
const idVec3 &org = physicsObj.GetOrigin();
gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), org, gameLocal.msec );
gameRenderWorld->DebugLine( colorBlue, org, move.moveDest, gameLocal.msec, true );
gameRenderWorld->DebugLine( colorYellow, GetEyePosition(), GetEyePosition() + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 16.0f, gameLocal.msec, true );
}
}
/*
=====================
idAI::SlideMove
=====================
*/
void idAI::SlideMove( void ) {
idVec3 delta;
idVec3 goalDelta;
float goalDist;
monsterMoveResult_t moveResult;
idVec3 newDest;
idVec3 oldorigin = physicsObj.GetOrigin();
idMat3 oldaxis = viewAxis;
move.fl.blocked = false;
if ( move.moveCommand < NUM_NONMOVING_COMMANDS ){
move.lastMoveOrigin.Zero();
move.lastMoveTime = gameLocal.time;
}
move.obstacle = NULL;
if ( ( move.moveCommand == MOVE_FACE_ENEMY || ForceFaceEnemy() ) && enemy.ent ) {
TurnToward( enemy.lastKnownPosition );
move.seekPos = move.moveDest;
} else if ( ( move.moveCommand == MOVE_FACE_ENTITY ) && move.goalEntity.GetEntity() ) {
TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() );
move.seekPos = move.moveDest;
} else if ( GetMovePos( move.seekPos ) ) {
CheckObstacleAvoidance( move.seekPos, newDest );
TurnToward( newDest );
move.seekPos = newDest;
}
// FIXME: this stuff should really move to GetMovePos (Steering)
if ( move.moveCommand == MOVE_SLIDE_TO_POSITION ) {
if ( gameLocal.time < move.startTime + move.duration ) {
move.seekPos = move.moveDest - move.moveDir * MS2SEC( move.startTime + move.duration - gameLocal.time );
} else {
move.seekPos = move.moveDest;
move.fl.allowAnimMove = true;
move.fl.allowPrevAnimMove = false;
StopMove( MOVE_STATUS_DONE );
}
}
if ( move.moveCommand == MOVE_TO_POSITION ) {
goalDelta = move.moveDest - oldorigin;
goalDist = goalDelta.LengthFast();
if ( goalDist < delta.LengthFast() ) {
delta = goalDelta;
}
}
idVec3 vel = physicsObj.GetLinearVelocity();
float z = vel.z;
idVec3 predictedPos = oldorigin + vel * AI_SEEK_PREDICTION;
// seek the goal position
goalDelta = move.seekPos - predictedPos;
vel -= vel * AI_FLY_DAMPENING * MS2SEC( gameLocal.msec );
vel += goalDelta * MS2SEC( gameLocal.msec );
// cap our speed
vel.Truncate( move.fly_speed );
vel.z = z;
physicsObj.SetLinearVelocity( vel );
physicsObj.UseVelocityMove( true );
RunPhysics();
if ( ( move.moveCommand == MOVE_FACE_ENEMY || ForceFaceEnemy() ) && enemy.ent ) {
TurnToward( enemy.lastKnownPosition );
} else if ( ( move.moveCommand == MOVE_FACE_ENTITY ) && move.goalEntity.GetEntity() ) {
TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() );
} else {
if ( vel.ToVec2().LengthSqr() > 0.1f ) {
TurnToward( vel.ToYaw() );
}
}
Turn();
if ( DebugFilter(ai_debugMove) ) { // Slide Move
gameRenderWorld->DebugLine( colorCyan, oldorigin, physicsObj.GetOrigin(), 5000 );
}
moveResult = physicsObj.GetMoveResult();
idEntity *blockEnt = physicsObj.GetSlideMoveEntity();
// RAVEN BEGIN
// jnewquist: Use accessor for static class type
if ( blockEnt && blockEnt->IsType( idMoveable::GetClassType() ) && blockEnt->GetPhysics()->IsPushable() ) {
// RAVEN END
KickObstacles( viewAxis[ 0 ], move.kickForce, blockEnt );
}
BlockedFailSafe();
move.fl.onGround = physicsObj.OnGround();
idVec3 org = physicsObj.GetOrigin();
if ( oldorigin != org ) {
TouchTriggers();
}
if ( DebugFilter(ai_debugMove) ) { // SlideMove
gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), org, gameLocal.msec );
gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), move.moveDest, gameLocal.msec );
gameRenderWorld->DebugLine( colorYellow, GetEyePosition(), GetEyePosition() + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 16.0f, gameLocal.msec, true );
DrawRoute();
}
}
/*
=====================
idAI::AnimMove
=====================
*/
void idAI::AnimMove( void ) {
if ( ai_useRVMasterMove.GetBool ( ) ) {
RVMasterMove();
return;
}
idVec3 delta;
idVec3 goalDelta;
float goalDist;
monsterMoveResult_t moveResult;
idVec3 newDest;
// RAVEN BEGIN
// cdr: Alternate Routes Bug
idReachability* goalReach;
// RAVEN END
idVec3 oldorigin = physicsObj.GetOrigin();
idMat3 oldaxis = viewAxis;
if ( move.moveCommand < NUM_NONMOVING_COMMANDS ){
move.lastMoveOrigin.Zero();
//move.lastMoveTime = gameLocal.time;
}
move.obstacle = NULL;
if ( move.moveCommand == MOVE_FACE_ENEMY && enemy.ent ) {
TurnToward( enemy.lastKnownPosition );
move.goalPos = oldorigin;
move.seekPos = oldorigin;
} else if ( ( move.moveCommand == MOVE_FACE_ENTITY ) && move.goalEntity.GetEntity() ) {
TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() );
move.goalPos = oldorigin;
move.seekPos = oldorigin;
} else if ( move.moveCommand >= NUM_NONMOVING_COMMANDS ) {
if ( ReachedPos( move.moveDest, move.moveCommand, move.range ) ) {
StopMove( MOVE_STATUS_DONE );
} else {
move.moveStatus = MOVE_STATUS_MOVING;
// Otherwise, Update The Seek Pos
if ( !aifl.simpleThink && GetMovePos( move.goalPos, &goalReach ) ) {
if ( move.moveCommand != MOVE_WANDER ) {
CheckObstacleAvoidance( move.goalPos, move.seekPos, goalReach );
} else {
move.seekPos = move.goalPos;
}
DirectionalTurnToward ( move.seekPos );
}
}
}
Turn();
goalDelta = move.seekPos - oldorigin;
goalDist = goalDelta.LengthFast();
// FIXME: this stuff should really move to GetMovePos (Steering)
if ( move.moveCommand == MOVE_SLIDE_TO_POSITION ) {
if ( gameLocal.time < move.startTime + move.duration ) {
move.goalPos = move.moveDest - move.moveDir * MS2SEC( move.startTime + move.duration - gameLocal.time );
delta = move.goalPos - oldorigin;
delta.z = 0.0f;
} else {
delta = move.moveDest - oldorigin;
delta.z = 0.0f;
move.fl.allowAnimMove = true;
move.fl.allowPrevAnimMove = false;
StopMove( MOVE_STATUS_DONE );
}
} else if ( move.fl.allowAnimMove ) {
GetAnimMoveDelta( oldaxis, viewAxis, delta );
} else if ( move.fl.allowPrevAnimMove ) {
GetAnimMoveDelta( oldaxis, viewAxis, delta );
float speed = delta.LengthFast();
delta = goalDelta;
delta.Normalize();
delta *= speed;
} else {
delta.Zero();
}
if ( move.moveCommand > NUM_NONMOVING_COMMANDS ) {
//actually *trying* to move to a goal
if ( goalDist < delta.LengthFast() ) {
delta = goalDelta;
}
}
physicsObj.SetDelta( delta );
physicsObj.ForceDeltaMove( move.fl.noGravity );
RunPhysics();
moveResult = physicsObj.GetMoveResult();
idEntity *blockEnt = physicsObj.GetSlideMoveEntity();
// RAVEN BEGIN
// jnewquist: Use accessor for static class type
if ( blockEnt && blockEnt->IsType( idMoveable::GetClassType() ) && blockEnt->GetPhysics()->IsPushable() ) {
// RAVEN END
KickObstacles( viewAxis[ 0 ], move.kickForce, blockEnt );
}
BlockedFailSafe();
move.fl.onGround = physicsObj.OnGround();
const idVec3& org = physicsObj.GetOrigin();
if ( oldorigin != org ) {
TouchTriggers();
}
if ( DebugFilter(ai_debugMove) ) { // AnimMove : GREEN / RED Bounds & Move Dest
gameRenderWorld->DebugLine( colorCyan, oldorigin, org, 5000 );
gameRenderWorld->DebugBounds( (team==0)?(colorGreen):(colorRed), physicsObj.GetBounds(), org, gameLocal.msec );
if (!ReachedPos( move.moveDest, move.moveCommand, move.range )) {
gameRenderWorld->DebugBounds( (team==0)?(colorGreen):(colorRed), physicsObj.GetBounds(), move.moveDest, gameLocal.msec );
gameRenderWorld->DebugArrow( (team==0)?(colorGreen):(colorRed), org, move.moveDest, 4, gameLocal.msec );
}
DrawRoute();
}
}
/*
=====================
idAI::CustomMove
=====================
*/
void idAI::CustomMove( void ) {
// derived class must implement this
}
/*
======================================================================================
NEW MOVE CODE BEGIN
======================================================================================
*/
struct rvObstacle;
const float REACHED_RADIUS = 4.0f;
const float REACHED_RADIUS_SQUARE = REACHED_RADIUS*REACHED_RADIUS;
/*
=====================
LineIntersection2D
=====================
*/
bool LineIntersection2D(const idVec3& A, const idVec3& B, const idVec3& C, const idVec3& D, idVec3& contactPoint) {
// Test If Parallel
//------------------
float q = (((B.x-A.x)*(D.y-C.y))-((B.y-A.y)*(D.x-C.x)));
if (fabsf(q)<0.01f) {
return false;
}
// Test CD Edge
//--------------
float s = (((A.y-C.y)*(B.x-A.x))-((A.x-C.x)*(B.y-A.y))) / q;
if (s<0.0f) {
return false;
}
if (s>1.0f) {
return false;
}
// Test AB Edge
//--------------
float r = (((A.y-C.y)*(D.x-C.x))-((A.x-C.x)*(D.y-C.y))) / q;
if (r<0.0f) {
return false;
}
if (r>1.0f) {
return false;
}
contactPoint = A + r*(B-A);
return true;
}
/*
=====================
rvWindingBox
Clockwise winding with a simple line intersection test
[1]---->[2]
^ |
| verts |
| V
[0]<----[3]
=====================
*/
struct rvWindingBox {
idVec3 verts[4];
aasArea_t* areas[4];
rvObstacle* obstacles[4]; // CDR_TODO: Should be a list of obstacles for each vertex
/*
=====================
Initialize
=====================
*/
void Initialize() {
for (int i=0; i<4; i++) {
verts[i] = vec3_zero;
areas[i] = NULL;
obstacles[i] = NULL;
}
}
/*
=====================
FromBounds
=====================
*/
void FromBounds(const idBounds& b) {
verts[0].x = b[0].x;
verts[0].y = b[0].y;
verts[0].z = b[0].z;
verts[1].x = b[0].x;
verts[1].y = b[1].y;
verts[1].z = b[0].z;
verts[2].x = b[1].x;
verts[2].y = b[1].y;
verts[2].z = b[0].z;
verts[3].x = b[1].x;
verts[3].y = b[0].y;
verts[3].z = b[0].z;
}
/*
=====================
LineIntersection
=====================
*/
bool LineIntersection(const idVec3& start, const idVec3& end, idVec3& contactPoint, int& v1, int& v2) const {
for (int i=0; i<4; i++) {
v1 = i;
v2 = (i<3)?(i+1):(0);
if (start.IsLeftOf(verts[v1], verts[v2]) && LineIntersection2D(start, end, verts[v1], verts[v2], contactPoint)) {
return true;
}
}
return false;
}
/*
=====================
PointInside
=====================
*/
bool PointInside(const idVec3& point) const {
for (int i=0; i<4; i++) {
const idVec3& vert1 = verts[i];
const idVec3& vert2 = verts[(i<3)?(i+1):(0)];
if (point.IsLeftOf(vert1, vert2)) {
return false;
}
}
return true;
}
/*
=====================
DrawDebugGraphics
=====================
*/
bool DrawDebugGraphics() const {
for (int i=0; i<4; i++) {
const idVec3& vert1 = verts[i];
const idVec3& vert2 = verts[(i<3)?(i+1):(0)];
gameRenderWorld->DebugLine(colorYellow, vert1, vert2, gameLocal.msec);
if (!areas[i] || obstacles[i]) {
gameRenderWorld->DebugLine(colorRed, vert1, vert1+idVec3(0.0f,0.0f,16.0f), gameLocal.msec);
} else if (areas[i]) {
// gameRenderWorld->DebugLine(colorYellow, vert1, areas[i]->center, gameLocal.msec);
}
}
return true;
}
};
/*
=====================
rvMarker
A marker represents an obstacle within
an area. Any single obstacle can have
any number of markers in any number of
areas that it touches
=====================
*/
struct rvMarker {
rvObstacle* obstacle;
aasArea_t* area;
rvMarker* prev;
rvMarker* next;
};
rvPool<rvMarker, 255> markerPool;
/*
=====================
rvObstacle
=====================
*/
struct rvObstacle {
static idAASFile* searchFile;
static idBounds searchBounds;
entityPointer_t entity;
idVec3 origin;
idVec3 originFuture;
int lastTimeMoved;
bool pendingUpdate;
rvWindingBox windings[3];
idList<rvMarker*> markers;
idList<aasArea_t*> areas;
/*
============
Initialize
============
*/
void Initialize(idEntity* ent) {
entity = ent;
origin = vec3_zero;
originFuture = vec3_zero;
lastTimeMoved = 0;
pendingUpdate = true;
for (int i=0; i<3; i++) {
windings[i].Initialize();
}
markers.Clear();
areas.Clear();
}
/*
============
DrawDebugGraphics
============
*/
void DrawDebugGraphics() {
if ((gameLocal.time - lastTimeMoved) < 1000) {
gameRenderWorld->DebugArrow(colorBrown, origin, originFuture, 3, gameLocal.msec);
}
windings[0].DrawDebugGraphics();
}
/*
============
RemoveMarkers
============
*/
void RemoveMarkers() {
static rvMarker* marker;
for (int i=0; i<markers.Num(); i++) {
marker = markers[i];
assert(marker->obstacle==this);
if (marker->area->firstMarker==marker) {
marker->area->firstMarker = marker->next;
}
if (marker->prev) {
marker->prev->next = marker->next;
}
if (marker->next) {
marker->next->prev = marker->prev;
}
marker->obstacle = NULL;
markerPool.free(marker);
}
markers.Clear();
}
/*
============
SearchAreas_r
============
*/
void SearchAreas_r( int nodeNum ) {
int side;
const aasNode_t *node;
while( nodeNum != 0 ) {
// Negative nodeNum signifies a Leaf Area
if ( nodeNum < 0 ) {
if ( searchFile->GetArea(-nodeNum).flags&AREA_REACHABLE_WALK ) {
areas.AddUnique( &searchFile->GetArea(-nodeNum) );
}
break;
}
node = &searchFile->GetNode( nodeNum );
side = searchBounds.PlaneSide( searchFile->GetPlane( node->planeNum ) );
if ( side == PLANESIDE_BACK ) {
nodeNum = node->children[1];
}
else if ( side == PLANESIDE_FRONT ) {
nodeNum = node->children[0];
}
else {
SearchAreas_r( node->children[1] );
nodeNum = node->children[0];
}
}
}
/*
============
PointInsideArea
============
*/
bool PointInsideArea( const idVec3& point, aasArea_t* area ) {
int i, faceNum;
for ( i = 0; i < area->numFaces; i++ ) {
faceNum = searchFile->GetFaceIndex(area->firstFace + i);
const aasFace_t& face = searchFile->GetFace(abs( faceNum ));
if (!(face.flags & FACE_FLOOR)) {
const idPlane& plane = searchFile->GetPlane(face.planeNum ^ INTSIGNBITSET( faceNum ));
if (plane.Side(point) == PLANESIDE_BACK) {
return false;
}
}
}
return true;
}
/*
============
AddMarkers
============
*/
void AddMarkers() {
static rvMarker* marker;
for (int i=0; i<areas.Num() && !markerPool.full(); i++) {
// Allocate A Marker
//-------------------
marker = markerPool.alloc();
// assert(marker->obstacle==NULL);
// Setup The New Marker
//----------------------
marker->obstacle = this;
marker->area = areas[i];
marker->next = NULL;
marker->prev = NULL;
// Fixup Any Existing Linked List (First Marker)
//-----------------------------------------------
if (marker->area->firstMarker) {
marker->area->firstMarker->prev = marker;
marker->next = marker->area->firstMarker;
}
// Add This Marker At THe Area's Head
//------------------------------------
marker->area->firstMarker = marker;
markers.Append(marker);
}
}
/*
============
Update
============
*/
bool Update() {
static rvMarker* marker;
static rvObstacle* obstacle;
static aasArea_t* area;
static idVec3 expand;
static float speed;
static float distance;
static idVec3 direction;
static int aasFileNum, t, v, a;
static idList<rvObstacle*> touched;
static rvWindingBox* myWinding;
pendingUpdate = false;
idEntity* ent = entity.GetEntity();
if (!ent || ent->health<=0 || ent->fl.hidden || !ent->GetPhysics()) {
RemoveMarkers();
return false; // Means This Obstacle Structure Should Be Retired
}
// Only Update If We've Moved Far Enough
//---------------------------------------
idPhysics* physics = ent->GetPhysics();
if (origin.Dist2XY(physics->GetOrigin())<100.0f) { // CDR_TODO: Scale This By Distance To Player, Other Obstacles Near...
return true;
}
// Get The Obstacle's Seek Direction & Speed
//-------------------------------------------
if (ent->IsType(idAI::GetClassType())) {
speed = (((idAI*)ent)->move.fl.done)?(0.0f):(physics->GetLinearVelocity().LengthFast()) * 2.5f;
direction = (((idAI*)ent)->move.seekPos - physics->GetOrigin());
distance = direction.NormalizeFast();
// Cap The Projection To The Seek Position
if (speed > distance) {
speed = distance;
}
} else {
direction = physics->GetLinearVelocity();
speed = direction.NormalizeFast() * 2.5f;
}
origin = physics->GetOrigin();
originFuture = origin + (direction*speed); // 2.5 seconds into the future predicion
lastTimeMoved = gameLocal.time;
// Remove Old Markers
//--------------------
RemoveMarkers();
// Get The Entity's Bounds And Areas That These Bounds Cover
//-----------------------------------------------------------
for (aasFileNum=0; aasFileNum<gameLocal.GetNumAAS() && aasFileNum<3; aasFileNum++) {
if (!gameLocal.GetAAS(aasFileNum)) {
continue;
}
areas.Clear();
touched.Clear();
myWinding = &windings[aasFileNum];
searchFile = gameLocal.GetAAS(aasFileNum)->GetFile();
searchBounds = physics->GetAbsBounds();
expand = searchFile->GetSettings().boundingBoxes[0].Size();
expand[0] *= 0.5f;
expand[1] *= 0.5f;
expand[2] = 0;
expand[0] += REACHED_RADIUS;
expand[1] += REACHED_RADIUS;
searchBounds.ExpandSelf(expand);
SearchAreas_r(1);
// Setup The Winding From The Bounds
//-----------------------------------
myWinding->FromBounds(searchBounds);
// Setup Each Vertex On The Winding
//----------------------------------
for (v=0; v<4; v++) {
const idVec3& myVertex = myWinding->verts[v];
myWinding->areas[v] = NULL;
for (a=0; a<areas.Num(); a++) {
aasArea_t* area = areas[a];
if (PointInsideArea(myVertex, area)) {
myWinding->areas[v] = area;
// Search This Area For All Obstacles That May Contain This Vertex
//-----------------------------------------------------------------
obstacle = NULL;
for (marker=area->firstMarker; marker; marker=marker->next) {
if (marker->obstacle->windings[aasFileNum].PointInside(myVertex)) {
obstacle = marker->obstacle;
break;
}
}
// Update The Obstacle On The Winding
//------------------------------------
if (myWinding->obstacles[v] && myWinding->obstacles[v]!=obstacle) {
touched.AddUnique(myWinding->obstacles[v]);
}
if (obstacle) {
touched.AddUnique(obstacle);
}
myWinding->obstacles[v] = obstacle;
break;
}
}
}
// Touched Obstacles Need To Test Their Verts Against This Winding
//-----------------------------------------------------------------
for (t=0; t<touched.Num(); t++) {
obstacle = touched[t];
for (v=0; v<4; v++) {
if (myWinding->PointInside(obstacle->windings[aasFileNum].verts[v])) {
obstacle->windings[aasFileNum].obstacles[v] = this;
} else if (obstacle->windings[aasFileNum].obstacles[v]==this) {
obstacle->windings[aasFileNum].obstacles[v] = NULL;
}
}
}
// AddMarkers In Each Found Area
//-------------------------------
AddMarkers();
}
return true;
}
/*
=====================
VertexValid
=====================
*/
static bool VertexValid(const rvWindingBox& bounds, int v, const aasArea_t* inArea, const idEntity* ignore) {
return (bounds.areas[v] && (bounds.obstacles[v]==NULL || bounds.obstacles[v]->entity.GetEntity()==ignore));
}
/*
============
MovedRecently
============
*/
bool MovedRecently(int time=800) {
if ((gameLocal.time - lastTimeMoved) < time) {
return true;
}
// idEntity* ent = entity.GetEntity();
// if (ent && ent->IsType(idAI::GetClassType())) {
// return !((idAI*)ent)->move.fl.done;
// }
return false;
}
};
rvIndexPool<rvObstacle, 50, MAX_GENTITIES> obstaclePool;
idAASFile* rvObstacle::searchFile;
idBounds rvObstacle::searchBounds;
/*
=====================
rvObstacleFinder
=====================
*/
struct rvObstacleFinder {
idList<rvObstacle*> obstaclesPendingUpdate;
int obstaclesUpdateTime;
struct traceResult_t {
rvObstacle* obstacle;
idVec3 endPoint;
float distance;
int v1;
int v2;
bool v1Valid;
bool v2Valid;
};
traceResult_t contact;
/*
============
Initialize
============
*/
void Initialize() {
markerPool.clear();
obstaclePool.clear();
obstaclesPendingUpdate.Clear();
obstaclesUpdateTime = 0;
memset(&contact, 0, sizeof(contact));
// Clear All The Marker Pointers In Any Areas
//--------------------------------------------
for (int i=0; i<gameLocal.GetNumAAS(); i++) {
if (gameLocal.GetAAS(i)) {
idAASFile* file = gameLocal.GetAAS(i)->GetFile();
for (int a=0; a<file->GetNumAreas(); a++) {
file->GetArea(a).firstMarker = NULL;
}
}
}
}
/*
============
DrawDebugGraphics
============
*/
void DrawDebugGraphics() {
static int nextDrawTime=0;
static rvObstacle* obstacle;
if (nextDrawTime>=gameLocal.time) {
return;
}
nextDrawTime = gameLocal.time;
for (int i=0; i<MAX_GENTITIES; i++) {
if (obstaclePool.valid(i)) {
obstaclePool[i]->DrawDebugGraphics();
}
}
}
/*
============
UpdateObstacles
============
*/
void UpdateObstacles() {
static rvObstacle* obstacle;
static float entityNum;
if (obstaclesUpdateTime < gameLocal.time && obstaclesPendingUpdate.Num()) {
obstaclesUpdateTime = gameLocal.time + 50;
// CDR_TODO: Priority Queue Of Updates? Track Last Update Time Perhaps?
while (obstaclesPendingUpdate.Num()) {
obstacle = obstaclesPendingUpdate.StackTop();
obstaclesPendingUpdate.StackPop();
if (!obstacle->Update()) {
obstaclePool.free(obstacle->entity.GetEntityNum());
}
}
}
}
/*
============
MarkEntityForUpdate
============
*/
void MarkEntityForUpdate(idEntity* ent) {
// Ignore Non Physics Entities
//-----------------------------
if (!ent || !ent->GetPhysics()) {
return;
}
// Ignore Obstacles That Are Already Pending
//--------------------------------------------
if (obstaclePool.valid(ent->entityNumber) && obstaclePool[ent->entityNumber]->pendingUpdate) {
return;
}
if (!obstaclePool.valid(ent->entityNumber)) {
// If No More Obstacles Are Available, Ignore This One
//-----------------------------------------------------
if (obstaclePool.full()) {
return;
}
obstaclePool.alloc(ent->entityNumber)->Initialize(ent);
}
obstaclePool[ent->entityNumber]->pendingUpdate = true;
obstaclesPendingUpdate.Append(obstaclePool[ent->entityNumber]);
}
/*
============
RecordContact
============
*/
void RecordContact(float maxDistance, const idVec3& start, rvObstacle* obstacle, const idVec3& point, int vert1=-1, int vert2=-1) {
static float distance;
distance = point.DistXY(start);
if ((maxDistance==0.0f || distance<maxDistance) && (contact.distance > distance || !contact.obstacle)) {
contact.obstacle = obstacle;
contact.endPoint = point;
contact.distance = distance;
contact.v1 = vert1;
contact.v2 = vert2;
}
}
/*
============
RayTrace
============
*/
bool RayTrace(float maxDistance, const aasArea_t* area, const idVec3& start, const idVec3& stop, int aasNum, const idEntity* ignore1, const idEntity* ignore2=NULL, const idEntity* ignore3=NULL) {
static rvMarker* marker;
static idVec3 p;
static int v1;
static int v2;
static rvObstacle* ignoreA;
static rvObstacle* ignoreB;
static rvObstacle* ignoreC;
static idEntity* ent;
static idVec3 startBack;
static idVec3 direction;
static bool pulledBack;
ignoreA = (ignore1 && obstaclePool.valid(ignore1->entityNumber))?(obstaclePool[ignore1->entityNumber]):(NULL);
ignoreB = (ignore2 && obstaclePool.valid(ignore2->entityNumber))?(obstaclePool[ignore2->entityNumber]):(NULL);
ignoreC = (ignore3 && obstaclePool.valid(ignore3->entityNumber))?(obstaclePool[ignore3->entityNumber]):(NULL);
contact.obstacle = NULL;
pulledBack = false;
for (marker=area->firstMarker; marker; marker=marker->next) {
if (marker->obstacle==NULL || marker->obstacle==ignoreA || marker->obstacle==ignoreB || marker->obstacle==ignoreC) {
continue;
}
// Handle Moving Obstacles Differently
//-------------------------------------
if (marker->obstacle->MovedRecently()) {
// Ignore Moving Actors With Higher Entity Numbers
//-------------------------------------------------
ent = marker->obstacle->entity.GetEntity();
if (ent && ignore1 && ent->entityNumber>ignore1->entityNumber && ent->IsType(idActor::GetClassType())) {
continue;
}
// Pull The Start Back To Avoid Starting "In Solid"
//--------------------------------------------------
if (!pulledBack) {
pulledBack = true;
direction = stop - start;
direction.Normalize();
startBack = start - (direction*12.0f); // CDR_TODO: Use Radius
}
// Record Contact With The Moving Obstacle's Predicted Path
//----------------------------------------------------------
if (LineIntersection2D(startBack, stop, marker->obstacle->origin, marker->obstacle->originFuture, p)) {
RecordContact(maxDistance, startBack, marker->obstacle, p);
}
// Stationary Obstacles
//----------------------
} else {
// CDR_TODO: Special Line Intersection Test For Player Forward Aim And On Same Team
//----------------------------------------------------------------------------------
// Pull The Start Back To Avoid Starting "In Solid"
//--------------------------------------------------
if (!pulledBack) {
pulledBack = true;
direction = stop - start;
direction.Normalize();
startBack = start - (direction*12.0f); // CDR_TODO: Use Radius
}
// Record Contact With The Stationary Obstacle's Position
//--------------------------------------------------------
if (marker->obstacle->windings[aasNum].LineIntersection(startBack, stop, p, v1, v2)) {
RecordContact(maxDistance, startBack, marker->obstacle, p, v1, v2);
}
}
}
if (contact.obstacle) {
if (contact.obstacle->MovedRecently()) {
contact.v1Valid = true;
contact.v2Valid = true;
} else {
contact.v1Valid = rvObstacle::VertexValid(contact.obstacle->windings[aasNum], contact.v1, area, ignore1);
contact.v2Valid = rvObstacle::VertexValid(contact.obstacle->windings[aasNum], contact.v2, area, ignore1);
}
return true;
}
return false;
}
};
rvObstacleFinder obstacleFinder;
/*
=====================
rvPathFinder
=====================
*/
class rvPathFinder {
public:
/*
=====================
visitNode
=====================
*/
struct visitNode {
float costToGoal;
float travelCost;
bool closed;
int vertexNum;
idReachability* reach;
visitNode* from;
float cost() {
return travelCost + costToGoal;
}
};
static int visitSort( const void *a, const void *b ) {
return (int)( (*((visitNode**)b))->cost() - (*((visitNode**)a))->cost() );
}
enum {
MAX_VISITED = 512,
MAX_OPEN = 255,
MAX_PENDING = 60,
};
idAAS* myAAS;
float myRadius;
idMoveState* myMove;
int myTeam;
const idEntity* myIgnoreEntity;
const idEntity* myIgnoreEntity2;
bool drawVisitTree;
visitNode* next;
visitNode* open[MAX_OPEN];
int openCount;
bool openListUpdate;
visitNode* pending[MAX_PENDING];
visitNode* pendingBest;
int pendingCount;
visitNode visited[MAX_VISITED];
int visitedCount;
idHashIndex visitedIndexReach;
idHashIndex visitedIndexVert;
/*
=====================
Initialize
=====================
*/
void Initialize() {
openCount = 0;
openListUpdate = false;
pendingCount = 0;
pendingBest = NULL;
visitedCount = 0;
visitedIndexReach.Clear();
visitedIndexVert.Clear();
next = NULL;
}
/*
=====================
Close
=====================
*/
void Close(visitNode* node) {
node->closed = true;
}
/*
=====================
DrawVisitTree
=====================
*/
void DrawVisitTree() {
for (int i=0; i<visitedCount; i++) {
const idVec3& start = (visited[i].from)?(GetSeekPosition(visited[i].from)):(myMove->myPos);
const idVec3& stop = (GetSeekPosition(&visited[i]));
const idVec4& color = (visited[i].closed)?(colorOrange):(colorYellow);
const int duration = (visited[i].closed)?(3000):(500);
gameRenderWorld->DebugArrow(color, start, stop, 3, duration);
}
}
/*
=====================
XYLineIntersection
=====================
*/
bool XYLineIntersection(const idVec3& A, const idVec3& B, const idVec3& C, const idVec3& D, idVec3& P) {
float q = (((B.x-A.x)*(D.y-C.y))-((B.y-A.y)*(D.x-C.x)));
if (fabsf(q)>0.01f) {
float s = (((A.y-C.y)*(B.x-A.x))-((A.x-C.x)*(B.y-A.y))) / q;
if (s<0.0f || s>1.0f) {
return false;
}
float r = (((A.y-C.y)*(D.x-C.x))-((A.x-C.x)*(D.y-C.y))) / q;
if (r>1.0f) {
P = B;
return false;
}
if (r<0.0f) {
P = A;
return false;
}
P = A + r*(B-A);
return true;
}
// Lines Are Parallel, No Intersection
return false;
}
/*
=====================
GetSeekPosition
=====================
*/
const idVec3& GetSeekPosition(idReachability* reach, int vertexNum) {
return ((vertexNum)?(myAAS->GetFile()->GetVertex(vertexNum)):(reach->start));
}
/*
=====================
GetSeekPosition
=====================
*/
const idVec3& GetSeekPosition(visitNode* node) {
return GetSeekPosition(node->reach, node->vertexNum);
}
/*
=====================
Success
=====================
*/
bool Success(visitNode* node) {
static idVec3 edgeA;
static idVec3 edgeB;
static idVec3 intersect;
static idVec3 direction;
static idVec3 smoothedPos;
static int at;
static int count;
static rvMarker* marker;
static bool isCorner;
// Always Add The Goal Pos At The End Of The Path
//------------------------------------------------
myMove->path[myMove->pathLen].reach = NULL;
myMove->path[myMove->pathLen].seekPos = myMove->goalPos;
myMove->pathLen ++;
// Build The Path
//----------------
while (node && myMove->pathLen<MAX_PATH_LEN) {
myMove->path[myMove->pathLen].reach = node->reach;
myMove->path[myMove->pathLen].seekPos = GetSeekPosition(node);
myMove->pathLen ++;
node = node->from;
}
// Additional Path Point Modifications
//-------------------------------------
count = myMove->pathLen-1;
for (at=count; at>0; at--) {
pathSeek_t& pathPrev = myMove->path[at+1];
pathSeek_t& pathAt = myMove->path[at];
pathSeek_t& pathNext = myMove->path[at-1];
myAAS->GetEdge(pathAt.reach->edgeNum, edgeA, edgeB);
// Smooth The Path One Pass
//--------------------------
if (pathAt.reach->travelType==TFL_WALK) {
const idVec3& walkA = (at==count) ? (myMove->myPos) :(pathPrev.seekPos);
const idVec3& walkB = (at==0) ? (myMove->goalPos) :(pathNext.seekPos);
isCorner = !XYLineIntersection(edgeA, edgeB, walkA, walkB, smoothedPos);
// If The Smoothed Position Is Not Blocked By An Obstacle
//--------------------------------------------------------
aasArea_t* area = &myAAS->GetFile()->GetArea(pathAt.reach->toAreaNum);
for (marker=area->firstMarker; marker; marker=marker->next) {
if (!marker->obstacle || marker->obstacle->entity.GetEntity()==myIgnoreEntity || marker->obstacle->entity.GetEntity()==myIgnoreEntity2) {
continue;
}
if (marker->obstacle->windings[0/*CDR_TODO: use aasNum*/].PointInside(smoothedPos)) {
break;
}
}
if (!marker) {
pathAt.seekPos = smoothedPos;
// Push Away From The Corner A Bit
//---------------------------------
if (isCorner) {
smoothedPos.ProjectToLineSeg(walkA, walkB);
direction = pathAt.seekPos - smoothedPos;
direction.NormalizeFast();
pathAt.seekPos += direction * (REACHED_RADIUS+1.0f);
}
}
}
}
// CDR_TODO: Record Statistics Here
if (drawVisitTree) {
DrawVisitTree();
}
return true;
}
/*
=====================
Failure
=====================
*/
bool Failure() {
// CDR_TODO: Record Statistics Here
if (drawVisitTree) {
DrawVisitTree();
}
return false;
}
/*
=====================
ErrorCondition
=====================
*/
bool ErrorCondition() {
assert(0);
// Stats?
return false;
}
/*
=====================
SelectNextVisitedNode
=====================
*/
visitNode* SelectNextVisitedNode() {
if (pendingBest && (!openCount || open[openCount-1]->cost() > pendingBest->cost())) {
next = pendingBest;
pendingBest = NULL;
openListUpdate = true;
return next;
}
if (openCount) {
openCount--;
return open[openCount];
}
return NULL;
}
/*
=====================
Open
=====================
*/
void Open(visitNode* node) {
node->closed = false;
// If This Node Is Cheaper Than The Existing Best Open Node, Add It To The End Of The Open List
//----------------------------------------------------------------------------------------------
if (openCount<MAX_OPEN) {
if (!openCount || (node->cost() < open[openCount-1]->cost())) {
open[openCount] = node;
openCount++;
return;
}
}
// Otherwise, Add It To The Pending List
//---------------------------------------
if (pendingCount<MAX_PENDING) {
pending[pendingCount] = node;
pendingCount++;
if (!pendingBest || (node->cost() < pendingBest->cost())) {
pendingBest = node;
}
}
}
/*
=====================
UpdateOpenList
=====================
*/
void UpdateOpenList() {
// List Is Updated Now, So Remove The Flag
//-----------------------------------------
openListUpdate = false;
// Add All Pending Nodes To The Open List
//----------------------------------------
for (int i=0; i<pendingCount; i++) {
if (openCount<MAX_OPEN && pending[i] && !pending[i]->closed) {
open[openCount] = pending[i];
openCount ++;
}
}
pendingCount = 0;
pendingBest = NULL;
// Sort The Open List
//--------------------
qsort( (void*)open, (size_t)openCount, (size_t)sizeof(visitNode*), visitSort );
}
/*
=====================
WasVisited
=====================
*/
visitNode* WasVisited(idReachability* reach) {
for (int i=visitedIndexReach.First(abs(reach->edgeNum)); i>=0; i=visitedIndexReach.Next(i)) {
if (visited[i].reach==reach) {
return &visited[i];
}
}
return NULL;
}
/*
=====================
WasVisited
=====================
*/
visitNode* WasVisited(int vertexNum) {
for (int i=visitedIndexVert.First(vertexNum); i>=0; i=visitedIndexVert.Next(i)) {
if (visited[i].vertexNum==vertexNum) {
return &visited[i];
}
}
return NULL;
}
/*
=====================
TravelCost
=====================
*/
float TravelCost(idReachability* reach, int vertexNum, visitNode* from) {
static float distance;
const idVec3& start = (from)?(GetSeekPosition(from)):(myMove->myPos);
const idVec3& stop = GetSeekPosition(reach, vertexNum);
// Test For Any Obstacles In The Way
//-----------------------------------
const aasArea_t* area = &myAAS->GetFile()->GetArea((from)?(from->reach->toAreaNum):(myMove->myArea));
if (obstacleFinder.RayTrace(0.0f, area, start, stop, 0/*CDR_TODO: Get myAASNum*/, myIgnoreEntity, myIgnoreEntity2)) {
// If It Is Not Possible To Steer Around, Then This Edge Is Completely Invalid
//-----------------------------------------------------------------------------
if (!obstacleFinder.contact.v1Valid && !obstacleFinder.contact.v2Valid) {
return 0.0f;
}
// Completely Disable Any Points Completely Covered By An Obstacle
//-----------------------------------------------------------------
if (obstacleFinder.contact.obstacle->windings[0/*CDR_TODO: Get myAASNum*/].PointInside(stop)) {
return 0.0f;
}
distance += 128.0f;
}
// Compute Standard Distance
//---------------------------
distance = start.Dist(stop);
if (from) {
distance += from->travelCost + from->reach->travelTime;
}
return distance;
}
/*
=====================
Visit
=====================
*/
void Visit(idReachability* reach, int vertexNum, visitNode* from) {
static visitNode* visit;
static float travelCost;
// If Full, Stop Visiting Anything
//---------------------------------
if (visitedCount>=MAX_VISITED) {
return;
}
// Compute Travel Cost, And Test To See If This Edge Is Blocked
//--------------------------------------------------------------
travelCost = TravelCost(reach, vertexNum, from);
if (travelCost==0.0f) {
return;
}
// If The Visited Version Is Already Less Costly, Then Ignore This Reach
//-----------------------------------------------------------------------
visit = (vertexNum)?(WasVisited(vertexNum)):(WasVisited(reach));
if (visit && visit->travelCost<=travelCost) {
return;
}
// Reopen Any Nodes That Were Closed
//-----------------------------------
if (visit && visit->closed) {
Open(visit);
// Otherwise, If The Node Is Already In The Open List, Just Change The Cost And Mark The List For Resorting
//----------------------------------------------------------------------------------------------------------
} else if (visit) {
visit->from = from;
visit->travelCost = travelCost;
openListUpdate = true;
// Must Never Have Visited This Node Before, So Make A Whole New One
//-------------------------------------------------------------------
} else {
const idVec3& pos = GetSeekPosition(reach, vertexNum);
// Constant Data (Will Never Change)
//-----------------------------------
visit = &visited[visitedCount];
visit->reach = reach;
visit->vertexNum = vertexNum;
visit->costToGoal = pos.Dist(myMove->goalPos);
// Temporary Data
//----------------
visit->from = from;
visit->travelCost = travelCost;
visit->closed = false;
// Add It To The Hash Table To Be Found Later
//--------------------------------------------
if (!vertexNum) {
visitedIndexReach.Add(abs(reach->edgeNum), visitedCount);
} else {
visitedIndexVert.Add(vertexNum, visitedCount);
}
visitedCount++;
// Mark It As Open
//-----------------
Open(visit);
}
}
/*
=====================
VisitReach
This function first visits the center of the reachability, and then if
the edge is long enough and close enough to the start or end of the
path, it visits the verts as well
=====================
*/
void VisitReach(idReachability *reach, visitNode* from) {
static int verts[2];
static int vertexNum;
static idVec3 start;
static idVec3 stop;
// If Full, Stop Visiting Anything
//---------------------------------
if (visitedCount>=MAX_VISITED) {
return;
}
// Ignore Any Reach To The Area We Came From
//-------------------------------------------
if (from && from->reach->fromAreaNum==reach->toAreaNum) {
return;
}
// Ignore Any Reach That Does Not Match Our Travel Flags
//-------------------------------------------------------
if (reach->travelType&TFL_INVALID || !(reach->travelType&myMove->travelFlags)) {
return;
}
// Visit The Center Of The Reach
//-------------------------------
Visit(reach, 0, from);
// If Running Low On Visit Space, Stop Adding Verts
//--------------------------------------------------
if (visitedCount>=(int)((float)MAX_VISITED * 0.85f)) {
return;
}
// If Edge Is Far From Start and Goal, Don't Add Verts
//-----------------------------------------------------
if (!reach->fromAreaNum!=myMove->myArea &&
!reach->toAreaNum!=myMove->myArea &&
!reach->fromAreaNum!=myMove->goalArea &&
!reach->toAreaNum!=myMove->goalArea &&
reach->start.Dist2XY(myMove->myPos)>22500.0f/*(250*250)*/ && reach->start.Dist2XY(myMove->goalPos)>22500.0f/*(250*250)*/) {
return;
}
// If This Edge Is Small Enough, Just Skip The Verts
//---------------------------------------------------
myAAS->GetEdge(reach->edgeNum, start, stop);
if (start.Dist2XY(stop)<6400.0f/*(80*80)*/) {
return;
}
// Ok, So Visit The Verts Too
//----------------------------
myAAS->GetEdgeVertexNumbers(reach->edgeNum, verts);
for (int i=0; i<2 && visitedCount<MAX_VISITED; i++) {
Visit(reach, verts[i], from);
}
}
/*
=====================
VisitArea
=====================
*/
void VisitArea(int areaNum, visitNode* from=NULL) {
idReachability* reach;
const aasArea_t& area = myAAS->GetFile()->GetArea(areaNum);
// If Visiting From Another Node, Close That Node Now
//----------------------------------------------------
if (from) {
// If Already Closed
//-------------------
if (from->closed) {
if (openListUpdate) {
UpdateOpenList(); // this is kind of hacky...
}
// Don't Bother To Look At These Reaches
//---------------------------------------
return;
// Otherwise, Close It
//---------------------
} else {
Close(from);
}
}
// Ok, Iterate All Reachabilities Within This Area
//-------------------------------------------------
for (reach=area.reach; reach && visitedCount<MAX_VISITED; reach=reach->next) {
VisitReach(reach, from);
}
// Finally, If Necessary, Update The Open List Now
//-------------------------------------------------
if (openListUpdate) {
UpdateOpenList();
}
}
/*
=====================
FindPath
=====================
*/
bool FindPath(idAAS* aas, idMoveState& move, float radius, bool inDebugMode, idEntity* ignoreEntity, idEntity* ignoreEntity2) {
myAAS = aas;
myMove = &move;
myRadius = radius;
drawVisitTree = inDebugMode;
myIgnoreEntity = ignoreEntity;
myIgnoreEntity2 = ignoreEntity2;
myMove->pathArea = myMove->goalArea;
myMove->pathTime = gameLocal.GetTime();
myMove->pathLen = 0;
openCount = 0;
openListUpdate = false;
pendingCount = 0;
pendingBest = NULL;
visitedCount = 0;
visitedIndexReach.Clear();
visitedIndexVert.Clear();
if (!myMove->myArea) {
return ErrorCondition();
}
const aasArea_t* myArea = &myAAS->GetFile()->GetArea(myMove->myArea);
const aasArea_t* goalArea = &myAAS->GetFile()->GetArea(myMove->goalArea);
// Special Case For Starting In The Goal Area
//--------------------------------------------
if (myMove->myArea==myMove->goalArea) {
// Test For Any Obstacles In The Way
//-----------------------------------
if (!obstacleFinder.RayTrace(0.0f, myArea, myMove->myPos, myMove->goalPos, 0/*CDR_TODO: Get myAASNum*/, myIgnoreEntity, myIgnoreEntity2)) {
return Success(NULL);
}
// If There Is An Obstacle But We Think We Can Steer Around It, Then We've Still Succeeded
//-----------------------------------------------------------------------------------------
if (obstacleFinder.contact.v1Valid || obstacleFinder.contact.v2Valid) {
return Success(NULL);
}
}
// Start With My Area
//--------------------
VisitArea(myMove->myArea);
// While Reachabilities Are Still Pending
//----------------------------------------
while (openCount || pendingCount) {
// Select Next Visited Node
//--------------------------
next = SelectNextVisitedNode();
if (!next) {
return ErrorCondition();
}
// If This Node Reaches Our Target Destination, We've Succeeded
//--------------------------------------------------------------
if (next->reach->toAreaNum==myMove->goalArea) {
next->closed = true;
// Test For Any Obstacles In The Way
//-----------------------------------
if (!obstacleFinder.RayTrace(0.0f, goalArea, GetSeekPosition(next), myMove->goalPos, 0/*CDR_TODO: Get myAASNum*/, myIgnoreEntity, myIgnoreEntity2)) {
return Success(next);
}
// Or If There Is An Obstacle, But One Of The Verts Is Valid, Then This Is Still A Safe Course
//---------------------------------------------------------------------------------------------
if (obstacleFinder.contact.v1Valid || obstacleFinder.contact.v2Valid) {
return Success(next);
}
// Visit All Reaches In The Next Area
//-------------------------------------
} else {
VisitArea(next->reach->toAreaNum, next);
}
}
return Failure();
}
};
rvPathFinder pathFinder;
void AI_EntityMoved(idEntity* ent) {
obstacleFinder.MarkEntityForUpdate(ent);
}
void AI_MoveInitialize() {
pathFinder.Initialize();
obstacleFinder.Initialize();
}
/*
=====================
idAI::NewCombinedMove
=====================
*/
void idAI::RVMasterMove( void ) {
static idVec3 mySeekDelta;
static idVec3 mySeekDirection;
static float mySeekDistance;
static float moveDistance;
static idVec3 moveDelta;
static idEntity* moveBlockEnt;
static monsterMoveResult_t moveResult;
//====================================================================
// UPDATE DATA
// A simple first step for movement is to get data from the command
// and goal arguments and compute changes if necessary.
//====================================================================
bool seekMove = CanMove() && move.moveCommand>=NUM_NONMOVING_COMMANDS;
bool seekTurn = CanTurn() && move.moveCommand> MOVE_NONE;
idMat3 myAxis = viewAxis;
const idVec3& myPos = physicsObj.GetOrigin();
const idBounds& myBounds = physicsObj.GetBounds();
float myRadius = myBounds.Size().x / 2.0f;
idVec3 myPosOld = move.myPos; // only used for debug graphics
bool myPosMoved = false; // only used for debug graphics
const idEntity* goalEntity = move.goalEntity.GetEntity();
const idVec3& goalPos = (goalEntity)?(LastKnownPosition(goalEntity)):(move.moveDest);
// Update My Position And Area
//-----------------------------
if (move.myArea==0 || move.myPos.Dist2XY(myPos)>20.0f) {
move.myPos = myPos;
move.myArea = PointReachableAreaNum(move.myPos);
myPosMoved = true;
}
aasArea_t* myArea = &aas->GetFile()->GetArea(move.myArea);
// Update Goal Position And Area
//-------------------------------
if ((seekMove || seekTurn) && move.goalPos.Dist2XY(goalPos)>20.0f) {
move.goalPos = goalPos;
move.goalArea = PointReachableAreaNum(move.goalPos);
}
// If Reached The Goal Position, Then Stop Moving
//------------------------------------------------
if (seekMove && ReachedPos( move.goalPos, move.moveCommand, move.range )) {
StopMove( MOVE_STATUS_DONE );
seekMove = false;
move.pathLen = 0;
}
// Update The Obstacle Markers
//-----------------------------
obstacleFinder.UpdateObstacles();
//====================================================================
// PATH FINDING
// If the goal area is not the same as myArea, then we need to run
// a pathfinding search to get to the goal area. This operation
// will alter the seek position
//====================================================================
if (seekMove) {
// If No Path Exists, Find One
//-----------------------------
if (move.pathArea!=move.goalArea || (!move.pathLen && move.pathTime < (gameLocal.time-10000))) {
pathFinder.FindPath(aas, move, myRadius, DebugFilter(ai_debugMove), this, move.goalEntity.GetEntity());
}
// Set The SeekPos
//-----------------
if (move.pathLen) {
move.seekPos = move.path[move.pathLen-1].seekPos;
// If We've Reached The Next Path Pos, Pop It Off The List And Continue
//----------------------------------------------------------------------
if (move.seekPos.Dist2XY(move.myPos)<REACHED_RADIUS_SQUARE) {
// CDR_TODO: Reached Callback Here
move.pathLen--;
if (move.pathLen) {
move.seekPos = move.path[move.pathLen-1].seekPos;
}
}
} else {
// Pathfinding Failure! Stand Here And Retry In A Few Seconds
//-------------------------------------------------------------
seekMove = false;
move.seekPos = move.myPos;
move.blockTime = gameLocal.time + 1600;
}
} else {
move.seekPos = move.goalPos;
}
//====================================================================
// STEERING
// The seek target is where AI will attempt to turn and move toward
// With simple commands the seek origin is just the goal origin,
// however with pathfinding and obsticle avoidance the seek origin
// will usually be somewhere between the actor and his goal.
//====================================================================
move.fl.obstacleInPath = false; // Makes Character Walk
move.fl.blocked = false; // Makes Character Stop Moving
if (seekMove) {
mySeekDelta = move.seekPos - move.myPos;
mySeekDirection = mySeekDelta;
mySeekDistance = mySeekDirection.NormalizeFast();
// Test For Obstacles In The Path
//--------------------------------
move.fl.obstacleInPath = obstacleFinder.RayTrace(128.0f, myArea, move.myPos, move.seekPos, 0/*CDR_TODO: Get actual aasNumber*/, this, goalEntity);
if (move.fl.obstacleInPath) {
rvObstacleFinder::traceResult_t& tr = obstacleFinder.contact;
move.obstacle = tr.obstacle->entity;
const rvWindingBox& bounds = tr.obstacle->windings[0/*CDR_TODO: Get actual aasNumber*/];
// Is The Obstacle Standing On Seek Position
//-------------------------------------------
if (bounds.PointInside(move.seekPos)) {
if (move.myArea!=move.goalArea) {
move.fl.blocked = true;
move.blockTime = gameLocal.time + 1150;
move.pathArea = 0; // force a refind path next update
} else {
// Otherwise, Stop And Wait For It To Move
//-----------------------------------------
move.fl.blocked = true;
move.blockTime = gameLocal.time + 1050;
// CDR_TODO: Issue MoveDestInvalid() Callback Here
}
}
// Is The Obstacle About To Cross My Path?
//-----------------------------------------
else if (tr.v1==-1) { // CDR_TODO: -1 is an obtouse way to detect this
move.fl.blocked = true;
move.blockTime = gameLocal.time + 1500;
}
// Ok, So Let's Try To Steer Around The Obstacle
//-----------------------------------------------
else {
// If Neither Vertex Is Valid, Need To Refind Path
//-------------------------------------------------
if (!tr.v1Valid && !tr.v2Valid) {
move.fl.blocked = true;
move.blockTime = gameLocal.time + 3250;
move.pathArea = 0; // force a refind path next update
} else {
int vertex;
// Otherwise, Choose The Best Valid Vertex
//-----------------------------------------
if (!tr.v2Valid || bounds.areas[tr.v2]!=myArea) {
vertex = tr.v1;
} else if (!tr.v1Valid || bounds.areas[tr.v2]!=myArea) {
vertex = tr.v2;
} else {
// CDRTODO: Record clockwise / counter clockwise and only test this once.
vertex = (tr.obstacle->origin.IsLeftOf(move.myPos, move.seekPos))?(tr.v2):(tr.v1);
}
// Get Close The Contact Point if The Choosen Vertex Is Not In This Area, Before Going Toward The Vertex
//-------------------------------------------------------------------------------------------------------
if (bounds.areas[vertex]!=myArea && move.myPos.Dist2XY(tr.endPoint)>400.0f /*20*20*/) {
move.seekPos = tr.endPoint;
} else {
move.seekPos = bounds.verts[vertex];
}
// And Recompute The Seek Vectors
//--------------------------------
mySeekDelta = move.seekPos - move.myPos;
mySeekDirection = mySeekDelta;
mySeekDistance = mySeekDirection.NormalizeFast();
}
}
}
}
//====================================================================
// TURNING
// Having finialized our seek position, it is now time to turn
// toward it.
//====================================================================
if (seekTurn) {
DirectionalTurnToward(move.seekPos);
}
Turn();
//====================================================================
// VELOCITY
// Now we need to get the instantanious velocity vector, which will
// usually come right from the animation
//====================================================================
if (move.moveType == MOVETYPE_ANIM) {
if ( move.fl.allowAnimMove || move.fl.allowPrevAnimMove ) {
GetAnimMoveDelta( myAxis, viewAxis, moveDelta );
} else {
moveDelta.Zero();
}
} else {
// CDR_TODO: Other movetypes here
}
// If Doing Seek Move, Cap The Delta To Avoid Overshooting The Seek Position
//---------------------------------------------------------------------------
if (seekMove && moveDelta!=vec3_zero) {
moveDistance = moveDelta.LengthFast();
if (mySeekDistance<0.5f) {
moveDelta = vec3_zero;
} else if (mySeekDistance<moveDistance) {
moveDelta = mySeekDelta;
} else if (mySeekDistance<64.0f || (!move.fl.allowAnimMove && move.fl.allowPrevAnimMove)) {
moveDelta = mySeekDelta * (moveDistance/mySeekDistance);
}
}
//====================================================================
// PHYSICS SIMULATION
// Now we need to get the instantanious velocity vector, which will
// usually come right from the animation
//====================================================================
physicsObj.SetDelta( moveDelta );
physicsObj.ForceDeltaMove( move.fl.noGravity );
RunPhysics();
// Record The Results
//--------------------
move.fl.onGround = physicsObj.OnGround();
moveResult = physicsObj.GetMoveResult();
moveBlockEnt = physicsObj.GetSlideMoveEntity();
// Push Things Out Of The Way
//----------------------------
if (moveBlockEnt && moveBlockEnt->IsType( idMoveable::GetClassType() ) && moveBlockEnt->GetPhysics()->IsPushable()) {
KickObstacles( viewAxis[ 0 ], move.kickForce, moveBlockEnt );
}
// Touch Triggers
//----------------
if (moveDelta!=vec3_zero ) {
if (moveResult!=MM_BLOCKED) {
TouchTriggers();
}
}
//====================================================================
// DEBUG GRAPHICS
//====================================================================
idVec3 origin = physicsObj.GetOrigin();
if (DebugFilter(ai_debugMove)) {
static const idVec3 upPole(0.0f, 0.0f, 60.0f);
static const idVec3 upSeek(0.0f, 0.0f, 3.0f);
gameRenderWorld->DebugBounds(colorMagenta, physicsObj.GetBounds(), origin, gameLocal.msec); // Bounds: MAGENTA
gameRenderWorld->DebugArrow(colorGreen, origin+upSeek, move.seekPos + upSeek, 5, gameLocal.msec); // Seek: GREEN
gameRenderWorld->DebugLine(colorPurple, move.goalPos, move.goalPos + upPole, gameLocal.msec); // Goal: PURPLE
if (myPosMoved) {
gameRenderWorld->DebugLine(colorCyan, myPosOld, move.myPos, 800); // Trail: CYAN
}
if (move.pathLen) {
for (int i=move.pathLen-1; i>=0; i--) {
gameRenderWorld->DebugLine(colorBlue, origin, move.path[i].seekPos, gameLocal.msec); // FoundPath: BLUE
origin = move.path[i].seekPos;
}
}
// PathVisited: ORANGE
// PathOpened: YELLOW
}
if (DebugFilter(ai_showObstacleAvoidance)) {
static const idVec3 upSeek(0.0f, 0.0f, 3.0f);
const idVec3& obstaclePos = (move.obstacle.GetEntity())?(move.obstacle->GetPhysics()->GetOrigin()):(move.seekPos);
if (!DebugFilter(ai_debugMove)) {
gameRenderWorld->DebugArrow(colorGreen, origin+upSeek, move.seekPos + upSeek, 5, gameLocal.msec); // Seek: GREEN
}
if (move.blockTime>gameLocal.time) {
gameRenderWorld->DebugArrow(colorRed, move.myPos, obstaclePos, 3, gameLocal.msec); // Blocked Obstacle: RED
} else if (move.fl.obstacleInPath) {
gameRenderWorld->DebugArrow(colorWhite, move.myPos, obstaclePos, 3, gameLocal.msec); // Walk Obstacle: WHITE
}
// ObstacleBox: YELLOW
// VertexInvalid: RED
// FuturePosition: BROWN
obstacleFinder.DrawDebugGraphics();
}
}
/*
=====================
idAI::Move
=====================
*/
void idAI::Move ( void ) {
if ( ai_speeds.GetBool ( ) ) {
aiManager.timerMove.Start ( );
}
switch( move.moveType ) {
case MOVETYPE_DEAD:
DeadMove();
break;
case MOVETYPE_FLY :
FlyMove();
break;
case MOVETYPE_STATIC :
StaticMove();
break;
case MOVETYPE_ANIM :
AnimMove();
break;
case MOVETYPE_SLIDE :
SlideMove();
break;
case MOVETYPE_PLAYBACK:
PlaybackMove();
break;
case MOVETYPE_CUSTOM:
CustomMove();
break;
}
if ( ai_speeds.GetBool ( ) ) {
aiManager.timerMove.Stop ( );
}
}