[ News | About | Media | Downloads | Tutorials | Contact ]

Tutorials

Back to Tutorials

Half-Life 2: Short Stories - Xenian Pack

This is the Xenians Pack meant for fellow modders who which to use the Alien Grunts and Alien Controllers from Human Error in their mod. It includes the source code, models & materials, model source (smd and .qc) files, particle and script files. You can download the files from: here.

You can download a test map here.

Thanks for Bluefire for testing this Tutorial and helping me make it work. I hope this is useful for someone. The AI is still in-progress, and if anyone has suggestions how to make it better, great. If I forgot to add any more things you need to change / fix in the base classes, or if you have any questions or comments, let me know. I can be reached at au-heppa@hlssmod.net.

General

Some of the code is pretty old, and were written when I was still learning to code on Source. There might be bugs, or odd ways of doing things. I decided to release the code in pretty much impulse, so I didn't bother to clean anything up. It is what it is. This code is also the current code I am using in Human Error Co-op, so some of the code might point to code that was done for that multiplayer and or the Traitor Mode.

For Alien Controller, the telekinesis was disable for the Multiplayer, but you can re-enable it by defining HLSS_CONTROLLER_TELEKINESIS somewhere in the header file. Likewise Ragdoll grabbing has been disabled in the multiplayer, you can re-enable it by uncommenting the section in CNPC_AlienGrunt::MeleeAttack1Condtions.

There are some basic things I am leaving out here. I haven't done any tutorials before, and I am not entirely sure how detailed info I should add. You need to, for example, add CLASS_ALIENGRUNT, CLASS_ALIENCONTROLLER, CLASS_BEE into list in baseentity.h and then define the default relationships for all three classes in CHalfLife2::InitDefaultAIRelationships( void ). More info here. Just for sake of testing you can use CLASS_COMBINE instead. In the Classify() of both NPCs, use "return CLASS_COMBINE" instead of CLASS_ALIENCONTROLLER or CLASS_ALIENGRUNT.

You also need to remember to add the c_npc_aliencontroller.cpp to the client side while the other files are for the server.

In the code all sk_aliencontroller_health, sk_aliencontroller_dmg_claw, sk_aliengrunt_health, sk_aliengrunt_dmg_claw are set to zero. You can change them in code or add them to your skill.cfg. The values in Human Error were:
sk_aliengrunt_health "170"
sk_aliengrunt_dmg_claw "25"
sk_aliencontroller_health "80"
sk_aliencontroller_dmg_claw "16"

Alien Controller - SetSchedule

Because of the Alien Controller fly code was done: navigation was done by independent attack point selection, not affected by schedules. Because of this I had to make sure that the navigation path wouldn't be cleared every time the schedule would change. The SetSchedule function from the CAI_BaseNPC needed to be made virtual and overridden in the Alien Controller code. (Not the best way to do code, probably, but I haven't had yet time to redo the way Alien Controllers work). In ai_basenpc_schedule.cpp:

void CAI_BaseNPC::SetSchedule( CAI_Schedule *pNewSchedule, bool DontClearGoal )
{
	Assert( pNewSchedule != NULL );
	
	m_ScheduleState.timeCurTaskStarted = m_ScheduleState.timeStarted = gpGlobals->curtime;
	m_ScheduleState.bScheduleWasInterrupted = false;
	
	m_pSchedule = pNewSchedule ;
	ResetScheduleCurTaskIndex();
	SetTaskStatus( TASKSTATUS_NEW );
	m_failSchedule = SCHED_NONE;
	bool bCondInPVS = HasCondition( COND_IN_PVS );
	m_Conditions.ClearAll();
	if ( bCondInPVS ) 
		SetCondition( COND_IN_PVS );
	m_bConditionsGathered = false;
	if (!DontClearGoal) //TERO: THIS IS THE ONLY THING WE ADD/CHANGE HERE
		GetNavigator()->ClearGoal();
	m_InverseIgnoreConditions.SetAll();
	Forget( bits_MEMORY_TURNING );


	//TERO: after this is just the same as the original
	...
	...
	...
}
You only really need to add the bit with DontClearGloal, the rest of the function should stay the same. In ai_basenpc.h you need to change the declaration of SetSchedule into:

	virtual void		SetSchedule( CAI_Schedule *pNewSchedule, bool DontClearGoal = false );

Alien Controller - Centered Hull

