Blastrobots

Blastrobots is a shared-screen multiplayer game inspired by the classic arcade style and built using UDK. It features a variety of weapons, a unique weapon combination system and a cooperative revive mechanic.

I worked on this project as a programmer. My responsibilities included the game’s artificial intelligence systems, implementing gameplay mechanics and providing scripting support for the level design team.

The game was developed by a team of thirteen over four months. The Wasted Talent team was extremely passionate and I enjoyed collaborating as a team and with each discipline independently.

Developing the AI

Developing the AI

Collaborating with the Game Designer and Level Designers, I implemented the enemy AI for Blastrobots. Our iterative design process allowed us to prototype enemies, find what about them was fun and what needed to be changed, then keep refining that prototype. The result is a wide variety of enemies, each with a unique personality in movement and attack styles.

Enemy Types

  • Astronaut
    -Fodder enemy, keeps at mid-range and fires periodically
  • Rushbot
    -Flies up to player and explodes
  • Laser Guardbot
    -Darts around screen, pausing to shoot players from long-range
  • Flamethrower Guardbot
    -Agressive short-range enemy, periodically overheats and explodes when killed
  • Rocket Guardbot
    -Exclusively long-range enemy, fires volleys of rockets
  • Spread Guardbot
    -Darts in and out of combat, turns on shield then fires extended volley of shots
  • Mobile Spanwer
    -Meanders around combat area, extremely durable and spawns other enemies
  • Turette(Boss)
    -Final boss for campaign, progresses through three phases and utilizes a variety of attacks

View Code

class BlastroAI extends UTBot
	abstract;

const MoveAngleGraduations = 16;
const OVERMOVE_RATIO = 2.6;
const STRAFE_ATTEMPTS = 4;

//=========================================================================
//=========================================================================
enum AIAction
{
	AI_WAIT, AI_APPROACH, AI_FLEE, AI_WANDER
};

var EnemyPawn ownPawn;
var BlastroPawn curTarget;

var AIAction curAction;
var Vector curDestination;

var Vector destDisp;
var float destDist;

var bool recentlyBumped;
var Vector bumpVec;

var float moveStepDist;

var Vector lastMoveVec;

// For movement sound
//
var SoundCue MovementSound;
var AudioComponent MovementSoundLoop;
var bool bShouldLoopMovementSound;

// Subclass preferences
var float MinPreferredDistance;
var float MaxPreferredDistance;
var float RangeTolerance;

var float FireRate;
var float MinFireRange;
var float MaxFireRange;

var bool bKeepPlayerInSight;
var float DecisionRate;

var float curDecisionRate;

var bool bFaceTarget;

var BlastroWeapon aiWeapon;
var class< BlastroWeapon > aiWeaponClass;

var float AIDifficulty;

//=========================================================================
// Difficulty settings
var float EasyFireRate;
var float HardFireRate;
var float InsaneFireRate;

var float EasyHealthModifier;
var float HardHealthModifier;
var float InsaneHealthModifier;

var float EasySpeedModifier;
var float HardSpeedModifier;
var float InsaneSpeedModifier;

//=========================================================================
function HearNoise( float Loudness, Actor NoiseMaker, optional name NoiseType ) { }

//=========================================================================
function AdjustToCurrentDifficulty()
{
	local EnemyPawn myPawn;
	myPawn = EnemyPawn( Pawn );

	myPawn.currentDifficultyHealthScale = DifficultyLerp( EasyHealthModifier, 1.0, HardHealthModifier, InsaneHealthModifier );
	myPawn.MaxDesiredSpeed *= DifficultyLerp( EasySpeedModifier, 1.0, HardSpeedModifier, InsaneSpeedModifier );

	FireRate = DifficultyLerp( EasyFireRate, default.FireRate, HardFireRate, InsaneFireRate );
}

//=========================================================================
function float DifficultyLerp( float easy, float normal, float hard, float insane )
{
	local float alpha;
	
	alpha = AIDifficulty;
	if ( alpha < 0.0 ) alpha = 0.0;
	if ( alpha > 5.0 ) alpha = 5.0;

	if ( AIDifficulty < 1.0 )
		return Lerp( easy, normal, AIDifficulty );
	else if ( AIDifficulty < 2.0 )
		return Lerp( normal, hard, AIDifficulty - 1.0 );
	else
		return Lerp( hard, insane, ( AIDifficulty - 2.0 ) / 3.0 );
}

//=========================================================================
//=========================================================================
function PostBeginPlay()
{
	Super.PostBeginPlay();

	AIDifficulty = BlastroGame( WorldInfo.Game ).globalDifficulty;
}

//=========================================================================
//=========================================================================
function Possess( Pawn aPawn, bool bVehicleTransition )
{
	Super.Possess( aPawn, bVehicleTransition );
	
	AdjustToCurrentDifficulty();
	EnemyPawn( Pawn ).ReassessHealth();

	aiWeapon = GivePawnBlastroWeapon( aiWeaponClass );
}

