/* =============================================================================== 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(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(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->entityNumberentityNumber && 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 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 markers; idList 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; iobstacle==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; iobstacle==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 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; aasFileNumGetFile(); 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; aareas[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; tPointInside(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 obstaclePool; idAASFile* rvObstacle::searchFile; idBounds rvObstacle::searchBounds; /* ===================== rvObstacleFinder ===================== */ struct rvObstacleFinder { idList 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; iGetFile(); for (int a=0; aGetNumAreas(); 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; iDrawDebugGraphics(); } } } /* ============ 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 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; imyPos); 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->pathLenpath[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 (openCountcost() < open[openCount-1]->cost())) { open[openCount] = node; openCount++; return; } } // Otherwise, Add It To The Pending List //--------------------------------------- if (pendingCountcost() < 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; iclosed) { 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 && visitedCountGetFile()->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 && visitedCountnext) { 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)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 (mySeekDistanceIsType( 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 ( ); } }