You need to add the hull type of the Alien Controller into the hull types. To do this go to ai_hull.h and in the definition of the enum Hull_t after HULL_MEDIUM_TALL (Hunter) add HULL_HUMAN_CENTERED. Then in Hull_Bits_t under bits_MEDIUM_TALL_HULL add:
	bits_HUMAN_CENTERED			=	0x00000400,

In ai_hull.cpp under ai_hull_t Human_Hull_Centered (not HL1_DLL) add:

ai_hull_t Human_Hull_Centered (bits_HUMAN_CENTERED, "HUMAN_HULL_CENTERED", Vector(-16,-16, 0), Vector(16, 16, 35), Vector(-8,-8,0), Vector(8, 8, 35) );

In the definition of ai_hull_t* hull[NUM_HULLS] you need to add, under &Medium_Tall_Hull:

&Human_Hull_Centered,

Alien Controller - Simplify Fly Path

First go to ai_navigator.h and change the declaration of SimplifyFlyPath( const AI_ProgressFlyPathParams_t &params ) into:

	bool SimplifyFlyPath(  const AI_ProgressFlyPathParams_t &params, float flOffset = 0.0f );
Then go to ai_navigator.cpp and change SimplifyFlyPath( const AI_ProgressFlyPathParams_t &params ) into:

bool CAI_Navigator::SimplifyFlyPath(  const AI_ProgressFlyPathParams_t ¶ms, float flOffset )
{
	if ( !GetPath()->GetCurWaypoint() )
		return false;

	if ( m_flNextSimplifyTime > gpGlobals->curtime)
		return false;

	m_flNextSimplifyTime = gpGlobals->curtime + FLY_ROUTE_SIMPLIFY_TIME_DELAY;

	if ( params.bTrySimplify && SimplifyPathForward( FLY_ROUTE_SIMPLIFY_LOOK_DIST ) )
		return true;

	//TERO: added
	Vector vecOrigin = GetLocalOrigin() + Vector(0,0,flOffset);

	// don't shorten path_corners
	bool bIsStrictWaypoint = ( !params.bTrySimplify || ( (GetPath()->CurWaypointFlags() & (bits_WP_TO_PATHCORNER|bits_WP_DONT_SIMPLIFY) ) != 0 ) );

	Vector dir = GetCurWaypointPos() - vecOrigin;
	float length = VectorNormalize( dir );
	
	if ( !bIsStrictWaypoint || length < params.strictPointTolerance )
	{
		// FIXME: This seems strange... Why should this condition ever be met?
		// Don't advance your waypoint if you don't have one!
		if (GetPath()->CurWaypointIsGoal())
			return false;

		AIMoveTrace_t moveTrace;
		GetMoveProbe()->MoveLimit( NAV_FLY, vecOrigin, GetPath()->NextWaypointPos(),
			params.collisionMask, params.pTarget, &moveTrace);
		
		if ( moveTrace.flDistObstructed - params.blockTolerance < 0.01 || 
			 ( ( params.blockHandling == AISF_IGNORE) && ( moveTrace.fStatus == AIMR_BLOCKED_NPC ) ) )
		{
			AdvancePath();
			return true;
		}
		else if ( moveTrace.pObstruction && params.blockHandling == AISF_AVOID )
		{
			PrependLocalAvoidance( params.blockTolerance - moveTrace.flDistObstructed, moveTrace );
		}
	}

	return false;
}

Alien Grunt - Door Breaking

Alien Grunts (and Vortigaunts) were able to break doors flagged with the spawn flag SF_BREAKABLE_BY_AGRUNTS (you need to add this spawn flag to the door in your .fgd)

In doors.h after the definition of SF_DOOR_NEW_USE_RULES

#define SF_BREAKABLE_BY_AGRUNTS	   131072	// HUMAN ERROR: Is this door breakable by Alien Grunts? 
This should be the same number as the one in the .fgd

In BasePropDoor.h in the bottom of the class, just under m_OnLockedUse, CBasePropDoor add:

	COutputEvent m_OnBroken; //When the door gets broken

//HUMAN ERROR:
public:

	bool AreWeLocked( void );
	bool IsMyDoorLock( CBaseEntity *pLock );
	void BreakDoors(Vector vecOrigin, AngularImpulse angImpulse);
	void BreakDoor(Vector vecOrigin, AngularImpulse angImpulse);

	CBasePropDoor *HLSS_GetMaster( void ) { return m_hMaster; }