//=========================================================================
//=========================================================================
function BlastroWeapon GivePawnBlastroWeapon( class< BlastroWeapon > weaponType )
{
	local BlastroWeapon newWeapon;
	local UTPawn myPawn;

	if ( weaponType == none ) return none;

	myPawn = UTPawn( Pawn );
	if ( myPawn == none ) return none;

	// Spawn the blastro weapon
	newWeapon = Spawn( weaponType, self, , , , , );

	newWeapon.GiveTo( Pawn );
	Pawn.InvManager.SetCurrentWeapon( newWeapon );

	myPawn.CurrentWeaponAttachment.AttachTo( myPawn );

	// hide the weapon attachment mesh, but leave effects visible
	myPawn.CurrentWeaponAttachment.SetHidden( false );
	myPawn.CurrentWeaponAttachment.Mesh.SetRotation( rot(0, -16384, 0) ); // all are weapon attachments are sideways
	myPawn.CurrentWeaponAttachment.AttachComponent( myPawn.CurrentWeaponAttachment.Mesh );
	myPawn.CurrentWeaponAttachment.Mesh.SetHidden( true );

	newWeapon.SetOwner( Pawn );
	newWeapon.Instigator = Pawn;

	return newWeapon;
}

//=========================================================================
//=========================================================================
function SetMoveMultiplier( float multiplier )
{
	if ( Pawn != none )
	{
		Pawn.MaxDesiredSpeed = multiplier;
		moveStepDist = ownPawn.GroundSpeed * DecisionRate * OVERMOVE_RATIO * ownPawn.MaxDesiredSpeed;
	}
}

//=========================================================================
//=========================================================================
function Initialize(float InSkill, const out CharacterInfo BotInfo)
{
	Super.Initialize( InSkill, BotInfo );

	RotationRate.Yaw = 1.0;
	RotationRate.Pitch = 1.0;
	RotationRate.Roll = 1.0;
	AcquisitionYawRate = 1.0;
}

//=========================================================================
//=========================================================================
event bool NotifyBump(Actor Other, Vector HitNormal)
{
	recentlyBumped = true;
	bumpVec = HitNormal;

	StopLatentExecution();

	curDecisionRate = 0.15;

	Disable( 'NotifyBump' );
	Settimer( 0.20, false, 'EnableBumps' );
	SetTimer( 0.01, false, 'AILogicTick' );

	return false;
}

//=========================================================================
//=========================================================================
function EnableBumps() { enable('NotifyBump'); }
protected event ExecuteWhatToDoNext() { GotoState( 'BlastroAIMain' ); }

//=========================================================================
//=========================================================================
event WhatToDoNext()
{
	if (Pawn == None) return;

	RetaskTime = 0.0;
	DecisionComponent.bTriggered = true;
}

//=========================================================================
//=========================================================================
function bool DoPreventDeath()
{
	return false;
}

//=========================================================================
//=========================================================================
function bool Invincible()
{
	return false;
}

//=========================================================================
//=========================================================================
event AIFireTick()
{
	local Vector disp;
	local float distSq;

	if ( ownPawn == none ) return;

	// Do we see our target?
	if ( curTarget != none && FastTrace( curTarget.Location, Pawn.Location ) )
	{
		disp = curTarget.Location - Pawn.Location;
		distSq = disp dot disp;
		
		// Is our target close enough?
		if ( distSq >= MinFireRange * MinFireRange && distSq <= MaxFireRange * MaxFireRange )
			AIFire();
	}

	if ( FireRate != 0 ) SetTimer( FireRate + Rand( FireRate * 100 ) / 50, false, 'AIFireTick' );
}

//=========================================================================
//=========================================================================
function AIFire()
{
	// Override in subclasses
}

//=========================================================================
//=========================================================================
function ReassessTarget()
{
	local BlastroPawn curPlayerPawn;

	local BlastroPawn closestValidPawn;
	local Vector disp;
	local float closestDist;

	closestValidPawn = none;

	foreach AllActors( class'BlastroPawn', curPlayerPawn )
	{
		if ( !curPlayerPawn.IsAliveAndWell() || curPlayerPawn.bIsOverheating ) continue;

		if ( closestValidPawn == none )
		{
			closestValidPawn = curPlayerPawn;

			disp = Pawn.Location - curPlayerPawn.Location;
			closestDist = disp dot disp;
		}
		else
		{
			disp = Pawn.Location - curPlayerPawn.Location;
			if ( disp dot disp < closestDist )
			{
				closestValidPawn = curPlayerPawn;
				closestDist = disp dot disp;
			}
		}
	}

	curTarget = closestValidPawn;
}

//=========================================================================
//=========================================================================
function SetAction( AIAction act, Vector dest )
{
	curAction = act;
	curDestination = dest;
	
	if ( curAction == AI_WAIT )
	{
		if ( curTarget == none )
			Pawn.SetDesiredRotation( Pawn.Rotation, true, false, 0.0, false );

		// If waiting, don't play movement sound loop
		//
		if ( MovementSoundLoop != none )
			MovementSoundLoop.Stop();
	}
	else
	{
		Pawn.LockDesiredRotation( false, true );
	}

	StopLatentExecution();
}

