/*
* Neural Tanks is a game created by Eddie O'Hagan that leverages Neural Networking
* to use for AI. This was created in Unreal Engine 4 which is developed by Epic Games.
* (Copyright Epic Games, Inc. All Rights Reserved.)
*
* Smart Tanks was originally developed by Mat Buckland in 2002 as an example
* on how Neural Networking can be used to train a set of minesweepers
* to collect mines. I (Eddie O'Hagan) updated this project in 2024 to use more
* Object Oriented Programming and modern practices. To see the original tutorial;
* visit <see href="http://www.ai-junkie.com/ann/evolved/nnt1.html">Neural Networking Tutorial</see>
* and <see href="http://www.ai-junkie.com/ga/intro/gat1.html">Genetic Algorithm Tutorial</see>
*/
#include "PhysicsTankPawn.h"
#include "../SavingAndLoading/TankItemData.h"
#include "../Utils/TrackPos.h"
#include "../Utils/TrackPoint.h"
#include "../Utils/NeuralTanksUtils.h"
#include <Components/AudioComponent.h>
#include <Components/SplineComponent.h>
#include <Components/HierarchicalInstancedStaticMeshComponent.h>
#include <Components/LineBatchComponent.h>
#include <Components/StaticMeshComponent.h>
#include <Engine/StaticMesh.h>
#include <Kismet/GameplayStatics.h>
#include <Particles/ParticleSystemComponent.h>
#include <PhysicsEngine/PhysicsConstraintComponent.h>
#include <SceneManagement.h>

/// <summary>
/// Default constructor.
/// </summary>
APhysicsTankPawn::APhysicsTankPawn() : Super()
{
	myIsDestroyed = false;
	myIsShiftingGears = false;
	myHasStarted = false;
	myStartupTime = 5.0f;
	myTankHandling = 1.0f;
	myRPMRate = 0.01f;
	myBreakingRate = -0.1f;
	myBreakingThreshold = 6.0f;
	myTrackSpeedMultiplier = 1.0f;
	myTrackUpdateFrequency = 0.02f;
	myTrackLength = 20.0f;
	myTrackZOffset = -35.0f;
	mySuspensionZOffset = 20.0f;
	mySuspensionStrength = 1500.0f;
	mySuspensionDamping = 500.0f;
	myWheelRotationMultiplier = 25.0f;
	myWheelDistanceFromCenter = 150.0f;

	PrimaryActorTick.bCanEverTick = true;
}

/// <summary>
/// Called when this blueprint is first opened or initialized. This is equivalent to a 
/// constructor but for Unreal Engine. 
/// </summary>
/// <param name="transform">The transform the actor was constructed at.</param>
void APhysicsTankPawn::OnConstruction(const FTransform& transform)
{
	TArray<UActorComponent*> components;

	GetComponents(components);

	for (size_t index = 0U; index < components.Num(); index++)
	{
		myComponents.Add(components[index]->GetFName(), components[index]);
	}

	FindComponents();
}

/// <summary>
/// Called when this tank is first spawned in the world.
/// </summary>
void APhysicsTankPawn::BeginPlay()
{
	Super::BeginPlay();

	Initialize();
}

/// <summary>
/// Starts the tank (and allows control of it) after myStartupTime seconds.
/// </summary>
void APhysicsTankPawn::Initialize()
{
	AdjustSuspensions();

	if (myCamera != NULL)
	{
		myCamera->Activate();
	}

	//Attempt to get the level's top down camera.
	myLevelCamera = Cast<ACameraActor>(UGameplayStatics::GetActorOfClass(GetWorld(), ACameraActor::StaticClass()));

	StartEngineSounds();

	//Wait 5 seconds before tank is drive-able.
	FTimerHandle timerHandle;
	GetWorldTimerManager().SetTimer(timerHandle, this, &APhysicsTankPawn::StartTank, myStartupTime, false);
}

/// <summary>
/// Enables control of this tank and kicks off 3 septate tick functions 
/// running at different frequencies. These are used update the 
/// track, speed, and handling of the tank. Also starts the sound effects
/// for the tank.
/// </summary>
void APhysicsTankPawn::StartTank()
{
	//Stop engine start audio and play the running audio.
	myEngineStartAudio->FadeOut(3.0f, 0.0f);
	myEngineRunningAudio->Activate();
	myEngineRunningAudio->Play();

	myHasStarted = true;

	//Enable engine heat particle effects.
	myHeatParticles->Activate();

	FTimerHandle handlingTickTimerHandle;
	GetWorldTimerManager().SetTimer(handlingTickTimerHandle, this, &APhysicsTankPawn::HandlingTickFunc, 0.01f, true);

	FTimerHandle trackMovementTickTimerHandle;
	GetWorldTimerManager().SetTimer(trackMovementTickTimerHandle, this, &APhysicsTankPawn::TrackMovementTickFunc, myTrackUpdateFrequency, true);

	FTimerHandle speedTickTimerHandle;
	GetWorldTimerManager().SetTimer(speedTickTimerHandle, this, &APhysicsTankPawn::SpeedTickFunc, 0.1f, true);
}

/// <summary>
/// Stops control of the tank (and physically stops the tank), stops the 
/// engine sound effects, and disables heat particles.
/// </summary>
void APhysicsTankPawn::StopTank()
{
	myEngineRunningAudio->FadeOut(3.0f, 0.0f);

	myHasStarted = false;

	myHeatParticles->Deactivate();

	myTrackSpeedLeft = 0;
	myTrackSpeedRight = 0;

	myForwardLeftForce = 0;
	myForwardRightForce = 0;

	myCurrentGear = 0;
	myMoveLeftTrackForwardInputValue = 0;
	myMoveRightTrackForwardInputValue = 0;

	myLeftRPM = 0;
	myRightRPM = 0;
}