In props.cpp inside the BEGIN_DATADESC(CBasePropDoor) under DEFINE_OUTPUT(m_OnLockedUse, "OnLockedUse" ), add:
	DEFINE_OUTPUT(m_OnBroken, "OnBroken" ),
In props.cpp you should add:

bool CBasePropDoor::AreWeLocked( void )
{
	if (GetMaster())
	{
		return GetMaster()->IsDoorLocked();
	}

	return IsDoorLocked();
}

bool CBasePropDoor::IsMyDoorLock( CBaseEntity *pLock )
{
	if (GetMaster())
	{
		return GetMaster()->IsMyDoorLock( pLock );
	}

	int	numDoors = m_hDoorList.Count();

	CBasePropDoor *pLinkedDoor = NULL;

	// Open all linked doors
	for ( int i = 0; i < numDoors; i++ )
	{
		pLinkedDoor = m_hDoorList[i];

		if ( pLinkedDoor != NULL && pLinkedDoor == pLock->GetParent())
		{
			return true;
		}
	}

	return false;
}

void CBasePropDoor::BreakDoors(Vector vecOrigin, AngularImpulse angImpulse)
{
	//TERO: only break friends if we are closed
	if (IsDoorClosed())
	{
		if (GetMaster())
		{
			GetMaster()->BreakDoors(vecOrigin, angImpulse);
			return;
		}

		/*CBasePropDoor *pTarget = NULL;
		while ( (pTarget = (CBasePropDoor*)gEntList.FindEntityByName( pTarget, m_SlaveName )) != NULL)
		{
			if (pTarget != this)
			{
				pTarget->BreakDoor(vecOrigin, angImpulse);
			}
		}*/

		int	numDoors = m_hDoorList.Count();

		CBasePropDoor *pLinkedDoor = NULL;

		// Open all linked doors
		for ( int i = 0; i < numDoors; i++ )
		{
			pLinkedDoor = m_hDoorList[i];

			if ( pLinkedDoor != NULL )
			{
				// If the door isn't already moving, get it moving
				pLinkedDoor->BreakDoor(vecOrigin, angImpulse);
			}
		}
	}

	BreakDoor(vecOrigin, angImpulse);
}

void CBasePropDoor::BreakDoor(Vector vecOrigin, AngularImpulse angImpulse)
{
	//DevMsg("trying to create  physics prop");

	// Try to create entity
	CPhysicsProp *pProp = dynamic_cast< CPhysicsProp * >( CreateEntityByName( "prop_physics_multiplayer" ) );
	if ( pProp )
	{
		char buf[512];
		// Pass in standard key values
		Q_snprintf( buf, sizeof(buf), "%.10f %.10f %.10f", GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z );
		pProp->KeyValue( "origin", buf );
		Q_snprintf( buf, sizeof(buf), "%.10f %.10f %.10f", GetAbsAngles().x, GetAbsAngles().y, GetAbsAngles().z );
		pProp->KeyValue( "angles", buf );
		pProp->KeyValue( "model", STRING(GetModelName()) );
		pProp->KeyValue( "fademindist", "-1" );
		pProp->KeyValue( "fademaxdist", "0" );
		pProp->KeyValue( "fadescale", "1" );
		pProp->KeyValue( "inertiaScale", "1.0" );
		pProp->KeyValue( "physdamagescale", "0.1" );

		pProp->Precache();
		DispatchSpawn( pProp );
		pProp->m_nSkin = m_nSkin;
		pProp->SetBodygroup(1, GetBodygroup(1));
		pProp->Activate();

		IPhysicsObject *pPhysObj = pProp->VPhysicsGetObject();

		if( pPhysObj )
		{
			Vector v = WorldSpaceCenter() - vecOrigin;
			VectorNormalize(v);

			// Send the object at 800 in/sec toward the enemy.  Add 200 in/sec up velocity to keep it
			// in the air for a second or so.
			v = v * 1400;
			v.z += 100;

			pPhysObj->AddVelocity( &v, &angImpulse );
		}

		//TERO: since interactive debris doesn't allow collision with player, I needed to code this crap
		pProp->SetCollisionGroup( COLLISION_GROUP_DEBRIS );
		//TERO: the door should become debris after a time
		
		//CHLSS_Debris_Maker::Create( pProp );

		m_OnBroken.FireOutput( this, this );

		//m_OnOpen.FireOutput( this, this );

		RemoveSpawnFlags(SF_BREAKABLE_BY_AGRUNTS);


		UTIL_Remove( this );
	}
}

