mirror of
https://github.com/tribes2/engine.git
synced 2026-01-20 03:34:48 +00:00
832 lines
26 KiB
C++
832 lines
26 KiB
C++
//-----------------------------------------------------------------------------
|
|
// V12 Engine
|
|
//
|
|
// Copyright (c) 2001 GarageGames.Com
|
|
// Portions Copyright (c) 2001 by Sierra Online, Inc.
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#include "ai/graph.h"
|
|
#include "platform/profiler.h"
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
|
|
// Go through the construct function since we want to reconstruct on mission cycle.
|
|
NavigationPath::NavigationPath()
|
|
{
|
|
constructThis();
|
|
}
|
|
|
|
void NavigationPath::constructThis()
|
|
{
|
|
mState.constructThis();
|
|
mCurEdge = NULL;
|
|
mTimeSlice = 0;
|
|
mSaveSeekNode = -1;
|
|
mSearchDist = 0.0f;
|
|
mUserStuck = true;
|
|
mForceSearch = true;
|
|
mRepathCounter = 10000;
|
|
mSaveDest.set(1e7, 1e7, 1e7);
|
|
setRedoDist();
|
|
mPctThreshSqrd = 1.0;
|
|
mPathIndoors = mAreIndoors = false;
|
|
mAwaitingSearch = false;
|
|
mTeam = 0;
|
|
mBusyJetting = false;
|
|
mSearchWasValid = true;
|
|
mAdjustSeek.set(0,0,0);
|
|
dMemset(mSavedEndpoints, 0, sizeof(mSavedEndpoints));
|
|
GraphEdge newEdge;
|
|
mSaveLastEdge = newEdge;
|
|
mStopCounter = 0;
|
|
mCastZ = mCastAng = 0.0;
|
|
mCastAverage.set(0,0,0);
|
|
mEstimatedEdge = NULL;
|
|
mEstimatedEnergy = 0.0;
|
|
mFindHere.init();
|
|
}
|
|
|
|
// Since some of the code is in elsewhere (graphSmooth), we should package up the
|
|
// relevant state for it... Doesn't seem like the best organization, but the idea is
|
|
// that the volume code "knows" more about smoothing paths indoor than we do here,
|
|
// and so that code should be separated...
|
|
void NavigationPath::State::constructThis()
|
|
{
|
|
thisEdgeJetting = false;
|
|
nextEdgeJetting = false;
|
|
nextEdgePrecise = false;
|
|
curSeekNode = 0;
|
|
seekLoc.set(0,0,0);
|
|
path.clear();
|
|
visit.clear();
|
|
edges.clear();
|
|
hereLoc.set(0,0,0);
|
|
destLoc.set(0,0,0);
|
|
}
|
|
|
|
// Fetch the node along the path, undoing any bit-flipped Transient indices.
|
|
GraphNode * NavigationPath::getNode(S32 idx)
|
|
{
|
|
S32 node = mState.path[idx];
|
|
S32 toggle = -S32(node < 0);
|
|
return gNavGraph->lookupNode(node ^ toggle);
|
|
}
|
|
|
|
// Get location in path, handling case where endpoints were transients.
|
|
Point3F NavigationPath::getLoc(S32 idx)
|
|
{
|
|
S32 node = mState.path[idx];
|
|
if(node >= 0)
|
|
return gNavGraph->lookupNode(node)->location();
|
|
else
|
|
return mSavedEndpoints[idx > 0];
|
|
}
|
|
|
|
// Save endpoints of search (flagged with negative indices). A NULL dstNode
|
|
// is passed for searches where destination is a node in graph.
|
|
void NavigationPath::saveEndpoints(TransientNode* srcNode, TransientNode* dstNode)
|
|
{
|
|
mSavedEndpoints[0] = srcNode->location();
|
|
mState.path.first() ^= S32(-1);
|
|
if (dstNode)
|
|
{
|
|
mSavedEndpoints[1] = dstNode->location();
|
|
mState.path.last() ^= S32(-1);
|
|
}
|
|
|
|
// These edges are used for the path advancing. Must take care with edges
|
|
// coming off of the transients - the last one must be saved since it is popped
|
|
// after the search (source connection INTO graph remains though)
|
|
setSizeAndClear(mState.edges, getMax(mState.path.size()-1, 0));
|
|
for (S32 i = mState.edges.size(); i > 0; i--)
|
|
{
|
|
GraphEdge * edgePtr = getNode(i-1)->getEdgeTo(getNode(i));
|
|
AssertFatal(edgePtr, "All edges should exist in path");
|
|
mState.edges[i - 1] = edgePtr;
|
|
}
|
|
if (mState.edges.size())
|
|
{
|
|
mSaveLastEdge = * mState.edges.last();
|
|
mState.edges.last() = & mSaveLastEdge;
|
|
}
|
|
}
|
|
|
|
// This is for renderer - though we'll use a similar node marking scheme for
|
|
// implementing avoidance / randomization of paths.
|
|
void NavigationPath::markRenderPath()
|
|
{
|
|
for(S32 i = getMax(mState.curSeekNode - 1, 0); i < mState.path.size(); i++)
|
|
getNode(i)->setOnPath();
|
|
}
|
|
|
|
// Revised path search to go off of the transient nodes.
|
|
void NavigationPath::computePath(TransientNode& srcNode, TransientNode& dstNode)
|
|
{
|
|
mSearchDist = 0.0f;
|
|
mSearchWasValid = false;
|
|
|
|
// Perform the search if the push indicates these two can (probably) reach.
|
|
if (gNavGraph->pushTransientPair(srcNode, dstNode, mTeam, mJetCaps))
|
|
{
|
|
PROFILE_START(PathComputation);
|
|
|
|
mState.path.clear();
|
|
mState.curSeekNode = 0;
|
|
GraphSearch * searcher = gNavGraph->getMainSearcher();
|
|
searcher->setAStar(true);
|
|
searcher->setTeam(mTeam);
|
|
searcher->setThreats(gNavGraph->getThreatSet(mTeam));
|
|
searcher->setRandomize(true);
|
|
#if _GRAPH_PART_
|
|
searcher->setRatings(gNavGraph->jetManager().getRatings(mJetCaps));
|
|
#endif
|
|
|
|
searcher->performSearch(&srcNode, &dstNode);
|
|
|
|
// Path fetcher tells us if search failed.
|
|
if (searcher->getPathIndices(mState.path))
|
|
{
|
|
|
|
|
|
|
|
mSearchDist = searcher->searchDist();
|
|
setSizeAndClear(mState.visit, mState.path.size());
|
|
saveEndpoints(&srcNode, &dstNode);
|
|
mState.curSeekNode = 1;
|
|
setEdgeConstraints();
|
|
mAwaitingSearch = false;
|
|
mSearchWasValid = true;
|
|
}
|
|
else
|
|
{
|
|
// Note - we need to know if the transient connections into the graph were
|
|
// all make-able by the bot (they should all go through the same evaluation
|
|
// to check the connections). I think this assert arose because a bot had
|
|
// transient connection that couldn't be made.
|
|
// // A search should only fail due to force fields. Armor capabilities should be
|
|
// // adequately predicted in advance.
|
|
// AssertFatal(gNavGraph->haveForceFields(), "Failed to predict failure");
|
|
//
|
|
// // Force field management will update this team's partition accordingly.
|
|
// gNavGraph->newPartition(searcher, mTeam);
|
|
|
|
if (gNavGraph->haveForceFields())
|
|
{
|
|
// Force field management will update this team's partition accordingly.
|
|
gNavGraph->newPartition(searcher, mTeam);
|
|
}
|
|
}
|
|
PROFILE_END(); // PathComputation
|
|
}
|
|
else {
|
|
#if _GRAPH_WARNINGS_
|
|
NavigationGraph::warning("Search tried across islands or partitions");
|
|
#endif
|
|
}
|
|
|
|
// Unconnects from graph.
|
|
gNavGraph->popTransientPair(srcNode, dstNode);
|
|
}
|
|
|
|
// Just want all the post-pathCompute stuff separated out:
|
|
void NavigationPath::afterCompute()
|
|
{
|
|
mSaveDest = mState.destLoc;
|
|
mForceSearch = false;
|
|
mRepathCounter = 0;
|
|
if ( mRedoMode == OnPercent ) {
|
|
F32 pctThresh = (mRedoPercent * mSearchDist);
|
|
mPctThreshSqrd = (pctThresh * pctThresh);
|
|
}
|
|
mUserStuck = false;
|
|
}
|
|
|
|
bool NavigationPath::updateTransients(TransientNode& hereNode,
|
|
TransientNode& destNode, bool forceRedo)
|
|
{
|
|
// Nodes could be invalid-
|
|
if (mHook.iGrowOld())
|
|
mLocateHere.reset(), mLocateDest.reset();
|
|
|
|
if (forceRedo)
|
|
mLocateHere.forceCheck(), mLocateDest.forceCheck();
|
|
|
|
// This does all the work of figuring out how to connect-
|
|
mLocateHere.update(mState.hereLoc);
|
|
mLocateDest.update(mState.destLoc);
|
|
|
|
// Install connections found, and update locs-
|
|
hereNode.setEdges(mLocateHere.getEdges());
|
|
destNode.setEdges(mLocateDest.getEdges());
|
|
hereNode.setClosest(mLocateHere.bestMatch());
|
|
destNode.setClosest(mLocateDest.bestMatch());
|
|
hereNode.setLoc(mState.hereLoc);
|
|
destNode.setLoc(mState.destLoc);
|
|
|
|
// NOTE! Only hook one way due to how Push-Search-Pop works (computePath() above).
|
|
bool needToJet;
|
|
if (mLocateHere.canHookTo(mLocateDest, needToJet)) {
|
|
GraphEdge& edge = hereNode.pushEdge(&destNode);
|
|
if (needToJet)
|
|
edge.setJetting();
|
|
gNavGraph->jetManager().initEdge(edge, &hereNode, &destNode);
|
|
}
|
|
|
|
return forceRedo; // not used
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
// Periodic path updates. Return true if there are nodes to traverse.
|
|
|
|
#define SearchWaitTicks 20
|
|
#define AllowVisitTicks 21
|
|
|
|
bool NavigationPath::checkPathUpdate(TransientNode& hereNode, TransientNode& destNode)
|
|
{
|
|
bool recompute = mForceSearch;
|
|
// bool recompute = false;
|
|
|
|
bool changedSeekNode = (mSaveSeekNode != mState.curSeekNode);
|
|
mSaveSeekNode = mState.curSeekNode;
|
|
|
|
if (!recompute) {
|
|
F32 threshold = (mRedoMode == OnDist ? mRedoDistSqrd : mPctThreshSqrd);
|
|
bool canSearch = (++mRepathCounter > SearchWaitTicks) && !userMustJet();
|
|
bool needSearch = ((mState.destLoc - mSaveDest).lenSquared() > threshold);
|
|
|
|
if (mUserStuck)
|
|
needSearch = true;
|
|
|
|
if (needSearch) {
|
|
// We need to make the canSearch check a little more strict and wait a little
|
|
// extra time, or search when the node has been updated.
|
|
if (canSearch && (changedSeekNode || mRepathCounter > AllowVisitTicks))
|
|
recompute = true;
|
|
else
|
|
mAwaitingSearch = true;
|
|
}
|
|
}
|
|
if (recompute)
|
|
mAwaitingSearch = true;
|
|
|
|
updateTransients(hereNode, destNode, recompute);
|
|
|
|
if (recompute) {
|
|
computePath(hereNode, destNode);
|
|
afterCompute();
|
|
}
|
|
|
|
if (weAreIndoors())
|
|
mAreIndoors = true;
|
|
else {
|
|
if(checkOutsideAdvance())
|
|
advanceByOne();
|
|
}
|
|
|
|
// Update path
|
|
if (mState.curSeekNode < mState.path.size())
|
|
{
|
|
GraphNode * fromNode = (mState.curSeekNode > 0 ? getNode(mState.curSeekNode-1) : NULL);
|
|
|
|
if (fromNode && gNavGraph->useVolumeTraverse(fromNode, mCurEdge))
|
|
{
|
|
S32 adv = gNavGraph->checkIndoorSkip(mState);
|
|
|
|
if (adv)
|
|
{
|
|
// Con::printf("Advanced through %d indoor nodes", adv);
|
|
while (adv--)
|
|
{
|
|
AssertFatal(canAdvance(), "Nav path tried illegal advance");
|
|
advanceByOne();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (gNavGraph->volumeTraverse(fromNode, mCurEdge, mState))
|
|
{
|
|
if (canAdvance()) //==> Shouldn't it always be possible to advance?
|
|
advanceByOne();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mState.seekLoc = getLoc(mState.curSeekNode);
|
|
F32 threshold = visitRadius() + 0.22;
|
|
if(within_2D(mState.hereLoc, mState.seekLoc, threshold) && canAdvance()) {
|
|
advanceByOne();
|
|
if (mState.curSeekNode < mState.path.size())
|
|
mState.seekLoc = getLoc(mState.curSeekNode); // re-fetch!
|
|
}
|
|
}
|
|
}
|
|
|
|
return (mState.curSeekNode < mState.path.size());
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
|
|
// The consumer of the NavigationPath must call this every frame.
|
|
bool NavigationPath::updateLocations(const Point3F& here, const Point3F& there)
|
|
{
|
|
// This addition to the Z is important because we don't match to indoor nodes that
|
|
// we are below. Not pretty, but the alternative is most extra LOS calls, or other
|
|
// potential ambiguities in the node matching process. Also see the wall scanning
|
|
// code below, which is 'cognizant' of these numbers.
|
|
(mState.hereLoc = here).z += 0.4;
|
|
(mState.destLoc = there).z += 0.4;
|
|
|
|
mAreIndoors = false; // (default - get's changed if indoors)
|
|
|
|
if( !NavigationGraph::gotOneWeCanUse()) {
|
|
mState.seekLoc = mState.destLoc;
|
|
mSearchWasValid = false;
|
|
}
|
|
else {
|
|
// Threat manager needs to be called frequently-
|
|
gNavGraph->threats()->monitorThreats();
|
|
gNavGraph->monitorForceFields();
|
|
|
|
// Note hook nodes must be fetched every frame (in case of graph revisions).
|
|
if (!checkPathUpdate(mHook.getHook1(), mHook.getHook2()))
|
|
mState.seekLoc = mState.destLoc;
|
|
|
|
// Debug info-
|
|
markRenderPath();
|
|
|
|
// This just walks the graph looking for malfeasance
|
|
// gNavGraph->patrolForProblems();
|
|
}
|
|
|
|
// Movement code sets while jetting, each set lasts for one frame
|
|
mBusyJetting = false;
|
|
|
|
// Busy-dec the stop counter, and check wall avoidance when non-zero.
|
|
if (--mStopCounter <= 0)
|
|
mStopCounter = 0;
|
|
else
|
|
checkWallAvoid();
|
|
|
|
// Return status of the last search performed-
|
|
return mSearchWasValid;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
// Management of path advancing.
|
|
|
|
|
|
// This assumes we have a graph and are seeking a node within the list.
|
|
S32 NavigationPath::checkOutsideAdvance()
|
|
{
|
|
if(canAdvance())
|
|
{
|
|
S32 seekNode = mState.curSeekNode + 1;
|
|
if (seekNode < mState.path.size() && !mState.nextEdgeJetting)
|
|
{ // See if we can get to the next one. We're doing this in 2D, which may be Ok
|
|
// in the long run too, though we might track maximum heights along the way.
|
|
// First we check for registered threats along this segment.
|
|
Point3F srcLoc(mState.hereLoc);
|
|
Point3F dstLoc(getLoc(seekNode));
|
|
if (gNavGraph->threats()->sanction(srcLoc, dstLoc, mTeam))
|
|
{
|
|
srcLoc.z = dstLoc.z = 0.0f;
|
|
F32 pct = gNavGraph->mTerrainInfo.checkOpenTerrain(srcLoc, dstLoc);
|
|
if (pct == 1.0)
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool NavigationPath::canAdvance()
|
|
{
|
|
if (mState.curSeekNode < mState.path.size())
|
|
if (!mState.thisEdgeJetting)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
F32 NavigationPath::visitRadius()
|
|
{
|
|
// if (mState.nextEdgeJetting || mAreIndoors || mPathIndoors)
|
|
if (mState.nextEdgePrecise || mAreIndoors || mPathIndoors)
|
|
return 0.4;
|
|
else
|
|
return 2.6;
|
|
}
|
|
|
|
// See if the next destination is a Jetting edge. An assumption of this routine
|
|
// is that it is only called when a new path is computed, or when the path is
|
|
// advanced. Else problems - such as transient source having new connections
|
|
// which don't relate to the current path, etc.
|
|
void NavigationPath::setEdgeConstraints()
|
|
{
|
|
mState.thisEdgeJetting = false;
|
|
mState.nextEdgeJetting = false;
|
|
mState.nextEdgePrecise = false;
|
|
mPathIndoors = false;
|
|
mCurEdge = NULL;
|
|
|
|
if (mState.curSeekNode > 0 && mState.curSeekNode < mState.path.size())
|
|
{
|
|
GraphNode * curSeekNode = getNode(mState.curSeekNode);
|
|
GraphNode * prevSeekNode = getNode(mState.curSeekNode-1);
|
|
|
|
mCurEdge = mState.edges[mState.curSeekNode - 1];
|
|
|
|
if (curSeekNode->indoor() || prevSeekNode->indoor())
|
|
mPathIndoors = true;
|
|
|
|
mState.thisEdgeJetting = mCurEdge->isJetting();
|
|
|
|
// Configure the edge. Hop over is a U8 with 3 bits of precision.
|
|
if (mState.thisEdgeJetting)
|
|
{
|
|
mNavJetting.init();
|
|
if (mCurEdge->hasHop())
|
|
mNavJetting.mHopOver = mCurEdge->getHop();
|
|
}
|
|
|
|
if (mState.thisEdgeJetting)
|
|
mState.seekLoc = getLoc(mState.curSeekNode);
|
|
|
|
if (mState.curSeekNode+1 < mState.path.size())
|
|
{
|
|
GraphEdge * nextEdge = mState.edges[mState.curSeekNode];
|
|
mState.nextEdgeJetting = nextEdge->isJetting();
|
|
|
|
// This flags that we need to go to the node location.
|
|
mState.nextEdgePrecise = (mState.nextEdgeJetting || !nextEdge->isBorder());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Called when a node has been visited. Mark it for later avoidance (path randomizing)
|
|
// if such is enabled and reasonable. Don't mark large terrain nodes or transients.
|
|
void NavigationPath::randomization()
|
|
{
|
|
GraphNode * node = getNode(mState.curSeekNode);
|
|
if (!node->transient() && node->getLevel() <= 2)
|
|
node->setAvoid(60000);
|
|
}
|
|
|
|
// The path is advanced through only two interface functions, this one and the next.
|
|
void NavigationPath::advanceByOne()
|
|
{
|
|
AssertFatal(canAdvance(), "advanceByOne() called incorrectly");
|
|
randomization();
|
|
mState.curSeekNode++;
|
|
setEdgeConstraints();
|
|
}
|
|
|
|
// When user is done - they inform the system so it can advance.
|
|
void NavigationPath::informJetDone()
|
|
{
|
|
AssertFatal(mState.thisEdgeJetting, "informJetDone() only called when jetting");
|
|
randomization();
|
|
mState.curSeekNode++;
|
|
setEdgeConstraints();
|
|
}
|
|
|
|
// User queries to see if they now must jet.
|
|
bool NavigationPath::userMustJet() const
|
|
{
|
|
return (mState.path.size() > 0 && mState.thisEdgeJetting);
|
|
}
|
|
|
|
void NavigationPath::forceSearch()
|
|
{
|
|
if (userMustJet() && _GRAPH_WARNINGS_)
|
|
NavigationGraph::warning("Searched forced while user is jetting");
|
|
mForceSearch = true;
|
|
}
|
|
|
|
// Here we use the path randomization to hopefully get the guy off the path. We back up
|
|
// through the list of any that were skipped - since we may not have even come to those
|
|
// yet. And then we go forward by one.
|
|
void NavigationPath::informStuck(const Point3F& stuckLoc, const Point3F& stuckDest)
|
|
{
|
|
stuckLoc; stuckDest;
|
|
S32 start = getMax(mState.curSeekNode - 1, 1);
|
|
S32 end = getMin(mState.curSeekNode + 1, mState.path.size() - 1);
|
|
|
|
while (start > 1 && mState.visit[start].test(State::Skipped))
|
|
start--;
|
|
|
|
while (start < end) {
|
|
GraphNode * node = getNode(start++);
|
|
AssertFatal(!node->transient(), "informStuck() didn't skip transients");
|
|
node->setAvoid(15 * 1000, true);
|
|
}
|
|
|
|
mUserStuck = true;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
|
|
U32 gCountStoppedTicks = 0;
|
|
// F32 gVelSquaredThresh = 0.1;
|
|
F32 gVelSquaredThresh = -1.0; // Take out for moment - needs more testing.
|
|
|
|
// When not moving but should be, this gets called - when in Express/Walk mode.
|
|
void NavigationPath::informProgress(const VectorF& vel)
|
|
{
|
|
F32 velSquared = vel.lenSquared();
|
|
if (velSquared < gVelSquaredThresh)
|
|
{
|
|
if ((mStopCounter += 2) > 10)
|
|
gCountStoppedTicks++;
|
|
}
|
|
else
|
|
{
|
|
// Once not stopped, then limit how much we have to count down. But we do
|
|
// want that time as well for fading the running mCastAverage below.
|
|
mStopCounter = getMin(mStopCounter, 7);
|
|
}
|
|
}
|
|
|
|
// A nice angle for scanning around with good coverage and consistent change. These
|
|
// are relative prime numbers a little over 3/8 of the cycle points (360.0, 1.90000)
|
|
static const F64 sCycleAngle = mDegToRad(F64(137.3));
|
|
static const F64 sCycleHeight = 0.37747;
|
|
static const U32 sCycleMask = (InteriorObjectType|TerrainObjectType);
|
|
static const F32 sAverageTheNew = (1.0 / 8.0);
|
|
static const F32 sAverageTheOld = (1.0 - sAverageTheOld);
|
|
|
|
// Monitor the stop counter. When it persists, look for walls and such to move
|
|
// away from. We do this with rotating LOS checks, which also have to move up and
|
|
// down to have the best chance of finding obstacles. Cycling on 1.9 height covers
|
|
// range from 0.4 to 2.3 (see above addition to supplied here loc z).
|
|
void NavigationPath::checkWallAvoid()
|
|
{
|
|
if (mStopCounter > 3)
|
|
{
|
|
if ((mCastAng += sCycleAngle) > M_2PI)
|
|
mCastAng -= M_2PI;
|
|
if ((mCastZ += sCycleHeight) > 1.9)
|
|
mCastAng -= 1.9;
|
|
|
|
F32 angle32 = F32(mCastAng);
|
|
Point3F startPoint( mState.hereLoc.x, mState.hereLoc.y, mState.hereLoc.z + F32(mCastZ) );
|
|
Point3F lineOut( mCos(angle32), mSin(angle32), 0);
|
|
|
|
// Get the endpoint out a little ways. Find intersection and convert back
|
|
// to an offset which we'll roll into running average of offsets.
|
|
(lineOut *= 7.0) += startPoint;
|
|
RayInfo coll;
|
|
gServerContainer.castRay(startPoint, lineOut, sCycleMask, &coll);
|
|
coll.point -= startPoint;
|
|
|
|
// Get the running average-
|
|
coll.point *= sAverageTheNew;
|
|
mCastAverage *= sAverageTheOld;
|
|
mCastAverage += coll.point;
|
|
}
|
|
else
|
|
{
|
|
// In first few frames free, or first frames stuck, fade down the average.
|
|
mCastAverage *= (2.0 / 3.0);
|
|
}
|
|
}
|
|
|
|
static Point3F reignIn(const Point3F& S, Point3F D, F32 cap)
|
|
{
|
|
D -= S;
|
|
if (D.lenSquared() > (cap * cap))
|
|
D.normalize(cap);
|
|
return D += S;
|
|
}
|
|
|
|
// This is where user fetches location to seek. We now do additional checks to
|
|
// reign in this distance when near steep things.
|
|
const Point3F& NavigationPath::getSeekLoc(const VectorF&)
|
|
{
|
|
// if (NavigationGraph::gotOneWeCanUse())
|
|
// mAdjustSeek = reignIn(mState.hereLoc, mState.seekLoc, 10.0);
|
|
// else
|
|
if (mStopCounter)
|
|
{
|
|
// Seek away from collision point if that exists-
|
|
mAdjustSeek = mCastAverage;
|
|
mAdjustSeek *= F32(mStopCounter);
|
|
mAdjustSeek += mState.hereLoc;
|
|
}
|
|
else
|
|
mAdjustSeek = mState.seekLoc;
|
|
|
|
return mAdjustSeek;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
|
|
// Convey the jetting ability to the jet manager. We let the aiConnection go ahead
|
|
// and retrieve this data from Player since we've so far kept from #including Player.h.
|
|
void NavigationPath::setJetAbility(const JetManager::Ability & ability)
|
|
{
|
|
if (gNavGraph) {
|
|
if (_GRAPH_PART_ && gNavGraph->jetManager().update(mJetCaps, ability)) {
|
|
// A bunch of work just happened, let's forestall searches a little-
|
|
mRepathCounter = getMin(SearchWaitTicks >> 1, mRepathCounter);
|
|
}
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
|
|
// How far across open terrain user can go from src to dst. dstLoc parameter is
|
|
// updated accordingly, and a percentage from 0 to 1 is returned.
|
|
F32 NavigationPath::checkOpenTerrain(const Point3F& srcLoc, Point3F& dstLoc)
|
|
{
|
|
if (NavigationGraph::gotOneWeCanUse())
|
|
return gNavGraph->mTerrainInfo.checkOpenTerrain(srcLoc, dstLoc);
|
|
dstLoc = srcLoc;
|
|
return 0.0;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
// A couple of path lookahead functions-
|
|
|
|
// Get a point that is dist along the path.
|
|
Point3F NavigationPath::getLocOnPath(F32 dist)
|
|
{
|
|
Point3F currLoc(mState.hereLoc);
|
|
|
|
for (S32 i = mState.curSeekNode; i < mState.path.size() && dist > 0.01; i++)
|
|
{
|
|
Point3F nextLoc = getLoc(i);
|
|
VectorF diffVec = nextLoc;
|
|
F32 len = (diffVec -= currLoc).len();
|
|
|
|
if (len < dist)
|
|
{
|
|
currLoc = nextLoc;
|
|
dist -= len;
|
|
}
|
|
else
|
|
{
|
|
currLoc += (diffVec *= (dist / len));
|
|
break;
|
|
}
|
|
}
|
|
return currLoc;
|
|
}
|
|
|
|
// Find distance remaining on path by stepping through it - stop if user has given a
|
|
// threshold dist outside of which they don't care (defaults to huge).
|
|
F32 NavigationPath::distRemaining(F32 maxCare)
|
|
{
|
|
Point3F currLoc(mState.hereLoc);
|
|
F32 dist = 0.0f;
|
|
|
|
for (S32 i = mState.curSeekNode; i < mState.path.size(); i++)
|
|
{
|
|
Point3F nextLoc = getLoc(i);
|
|
|
|
if ((dist += (nextLoc - currLoc).len()) > maxCare)
|
|
return maxCare;
|
|
|
|
currLoc = nextLoc;
|
|
}
|
|
return dist;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
|
|
// Find next edge that requires jetting if it's within maxDist. If found, then we
|
|
// estimate the needed energy to complete the hop. Since the estimation is a little
|
|
// lengthy - we remember which edge we computed for and only do it once.
|
|
F32 NavigationPath::jetWillNeedEnergy(F32 maxDist)
|
|
{
|
|
if (!mState.thisEdgeJetting)
|
|
{
|
|
F32 accumDist = 0.0f;
|
|
for (S32 i = mState.curSeekNode; i < mState.edges.size(); i++)
|
|
{
|
|
// Let's only do a len() calculation to the first node, otherwise use distance
|
|
// on the edge. (First len() required because the bot's moving).
|
|
F32 legDist;
|
|
if (i == mState.curSeekNode)
|
|
legDist = (getLoc(i) - mState.hereLoc).len();
|
|
else
|
|
legDist = mState.edges[i - 1]->mDist;
|
|
|
|
if ((accumDist += legDist) < maxDist)
|
|
{
|
|
GraphEdge * nextEdge = mState.edges[i];
|
|
if (nextEdge->isJetting())
|
|
return estimateEnergy(nextEdge);
|
|
}
|
|
}
|
|
}
|
|
return 0.0;
|
|
}
|
|
|
|
// Get the jet manager's estimation, and remember that we did this work for this
|
|
// edge since JetManager::estimateEnergy() does a bit o' math.
|
|
F32 NavigationPath::estimateEnergy(const GraphEdge * edge)
|
|
{
|
|
if (mEstimatedEdge != edge)
|
|
{
|
|
mEstimatedEnergy = gNavGraph->jetManager().estimateEnergy(mJetCaps, edge);
|
|
mEstimatedEdge = edge;
|
|
}
|
|
return mEstimatedEnergy;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
|
|
// Return true if we are outside, and set the roam radius (in param) if so.
|
|
bool NavigationPath::locationIsOutdoors(const Point3F &location, F32* roamRadPtr)
|
|
{
|
|
F32 roamRadius = 1e12; // Default is outdoors, with unlimited roam room.
|
|
|
|
if (NavigationGraph::gotOneWeCanUse())
|
|
{
|
|
if (!gNavGraph->haveTerrain())
|
|
return false;
|
|
|
|
Point3F tempLocation(location.x, location.y, 0.0f);
|
|
if (gNavGraph->mTerrainInfo.inGraphArea(tempLocation))
|
|
{
|
|
SphereF sphere;
|
|
if (gNavGraph->mTerrainInfo.locToIndexAndSphere(sphere, tempLocation) >= 0)
|
|
roamRadius = sphere.radius; // fall through to true
|
|
else
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (roamRadPtr)
|
|
*roamRadPtr = roamRadius;
|
|
|
|
return true;
|
|
}
|
|
|
|
// By default (with no graph), this is false.
|
|
bool NavigationPath::weAreIndoors() const
|
|
{
|
|
if (NavigationGraph::gotOneWeCanUse())
|
|
if (!gNavGraph->haveTerrain())
|
|
return true;
|
|
else if (gNavGraph->mTerrainInfo.inGraphArea(mState.hereLoc))
|
|
return !gNavGraph->findTerrainNode(mState.hereLoc);
|
|
|
|
return false;
|
|
}
|
|
|
|
bool NavigationPath::getPathNodeLoc(S32 ahead, Point3F& loc)
|
|
{
|
|
S32 dest = mState.curSeekNode + ahead;
|
|
|
|
if (dest < mState.path.size() && dest > 0)
|
|
{
|
|
GraphEdge * beforeEdge = mState.edges[dest - 1];
|
|
if (!beforeEdge->isJetting())
|
|
{
|
|
if (beforeEdge->isBorder())
|
|
loc = gNavGraph->getBoundary(beforeEdge->mBorder).seekPt;
|
|
else
|
|
loc = getNode(dest)->location();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Does the bot have to jet into a vehicle?
|
|
bool NavigationPath::intoMount() const
|
|
{
|
|
if (mLocateDest.isMounted())
|
|
if (mState.curSeekNode == mState.path.size() - 1)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
bool NavigationPath::canReachLoc(const Point3F& dst)
|
|
{
|
|
if (NavigationGraph::gotOneWeCanUse())
|
|
{
|
|
// mFindHere is a FindGraphNode which remembers results of closest node searches.
|
|
mFindHere.setPoint(mState.hereLoc, mFindHere.closest());
|
|
FindGraphNode findDest(dst);
|
|
return gNavGraph->canReachLoc(mFindHere, findDest, mTeam, mJetCaps);
|
|
}
|
|
else
|
|
return true;
|
|
}
|
|
|
|
|
|
void NavigationPath::missionCycleCleanup()
|
|
{
|
|
constructThis();
|
|
mJetCaps.reset();
|
|
mNavJetting.init();
|
|
mHook.reset();
|
|
mFindHere.init();
|
|
mLocateHere.cleanup();
|
|
mLocateDest.cleanup();
|
|
}
|
|
|