Prevent security guards from accidentally hitting a player - FreeSlave/halflife-updated GitHub Wiki

The Problem

Barney doesn't take into account the ally player when shooting his enemies. So if there's a player in the line of fire, the security guard still continues shooting, hurting the player.

The Research

Half-Life SDK actually has a piece of code that checks for friendly fire. But it's only for squad monsters and it checks only for members of the squad the monster is part of. We can re-use this code for barney and adjust it to take into account the ally player.

The Solution

Open dlls/barney.cpp. Include the plane.h header as we're going to need CPlane class definition.

#include "plane.h"

Then somewhere in CBarney class (e.g. after the Killed function) declare a new function:

bool NoFriendlyFire();

Let's implement it.

bool CBarney::NoFriendlyFire()
{
	if( m_hEnemy != 0 )
	{
		UTIL_MakeVectors( UTIL_VecToAngles( m_hEnemy->Center() - pev->origin ) );
	}
	else
	{
		// if there's no enemy, pretend there's a friendly in the way, so the grunt won't shoot.
		return false;
	}

	CPlane backPlane;
	CPlane leftPlane;
	CPlane rightPlane;

	Vector vecLeftSide;
	Vector vecRightSide;
	Vector v_left;
	Vector v_dir;

	v_dir = gpGlobals->v_right * ( pev->size.x * 1.5f );
	vecLeftSide = pev->origin - v_dir;
	vecRightSide = pev->origin + v_dir;

	v_left = gpGlobals->v_right * -1.0f;

	leftPlane.InitializePlane( gpGlobals->v_right, vecLeftSide );
	rightPlane.InitializePlane( v_left, vecRightSide );
	backPlane.InitializePlane( gpGlobals->v_forward, pev->origin );

	for( int k = 1; k <= gpGlobals->maxClients; k++ )
	{
		CBaseEntity* pPlayer = UTIL_PlayerByIndex(k);
		if (pPlayer && pPlayer->IsPlayer() && IRelationship(pPlayer) == R_AL && pPlayer->IsAlive())
		{
			if( backPlane.PointInFront( pPlayer->pev->origin ) &&
				leftPlane.PointInFront( pPlayer->pev->origin ) &&
				rightPlane.PointInFront( pPlayer->pev->origin ) )
			{
				//ALERT(at_aiconsole, "%s: Ally player at fire plane!\n", STRING(pev->classname));
				// player is in the check volume! Don't shoot!
				return false;
			}
		}
	}

	return true;
}

It looks very similar to CSquadMonster::NoFriendlyFire, but instead of checking for squad members it checks for an ally player in the line of fire.

Now we have a function, but we haven't really used it anywhere yet. Let's go for it. Somewhere before CBarney class declaration add the following lines:

#define bits_COND_BARNEY_NOFIRE	( bits_COND_SPECIAL1 )

enum
{
	TASK_BARNEY_CHECK_FIRE = LAST_COMMON_TASK + 1,
};

Here we introduce the new condition bits_COND_BARNEY_NOFIRE that will be used to interrupt the range attack schedule and the new task (TASK_BARNEY_CHECK_FIRE) which will be a part of the range attack schedule.

Go to the CBarney::StartTask and modify it:

- CTalkMonster::StartTask( pTask );	
+ switch ( pTask->iTask ) {
+ case TASK_BARNEY_CHECK_FIRE:
+ 	if( !NoFriendlyFire() )
+ 	{
+ 		SetConditions( bits_COND_BARNEY_NOFIRE );
+ 	}
+ 	TaskComplete();
+ 	break;
+ default:
+ 	CTalkMonster::StartTask( pTask );
+ }

We've implemented a task and set a condition bit. But the task itself is not part of any schedule anymore. Barney uses default schedule for range attack, so we'll need to re-define this schedule for him.

Somewhere before DEFINE_CUSTOM_SCHEDULES add this code:

// primary range attack
Task_t tlBaRangeAttack1[] =
{
	{ TASK_STOP_MOVING, 0 },
	{ TASK_FACE_ENEMY, (float)0 },
	{ TASK_BARNEY_CHECK_FIRE, (float)0 },
	{ TASK_RANGE_ATTACK1, (float)0 },
};

Schedule_t slBaRangeAttack1[] =
{
	{
		tlBaRangeAttack1,
		ARRAYSIZE( tlBaRangeAttack1 ),
		bits_COND_NEW_ENEMY |
		bits_COND_ENEMY_DEAD |
		bits_COND_LIGHT_DAMAGE |
		bits_COND_HEAVY_DAMAGE |
		bits_COND_ENEMY_OCCLUDED |
		bits_COND_NO_AMMO_LOADED |
		bits_COND_BARNEY_NOFIRE |
		bits_COND_HEAR_SOUND,
		bits_SOUND_DANGER,
		"Range Attack1"
	},
};

It looks very similar to the standard slRangeAttack1 schedule (defined in defaultai.cpp), but it has bits_COND_BARNEY_NOFIRE as an interruption bit and the TASK_BARNEY_CHECK_FIRE task before the range attack task.

You can also add the schedule to the schedule list:

slIdleBaStand,
+ slBaRangeAttack1,

Let's make Barney use this schedule. Go to the CBarney::GetScheduleOfType and this case to the switch:

case SCHED_RANGE_ATTACK1:
	return slBaRangeAttack1;

We're ready! Build the libraries and check it in the game. E.g. you can load the map c1a1, get to the security guard on the upper floor and stay on the line of fire between him and zombie. Barney shouldn't shoot.

Patch