You also need to make OpenPropDoorNow( CBasePropDoor *pDoor ); virtual in ai_basenpc.h.

FGD

@NPCClass base(BaseNPC, TalkNPC, Targetname) studio("models/aliengrunt.mdl") = npc_aliengrunt : "Alien Grunt"
[
	spawnflags(Flags) = 
	[
		65536 :  "Can grab enemy ragdoll" : 0
		131072 : "Wait for ragdoll grab before shooting bees." : 0
	]

	input EnableRagdollGrab(void)	: "Enable enemy ragdoll grabbing."
	input DisableRagdollGrab(void)	: "Disable enemy ragdoll grabbing."
	input DisableWaitRagdollGrab(void) : "Disable waiting for ragdoll grab."

	input AttackItem(string) : "Attack prop_physics, or prop_door_rotating."

]

@NPCClass base(BaseNPC, TalkNPC) studio("models/controller.mdl") = npc_aliencontroller : "Alien Controller"
[
	spawnflags(Flags) = 
	[
		65536 :  "Can use Telekinesis" : 1
	]


	landed(choices) : "Start Flying" : 1 : "Should the Alien Controller start flying." =
	[
		0 : "No"
		1 : "Yes"
	]
	canland(choices) : "Can Land / Lift off" : 1 : "Can the Alien Controller Land / Lift off." =
	[
		0 : "No"
		1 : "Yes"
	]

	preferred_height(float) : "Preferred Height" : 256 : "The height we want to fly at."

	output OnLand(void) 	: "When Alien Controller lands on ground to walk."
	output OnLiftOff(void)	: "When Alien Controller lifts off from groun to fly."

	input EnableLanding(void)	: "Enable Landing / Lifting off."
	input DisableLanding(void) 	: "Disable Landing / Lifting off."

	input EnableTelekinesis(void)	: "Enable Telekinesis."
	input DisableTelekinesis(void) 	: "Disable Telekinesis."
	input StopScriptingAndGesture(void) : "Stop scripting and remove all gestures."

	output OnFoundEnemy(void) : "Fired when a non-player enemy is spotted within the inner radius."
]

Also for the breakable doors you need to add the spawn flag for the prop_door_rotating. Add it or replace the current prop_door_rotating bit, and it will override it from any base .fgd file:


@PointClass base(Targetname, Parentname, Angles, Global, Studiomodel) studioprop() = prop_door_rotating : 
	"An entity used to place a door in the world."
[


	spawnflags(flags) =
	[
		1 : "Starts Open" : 0
		//512: "NPCs Can't" : 0
		2048: "Starts locked" : 0
		4096: "Door silent (No sound, and does not alert NPCs)" : 0
		8192: "Use closes" : 1
		16384 : "Door silent to NPCS (Does not alert NPCs)" : 0
		32768 : "Ignore player +USE" : 0
		//65536 : "SF_DOOR_NEW_USE_RULES"  : 0
		131072 : "Breakable by Alien Grunts" : 0
	]

	output OnBroken(void) : "Fired when the door is broken by an NPC."


	//BELOW THIS IT'S JUST THE SAME AS THE ORIGINAL
	...
	...
	...
]

Script Files

For the Sounds you need to add the two script files into the sound manifest (The sound themselves are not included in this packet). Edit scripts/game_sounds_manifest.txt. Add the two lines into the bottom:

	"precache_file"		"scripts/npc_sounds_aliengrunt.txt"
	"precache_file"		"scripts/npc_sounds_aliencontroller.txt"
Both the Alien Controller and the Alien Grunt use response rules. You need to add the response rules files into the manifest. The two response rules scripts files from the talker folder go to [modpath]\scripts\talker\. In the same folder there should be response_rules.txt where you need to add in to the bottom:

#include "talker/npc_aliengrunt.txt"
#include "talker/npc_aliencontroller.txt"



Back to Tutorials

[ News | About | Media | Downloads | Tutorials | Contact ]



Valve, the Valve logo, Half-Life, the Half-Life logo, the Lambda logo, Steam, the Steam logo, Source, and the Source logo are trademarks and/or registered trademarks of Valve Software. All information is copyright their respective authors. Website designed by my brother.