//=========================================================================
//=========================================================================
function bool MoveTowards( Actor A )
{
	if ( NavigationHandle.ActorReachable( A ) )
	{
		// Target can be reached by directly walking
		curDestination = A.Location;
	}
	else
	{
		// Target _cannot_ be reached by directly walking
		NavigationHandle.SetFinalDestination( A.Location );

		// Clear cache and constraints (ignore recycling for the moment)
		NavigationHandle.PathConstraintList = none;
		NavigationHandle.PathGoalList = none;

		// Create constraints
		class'NavMeshPath_Toward'.static.TowardGoal( NavigationHandle, A);
		class'NavMeshGoal_At'.static.AtActor( NavigationHandle, A );

		if ( !NavigationHandle.FindPath() ) return false;

		NavigationHandle.GetNextMoveLocation( curDestination, Pawn.GetCollisionRadius() );
	}
	
	SetAction( AI_APPROACH, curDestination );
	return true;
}

//=========================================================================
//=========================================================================
function bool FleeFrom( Actor A )
{
	local Vector idealVec;
	local Vector testDest;
	local Rotator curAngle;
	local int flip;
	
	for ( idealVec = Normal( Pawn.Location - A.Location ) * moveStepDist;
		curAngle.Yaw <= 65536 * 5 / 16;
		curAngle.Yaw += 65536 / MoveAngleGraduations )
	{
		for ( flip = 0; flip < 2; ++flip )
		{
			testDest = Pawn.Location + TransformVectorByRotation( curAngle, idealVec, ( flip == 1 ) );

			// Can we get there in the first place?
			if ( NavigationHandle.PointReachable( testDest ) )
			{
				// This is our final choice if we we can see the player from that
				// spot *or* we don't care about keeping the player in sight
				if ( !bKeepPlayerInSight || FastTrace( testDest, curTarget.Location ) )
				{
					SetAction( AI_FLEE, testDest );
					return true;
				}
			}
		}
	}
	
	// Couldn't find any viable flee point
	return false;
}

//=========================================================================
//=========================================================================
function bool IsPositionSuitable( Vector testPos )
{
	local Vector testDisp;
	local float testDistSq;

	local Vector curDisp;
	local float curDistSq;

	local Vector moveVec;

	// Test for range problems (irrelevant if no target)
	if ( curTarget != none )
	{
		testDisp = testPos - curTarget.Location;
		testDistSq = testDisp dot testDisp;

		curDisp = curTarget.Location - Pawn.Location;
		curDistSq = curDisp dot curDisp;

		moveVec = testPos - Pawn.Location;

		if ( testDistSq > MaxPreferredDistance * MaxPreferredDistance )
		{
			// If we were close enough before, this position is bad
			if ( curDistSq <= MaxPreferredDistance * MaxPreferredDistance )
				return false;
			
			// If we are too far away, this would need to move us closer
			if ( curDisp dot moveVec <= 0 ) return false;
		}
		else if ( testDistSq < MinPreferredDistance * MinPreferredDistance )
		{
			// If we were too close before, this position is abd
			if ( curDistSq >= MinPreferredDistance * MinPreferredDistance )
				return false;
			
			// If we are too close, this would need to move us away
			if ( curDisp dot moveVec >= 0 ) return false;
		}
	}
	
	// Check for sight
	if ( curTarget != none && bKeepPlayerInSight && !FastTrace( testPos, curTarget.Location ) )
		return false;

	return true;
}

//=========================================================================
//=========================================================================
function bool KeepMoving( int error = 0 )
{
	local Vector testDest;
	local Rotator errorAngle;

	errorAngle.Yaw = Rand( error ) - error / 2;

	if ( lastMoveVec dot lastMoveVec == 0.0 ) return false;

	testDest = Pawn.Location + TransformVectorByRotation( errorAngle, lastMoveVec * moveStepDist );
	
	if ( NavigationHandle.PointReachable( testDest ) && IsPositionSuitable( testDest ) )
	{
		SetAction( AI_WANDER, testDest );
		return true;
	}

	return false;
}

//=========================================================================
//=========================================================================
function bool MoveRandomly()
{
	local Vector basisVec;

	local int graduation[ MoveAngleGraduations ];
	local int remainingAngles;
	local int i;
	local int curGraduation;
	
	local Vector testDest;
	local Vector bestDest;
	local bool foundDest;

	local Rotator curAngle;

	basisVec.X = moveStepDist;

	for ( i = 0; i < MoveAngleGraduations; ++i ) graduation [ i ] = i;
	remainingAngles = MoveAngleGraduations;
	
	foundDest = false;

	// Pick one of the remaining graduations to try
	while ( remainingAngles > 0 )
	{
		i = Rand( remainingAngles );
		curGraduation = graduation[ i ];
		--remainingAngles;
		graduation[ i ] = graduation[ remainingAngles ];

		curAngle.Yaw = curGraduation * ( 65536 / MoveAngleGraduations );
		testDest = Pawn.Location + TransformVectorByRotation( curAngle, basisVec );

		// Can we get there in the first place?
		if ( NavigationHandle.PointReachable( testDest ) )
		{
			if ( IsPositionSuitable( testDest ) )
			{
				SetAction( AI_WANDER, testDest );
				return true;
			}
			else
			{
				foundDest = true;
				bestDest = testDest;
			}
		}
	}
	
	if ( foundDest )
	{
		SetAction( AI_WANDER, bestDest );
		return true;
	}
	
	return false;
}