/// <summary>
/// Finds this tank's blueprint components and initializes them.
/// </summary>
void APhysicsTankPawn::FindComponents()
{
	size_t index;
	FVector relativeWheelPos;
	FVector currLeftSplinePos;
	UTrackPoint* currTrackPoint;

	Super::FindComponents();

	myTankTurret = Cast<UStaticMeshComponent>(GetComponentByName("Tank_Turret"));
	myTankTurret->SetStaticMesh(myTankTurretMesh);

	myLeftTurretMachineGunMount = Cast<UStaticMeshComponent>(GetComponentByName("Tank_Mount_LMGL"));
	myLeftTurretMachineGunMount->SetStaticMesh(myLeftTurretMachineGunMountMesh);

	myLeftTurretMachineGun = Cast<UStaticMeshComponent>(GetComponentByName("Tank_LMGL"));
	myLeftTurretMachineGun->SetStaticMesh(myLeftTurretMachineGunMesh);

	myRightTurretMachineGunMount = Cast<UStaticMeshComponent>(GetComponentByName("Tank_Mount_LMGR"));
	myRightTurretMachineGunMount->SetStaticMesh(myRightTurretMachineGunMountMesh);

	myRightTurretMachineGun = Cast<UStaticMeshComponent>(GetComponentByName("Tank_LMGR"));
	myRightTurretMachineGun->SetStaticMesh(myRightTurretMachineGunMesh);

	myTankBody = Cast<UStaticMeshComponent>(GetComponentByName("Tank_Body"));
	myTankBody->SetStaticMesh(myTankBodyMesh);

	myTrajectoryComponent = Cast<UStaticMeshComponent>(GetComponentByName("TrajectoryMesh"));

	myCenterTurretConstraintComponent = Cast<UPhysicsConstraintComponent>(GetComponentByName("C_Turret"));

	myCenterTurretCannonConstraintComponent = Cast<UPhysicsConstraintComponent>(GetComponentByName("C_TurretCannon"));

	myCamera = Cast<UCameraComponent>(GetComponentByName("CameraBody"));

	myLeftTrackHISM = Cast<UHierarchicalInstancedStaticMeshComponent>(GetComponentByName("Track_L"));
	myLeftTrackHISM->SetStaticMesh(myTrackMesh);

	myRightTrackHISM = Cast<UHierarchicalInstancedStaticMeshComponent>(GetComponentByName("Track_R"));
	myRightTrackHISM->SetStaticMesh(myTrackMesh);

	myLeftSpline = Cast<USplineComponent>(GetComponentByName("Spline_L"));

	myRightSpline = Cast<USplineComponent>(GetComponentByName("Spline_R"));

	myEngineStartAudio = Cast<UAudioComponent>(GetComponentByName("AudioEngineStart"));

	myEngineRunningAudio = Cast<UAudioComponent>(GetComponentByName("AudioEngine"));

	myTracksRunningAudio = Cast<UAudioComponent>(GetComponentByName("AudioTracks"));

	myHeatParticles = Cast<UParticleSystemComponent>(GetComponentByName("P_Heat"));

	myLeftWheels.Empty();
	myLeftWheels.Reserve(0);

	myLeftWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WL1")));
	myLeftWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WL2")));
	myLeftWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WL3")));
	myLeftWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WL4")));
	myLeftWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WL5")));
	myLeftWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WL6")));
	myLeftWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WL7")));
	myLeftWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WL8")));
	myLeftWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WL9")));

	for (index = 0U; index < myLeftWheels.Num(); index++)
	{
		myLeftWheels[index]->SetStaticMesh(myTankWheelMesh);

		if (index == myLeftWheels.Num() - 1)
		{
			myLeftWheels[index]->SetStaticMesh(myTankMasterWheelMesh);
		}
	}

	myRightWheels.Empty();
	myRightWheels.Reserve(0);

	myRightWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WR1")));
	myRightWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WR2")));
	myRightWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WR3")));
	myRightWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WR4")));
	myRightWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WR5")));
	myRightWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WR6")));
	myRightWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WR7")));
	myRightWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WR8")));
	myRightWheels.Add(Cast<UStaticMeshComponent>(GetComponentByName("WR9")));

	for (index = 0U; index < myRightWheels.Num(); index++)
	{
		myRightWheels[index]->SetStaticMesh(myTankWheelMesh);

		if (index == myRightWheels.Num() - 1)
		{
			myRightWheels[index]->SetStaticMesh(myTankMasterWheelMesh);
		}
	}

	for (index = 0U; index < myRightWheels.Num(); index++)
	{
		relativeWheelPos = myRightWheels[index]->GetRelativeLocation();

		relativeWheelPos.Y = myWheelDistanceFromCenter;

		myRightWheels[index]->SetRelativeLocation(relativeWheelPos, false, NULL, ETeleportType::TeleportPhysics);
	}

	//Copy over the right wheel positions to left but change the Y distance.
	for (index = 0U; index < myRightWheels.Num(); index++)
	{
		relativeWheelPos = myRightWheels[index]->GetRelativeLocation();

		relativeWheelPos.Y = relativeWheelPos.Y * -1.0f;

		myLeftWheels[index]->SetRelativeLocation(relativeWheelPos, false, NULL, ETeleportType::TeleportPhysics);
	}

	myRightSpline->ClearSplinePoints();

	myTrackPoints.Empty();
	myTrackPoints.Reserve(0);

	myTrackPoints.Add(Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_0")));
	myTrackPoints.Add(Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_1")));
	myTrackPoints.Add(Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_2")));
	myTrackPoints.Add(Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_3")));
	myTrackPoints.Add(Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_4")));
	myTrackPoints.Add(Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_5")));
	myTrackPoints.Add(Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_6")));
	myTrackPoints.Add(Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_7")));
	myTrackPoints.Add(Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_8")));
	myTrackPoints.Add(Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_9")));
	myTrackPoints.Add(Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_10")));
	myTrackPoints.Add(Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_11")));
	myTrackPoints.Add(Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_12")));
	myTrackPoints.Add(Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_13")));
	myTrackPoints.Add(Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_14")));
	myTrackPoints.Add(Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_15")));

	myLeftWheelPhysicsConstraints.Empty();
	myLeftWheelPhysicsConstraints.Reserve(0);

	myLeftWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WL1")));
	myLeftWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WL2")));
	myLeftWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WL3")));
	myLeftWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WL4")));
	myLeftWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WL5")));
	myLeftWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WL6")));
	myLeftWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WL7")));
	myLeftWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WL8")));
	myLeftWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WL9")));

	myRightWheelPhysicsConstraints.Empty();
	myRightWheelPhysicsConstraints.Reserve(0);

	myRightWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WR1")));
	myRightWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WR2")));
	myRightWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WR3")));
	myRightWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WR4")));
	myRightWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WR5")));
	myRightWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WR6")));
	myRightWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WR7")));
	myRightWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WR8")));
	myRightWheelPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_WR9")));

	//Add the track points as spline points to the right spline. 
	for (index = 0U; index < myTrackPoints.Num(); index++)
	{
		myRightSpline->AddSplinePoint(myTrackPoints[index]->GetComponentLocation(), ESplineCoordinateSpace::World);

		myRightSpline->SetUpVectorAtSplinePoint(myRightSpline->GetNumberOfSplinePoints() - 1,
			myTrackPoints[index]->GetComponentRotation().Quaternion().GetUpVector(),
			ESplineCoordinateSpace::World);
	}

	myLeftTrackPoints.Empty();
	myLeftTrackPoints.Reserve(0);

	myRightTrackPoints.Empty();
	myRightTrackPoints.Reserve(0);

	currTrackPoint = NewObject<UTrackPoint>();
	currTrackPoint->mySplinePoint = 6;
	currTrackPoint->myTrackArrow = Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_6"));
	currTrackPoint->myAssignedWheel = Cast<UStaticMeshComponent>(GetComponentByName("WR2"));
	myRightTrackPoints.Add(currTrackPoint);

	currTrackPoint = NewObject<UTrackPoint>();
	currTrackPoint->mySplinePoint = 7;
	currTrackPoint->myTrackArrow = Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_7"));
	currTrackPoint->myAssignedWheel = Cast<UStaticMeshComponent>(GetComponentByName("WR3"));
	myRightTrackPoints.Add(currTrackPoint);

	currTrackPoint = NewObject<UTrackPoint>();
	currTrackPoint->mySplinePoint = 8;
	currTrackPoint->myTrackArrow = Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_8"));
	currTrackPoint->myAssignedWheel = Cast<UStaticMeshComponent>(GetComponentByName("WR4"));
	myRightTrackPoints.Add(currTrackPoint);

	currTrackPoint = NewObject<UTrackPoint>();
	currTrackPoint->mySplinePoint = 9;
	currTrackPoint->myTrackArrow = Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_9"));
	currTrackPoint->myAssignedWheel = Cast<UStaticMeshComponent>(GetComponentByName("WR5"));
	myRightTrackPoints.Add(currTrackPoint);

	currTrackPoint = NewObject<UTrackPoint>();
	currTrackPoint->mySplinePoint = 10;
	currTrackPoint->myTrackArrow = Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_10"));
	currTrackPoint->myAssignedWheel = Cast<UStaticMeshComponent>(GetComponentByName("WR6"));
	myRightTrackPoints.Add(currTrackPoint);

	currTrackPoint = NewObject<UTrackPoint>();
	currTrackPoint->mySplinePoint = 11;
	currTrackPoint->myTrackArrow = Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_11"));
	currTrackPoint->myAssignedWheel = Cast<UStaticMeshComponent>(GetComponentByName("WR7"));
	myRightTrackPoints.Add(currTrackPoint);

	currTrackPoint = NewObject<UTrackPoint>();
	currTrackPoint->mySplinePoint = 12;
	currTrackPoint->myTrackArrow = Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_12"));
	currTrackPoint->myAssignedWheel = Cast<UStaticMeshComponent>(GetComponentByName("WR8"));
	myRightTrackPoints.Add(currTrackPoint);

	//Left track points
	currTrackPoint = NewObject<UTrackPoint>();
	currTrackPoint->mySplinePoint = 6;
	currTrackPoint->myTrackArrow = Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_6"));
	currTrackPoint->myAssignedWheel = Cast<UStaticMeshComponent>(GetComponentByName("WL2"));
	myLeftTrackPoints.Add(currTrackPoint);

	currTrackPoint = NewObject<UTrackPoint>();
	currTrackPoint->mySplinePoint = 7;
	currTrackPoint->myTrackArrow = Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_7"));
	currTrackPoint->myAssignedWheel = Cast<UStaticMeshComponent>(GetComponentByName("WL3"));
	myLeftTrackPoints.Add(currTrackPoint);

	currTrackPoint = NewObject<UTrackPoint>();
	currTrackPoint->mySplinePoint = 8;
	currTrackPoint->myTrackArrow = Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_8"));
	currTrackPoint->myAssignedWheel = Cast<UStaticMeshComponent>(GetComponentByName("WL4"));
	myLeftTrackPoints.Add(currTrackPoint);

	currTrackPoint = NewObject<UTrackPoint>();
	currTrackPoint->mySplinePoint = 9;
	currTrackPoint->myTrackArrow = Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_9"));
	currTrackPoint->myAssignedWheel = Cast<UStaticMeshComponent>(GetComponentByName("WL5"));
	myLeftTrackPoints.Add(currTrackPoint);

	currTrackPoint = NewObject<UTrackPoint>();
	currTrackPoint->mySplinePoint = 10;
	currTrackPoint->myTrackArrow = Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_10"));
	currTrackPoint->myAssignedWheel = Cast<UStaticMeshComponent>(GetComponentByName("WL6"));
	myLeftTrackPoints.Add(currTrackPoint);

	currTrackPoint = NewObject<UTrackPoint>();
	currTrackPoint->mySplinePoint = 11;
	currTrackPoint->myTrackArrow = Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_11"));
	currTrackPoint->myAssignedWheel = Cast<UStaticMeshComponent>(GetComponentByName("WL7"));
	myLeftTrackPoints.Add(currTrackPoint);

	currTrackPoint = NewObject<UTrackPoint>();
	currTrackPoint->mySplinePoint = 12;
	currTrackPoint->myTrackArrow = Cast<UArrowComponent>(GetComponentByName("Arrow_T_P_12"));
	currTrackPoint->myAssignedWheel = Cast<UStaticMeshComponent>(GetComponentByName("WL8"));
	myLeftTrackPoints.Add(currTrackPoint);

	myLeftSpline->ClearSplinePoints();

	//Left spline is mirrored from the right spline so copy over the points and adjust the location.
	for (index = 0U; index < myRightSpline->GetNumberOfSplinePoints(); index++)
	{
		myRightSpline->SetSplinePointType(index, ESplinePointType::Curve);

		//Copy the right spline point position but replace the Y with the left spline's relative Y location.
		currLeftSplinePos = myRightSpline->GetLocationAtSplinePoint(index, ESplineCoordinateSpace::Local);
		currLeftSplinePos.Y = myLeftSpline->GetRelativeLocation().Y;

		myLeftSpline->AddSplinePointAtIndex(currLeftSplinePos, index, ESplineCoordinateSpace::Local);

		myLeftSpline->SetUpVectorAtSplinePoint(index,
			myRightSpline->GetUpVectorAtSplinePoint(index, ESplineCoordinateSpace::World),
			ESplineCoordinateSpace::World);
	}

	AddHismToTrack(myRightSpline, myRightTrackHISM, true);
	AddHismToTrack(myLeftSpline, myLeftTrackHISM, false);

	AdjustTrackSuspension();

	myOriginalLeftSplineLength = myLeftSpline->GetSplineLength();
	myOriginalRightSplineLength = myRightSpline->GetSplineLength();

	myLeftSuspensionPhysicsConstraints.Empty();
	myLeftSuspensionPhysicsConstraints.Reserve(0);

	myLeftSuspensionPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_SL2")));
	myLeftSuspensionPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_SL3")));
	myLeftSuspensionPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_SL4")));
	myLeftSuspensionPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_SL5")));
	myLeftSuspensionPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_SL6")));
	myLeftSuspensionPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_SL7")));
	myLeftSuspensionPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_SL8")));

	myRightSuspensionPhysicsConstraints.Empty();
	myRightSuspensionPhysicsConstraints.Reserve(0);

	myRightSuspensionPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_SR2")));
	myRightSuspensionPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_SR3")));
	myRightSuspensionPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_SR4")));
	myRightSuspensionPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_SR5")));
	myRightSuspensionPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_SR6")));
	myRightSuspensionPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_SR7")));
	myRightSuspensionPhysicsConstraints.Add(Cast<UPhysicsConstraintComponent>(GetComponentByName("C_SR8")));

	myTankBodyHitchStaticMeshComponent = Cast<UStaticMeshComponent>(GetComponentByName("Tank_Body_Hitch"));

	myTankBodyHitchPhysicsConstraintComponent = Cast<UPhysicsConstraintComponent>(GetComponentByName("Tank_Body_Hitch_Physics_Constraint"));
}

/// <summary>
/// Called once per frame to update the left/right track force based on player input.
/// </summary>
/// <param name="deltaTime">Time in milliseconds between frames.</param>
void APhysicsTankPawn::Tick(float deltaTime)
{
	float currLeftForce;
	float currRightForce;

	Super::Tick(deltaTime);

	if (myHasStarted == true)
	{
		if (myIsUsingSimpleControls == true)
		{
			if (myMoveLeftRightInputValue > 0.0f)
			{
				currLeftForce = (1.0f / (float)FMath::Clamp(myCurrentGear, 1, 8)) * 1.0f;
				currRightForce = (1.0f / (float)FMath::Clamp(myCurrentGear, 1, 8)) * -1.0f;
			}
			else if (myMoveLeftRightInputValue == 0.0f)
			{
				currLeftForce = myMoveLeftTrackForwardInputValue;
				currRightForce = myMoveRightTrackForwardInputValue;
			}
			else //if(myMoveLeftRightInputValue < 0.0f)
			{
				currLeftForce = (1.0f / (float)FMath::Clamp(myCurrentGear, 1, 8)) * -1.0f;
				currRightForce = (1.0f / (float)FMath::Clamp(myCurrentGear, 1, 8)) * 1.0f;
			}

			myForwardLeftForce = FMath::FInterpTo(myForwardLeftForce, currLeftForce, deltaTime, myTankHandling);
			myForwardRightForce = FMath::FInterpTo(myForwardRightForce, currRightForce, deltaTime, myTankHandling);
		}
		else
		{
			myForwardLeftForce = myMoveLeftTrackForwardInputValue;
			myForwardRightForce = myMoveRightTrackForwardInputValue;
		}

		//UE_LOG(LogTemp, Error, TEXT("myForwardLeftForce:%f	myForwardRightForce:%f"), myForwardLeftForce, myForwardRightForce);
	}
}

/// <summary>
/// Adjusts the axis input value based on the RPM torque curve and the current gear. This handles
/// movement for the tank based on the current provided axisInputValue and trackRPM.
/// </summary>
/// <param name="axisInputValue">The input axis value typically between 0.0 and 1.0.</param>
/// <param name="trackRPM">The calculated RPM for the track.</param>
void APhysicsTankPawn::MoveTrackForward(float& axisInputValue, float& trackRPM)
{
	if (myIsDestroyed == false && myIsShiftingGears == false)
	{
		if (axisInputValue > 0.0f) //Accelerate
		{
			//UE_LOG(LogTemp, Error, TEXT("a"));

			//Adjust the forward value based on the torque curve.
			trackRPM = FMath::Clamp(trackRPM + myRPMRate, 0.0f, 1.0f);
			axisInputValue = myTorqueCurve->GetFloatValue(trackRPM);

			//Check if we need to shift gears.
			TryShiftGears();
		}
		else if (axisInputValue == 0.0f) //Engine Break
		{
			//UE_LOG(LogTemp, Error, TEXT("b"));

			if (myCurrentGear < 0)
			{
				myCurrentGear = 0;
			}

			//Adjust the forward value based on the torque curve.
			trackRPM = FMath::Clamp(trackRPM - myRPMRate, 0.0f, 1.0f);
			axisInputValue = myTorqueCurve->GetFloatValue(trackRPM);

			if (myCurrentGear > 0)
			{
				ShiftGearDown();
			}
		}
		else //Break or Reverse
		{
			//UE_LOG(LogTemp, Error, TEXT("c"));
			if (mySpeed > myBreakingThreshold && myCurrentGear > 0) //Break
			{
				//UE_LOG(LogTemp, Error, TEXT("d"));
				trackRPM = FMath::Clamp(trackRPM - myBreakingRate, 0.0f, 1.0f);
				axisInputValue = myTorqueCurve->GetFloatValue(trackRPM);

				if (myCurrentGear > 0)
				{
					ShiftGearDown();
				}
			}
			else //Reverse
			{
				if (myIsUsingSimpleControls == true)
				{
					if (myCurrentGear == -1)
					{
						//UE_LOG(LogTemp, Error, TEXT("e"));
						trackRPM = FMath::Clamp(trackRPM + myRPMRate, 0.0f, 1.0f);
						axisInputValue = myTorqueCurve->GetFloatValue(trackRPM);
					}
					else
					{
						//UE_LOG(LogTemp, Error, TEXT("f"));
						trackRPM = 0.0f;
						axisInputValue = 0.0f;

						myIsShiftingGears = true;
						myCurrentGear = -1;
						myIsShiftingGears = false;
					}
				}
				else
				{
					//Reverse is treated the same as forward but in the negative direction.
					trackRPM = FMath::Clamp(trackRPM + myRPMRate, 0.0f, 1.0f);
					axisInputValue = -myTorqueCurve->GetFloatValue(trackRPM) * 1.0;
				}
			}
		}
	}
}

/// <summary>
/// Moves the tank forward with the provided axis input value.
/// </summary>
/// <param name="value">The axis input value.</param>
void APhysicsTankPawn::MoveForward(float value)
{
	myMoveLeftTrackForwardInputValue = value;
	myMoveRightTrackForwardInputValue = value;

	MoveTrackForward(myMoveLeftTrackForwardInputValue, myLeftRPM);
	MoveTrackForward(myMoveRightTrackForwardInputValue, myRightRPM);
}

/// <summary>
/// Moves the left track forward with the provided axis input value.
/// </summary>
/// <param name="value">The axis input value.</param>
void APhysicsTankPawn::MoveLeftTrackForward(float value)
{
	myMoveLeftTrackForwardInputValue = value;

	MoveTrackForward(myMoveLeftTrackForwardInputValue, myLeftRPM);
}

/// <summary>
/// Moves the right track forward with the provided axis input value.
/// </summary>
/// <param name="value">The axis input value.</param>
void APhysicsTankPawn::MoveRightTrackForward(float value)
{
	myMoveRightTrackForwardInputValue = value;

	MoveTrackForward(myMoveRightTrackForwardInputValue, myRightRPM);
}

/// <summary>
/// Turns the tank left/right depending on the provided axis value.
/// </summary>
/// <param name="value">The axis input value.</param>
void APhysicsTankPawn::MoveLeftRight(float value)
{
	myMoveLeftRightInputValue = value;
}

/// <summary>
/// Destroys this tank by stopping input, deactivates the sounds and effects.
/// </summary>
void APhysicsTankPawn::DestroyTank()
{
	int32 index;
	FTransform currTrackPieceTransform;

	if (myIsDestroyed == false)
	{
		DeActivateSoundsAndEffects();

		AActor::DetachAllSceneComponents(RootComponent, FDetachmentTransformRules::KeepWorldTransform);

		for (index = 0; index < myLeftWheelPhysicsConstraints.Num(); index++)
		{
			myLeftWheelPhysicsConstraints[index]->BreakConstraint();
		}

		for (index = 0; index < myRightWheelPhysicsConstraints.Num(); index++)
		{
			myRightWheelPhysicsConstraints[index]->BreakConstraint();
		}

		for (index = 0; index < myLeftSuspensionPhysicsConstraints.Num(); index++)
		{
			myLeftSuspensionPhysicsConstraints[index]->BreakConstraint();
		}

		for (index = 0; index < myRightSuspensionPhysicsConstraints.Num(); index++)
		{
			myRightSuspensionPhysicsConstraints[index]->BreakConstraint();
		}

		myTankTurret->RecreatePhysicsState();
		myTankTurret->AddImpulse(FVector(1000, 1000, 1000));

		myTankCannon->RecreatePhysicsState();
		myTankCannon->AddImpulse(FVector(1000, 1000, 1000));

		myTankBody->RecreatePhysicsState();
		myTankBody->AddImpulse(FVector(1000, 1000, 1000));

		myIsDestroyed = true;
	}
}

/// <summary>
/// Displays a trajectory crosshair for the player. By default, this draws the
/// final landing location of the projectile. If you change the drawLines variable
/// to true, you will see the full projectile path.
/// </summary>
/// <param name="initialVelocity">The initial/starting velocity of the projectile.</param>
void APhysicsTankPawn::DrawTrajectory(const FVector& initialVelocity)
{
	//Use projectile motion formulas to create a trajectory path.
	const bool drawLines = false;
	const float g = -GetWorld()->GetGravityZ(); //a = g = 9.81 m/s^2 (-981.0 in UE)
	float t;
	float totalTime;
	float sampleRate = 0.01f;
	FVector startLocation = myMuzzleArrow->GetComponentLocation();
	FVector currLocation = startLocation;
	FVector previousLocation;	
	
	totalTime = CalculateProjectileHangTime(startLocation, initialVelocity, g);

	//Prevent negative time.
	if (totalTime < 0.0f)
	{
		totalTime = 0.0f;
	}

	//Adjust the starting height based on the landscape the tank is sitting on.
	startLocation.Z += GetLandscapeHeightAtTankLocation();

	if (drawLines == true)
	{
		for (t = 0.0f; t <= totalTime; t += sampleRate)
		{
			previousLocation = currLocation;

			currLocation = CalculateProjectileMotion(startLocation, initialVelocity, g, t);

			GetWorld()->LineBatcher->DrawLine(previousLocation, currLocation, FLinearColor::Red, SDPG_World, 5, 1);

			//Draw a box at the end of the trajectory.
			if (t >= totalTime - sampleRate)
			{
				//Adjust height of the reticle if a landscape is in use (otherwise z remains the same).
				currLocation.Z = GetLandscapeHeightAtLocation(currLocation);
				
				FTransform boxTransform;
				boxTransform.SetLocation(currLocation);
				GetWorld()->LineBatcher->DrawSolidBox(FBox(FVector(-100, -100, -50), FVector(100, 100, 50)), boxTransform, FColor::Blue, SDPG_World, totalTime);
			}
		}
	}
	else
	{
		//Calculate just the ending location of the projectile.
		t = totalTime;

		currLocation = CalculateProjectileMotion(startLocation, initialVelocity, g, t);

		//Adjust height of the reticle if a landscape is in use (otherwise z remains the same).
		currLocation.Z = GetLandscapeHeightAtLocation(currLocation);

		//Move the reticle to the calculated world location.
		myTrajectoryComponent->SetWorldLocation(currLocation);
	}
}

/// <summary>
/// Calculates how long (in seconds) the projectile is in the air.
/// </summary>
/// <param name="startingLocation">The world/level X,Y,Z location of where the projectile started moving.</param>
/// <param name="initialVelocity">The initial/starting velocity of the projectile.</param>
/// <param name="gravityAcceleration">The acceleration of gravity (9.81 m/s^2 (-981.0 in UE)).</param>
/// <returns>The amount of time in seconds the projectile is in the air.</returns>
float APhysicsTankPawn::CalculateProjectileHangTime(const FVector& startingLocation, const FVector& initialVelocity, const float& gravityAcceleration)
{
	float numberUnderSquareRoot = (-initialVelocity.Z * -initialVelocity.Z) - 4 * (gravityAcceleration * 0.5f) * -startingLocation.Z;

	//Prevent negative square roots.
	if (numberUnderSquareRoot < 0.0f)
	{
		numberUnderSquareRoot = 0.0f;
	}

	//Use quadratic formula and algebra to solve for time (when Z = 0).
	//T = (V0z +- FMath::Sqrt(-V0z^2 - 4 * (g / 2) * -Z0)) / g
	float timeA = (initialVelocity.Z + FMath::Sqrt(numberUnderSquareRoot)) / gravityAcceleration;
	//float timeB = (initialVelocity.Z - FMath::Sqrt(numberUnderSquareRoot)) / gravityAcceleration; //Always negative so not used

	return timeA;
}

/// <summary>
/// Uses the classic Projectile Motion formula to determine the current projectile world location 
/// based on the provided values.
/// </summary>
/// <param name="startingLocation">The world/level X,Y,Z location of where the projectile started moving.</param>
/// <param name="initialVelocity">The initial/starting velocity of the projectile.</param>
/// <param name="gravityAcceleration">The acceleration of gravity (9.81 m/s^2 (-981.0 in UE)).</param>
/// <param name="time">The current time (in seconds) elapsed.</param>
/// <returns>The current projectile world location based on the provided values.</returns>
FVector APhysicsTankPawn::CalculateProjectileMotion(const FVector& startingLocation, const FVector& initialVelocity, const float& gravityAcceleration, const float& time)
{
	//Use projectile motion formulas to create a trajectory.
	FVector currLocation;

	//x = X0 + V0x*t
	currLocation.X = startingLocation.X + (initialVelocity.X * time);

	//y = Y0 + V0y*t
	currLocation.Y = startingLocation.Y + (initialVelocity.Y * time);

	//z = Z0 + V0z*t - (1/2)*a*t^2 [z is the vertical axis in unreal]
	currLocation.Z = startingLocation.Z + (initialVelocity.Z * time) - ((0.5f) * gravityAcceleration * (time * time));

	return currLocation;
}

/// <summary>
/// Adds the provided static mesh component trackHISMC to the provided spline. HISMC stands for
/// Hierarchical Static Mesh Component.
/// </summary>
/// <param name="spline">The complete track spline.</param>
/// <param name="trackHISMC">The static mesh piece of track.</param>
/// <param name="isRightTrack">true if the track is the right side spline, false otherwise.</param>
void APhysicsTankPawn::AddHismToTrack(USplineComponent* spline, UHierarchicalInstancedStaticMeshComponent* trackHISMC, const bool isRightTrack)
{
	int instanceID;
	float currCDist;
	size_t index;
	size_t lastIndex;
	FTransform currTrackTransform;
	UTrackPos* currTrackPos;

	if (isRightTrack == true)
	{
		myTrackDistancesRight.Empty();
		myTrackDistancesRight.Reserve(0);
	}
	else
	{
		myTrackDistancesLeft.Empty();
		myTrackDistancesLeft.Reserve(0);
	}
	
	lastIndex = ((float)spline->GetSplineLength() / (float)myTrackLength);
	for (index = 0U; index <= lastIndex; index++)
	{
		currCDist = index * myTrackLength;

		currTrackTransform = FTransform(spline->GetRotationAtDistanceAlongSpline(currCDist, ESplineCoordinateSpace::World),
										spline->GetLocationAtDistanceAlongSpline(currCDist, ESplineCoordinateSpace::World),
										GetActorScale());

		instanceID = trackHISMC->AddInstanceWorldSpace(currTrackTransform);

		currTrackPos = NewObject<UTrackPos>();
		currTrackPos->myInstance = instanceID;
		currTrackPos->myCDist = currCDist;
		currTrackPos->myHISMC = trackHISMC;
		currTrackPos->mySpline = spline;
		currTrackPos->myCurrentSplineLength = spline->GetSplineLength();

		if (isRightTrack == true)
		{
			myTrackDistancesRight.Add(currTrackPos);
		}
		else
		{
			myTrackDistancesLeft.Add(currTrackPos);
		}
	}
}

/// <summary>
/// Adjusts the position of the spline points to match the wheel positions.
/// </summary>
/// <param name="trackPoint">The specific spline point in the provided spline.</param>
/// <param name="spline">The track spline.</param>
void APhysicsTankPawn::AdjustSplinePointPerWheel(UTrackPoint* trackPoint, USplineComponent* spline)
{
	FVector worldWheelPos = trackPoint->myAssignedWheel->GetComponentTransform().GetLocation();
	FVector adjustedWheelPos = worldWheelPos + (myTankBody->GetUpVector() * myTrackZOffset * GetActorScale());

	spline->SetLocationAtSplinePoint(trackPoint->mySplinePoint, adjustedWheelPos, ESplineCoordinateSpace::World);
}

/// <summary>
/// Adjusts the left and right suspension physics constraint.
/// </summary>
void APhysicsTankPawn::AdjustSuspensions()
{
	size_t index;
	UPhysicsConstraintComponent* currConstraint;

	for (index = 0U; index < myLeftSuspensionPhysicsConstraints.Num(); index++)
	{
		currConstraint = myLeftSuspensionPhysicsConstraints[index];
		currConstraint->SetLinearPositionTarget(FVector(0.0f, 0.0f, mySuspensionZOffset));
		currConstraint->SetLinearDriveParams(mySuspensionStrength, mySuspensionDamping, 0.0f);
	}

	for (index = 0U; index < myRightSuspensionPhysicsConstraints.Num(); index++)
	{
		currConstraint = myRightSuspensionPhysicsConstraints[index];
		currConstraint->SetLinearPositionTarget(FVector(0.0f, 0.0f, mySuspensionZOffset));
		currConstraint->SetLinearDriveParams(mySuspensionStrength, mySuspensionDamping, 0.0f);
	}
}

/// <summary>
/// Adjusts the left and right track points to the wheels, also keeps the track lengths constant.
/// </summary>
void APhysicsTankPawn::AdjustTrackSuspension()
{
	size_t index;

	//Adjust the track spline to the wheel location.
	for (index = 0U; index < myRightTrackPoints.Num(); index++)
	{
		KeepTrackLengthConstant(true);

		AdjustSplinePointPerWheel(myRightTrackPoints[index], myRightSpline);
	}

	for (index = 0U; index < myLeftTrackPoints.Num(); index++)
	{
		KeepTrackLengthConstant(false);

		AdjustSplinePointPerWheel(myLeftTrackPoints[index], myLeftSpline);
	}
}

/// <summary>
/// Turns off all sound effects related to this tank.
/// </summary>
void APhysicsTankPawn::DeActivateSoundsAndEffects()
{
	myEngineStartAudio->Deactivate();
	myEngineRunningAudio->Deactivate();

	myHeatParticles->Deactivate();
}

/// <summary>
/// Calculates and updates the left and right track speed based 
/// on the input and other variables/multipliers.
/// </summary>
void APhysicsTankPawn::CalculateTrackSpeed()
{
	myTrackSpeedLeft = myRawLeftSpeed * myTrackSpeedMultiplier;

	UpdateTrackInstances(myTrackDistancesLeft, myTrackSpeedLeft);

	myTrackSpeedRight = myRawRightSpeed * myTrackSpeedMultiplier;

	UpdateTrackInstances(myTrackDistancesRight, myTrackSpeedRight);

	//UE_LOG(LogTemp, Error, TEXT("myTrackSpeedLeft:%f	myTrackSpeedRight:%f"), myTrackSpeedLeft, myTrackSpeedRight);
}

/// <summary>
/// Sets the linear dampening for the tank body when in and out of motion.
/// </summary>
void APhysicsTankPawn::HandleLinearDamping()
{
	//Reduces the slide of the tank when not moving.
	if (myMoveLeftTrackForwardInputValue == 0.0f && myMoveRightTrackForwardInputValue == 0.0f)
	{
		if (FMath::IsNearlyEqual(mySpeed, 0.0f, 1.0f) == true)
		{
			myTankBody->SetLinearDamping(55.0f);
		}
	}
	else
	{
		myTankBody->SetLinearDamping(0.0f);
	}
}

/// <summary>
/// Constantly keeps the left and right track lengths constant.
/// </summary>
/// <param name="isRightTrack">true if this is for the right track, false otherwise.</param>
void APhysicsTankPawn::KeepTrackLengthConstant(const bool isRightTrack)
{
	float rightSplineLength;
	float leftSplineLength;
	FVector currSplinePoint;

	if (isRightTrack == true)
	{
		rightSplineLength = myRightSpline->GetSplineLength();
		if (rightSplineLength > myOriginalRightSplineLength)
		{
			myRightTrackOffset = -1.0f;
		}
		else if (rightSplineLength == myOriginalRightSplineLength)
		{
			myRightTrackOffset = 0.0f;
		}
		else //if (rightSplineLength < myOriginalRightSplineLength)
		{
			myRightTrackOffset = 1.0f;
		}

		currSplinePoint = myRightSpline->GetLocationAtSplinePoint(0, ESplineCoordinateSpace::Local);
		currSplinePoint.X = currSplinePoint.X - myRightTrackOffset;

		myRightSpline->SetLocationAtSplinePoint(0, currSplinePoint, ESplineCoordinateSpace::Local);
	}
	else
	{
		leftSplineLength = myLeftSpline->GetSplineLength();
		if (leftSplineLength > myOriginalLeftSplineLength)
		{
			myLeftTrackOffset = -1.0f;
		}
		else if (leftSplineLength == myOriginalLeftSplineLength)
		{
			myLeftTrackOffset = 0.0f;
		}
		else //if (leftSplineLength < myOriginalLeftSplineLength)
		{
			myLeftTrackOffset = 1.0f;
		}

		currSplinePoint = myLeftSpline->GetLocationAtSplinePoint(0, ESplineCoordinateSpace::Local);
		currSplinePoint.X = currSplinePoint.X - myLeftTrackOffset;

		myLeftSpline->SetLocationAtSplinePoint(0, currSplinePoint, ESplineCoordinateSpace::Local);
	}
}

/// <summary>
/// Adjusts the volume and intensity of the sound effects based on RPM and speed.
/// </summary>
void APhysicsTankPawn::ModulateSounds()
{
	float rpmAvg = (myLeftRPM + myRightRPM) * 0.5f;

	//Adjust the heat particles based on the RPM.
	myHeatParticles->SetFloatParameter(TEXT("RPM"), rpmAvg);

	//Adjust the sound of the tank tracks based on speed.
	myTracksRunningAudio->SetFloatParameter(TEXT("RPM"), FMath::Clamp(mySpeed / 150.0f, 0.0f, 1.0f));

	//Adjust the sound of the engine audio based on the RPM.
	myEngineRunningAudio->SetFloatParameter(TEXT("RPM"), rpmAvg);
}

/// <summary>
/// Calculates the left and right master wheel speed based on the force, rotation multiplier,
/// and the current gear.
/// </summary>
void APhysicsTankPawn::CalculateWheelSpeed()
{
	if (myCurrentGear == 0)
	{
		myRawLeftSpeed = myForwardLeftForce * myWheelRotationMultiplier * 0.7f * 1.0;
	}
	else
	{
		myRawLeftSpeed = myForwardLeftForce * myWheelRotationMultiplier * 0.7f * myCurrentGear;
	}

	if (myCurrentGear == 0)
	{
		myRawRightSpeed = myForwardRightForce * myWheelRotationMultiplier * 0.7f * 1.0;
	}
	else
	{
		myRawRightSpeed = myForwardRightForce * myWheelRotationMultiplier * 0.7f * myCurrentGear;
	}
}

/// <summary>
/// Sets the wheel speed indirectly by setting the angular velocity of the attached 
/// wheel physics constraints.
/// </summary>
/// <param name="overrideWheelSpeed">Allows for overriding the wheel speed.</param>
void APhysicsTankPawn::SetWheelSpeed()
{
	size_t index;

	//Movement of wheels is best done by moving their physics constraints. 
	for (index = 0U; index < myLeftWheelPhysicsConstraints.Num(); index++)
	{
		myLeftWheelPhysicsConstraints[index]->SetAngularVelocityTarget(FVector(0.0f, myRawLeftSpeed * -0.2f, 0.0f));
		myLeftWheelPhysicsConstraints[index]->SetAngularDriveParams(0.0f, 1000.0f, 0.0f);
	}

	for (index = 0U; index < myRightWheelPhysicsConstraints.Num(); index++)
	{
		myRightWheelPhysicsConstraints[index]->SetAngularVelocityTarget(FVector(0.0f, myRawRightSpeed * -0.2f, 0.0f));
		myRightWheelPhysicsConstraints[index]->SetAngularDriveParams(0.0f, 1000.0f, 0.0f);
	}
}

/// <summary>
/// Increments the current gear (clamps the value between 0 and total number of gears + 1).
/// </summary>
void APhysicsTankPawn::ShiftGearUp()
{
	myIsShiftingGears = true;

	myCurrentGear = FMath::Clamp(myCurrentGear + 1, 0, myTotalNumberOfGears + 1);

	myIsShiftingGears = false;
}

/// <summary>
/// Decrements the current gear (clamps the value between 0 and total number of gears + 1).
/// </summary>
void APhysicsTankPawn::ShiftGearDown()
{
	myIsShiftingGears = true;

	myCurrentGear = FMath::Clamp(myCurrentGear - 1, 0, myTotalNumberOfGears + 1);

	myIsShiftingGears = false;
}

/// <summary>
/// Stars the sound effects for the engine revving up.
/// </summary>
void APhysicsTankPawn::StartEngineSounds()
{
	myEngineStartAudio->Activate();
	myEngineStartAudio->Play();
}

/// <summary>
/// Basically automatic transmission. This function gets called constantly to determine
/// if/when to shift up or shift down gears based on the current speed of the tank. 
/// </summary>
void APhysicsTankPawn::TryShiftGears()
{
	//Check if we need to change/shift gears up/down depending on the current speed and gear of the tank.
	if (myCurrentGear == 0)
	{
		ShiftGearUp();
	}
	else if (myCurrentGear == 1)
	{
		if (mySpeed > myGearOneSpeedThreshold)
		{
			ShiftGearUp();
		}
	}
	else if (myCurrentGear == 2)
	{
		if (mySpeed > myGearTwoSpeedThreshold)
		{
			ShiftGearUp();
		}
		else if (mySpeed < myGearOneSpeedThreshold)
		{
			ShiftGearDown();
		}
	}
	else if (myCurrentGear == 3)
	{
		if (mySpeed > myGearThreeSpeedThreshold)
		{
			ShiftGearUp();
		}
		else if(mySpeed < myGearTwoSpeedThreshold)
		{
			ShiftGearDown();
		}
	}
	else if (myCurrentGear == 4)
	{
		if (mySpeed > myGearFourSpeedThreshold)
		{
			ShiftGearUp();
		}
		else if(mySpeed < myGearThreeSpeedThreshold)
		{
			ShiftGearDown();
		}
	}
	else if (myCurrentGear == 5)
	{
		if (mySpeed > myGearFiveSpeedThreshold)
		{
			ShiftGearUp();
		}
		else if(mySpeed < myGearFourSpeedThreshold)
		{
			ShiftGearDown();
		}
	}
	else if (myCurrentGear == 6)
	{
		if (mySpeed > myGearSixSpeedThreshold)
		{
			ShiftGearUp();
		}
		else if(mySpeed < myGearFiveSpeedThreshold)
		{
			ShiftGearDown();
		}
	}
	else if (myCurrentGear == 7)
	{
		if (mySpeed > myGearSevenSpeedThreshold)
		{
			ShiftGearUp();
		}
		else if(mySpeed < myGearSixSpeedThreshold)
		{
			ShiftGearDown();
		}
	}
}

/// <summary>
/// Tick/Update function for updating wheel speed, adjusting suspension, and adjusts the orientations
/// of the turrets, and tank cannon.
/// </summary>
void APhysicsTankPawn::HandlingTickFunc()
{
	if (myIsDestroyed == false)
	{
		ModulateSounds();

		//Calculate the raw right/left speed of the wheels based on player input and multiplier(s).
		CalculateWheelSpeed();
		
		//Set the angular speed of the wheels based on the above calculations.
		SetWheelSpeed();

		//Adjust the track location based on the wheel speed/location.
		AdjustTrackSuspension();

		HandleLinearDamping();

		myCenterTurretConstraintComponent->SetAngularOrientationTarget(FRotator(0.0f, -myTurretY, 0.0f));
		myCenterTurretCannonConstraintComponent->SetAngularOrientationTarget(FRotator(myCannonPitch, 0.0f, 0.0f));

		myTankTurret->AddTorqueInRadians(FVector(0.0f, 0.0f, 1.0f));
		myTankCannon->AddTorqueInRadians(FVector(0.0f, 0.0f, 1.0f));

		DrawTrajectory(myCannonProjectileInitialSpeed * myMuzzleArrow->GetComponentRotation().Vector());
	}
}

/// <summary>
/// Tick/Update function that calculates the current track speed.
/// </summary>
void APhysicsTankPawn::TrackMovementTickFunc()
{
	if (myIsDestroyed == false)
	{
		CalculateTrackSpeed();
	}
}

/// <summary>
/// Tick/Update function that calculates the overall velocity magnitude 
/// (the speed) of this tank.
/// </summary>
void APhysicsTankPawn::SpeedTickFunc()
{
	FVector xyVelocity = GetVelocity();
	xyVelocity.Z = 0.0f;

	mySpeed = xyVelocity.Size() * 0.036f;

	//UE_LOG(LogTemp, Error, TEXT("mySpeed:%f"), mySpeed);
}

/// <summary>
/// Updates and moves the track parts in the left/right track spline depending on the current track speed
/// and the current spline length (or the adjusted spline length).
/// </summary>
/// <param name="tracks">The track points that make up the track spline.</param>
/// <param name="trackSpeed">The current speed of the track.</param>
void APhysicsTankPawn::UpdateTrackInstances(TArray<UTrackPos*> tracks, const float trackSpeed)
{
	bool shouldMarkRenderStateDirty;
	float currentTrackSpeed = trackSpeed;
	float currTrackMin;
	float currTrackMax;
	float currentSplineLength;
	float adjustedSplineDistance;
	float currentDistanceOnSpline;
	FVector updatedLocationAtDistance;
	FRotator updatedRotationAtDistance;
	UTrackPos* currentLoopST;

	if (currentTrackSpeed != 0.0f)
	{
		for (size_t trackIndex = 0U; trackIndex < tracks.Num(); trackIndex++)
		{
			//Calculate the position of the track piece based on its track length (since the length can change over time).
			currentLoopST = tracks[trackIndex];
			currentSplineLength = currentLoopST->mySpline->GetSplineLength();

			currTrackMin = currentLoopST->myCDist - myTrackLength;
			currTrackMax = currentLoopST->myCDist + myTrackLength;

			adjustedSplineDistance = (currentLoopST->myCDist * currentSplineLength) / FMath::Clamp(currentLoopST->myCurrentSplineLength, 0.001f, 1000000000.0f);

			currentDistanceOnSpline = FMath::Clamp(adjustedSplineDistance, currTrackMin, currTrackMax);

			//Check if the adjusted spline distance + the current speed of the track is greater then the original spline length.
			if (currentDistanceOnSpline + currentTrackSpeed >= currentSplineLength)
			{
				//If so, use the larger as the spline length.
				adjustedSplineDistance = (currentDistanceOnSpline + currentTrackSpeed) - currentSplineLength;
			}
			else //If the adjusted spline distance + the current speed is less than the original spline length.
			{
				//Check if the adjusted spline distance + the current speed is negative (moving in reverse?)
				if ((currentDistanceOnSpline + currentTrackSpeed) <= 0.0f)
				{
					//Use the larger spline length (same as the original if but for negative).
					adjustedSplineDistance = currentSplineLength - FMath::Abs(currentDistanceOnSpline + currentTrackSpeed);
				}
				else //If the adjusted spline distance + the current speed is positive but also less than its original length.
				{
					//Use the adjusted spline distance with current speed.
					adjustedSplineDistance = (currentDistanceOnSpline + currentTrackSpeed);
				}
			}

			//Get the corresponding location and rotation based on the adjusted distance.
			updatedLocationAtDistance = currentLoopST->mySpline->GetLocationAtDistanceAlongSpline(adjustedSplineDistance, ESplineCoordinateSpace::World);
			updatedRotationAtDistance = currentLoopST->mySpline->GetRotationAtDistanceAlongSpline(adjustedSplineDistance, ESplineCoordinateSpace::World);
			
			//True if this is the last track.
			shouldMarkRenderStateDirty = trackIndex == tracks.Num() - 1;
			
			//Reposition the actual static mesh component.
			currentLoopST->myHISMC->UpdateInstanceTransform(currentLoopST->myInstance,
															FTransform(updatedRotationAtDistance, updatedLocationAtDistance, GetActorScale()),
															true,
															shouldMarkRenderStateDirty,
															true);

			//Update the current track segment with the updated values.
			currentLoopST->myCDist = adjustedSplineDistance;
			currentLoopST->myCurrentSplineLength = currentSplineLength;
		}
	}
}

/// <summary>
/// Attaches the provided item to the hook/hitch on the back of the tank.
/// </summary>
/// <param name="itemData">The tank item to attach to the hook/hitch.</param>
/// <returns>true on success, false otherwise.</returns>
bool APhysicsTankPawn::AttachItemToTankBodyHitch(UTankItemData* itemData)
{
	UStaticMeshComponent* tankBodyMeshComponent;

	if (itemData != NULL)
	{
		tankBodyMeshComponent = Cast<UStaticMeshComponent>(GetComponentByName(TEXT("Tank_Body")));
		if (tankBodyMeshComponent != NULL)
		{
			myTankBodyHitchStaticMeshComponent->SetStaticMesh(itemData->StaticMesh);

			//Connect the physics constraint to the tank body and the item's component (static mesh component).
			myTankBodyHitchPhysicsConstraintComponent->ComponentName1.ComponentName = "Tank_Body_Hitch"; //Tank_Body_Hitch (The Static Mesh Component holding the item.
			myTankBodyHitchPhysicsConstraintComponent->ComponentName2.ComponentName = "Tank_Body"; //Tank_Body (The Static Mesh the component should attach to).
			myTankBodyHitchPhysicsConstraintComponent->ConstraintInstance.InitConstraint(myTankBodyHitchStaticMeshComponent->GetBodyInstance(), tankBodyMeshComponent->GetBodyInstance(), 1.0f, NULL);

			return true;
		}

		UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::AttachItemToTankBodyHitch(UTankItemData* itemData) Error: Unable to find static mesh component for Tank_Body!");
		return false;
	}

	UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::AttachItemToTankBodyHitch(UTankItemData* itemData) Error: itemData is NULL!");
	return false;
}

/// <summary>
/// Attaches the provided item to the top plates of the Tank's turret (the chest).
/// </summary>
/// <param name="itemData">The item to attach to the tank turret's forward plates.</param>
/// <returns>true on success, false otherwise.</returns>
bool APhysicsTankPawn::AttachItemToTankTurretChest(UTankItemData* itemData)
{
	USkeletalMeshComponent* tankTurretSkeletalMeshComponent;

	if (itemData != NULL)
	{
		if (itemData->EquipmentSlot == EEquipmentSlot::VE_LEFTCHEST)
		{
			tankTurretSkeletalMeshComponent = Cast<USkeletalMeshComponent>(GetComponentByName(TEXT("Tank_Turret_Left_Chest")));
		}
		else
		{
			tankTurretSkeletalMeshComponent = Cast<USkeletalMeshComponent>(GetComponentByName(TEXT("Tank_Turret_Right_Chest")));
		}

		if (tankTurretSkeletalMeshComponent != NULL)
		{
			tankTurretSkeletalMeshComponent->SetSkeletalMesh(itemData->SkeletalMesh);

			return true;
		}

		UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::AttachItemToTankTurretChest(UTankItemData* itemData) Error: Failed to find tank turret's skeletal mesh component!");
		return false;
	}

	UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::AttachItemToTankTurretChest(UTankItemData* itemData) Error: itemData is NULL!");
	return false;
}

/// <summary>
/// Returns true if this tank is started and able to be moved/controlled.
/// </summary>
/// <returns>true if this tank is started and able to be moved/controlled, false otherwise.</returns>
bool APhysicsTankPawn::GetHasStarted()
{
	return myHasStarted;
}

/// <summary>
/// Returns the current speed of this tank.
/// </summary>
/// <returns>The current speed of this tank.<returns>
FString APhysicsTankPawn::GetSpeedDisplayString()
{
	return FString::SanitizeFloat(mySpeed);
}

/// <summary>
/// Returns the current gear the tank is in.
/// </summary>
/// <returns>The current gear the tank is in.</returns>
FString APhysicsTankPawn::GetGearDisplayString()
{
	return FString::SanitizeFloat(myCurrentGear);
}

/// <summary>
/// Updates/Changes the corresponding tank mesh based on the provided tank item. 
/// </summary>
/// <param name="theItemData">The tank item to set to the tank.</param>
/// <returns>true on success, false otherwise.</returns>
bool APhysicsTankPawn::SetStaticMeshForEquipmentType(UTankItemData* theItemData)
{
	if (theItemData != NULL)
	{
		if (theItemData->EquipmentSlot == EEquipmentSlot::VE_BODY)
		{
			UStaticMeshComponent* tankBodyMeshComponent = Cast<UStaticMeshComponent>(GetComponentByName(TEXT("Tank_Body")));
			if (tankBodyMeshComponent != NULL)
			{
				return tankBodyMeshComponent->SetStaticMesh(theItemData->StaticMesh);
			}

			UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForEquipmentType(EEquipmentSlot, UStaticMesh*) Error: failed to cast the found body component to UStaticMeshComponent!");
		}
		else if (theItemData->EquipmentSlot == EEquipmentSlot::VE_CANNON)
		{
			UStaticMeshComponent* tankCannonMeshComponent = Cast<UStaticMeshComponent>(GetComponentByName(TEXT("Tank_Cannon")));
			if (tankCannonMeshComponent != NULL)
			{
				return tankCannonMeshComponent->SetStaticMesh(theItemData->StaticMesh);
			}

			UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForEquipmentType(EEquipmentSlot, UStaticMesh*) Error: failed to cast the found cannon component to UStaticMeshComponent!");
		}
		else if (theItemData->EquipmentSlot == EEquipmentSlot::VE_GEAR)
		{
			//gears = WR9/WL9
			UStaticMeshComponent* leftGear = Cast<UStaticMeshComponent>(GetComponentByName(TEXT("WL9")));
			if (leftGear != NULL)
			{
				if (leftGear->SetStaticMesh(theItemData->StaticMesh) == false)
				{
					UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForEquipmentType(EEquipmentSlot, UStaticMesh*) Error: failed to set static mesh for left gear!");
					return false;
				}
			}
			else
			{
				UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForEquipmentType(EEquipmentSlot, UStaticMesh*) Error: failed to cast the left Gear to UStaticMeshComponent!");
				return false;
			}
			
			UStaticMeshComponent* rightGear = Cast<UStaticMeshComponent>(GetComponentByName(TEXT("WR9")));
			if (rightGear != NULL)
			{
				return rightGear->SetStaticMesh(theItemData->StaticMesh);
			}
			else
			{
				UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForEquipmentType(EEquipmentSlot, UStaticMesh*) Error: failed to cast the right Gear to UStaticMeshComponent!");
				return false;
			}	
		}
		else if (theItemData->EquipmentSlot == EEquipmentSlot::VE_TRACK)
		{
			//track = Track_L/Track_R (not UStaticMeshComponnents but UHierarchicalInstancedStaticMeshComponent)
			UHierarchicalInstancedStaticMeshComponent* leftTrack = Cast<UHierarchicalInstancedStaticMeshComponent>(GetComponentByName(TEXT("Track_L")));
			if (leftTrack != NULL)
			{
				if (leftTrack->SetStaticMesh(theItemData->StaticMesh) == false)
				{
					UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForEquipmentType(EEquipmentSlot, UStaticMesh*) Error: failed to set static mesh for left track!");
					return false;
				}
			}
			else
			{
				UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForEquipmentType(EEquipmentSlot, UStaticMesh*) Error: failed to cast the left track to UHierarchicalInstancedStaticMeshComponent!");
				return false;
			}

			UHierarchicalInstancedStaticMeshComponent* rightTrack = Cast<UHierarchicalInstancedStaticMeshComponent>(GetComponentByName(TEXT("Track_R")));
			if (rightTrack != NULL)
			{
				return rightTrack->SetStaticMesh(theItemData->StaticMesh);
			}
			else
			{
				UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForEquipmentType(EEquipmentSlot, UStaticMesh*) Error: failed to cast the left track to UHierarchicalInstancedStaticMeshComponent!");
				return false;
			}
		}
		else if (theItemData->EquipmentSlot == EEquipmentSlot::VE_TURRET)
		{
			UStaticMeshComponent* tankTurretMeshComponent = Cast<UStaticMeshComponent>(GetComponentByName(TEXT("Tank_Turret")));
			if (tankTurretMeshComponent != NULL)
			{
				if (tankTurretMeshComponent->SetStaticMesh(theItemData->StaticMesh) == false)
				{
					UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForEquipmentType(EEquipmentSlot, UStaticMesh*) Error: failed to set static mesh for tank turret!");
					return false;
				}

				return true;
			}

			UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForEquipmentType(EEquipmentSlot, UStaticMesh*) Error: failed to cast the found turret component to UStaticMeshComponent!");
		}
		else if (theItemData->EquipmentSlot == EEquipmentSlot::VE_WHEEL)
		{
			FString currLeftWheelName;
			FString currRightWheelName;
			UStaticMeshComponent* foundLeftWheel;
			UStaticMeshComponent* foundRightWheel;
			
			//wheels = WR(1-8)/WL(1-8)
			for (int index = 1; index <= 8; index++)
			{
				currLeftWheelName = FString::Printf(TEXT("WL%d"), index);
				currRightWheelName = FString::Printf(TEXT("WR%d"), index);
				foundLeftWheel = Cast<UStaticMeshComponent>(GetComponentByName(FName(*currLeftWheelName)));
				foundRightWheel = Cast<UStaticMeshComponent>(GetComponentByName(FName(*currRightWheelName)));
				
				//Set static mesh for current left wheel.
				if (foundLeftWheel != NULL)
				{
					if (foundLeftWheel->SetStaticMesh(theItemData->StaticMesh) == false)
					{
						UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForEquipmentType(EEquipmentSlot, UStaticMesh*) Error: failed to set the static mesh for one of the left wheels!");
						return false;
					}
				}
				else
				{
					UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForEquipmentType(EEquipmentSlot, UStaticMesh*) Error: failed to cast the found left wheel component to UStaticMeshComponent!");
					return false;
				}

				//Set static mesh for current right wheel.
				if (foundRightWheel != NULL)
				{
					if (foundRightWheel->SetStaticMesh(theItemData->StaticMesh) == false)
					{
						UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForEquipmentType(EEquipmentSlot, UStaticMesh*) Error: failed to set the static mesh for one of the right wheels!");
						return false;
					}
				}
				else
				{
					UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForEquipmentType(EEquipmentSlot, UStaticMesh*) Error: failed to cast the found right wheel component to UStaticMeshComponent!");
					return false;
				}
			}

			return true;
		}
	}

	UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForEquipmentType(EEquipmentSlot, UStaticMesh*) Error: theStaticMesh was null!");
	return false;
}

/// <summary>
/// Updates/Changes the specified static mesh based on the provided component name.
/// </summary>
/// <param name="theNamedComponent">The name of the component on the tank blueprint.</param>
/// <param name="theStaticMesh">The static mesh to attach to the corresponding component.</param>
/// <returns>true on success, false otherwise.</returns>
bool APhysicsTankPawn::SetStaticMeshForNamedComponent(const FString theNamedComponent, UStaticMesh* theStaticMesh)
{
	UStaticMeshComponent* tankComponent;

	if (theNamedComponent.IsEmpty() == false && theStaticMesh != NULL)
	{
		tankComponent = Cast<UStaticMeshComponent>(GetComponentByName(FName(theNamedComponent)));
		if (tankComponent != NULL)
		{
			return tankComponent->SetStaticMesh(theStaticMesh);
		}

		UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForNamedComponent(const FString, UStaticMesh*) Error: tankComponent is null!");
		return false;
	}

	UNeuralTanksUtils::WriteToLog("APhysicsTankPawn::SetStaticMeshForNamedComponent(const FString, UStaticMesh*) Error: theNamedComponent parameter is empty or theStaticMesh parameter is null!");
	return false;
}