Improved retreat from danger - FreeSlave/halflife-updated GitHub Wiki

Some monsters in Half-Life can run to cover upon hearing the AI Danger sound. AI sounds are not actually audible, but they act as special markers that NPCs can perceive. Danger sounds are usually produced by grenades shortly before detonation, giving monsters the chance to run away once they "hear" it.

TASK_FIND_COVER_FROM_BEST_SOUND in dlls/schedule.cpp is a task that calls FindCover function which tries to find a node where the monster will not be visible to the threat. If found, such node is considered to be a suitable hideout for a monster and they will try to run to this spot. FindCover is used in multiple places in Half-Life SDK, but in this particular case it's not really necessary for a monster to hide from danger behind a cover. Just running away would be enough to escape the grenade impact radius. FindCover won't help in the open field as it won't find any cover, so we're going to write an alternative function for finding a node to run to. Let's name it FindRetreat.

First of all, go to dlls/basemonster.h and add a new function declaration, somewhere in CBaseMonster class declaration, e.g. right after the FindCover.

virtual bool FindCover(Vector vecThreat, Vector vecViewOffset, float flMinDist, float flMaxDist);
+ bool FindRetreat(Vector vecThreat, float flMinDist, float flMaxDist);

FindRetreat has a similar parameters to FindCover, but it does not need view offset of a threat. We also don't make it virtual, as you most likely are not going to override it in derived classes.

Now go to dlls/monsters.cpp and after FindCover implementation add this:

bool CBaseMonster::FindRetreat(Vector vecThreat, float flMinDist, float flMaxDist)
{
	int i;
	int iMyHullIndex;
	int iMyNode;
	int iThreatNode;
	float flDist;

	if (0 == flMaxDist)
	{
		// user didn't supply a MaxDist, so work up a crazy one.
		flMaxDist = 784;
	}

	if (flMinDist > 0.5 * flMaxDist)
	{
#if _DEBUG
		ALERT(at_console, "FindRetreat MinDist (%.0f) too close to MaxDist (%.0f)\n", flMinDist, flMaxDist);
#endif
		flMinDist = 0.5 * flMaxDist;
	}

	if (0 == WorldGraph.m_fGraphPresent || 0 == WorldGraph.m_fGraphPointersSet)
	{
		ALERT(at_aiconsole, "Graph not ready for findretreat!\n");
		return false;
	}

	iMyNode = WorldGraph.FindNearestNode(pev->origin, this);
	iThreatNode = WorldGraph.FindNearestNode(vecThreat, this);
	iMyHullIndex = WorldGraph.HullIndex(this);

	if (iMyNode == NO_NODE)
	{
		ALERT(at_aiconsole, "FindRetreat() - %s has no nearest node!\n", STRING(pev->classname));
		return false;
	}
	if (iThreatNode == NO_NODE)
	{
		// ALERT ( at_aiconsole, "FindRetreat() - Threat has no nearest node!\n" );
		iThreatNode = iMyNode;
		// return false;
	}

	// we'll do a rough sample to find nodes that are relatively nearby
	for (i = 0; i < WorldGraph.m_cNodes; i++)
	{
		int nodeNumber = (i + WorldGraph.m_iLastCoverSearch) % WorldGraph.m_cNodes;

		CNode& node = WorldGraph.Node(nodeNumber);

		// could use an optimization here!!
		flDist = (pev->origin - node.m_vecOrigin).Length();

		if (flDist >= flMinDist && flDist < flMaxDist)
		{
			// node is also closer to me than the threat, or the same distance from myself and the threat the node is good.
			if ((iMyNode == iThreatNode) || WorldGraph.PathLength(iMyNode, nodeNumber, iMyHullIndex, m_afCapability) <= WorldGraph.PathLength(iThreatNode, nodeNumber, iMyHullIndex, m_afCapability))
			{
				if (FValidateCover(node.m_vecOrigin) && MoveToLocation(ACT_RUN, 0, node.m_vecOrigin))
				{
					WorldGraph.m_iLastCoverSearch = nodeNumber + 1; // next monster that searches for cover node will start where we left off here.
					return true;
				}
			}
		}
	}
	return false;
}

As you can see the implementation is very similar to FindCover. However it doesn't use UTIL_TraceLine for finding a cover, thus it's a bit simpler.

Note also how I moved the WorldGraph.m_iLastCoverSearch = nodeNumber + 1; line right before the return. FindCover has this line executed every cycle, but it seems weird to change this variable in loop as nodeNumber is already calculated from it and i index variable. So you can apply the similar change to the FindCover function.

Now as we have both declaration and implementation, we can use the function in code. Go to dlls/schedule.cpp and make changes under the TASK_FIND_COVER_FROM_BEST_SOUND:

-if (pBestSound && FindCover(pBestSound->m_vecOrigin, g_vecZero, pBestSound->m_iVolume, CoverRadius()))
-{
-	// then try for plain ole cover
-	m_flMoveWaitFinished = gpGlobals->time + pTask->flData;
-	TaskComplete();
-}
-else
-{
-	// no coverwhatsoever. or no sound in list
-	TaskFail();
-}
+if (pBestSound)
+{
+	if (FindCover(pBestSound->m_vecOrigin, g_vecZero, pBestSound->m_iVolume, CoverRadius()))
+	{
+		// then try for plain ole cover
+		m_flMoveWaitFinished = gpGlobals->time + pTask->flData;
+		TaskComplete();
+	}
+	else if (FindRetreat(pBestSound->m_vecOrigin, pBestSound->m_iVolume, CoverRadius()))
+	{
+		m_flMoveWaitFinished = gpGlobals->time + pTask->flData;
+		TaskComplete();
+	}
+}
+// no coverwhatsoever. or no sound in list
+if (!TaskIsComplete()) {
+	TaskFail();
+}
break;

Monsters will still prefer nodes with cover, but if they can't find one they would just run to some node away from the danger.

Patch