//=========================================================================
//=========================================================================
function bool Strafe()
{
	local Vector basisVec;
	local Rotator angle;
	local Vector testPos;

	local float temp;
	local int i;
	local int j;

	// Move laterally with respect to the target
	if ( curTarget == none ) return false;

	basisVec = Pawn.Location - curTarget.Location;
	basisVec.Z = 0;
	basisVec = Normal( basisVec ) * moveStepDist;
	
	// Fast rotate 90 degrees
	temp = basisVec.X;
	basisVec.X = basisVec.Y;
	basisVec.Y = -temp;
	
	// Prefer to keep rotating about the player in the same direction
	// Of if we weren't moving pick a preference randomly
	if ( ( lastMoveVec dot basisVec < 0 ) || ( lastMoveVec dot lastMoveVec == 0 && Rand( 2 ) == 0 ) )
		basisVec = -basisVec;
	
	for ( i = 0; i < 2; ++i )
	{
		for ( j = 0; j < STRAFE_ATTEMPTS; ++j )
		{
			angle.Yaw = Rand( 63336 / 4 ) - 65536 / 8;
			testPos = Pawn.Location + TransformVectorByRotation( angle, basisVec );
			
			if ( IsPositionSuitable( testPos ) )
			{
				SetAction( AI_WANDER, testPos );
				return true;
			}
		}

		basisVec = -basisVec;
	}

	return false;
}

//=========================================================================
//=========================================================================
function bool PickTargetRelativeDestination()
{
	local Vector targetDisp;
	local float targetDistSq;
	local bool bTargetVisible;

	local bool bNeedToApproach;
	local bool bNeedToFlee;

	if ( recentlyBumped && MoveRandomly() ) return true;
	
	bTargetVisible = FastTrace( curTarget.Location, Pawn.Location,  );
	// Do we want to get closer, farther, or some other direction?
	
	targetDisp = curTarget.Location - Pawn.Location;
	targetDistSq = targetDisp dot targetDisp;
	
	bNeedToApproach = bKeepPlayerInSight && !bTargetVisible;

	if ( curAction == AI_APPROACH )
	{
		if ( targetDistSq > ( MaxPreferredDistance - RangeTolerance ) * 
			( MaxPreferredDistance - RangeTolerance ) )
			bNeedToApproach = true;
	}
	else
	{
		if ( targetDistSq > ( MaxPreferredDistance + RangeTolerance ) * 
			( MaxPreferredDistance + RangeTolerance ) )
			bNeedToApproach = true;
	}

	// Is our target too far away?
	if ( bNeedToApproach )
	{
		if ( !MoveTowards( curTarget ) )
		{
			//`log( "AI [" @ self @ "] unable to reach target, choosing new target" );
			//SetAction( AI_WAIT, curDestination );
			return false;
		}

		return true;
	}

	bNeedToFlee = false;

	if ( curAction == AI_FLEE )
	{
		if ( targetDistSq < ( MinPreferredDistance + RangeTolerance ) * 
			( MinPreferredDistance + RangeTolerance ) )
			bNeedToFlee = true;
	}
	else
	{
		if ( targetDistSq < ( MinPreferredDistance - RangeTolerance ) * 
			( MinPreferredDistance - RangeTolerance ) )
			bNeedToFlee = true;
	}
	
	// Is our target too close?
	if ( bNeedToFlee )
	{
		// Attempt to flee
		if ( FleeFrom( curTarget ) ) return true;
	}
	
	return false;
}

//=========================================================================
//=========================================================================
function bool PickWanderDestination()
{
	if ( curAction == AI_WANDER )
	{
		SetAction( AI_WAIT, curDestination );
		return true;
	}
	else
	{
		if ( MoveRandomly() ) return true;
	}
	return false;
}

//=========================================================================
//=========================================================================
function AILogicTick()
{
	if ( ownPawn == none ) return;

	// Move about as far in one step as it takes to make our next decision
	moveStepDist = ownPawn.GroundSpeed * DecisionRate * OVERMOVE_RATIO * ownPawn.MaxDesiredSpeed;
	//if ( lastMoveVec dot lastMoveVec == 0.0 ) lastMoveVec.X = 1.0;
		
	ReassessTarget();
		
	if ( curTarget != none && PickTargetRelativeDestination() )
	{

	}
	else if ( PickWanderDestination() )
	{

	}
	else
	{
		SetAction( AI_WAIT, Pawn.Location );
	}
		
	recentlyBumped = false;

	destDisp = curDestination - Pawn.Location;
	destDisp.Z = 0;
	lastMoveVec = Normal( destDisp );
	
	SetTimer( curDecisionRate, false, 'AILogicTick' );

	curDecisionRate = DecisionRate;
}

state Dead
{
ignores SeePlayer, EnemyNotVisible, HearNoise, ReceiveWarning, NotifyLanded, NotifyPhysicsVolumeChange,
		NotifyHeadVolumeChange, NotifyHitWall, NotifyBump, ExecuteWhatToDoNext;

	function BeginState(Name PreviousStateName)
	{
		if ( MovementSoundLoop != none && MovementSoundLoop.IsPlaying() )
			MovementSoundLoop.Stop();

		if ( Class == class'RushbotAI' )
			RushbotAI( self ).SirenSound.Stop();

		super.BeginState( PreviousStateName );
	}
}

//=========================================================================
//=========================================================================
state BlastroAIMain
{
	protected event ExecuteWhatToDoNext()
	{
		// Ignore UTBot default behaviors
	}

Begin:
	Instigator = Pawn;
	ownPawn = EnemyPawn( Pawn );
	curAction = AI_WAIT;
	curDecisionRate = DecisionRate;

	if ( FireRate != 0 ) SetTimer( 0.1, false, 'AIFireTick' );
	SetTimer( 0.1, false, 'AILogicTick' );

	if ( MovementSound != none && bShouldLoopMovementSound )
	{
		MovementSoundLoop = Instigator.CreateAudioComponent( MovementSound, false );
	}

	while ( Pawn != none )
	{
		// Play movement loop sound
		//
		if ( bShouldLoopMovementSound && !MovementSoundLoop.IsPlaying() )
			MovementSoundLoop.Play();

		if ( curAction == AI_WAIT )
		{
			if ( bFaceTarget )
				MoveTo( Pawn.Location, curTarget );
			else
				MoveTo( Pawn.Location );
			Sleep( 0.1 );
		}
		else
			if ( bFaceTarget )
				MoveTo( curDestination, curTarget );
			else
				MoveTo( curDestination );
	}

	MovementSoundLoop.Stop();

	ClearAllTimers();
	GotoState( 'Dead' );
}

//=========================================================================
//=========================================================================
defaultproperties
{
	MinPreferredDistance = 500.0
	MaxPreferredDistance = 1200.0
	RangeTolerance = 0.0

	FireRate = 1.0
	MinFireRange = 0.0
	MaxFireRange = 1200.0

	MovementSound = none
	bShouldLoopMovementSound = false

	DecisionRate = 0.4

	bKeepPlayerInSight = true
	bFaceTarget = true;
	
	// Difficulty settings
	EasyFireRate = 1.0;
	HardFireRate = 1.0;
	InsaneFireRate = 1.0;
	
	EasyHealthModifier = 0.90;
	HardHealthModifier = 1.15;
	InsaneHealthModifier = 2.5;

	EasySpeedModifier = 1.0;
	HardSpeedModifier = 1.0;
	InsaneSpeedModifier = 1.0;
}
class AstronautAI extends BlastroAI;

var int FireError;
var int FloatError;

//=========================================================================
//=========================================================================
function Possess( Pawn aPawn, bool bVehicleTransition )
{
	Super.Possess( aPawn, bVehicleTransition );
	
	PlayIdleAnim();
}

//=========================================================================
//=========================================================================
function PlayIdleAnim()
{
	EnemyPawn( Pawn ).FullBodyAnimSlot.PlayCustomAnim('BB_astronaut_01_idle', 1.0, 0.1, 0.1, true, true );
}

//=========================================================================
//=========================================================================
function bool PickTargetRelativeDestination()
{
	Pawn.MaxDesiredSpeed = 1.0;
	
	if ( Super.PickTargetRelativeDestination() ) return true;
	
	Pawn.MaxDesiredSpeed = 0.2;
	
	if ( KeepMoving( FloatError ) ) return true;

	if ( MoveRandomly() ) return true;

	SetAction( AI_WAIT, curDestination );	
	return true;
}

//=========================================================================
//=========================================================================
function AIFire()
{
	local Vector dir;
	local Vector idealDir;
	local Vector disp;
	local float dist;
	local Rotator error;
	local float speedEstimate;

	if ( curTarget == none ) return;

	disp = curTarget.Location - Pawn.Location;
	dist = Sqrt( disp dot disp );

	if ( AIDifficulty < 2.0 )
		speedEstimate = 0.002;
	else
		speedEstimate = 0.001;

	idealDir = Normal( curTarget.Location + curTarget.Velocity * dist * speedEstimate - Pawn.Location );

	dir = TransformVectorByRotation( error,  idealDir );

	Pawn.SetRotation( rotator(dir) );
	if ( AIDifficulty < 2.0 )
		aiWeapon.Fire( 0 );
	else
		aiWeapon.Fire( 1 );

	EnemyPawn( Pawn ).FullBodyAnimSlot.PlayCustomAnim('BB_astronaut_01_shoot', 1.0, 0.1, 0.1, false, false );
	SetTimer( 0.2333, false, 'PlayIdleAnim' );
}

//=========================================================================
//=========================================================================
DefaultProperties
{
	aiWeaponClass = class'BlastroWeaponLemon'
	//aiWeaponClass = class'BlastroWeaponLockonRocket'

	bKeepPlayerInSight = true

	MaxPreferredDistance = 900.0
	MinPreferredDistance = 200.0

	DecisionRate = 0.4

	FireRate = 1.67
	MinFireRange = 0.0
	MaxFireRange = 1200.0

	// 65536 / 4 = 90 degree cone
	FireError = 2048

	FloatError = 2048

	// Difficulty settings
	EasyFireRate = 2.1;
	HardFireRate = 1.3;
	InsaneFireRate = 0.4;
}
class SpreadGuardbotAI extends BlastroAI;

var float SpreadCooldown;
var float SpreadWarmup;

var int SpreadNumTrails;
var int SpreadTrail;
var int SpreadRotation;

var float SpreadFireRange;

var ParticleSystemComponent shieldEffect;

enum SpreadState
{
	SPREAD_COOLDOWN, SPREAD_APPROACH, SPREAD_FIRING
};

var SpreadState curSpreadState;

var float stateStartTime;

var int spreadFireCount;
var Rotator curSpreadAngle;

var int EasyTrails;
var int HardTrails;
var int InsaneTrails;

var int EasyRotation;
var int HardRotation;
var int InsaneRotation;

var float EasyCooldown;
var float HardCooldown;
var float InsaneCooldown;

//=========================================================================
//=========================================================================
function AdjustToCurrentDifficulty()
{
	Super.AdjustToCurrentDifficulty();

	SpreadNumTrails = DifficultyLerp( EasyTrails, default.SpreadNumTrails, HardTrails, InsaneTrails );
	SpreadTrail = SpreadNumTrails;

	SpreadRotation = DifficultyLerp( EasyRotation, default.SpreadRotation, HardRotation, InsaneRotation );

	SpreadCooldown = DifficultyLerp( EasyCooldown, default.SpreadCooldown, HardCooldown, InsaneCooldown );
}

//=========================================================================
//=========================================================================
function Possess( Pawn aPawn, bool bVehicleTransition )
{
	super.Possess( aPawn, bVehicleTransition );
	
	stateStartTime = WorldInfo.TimeSeconds - SpreadCooldown + Rand( SpreadCooldown * 25 ) * 0.01;
}

//=========================================================================
function SetShieldActive( bool bActive )
{
	local EnemyPawn myPawn;
	local int i;

	if ( ( bActive && shieldEffect != none ) || ( !bActive && shieldEffect == none ) ) return;

	if ( bActive )
	{
		shieldEffect = WorldInfo.MyEmitterPool.SpawnEmitter(
			ParticleSystem'BB_Item_Particles.Particles.spreadbot_shield_01',
			Pawn.Location, Pawn.Rotation, Pawn);
		
		if ( shieldEffect != none )
			shieldEffect.SetScale( 0.5 );

		// Turn on shield sound
		//
		PlaySound( SoundCue'BlastroSFX.FlameGun.Flame_Shoot_2_Cue' );

		myPawn = EnemyPawn( Pawn );
		if ( myPawn != none )
		{
			for ( i = 0; i < myPawn.Resistances.Length; ++i )
				myPawn.Resistances[ i ].amt = 1.0;
		}
	}
	else
	{
		shieldEffect.DetachFromAny();
		shieldEffect.DeactivateSystem();
		shieldEffect = none;

		myPawn = EnemyPawn( Pawn );
		if ( myPawn != none )
		{
			for ( i = 0; i < myPawn.Resistances.Length; ++i )
				myPawn.Resistances[ i ].amt = 0.0;
		}
	}
}

//=========================================================================
function SetSpreadState( SpreadState newState )
{
	if ( newState == curSpreadState ) return;

	if ( newState == SPREAD_COOLDOWN )
	{
		SetShieldActive( false );

		Pawn.MaxDesiredSpeed = 1.0;

		stateStartTime = WorldInfo.TimeSeconds;
	}

	if ( newState == SPREAD_APPROACH )
	{
		if ( AIDifficulty >= 2.0 )
			SetShieldActive( true );

		Pawn.MaxDesiredSpeed = 3.0;
	}

	if ( newState == SPREAD_FIRING )
	{
		SetShieldActive( true );

		Pawn.MaxDesiredSpeed = 1.0;
		spreadFireCount = -2;
		curSpreadAngle.Yaw = 0;
	}

	curSpreadState = newState;
}

//=========================================================================
//=========================================================================
event AIFireTick()
{
	local Vector disp;
	local float distSq;

	if ( ownPawn == none ) return;

	if ( curSpreadState == SPREAD_FIRING )
	{
		if ( ++spreadFireCount > SpreadTrail )
		{
			SetSpreadState( SPREAD_COOLDOWN );
		}
		else if ( spreadFireCount >= 0 )
		{
			AIFire();
			curSpreadAngle.Yaw += SpreadRotation;
		}
	}
	
	if ( curSpreadState == SPREAD_COOLDOWN )
	{
		if ( stateStartTime + SpreadCooldown < WorldInfo.TimeSeconds )
			SetSpreadState( SPREAD_APPROACH );
	}

	if ( curSpreadState == SPREAD_APPROACH )
	{
		// Do we see our target?
		if ( curTarget != none && FastTrace( curTarget.Location, Pawn.Location ) )
		{
			disp = curTarget.Location - Pawn.Location;
			distSq = disp dot disp;
		
			// Is our target close enough?
			if ( distSq >= MinFireRange * MinFireRange && distSq <= MaxFireRange * MaxFireRange )
				SetSpreadState( SPREAD_FIRING );
		}
	}

	SetTimer( FireRate, false, 'AIFireTick' );
}


//=========================================================================
//=========================================================================
function AIFire()
{
	local int i;
	local Vector dir;
	local Vector basis;
	local Rotator angle;
	local Rotator tmprot;
		
	basis.X = 1.0;

	tmprot = Pawn.Rotation;
	for ( i = 0; i < SpreadNumTrails; ++i )
	{
		angle.Yaw = curSpreadAngle.Yaw + i * ( 65536 / SpreadNumTrails );

		dir = TransformVectorByRotation( angle, basis );
		
		Pawn.SetRotation( Rotator(dir) );

		if ( AIDifficulty < 2.0 )
			aiWeapon.Fire( 0 );
		else
			aiWeapon.Fire( 1 );
	}
	Pawn.SetRotation( tmprot );
}

//=========================================================================
//=========================================================================
function bool PickTargetRelativeDestination()
{
	if ( curSpreadState == SPREAD_APPROACH )
	{
		if ( curTarget != none && MoveTowards( curTarget ) ) return true;

		// lost our target?
		SetSpreadState( SPREAD_COOLDOWN );
	}

	if ( curSpreadState == SPREAD_COOLDOWN )
	{
		if ( Super.PickTargetRelativeDestination() ) return true;

		if ( KeepMoving() ) return true;

		if ( Strafe() ) return true;

		if ( MoveRandomly() ) return true;
	}


	if ( curSpreadState == SPREAD_FIRING )
	{
		SetAction( AI_WAIT, Pawn.Location );
		return true;
	}

	return false;
}

//=========================================================================
event Destroyed()
{
	SetShieldActive( false );
}

//=========================================================================
DefaultProperties
{
	aiWeaponClass = class'BlastroWeaponShotgun'

	bKeepPlayerInSight = false

	MaxPreferredDistance = 1600.0
	MinPreferredDistance = 800.0

	DecisionRate = 0.25

	FireRate = .5//.3//0.15
	MinFireRange = 0.0
	MaxFireRange = 500.0

	SpreadCooldown = 12.0
	SpreadWarmup = 1.5
	SpreadRotation = 1024
	SpreadNumTrails = 8
	SpreadTrail = 8

	SpreadFireRange = 500

	curSpreadState = SPREAD_COOLDOWN

	// Difficulty settings
	EasyFireRate = 0.55;
	HardFireRate = 0.35;
	InsaneFireRate = 0.10;

	EasyTrails = 6;
	HardTrails = 10;
	InsaneTrails = 14;

	EasyRotation = 1024;
	HardRotation = 1024;
	InsaneRotation = 512;

	EasyCooldown = 14.0;
	HardCooldown = 7.0;
	InsaneCooldown = 2.0;
}
class LaserGuardbotAI extends BlastroAI;

const WAIT_TICKS = 9;
const MOVE_TICKS = 2;

var int counter;

enum LaserState {
	LASER_STATE_MOVING, LASER_STATE_WAITING, LASER_STATE_CHARGING, LASER_STATE_STOPPING
};

var LaserState curState;
var float stateEndTime;

var float StopTime;
var float NormalWaitTime;
var float PostFireWaitTime;
var float DartMoveTime;
var float ChargeTime;

var float EasyChargeTime;
var float HardChargeTime;
var float InsaneChargeTime;

var float EasyWaitTime;
var float HardWaitTime;
var float InsaneWaitTime;

var float EasyStopTime;
var float HardStopTime;
var float InsaneStopTime;


//=========================================================================
//=========================================================================
function AdjustToCurrentDifficulty()
{
	Super.AdjustToCurrentDifficulty();

	ChargeTime = DifficultyLerp( EasyChargeTime, default.ChargeTime, HardChargeTime, InsaneChargeTime );

	NormalWaitTime = DifficultyLerp( EasyWaitTime, default.NormalWaitTime, HardWaitTime, InsaneWaitTime );

	PostFireWaitTime = NormalWaitTime * ( default.PostFireWaitTime / default.NormalWaitTime );

	StopTime = DifficultyLerp( EasyStopTime, default.StopTime, HardStopTime, InsaneStopTime );
}

//=========================================================================
//=========================================================================
function AIFire()
{
	aiWeapon.CancelFire();

	aiWeapon.Fire( 0 );
}

//=========================================================================
//=========================================================================
function FireTrace()
{
	local Rotator newFacing;

	if ( curTarget == none ) return;

	newFacing = Pawn.DesiredRotation;
	newFacing.Yaw += Rand( 4192 ) - 4192 / 2;

	Pawn.SetRotation( newFacing );
	Pawn.SetDesiredRotation( newFacing, true, false, 0.0, false );

	aiWeapon.Fire( 1 );
}

//=========================================================================
//=========================================================================
function bool IsPlayerTargetable()
{
	local Vector disp;
	local float distSq;

	if ( ownPawn == none ) return false;

	// Do we see our target?
	if ( curTarget != none && FastTrace( curTarget.Location, Pawn.Location ) )
	{
		disp = curTarget.Location - Pawn.Location;
		distSq = disp dot disp;
		
		// Is our target close enough?
		if ( distSq >= MinFireRange * MinFireRange && distSq <= MaxFireRange * MaxFireRange )
			return true;
	}

	return false;
}

//=========================================================================
//=========================================================================
function AILogicTick()
{
	bKeepPlayerInSight = true;

	if ( ownPawn == none ) return;

	// Move about as far in one step as it takes to make our next decision
	moveStepDist = ownPawn.GroundSpeed * DecisionRate * OVERMOVE_RATIO * ownPawn.MaxDesiredSpeed;
	//if ( lastMoveVec dot lastMoveVec == 0.0 ) lastMoveVec.X = 1.0;
		
	ReassessTarget();

	if ( WorldInfo.TimeSeconds > stateEndTime )
	{
		// Move to next state
		switch ( curState )
		{
		case LASER_STATE_MOVING:
			curState = LASER_STATE_STOPPING;
			stateEndTime = WorldInfo.TimeSeconds + StopTime;

			SetAction( AI_WAIT, Pawn.Location );
			break;

		case LASER_STATE_STOPPING:
			if ( IsPlayerTargetable() )
			{
				FireTrace();
				curState = LASER_STATE_CHARGING;
				stateEndTime = WorldInfo.TimeSeconds + ChargeTime;
			}
			else
			{
				curState = LASER_STATE_WAITING;
				stateEndTime = WorldInfo.TimeSeconds + NormalWaitTime;
			}
			break;

		case LASER_STATE_CHARGING:
			AIFire();
			curState = LASER_STATE_WAITING;
			stateEndTime = WorldInfo.TimeSeconds + PostFireWaitTime;
			break;

		case LASER_STATE_WAITING:
			Pawn.LockDesiredRotation( false, true );

			// Play "dash" sound
			//
			PlaySound( SoundCue'BlastroSFX.Scifi_Passby1_Cue' );

			if ( curTarget != none && PickTargetRelativeDestination() ) { }
			else MoveRandomly();

			curState = LASER_STATE_MOVING;
			stateEndTime = WorldInfo.TimeSeconds + DartMoveTime * ( Rand( 40 ) + 80 ) * 0.01;
			break;
			
		}
	}
	else
	{
		// Still in current state
		switch ( curState )
		{
		case LASER_STATE_MOVING:
			if ( recentlyBumped )
			{
				stateEndTime = WorldInfo.TimeSeconds;
				curDecisionRate = 0.01;
				// TODO - bounce if recentlybumped
				
				break;
			}
			
			if ( !IsPlayerTargetable() )
				bKeepPlayerInSight = false;

			if ( !KeepMoving() )
			{
				stateEndTime = WorldInfo.TimeSeconds;
				curDecisionRate = 0.01;

			}

			break;

		case LASER_STATE_STOPPING:
		case LASER_STATE_CHARGING: 
		case LASER_STATE_WAITING: SetAction( AI_WAIT, Pawn.Location ); break;
		}		
	}

	recentlyBumped = false;

	destDisp = curDestination - Pawn.Location;
	destDisp.Z = 0;
	lastMoveVec = Normal( destDisp );
	
	SetTimer( curDecisionRate, false, 'AILogicTick' );

	curDecisionRate = DecisionRate;
}

//=========================================================================
//=========================================================================
DefaultProperties
{
	aiWeaponClass = class'BlastroWeaponLaser'

	curState = LASER_STATE_WAITING

	StopTime = 0.5
	NormalWaitTime = 1.0
	PostFireWaitTime = 0.625//0.5
	DartMoveTime = 0.5//2.0;
	ChargeTime = 0.6;

	stateEndTime = -1

	bKeepPlayerInSight = true

	MaxPreferredDistance = 1100.0
	MinPreferredDistance = 200.0

	DecisionRate = 0.10

	FireRate = 0.0
	MinFireRange = 0.0
	MaxFireRange = 1300.0

	counter = 0;

	// Difficulty settings
	EasyFireRate = 0.0;
	HardFireRate = 0.0;
	InsaneFireRate = 0.0;

	EasyChargeTime = 0.8;
	HardChargeTime = 0.4;
	InsaneChargeTime = 0.3;

	EasyWaitTime = 1.2;
	HardWaitTime = 0.8;
	InsaneWaitTime = 0.2;

	EasyStopTime = 0.5;
	HardStopTime = 0.4;
	InsaneStopTime = 0.1;
}