#include "gen_bot_header.h"

//harSens: Linux compile fix
extern CLeftOutput lcout;

// ==================================================================
// Code of CBot - Begin
// ==================================================================

void CBot::MakeTask(ETaskCode TaskCode)
{
	if (m_pCurrentBotTask)
	{
		if (m_pCurrentBotTask->TaskIdentify() == TaskCode)
			return; // don't make the task when its already made...

		m_ePrevTask = m_pCurrentBotTask->TaskIdentify();
		KillCurrentTask();
	}

	switch (TaskCode)
	{
	case TaskPowerup:
		m_pCurrentBotTask = new CPowerUpTask(this);
		break;
	case TaskTeleport:
		m_pCurrentBotTask = new CTeleportTask(this);
		break;
	case TaskBlock:
		m_pCurrentBotTask = new CBlockTask(this);
		break;
	case TaskNormalMovement:
		m_pCurrentBotTask = new CNormalMovementTask(this);
		break;
	case TaskKickEnemy:
		m_pCurrentBotTask = new CKickEnemyTask(this);
		break;
	case TaskCombat:
		m_pCurrentBotTask = new CCombatTask(this);
		break;
	case TaskFinishMelee:
		m_pCurrentBotTask = new CFinishMeleeTask(this);
		break;
	case TaskAscend:
		m_pCurrentBotTask = new CAscendTask(this);
		break;
	case TaskPowerstruggle:
		m_pCurrentBotTask = new CPowerStruggleTask(this);
		break;
	case TaskLookAround:
		m_pCurrentBotTask = new CLookAroundTask(this);
		break;
	case TaskRecoverHP:
		m_pCurrentBotTask = new CRecoverHPTask(this);
		break;
	case TaskWallJump:
		m_pCurrentBotTask = new CWallJumpTask(this);
		break;
	case TaskIdle:
		m_pCurrentBotTask = new CIdleTask(this);
		break;
	case TaskRecoverMelee:
		m_pCurrentBotTask = new CRecoverMeleeTask(this);
		break;
	case TaskJumpOffGround:
		m_pCurrentBotTask = new CJumpOffGroundTask(this);
		break;
	case TaskBlinded:
		m_pCurrentBotTask = new CBlindedTask(this);
		break;
	case TaskNormalJump:
		m_pCurrentBotTask = new CNormalJumpTask(this);
		break;
	case TaskShootNonChargeble:
		m_pCurrentBotTask = new CShootNonChargebleTask(this);
		break;
	case TaskSwoop:
		m_pCurrentBotTask = new CSwoopTask(this);
		break;
	case TaskCombo:
		m_pCurrentBotTask = new CComboTask(this);
		break;
	case TaskCounterMelee:
		m_pCurrentBotTask = new CCounterMeleeTask(this);
		break;
	case TaskHuntEnemy:
		m_pCurrentBotTask = new CHuntEnemyTask(this);
		break;
	case TaskDB:
		m_pCurrentBotTask = new CDBTask(this);
		break;
	case TaskShootCandy:
		m_pCurrentBotTask = new CShootCandyTask(this);
		break;
	case TaskShootScatterShots:
		m_pCurrentBotTask = new CShootScatterShotsTask(this);
		break;
	case TaskChargeAttack:
		m_pCurrentBotTask = new CChargeAttackTask(this);
		break;
	case TaskControlBeam:
		m_pCurrentBotTask = new CControlBeamTask(this);
		break;
	case TaskControlFriezaDisc:
		m_pCurrentBotTask = new CControlFriezaDiscTask(this);
		break;
	case TaskBeamJump:
		m_pCurrentBotTask = new CBeamJumpTask(this);
		break;
	case TaskKameTorp:
		m_pCurrentBotTask = new CKameTorpTask(this);
		break;
	case TaskGetSensu:
		m_pCurrentBotTask = new CGetSensuBeanTask(this);
		break;
	case TaskGetCandy:
		m_pCurrentBotTask = new CGetCandyTask(this);
		break;
	case TaskHeadToGoal:
		m_pCurrentBotTask = new CMoveToGoalTask(this);
		break;

	// tasks for super bots
	case TaskShootLaser1:
		m_pCurrentBotTask = new CShootLaser1Task(this);
		break;
	case TaskAbsorbHealth:
		m_pCurrentBotTask = new CAbsorbHealthTask(this);
		break;
	case TaskDrainEnts:
		m_pCurrentBotTask = new CDrainEntsTask(this);
		break;
	}

#ifdef _DEBUG
	if (!m_pCurrentBotTask)
	{
		lcout << "Bot Task " << TaskCode << " not found !!\n";
//		clog << "Task " << TaskCode << " not found !!\n";
	}
#endif
}

void CBot::MakeAvoidSwoopTask()
{
	bool CheckBack = false;

	if (RANDOM_LONG(0,1))
		CheckBack = true;

	short SwoopDir = GetBestEscapeSwoopDir(CheckBack);

	switch(SwoopDir)
	{
	case dir_left:
		MakeCombo(COMBO_TYPE_LEFT_SWOOP);
		break;
	case dir_right:
		MakeCombo(COMBO_TYPE_RIGHT_SWOOP);
		break;
	case dir_up:
		MakeCombo(COMBO_TYPE_UP_SWOOP);
		break;
	case dir_down:
		MakeCombo(COMBO_TYPE_DOWN_SWOOP);
		break;
	case dir_back:
		MakeCombo(COMBO_TYPE_BACKWARD_SWOOP);
		break;
	}
}

void CBot::KillCurrentTask()
{
	if (m_pCurrentBotTask)
	{
		m_ePrevTask = m_pCurrentBotTask->TaskIdentify();
		delete m_pCurrentBotTask;
		m_pCurrentBotTask = NULL;
	}
}

void CBot::BotPerformTasks()
{
	if (m_pCurrentBotTask == NULL)
		return;

	if (m_pCurrentBotTask->GetTaskState() == DONE)
	{
		KillCurrentTask();

		// hack hack... Since its not possible to create a task in a task, we do it here
		if (m_eTaskAfterTask != TaskNone)
		{
			MakeTask(m_eTaskAfterTask);
			m_eTaskAfterTask = TaskNone;
		}
		return;
	}

	if (m_pCurrentBotTask->GetTaskDelay() <= gpGlobals->time)
	{
		if (m_pCurrentBotTask->ShouldManageEnemies())
			m_pCurrentBotTask->ManageEnemies();

		m_pCurrentBotTask->TaskThink();
	}
	else
	{
		//ResetMoveSpeed(); // Don't move while 'delaying'
	}

	if (m_pCurrentBotTask->GetTaskState() == DONE)
	{
		KillCurrentTask();

		// hack hack... Since its not possible to create a task in a task, we do it here
		if (m_eTaskAfterTask != TaskNone)
		{
			MakeTask(m_eTaskAfterTask);
			m_eTaskAfterTask = TaskNone;
		}
	}
}

void CBot::CheckNecessarilyThings()
{
	bool CheckWJump = false;

	if (m_fBlock)
	{
		bool BotStopBlock = false;

		if (m_pCurrentBotTask)
		{
			if (m_pCurrentBotTask->TaskIdentify() != TaskBlock)
				BotStopBlock = true;
		}
		else
			BotStopBlock = true;

		if (BotStopBlock)
			StopBlock();
	}

    if (m_fBlinded)
	{
		MakeTask(TaskBlinded);
		return;
	}

	if (m_fBlockStruggling)
	{
		MakeTask(TaskBlock);
		return;
	}

	if (m_pCurrentBotTask)
	{
		if (m_pCurrentBotTask->TaskIdentify() != TaskWallJump)
			CheckWJump = true;
	}
	else
		CheckWJump = true;

	if (CheckWJump)
	{
		if ((m_fWallJump) || ((m_fMelee) && (m_Melee.m_eThrowStatus == THROW_WALLSTICK)))
		{
			MakeTask(TaskWallJump);
			return;
		}
	}

	if (m_fAscending)
	{
		MakeTask(TaskAscend);
		return;
	}

	// Should the bot behave idle ?
	if (BotManager.BotsAreIdle() || m_fBecomeCandy)
	{
		MakeTask(TaskIdle);
		return;
	}

	if (m_fMelee)
	{
		// Should the bot behave idle ?
		if ((m_Melee.m_eThrowStatus == THROW_LAY) || (m_Melee.m_fPreKnockStrug))
		{
			MakeTask(TaskIdle);
			return;
		}


		if (m_Melee.m_eAdvMeleeStatus == PREPUNCH_DEFEND)
		{
			MakeTask(TaskBlock);
			return;
		}

		if (m_Melee.m_eThrowStatus == THROW_LANDRECOVER)
		{
			MakeTask(TaskJumpOffGround);
			return;
		}
		else if ((m_Melee.m_eAdvMeleeStatus == GRAB_DEFEND) ||
				 ((m_Melee.m_eThrowStatus != THROW_DISABLE) &&
				  (m_Melee.m_eThrowStatus != THROW_WALLSTICK)))
		{
			MakeTask(TaskRecoverMelee);
			return;
		}

		if (m_Melee.m_eAdvMeleeStatus == PREPUNCH_ATTACK)
		{
			MakeTask(TaskFinishMelee);
			return;
		}
		else if ((m_Melee.m_eAdvMeleeStatus == CHARGE_ATTACK) ||
				 (m_Melee.m_eAdvMeleeStatus == CHARGE_DEFEND) ||
				 (m_Melee.m_eAdvMeleeStatus == EXE_ATTACK) ||
				 (m_Melee.m_eAdvMeleeStatus == EXE_DEFEND) ||
				 (m_Melee.m_eAdvMeleeStatus == STRUGGLE_DEFEND) ||
				 (m_Melee.m_eAdvMeleeStatus == STRUGGLE_ATTACK) ||
		 		 (m_Melee.m_eAdvMeleeStatus == STRUGGLE_BURST_ATTACK))
		{
			if (m_pCurrentBotTask)
			{
				// only counter melee when we don't charge a combo or throw...
				if (m_pCurrentBotTask->TaskIdentify() != TaskFinishMelee)
					MakeTask(TaskCounterMelee);
			}
			else
				MakeTask(TaskCounterMelee);

			return;
		}

		if (pev->movetype == MOVETYPE_PLAYERSWOOP)
		{
			if (m_pCurrentBotTask)
			{
				if (m_pCurrentBotTask->TaskIdentify() != TaskCombo)
				{
					if (m_sSwoopFlags == SWOOP_GOTODBGOAL)
					{
						if (CheckDragonBalls())
						{
							MakeTask(TaskDB);
							return;
						}
					}

					MakeTask(TaskSwoop);
					return;
				}
			}
			else
			{
				if (m_sSwoopFlags == SWOOP_GOTODBGOAL)
				{
					if (CheckDragonBalls())
					{
						MakeTask(TaskDB);
						return;
					}
				}

				MakeTask(TaskSwoop);
				return;
			}
		}
	}

	if (m_fPowerStruggling || m_fBlockStruggleAttacker)
	{
		MakeTask(TaskPowerstruggle);
		return;
	}

	if (m_fPowerUp)
	{
		// stop with powerup when we don't want to powerup...
		if (m_pCurrentBotTask)
		{
			if ((m_pCurrentBotTask->TaskIdentify() != TaskPowerup) &&
				(m_pCurrentBotTask->TaskIdentify() != TaskCombo))
				StopPowerUp();
		}
		else
			StopPowerUp();
	}

	if (m_fControl && !m_fBlockStruggleAttacker)
	{
		if (m_pActiveItem->m_iId == WEAPON_FRIEZADISC)
		{
			MakeTask(TaskControlFriezaDisc);
			return;
		}

		if (m_pCurrentBotTask)
		{
			if ((m_pCurrentBotTask->TaskIdentify() != TaskControlBeam) &&
				(m_pCurrentBotTask->TaskIdentify() != TaskBeamJump))
				MakeTask(TaskControlBeam);
		}
		else
			MakeTask(TaskControlBeam);

		return;
	}
}
void CBot::CheckBotInternalStatus()
{
	int StatusChecks = 0;
	bool CheckEverything = false;

	if (m_pCurrentBotTask)
	{
		StatusChecks = m_pCurrentBotTask->GetBotStatusChecks();
	}
	else
		CheckEverything = true;

	if ((StatusChecks & CheckKi) || (CheckEverything))
	{
		if (BotCanPowerUp())
		{
			if ((GetKi() < 1000) && (m_fBotPowerUpDelay <= gpGlobals->time) &&
				(!m_fPowerUp))
			{
				int MinKiLevel = 750;

				if (m_pBotEnemy)
				{
					float flDist = GetDistance(m_pBotEnemy->pev->origin);
					if (flDist < 1250.0f)
					{
						switch (m_pPersonality->aggression_level)
						{
						case Aggressive:
							MinKiLevel = 700;
							break;
						case Normal:
							MinKiLevel = 750;
							break;
						case Defensive:
							MinKiLevel = 800;
							break;
						}
					}
				}

				if (m_bSkillNr > 0)
					MinKiLevel -= (m_bSkillNr*80);

				if (m_sDangerLevel >= DANGER_MEDIUM_MIN_VAL)
				{
					if (m_sDangerLevel <= DANGER_MEDIUM_MAX_VAL)
						MinKiLevel = 100;
					else
						MinKiLevel = 20;
				}

				if (GetKi() <= MinKiLevel)
				{
					if ((GetKi() > 225) && (m_sDangerLevel >= DANGER_MEDIUM_MIN_VAL) &&
						(RANDOM_LONG(1, 100) <= m_pPersonality->teleport_percent))
					{
						PrintAI("Tele Powerup\n");
						MakeCombo(COMBO_TYPE_TELE_POWERUP);
					}
					else if (BotShouldFallDown())
					{
						PrintAI("Fall Powerup");
						MakeCombo(COMBO_TYPE_FALL_POWERUP);
					}
					else
					{
						MakeTask(TaskPowerup);
						StartPowerUp();
					}
					return;
				}
			}
		}
	}

	if ((StatusChecks & CheckFly) || (CheckEverything))
	{
		if (m_fCheckFlyDelay < gpGlobals->time)
		{
			if (RANDOM_LONG(1, 100) <= m_pPersonality->fly_percent)
			{
				m_bShouldFly = true;
			}
			else
			{
				m_bShouldFly = false;
			}

			m_fCheckFlyDelay = gpGlobals->time + RANDOM_FLOAT(15.0, 30.0);
		}

		if ((!m_fFlying) && (m_bShouldFly))
			StartFly();
		else if	((m_fFlying) && (m_bShouldFly == false))
			StopFly();
	}

	if ((StatusChecks & CheckAscend) || (CheckEverything))
	{
		if ((m_fLastAscendCheck <= gpGlobals->time) && (BotCanAscend()))
		{
			m_fLastAscendCheck = gpGlobals->time + 0.5f; // don't check every frame
			short AscendPercent = 0;

			if ((m_sDangerLevel == 0) ||
			    ((m_iDangerType == DangerEnemy) || (m_iDangerType == DangerCurrentEnemy)))
			{	
				AscendPercent = 100;
			}
			else
			{
				if (m_sDangerLevel <= DANGER_LIGHT_MIN_VAL)
					AscendPercent = 95;
				else if ((m_sDangerLevel >= DANGER_MEDIUM_MIN_VAL) &&
				         (m_sDangerLevel <= DANGER_MEDIUM_MAX_VAL))
					AscendPercent = 30;
				else
					AscendPercent = 0;
			}

			if (RANDOM_LONG(1, 100) <= AscendPercent)
			{
				if (BotShouldFallDown())
				{
					MakeCombo(COMBO_TYPE_FALL_ASCEND);
					return;
				}
				else
           			StartAscend();
			}
		}
	}

	if ((StatusChecks & CheckHealth) || (CheckEverything))
	{
		if ((m_iSensuBeans) && (pev->playerclass != 1)) // bot has a sensu bean?
		{
			// how more beans the bot has, how sooner he will take a bean
			float MinHealthToTakeABean;
			if (m_iSensuBeans >= 3)
				MinHealthToTakeABean = (float(m_iMaxHealth) * 0.75f);
			else if (m_iSensuBeans >= 2)
				MinHealthToTakeABean = (float(m_iMaxHealth) * 0.5f);
			else if (m_iSensuBeans >= 1)
				MinHealthToTakeABean = (float(m_iMaxHealth) * 0.25f);

			if (pev->health <= MinHealthToTakeABean)
			{
				PrintAI("Eating sensu bean...\n");
				SelectItem("weapon_sensu");
				pev->button |= IN_ATTACK;
			}
		}
		else if ((pev->playerclass == 1) && (GetHealth() <= 75))
		{
			// Only check if bot isn't defending db's or doesn't have an enemy
			if ((!m_pBotEnemy) || (m_eBotDBType != DB_DEFENDER) || (!BotManager.IsDBMode()))
			{
				short HealthGainPercent = 100;

				if (m_sDangerLevel > 0)
					HealthGainPercent -= (m_sDangerLevel / 1.5f);

				HealthGainPercent -= GetHealth();

				if (RANDOM_LONG(1, 100) <= HealthGainPercent)
				{
					MakeTask(TaskRecoverHP);
					return;
				}
			}
		}
	}

	return;
}

void CBot::CheckBotMiscThings()
{
	int MiscChecks = 0;
	bool CheckEverything = false;

	if (m_pCurrentBotTask)
	{
		MiscChecks = m_pCurrentBotTask->GetBotMiscChecks();
	}
	else
		CheckEverything = true;


	if ((MiscChecks & CheckDragonBall) || (CheckEverything))
	{
		if (m_fDBCheckDelay < gpGlobals->time)
		{
			m_fDBCheckDelay = gpGlobals->time + 0.5f;

			if (CheckDragonBalls())
			{
				MakeTask(TaskDB);
				return;
			}
		}
	}

	if (((MiscChecks & CheckForEnemies) || (CheckEverything)) && (BotManager.BotsSearchEnemies()))
	{
		m_pBotEnemy = GetBestEnemyFromList();

		if (m_pBotEnemy)
		{
			MakeTask(TaskCombat);
			return;
		}
	}

	if ((MiscChecks & CheckForChatMsg) || (CheckEverything))
	{
		// Can bots chat AND does the bot not allready picked a chat msg?
		if ((BotManager.ShouldBotsChat()) && (m_fBotSayTime < gpGlobals->time))
		{
			short RandNr = RANDOM_LONG(1, 100);

			// Time to look for an reply?
			if (m_fReplyDelay < gpGlobals->time)
			{
				if (RandNr <= m_pPersonality->chat_percent)
					ChatClass.LetBotCheckForReply(this);

				m_fReplyDelay = gpGlobals->time + RANDOM_FLOAT(5.5f, 15.0f);
			}

			// Time to say something random?
			if (m_fBotChatTime < gpGlobals->time)
			{
				if (RandNr <= m_pPersonality->chat_percent)
					ChatClass.LetBotSaySomething(this);

				m_fBotChatTime = gpGlobals->time + RANDOM_FLOAT(15.0f, 40.0f);
			}
		}
	}

	if ((MiscChecks & CheckForHunt) || (CheckEverything))
	{
		if (CheckForHuntEnemy())
		{
			MakeTask(TaskHuntEnemy);
			return;
		}
	}

	if ((MiscChecks & CheckForKameTorps) || (CheckEverything))
	{
		if (m_fCheckKameTorpTime < gpGlobals->time)
		{
			if (BotShouldMakeKameTorps())
			{
				MakeTask(TaskKameTorp);
				return;
			}
			m_fCheckKameTorpTime = gpGlobals->time + RANDOM_FLOAT(22.0, 40.0);
		}
	}

	if ((MiscChecks & CheckForCookie) || (CheckEverything))
	{
		if ((m_fCheckCookieTime < gpGlobals->time) && (pev->playerclass == 1))
		{
			m_fCheckCookieTime  = gpGlobals->time + 0.25f;

			CBaseEntity *pCookie = NULL, *pNearestCookie = NULL;
			float flNearestDist = 9999.9f, flDist; 
			while ((pCookie = UTIL_FindEntityByClassname(pCookie, "candy")) != NULL)
			{
				if (IsNull(pCookie))
					continue;

				flDist = GetDistance(pCookie->pev->origin);
				if (flDist > flNearestDist)
					continue;

				if ((EntIsInFOV(pCookie)) && (FVisible(pCookie->pev->origin)))
				{
					flNearestDist = flDist;
					pNearestCookie = pCookie;
				}
			}

			if (pNearestCookie)
			{
				AimToVec(pNearestCookie->pev->origin);
				m_pCandyTarget = pNearestCookie;
				MakeTask(TaskGetCandy);
				return;
			}
		}
	}

	if ((MiscChecks & CheckForSensu) || (CheckEverything))
	{
		if ((m_iSensuBeans < 5) && (m_fSearchForSensuDelay < gpGlobals->time))
		{
			m_fSearchForSensuDelay = gpGlobals->time + 0.25f;

			if (m_pTargetSensuBean = SearchSensuBean())
			{
				MakeTask(TaskGetSensu);
				return;
			}
		}
	}
}

// ==================================================================
// Code of CBot - End
// ==================================================================

// ==================================================================
// Code of Bot Tasks - Begin
// ==================================================================

// Base task class
CBaseTask::CBaseTask(CBot *pB)
{
	pBot = pB;
	pEdict = pB->edict();
	pEnemy = pBot->m_pBotEnemy;

	TaskState = BUSY;

	SetGeneralDelay();
}

CBaseTask::~CBaseTask()
{
	pBot = NULL;
	pEdict = NULL;
	pEnemy = NULL;
}

void CBaseTask::MakeCombo(EComboType CT)
{
	TaskState = DONE;
	pBot->m_eTaskAfterTask = TaskCombo;
	pBot->m_pBotCombo = new CBotCombo(pBot, CT);
}

void CBaseTask::SetGeneralDelay()
{
	if (pBot->IsSuperBot())
		Delay = gpGlobals->time; // Super bots don't have a reaction delay :-)
	else
	{
		Delay = gpGlobals->time + RANDOM_FLOAT(pBot->m_pBotSkill->flMinGenReactionDelay,
											   pBot->m_pBotSkill->flMaxGenReactionDelay);
	}
}

// Power up task
CPowerUpTask::~CPowerUpTask()
{
	pBot->StopPowerUp();
	pBot->m_fBotPowerUpDelay = gpGlobals->time + 0.6;
	pBot->m_sKiGainTarget = 0;
}

void CPowerUpTask::TaskThink()
{
	int MinKiLevel = GetKiTarget();

/*
	if (pBot->m_pBotEnemy)
	{
		float flDist = pBot->GetDistance(pBot->m_pBotEnemy->pev->origin);
		if (flDist < 1250.0f)
		{
			switch (pBot->m_pPersonality->aggression_level)
			{
			case Aggressive:
				MinKiLevel = 700;
				break;
			case Normal:
				MinKiLevel = 750;
				break;
			case Defensive:
				MinKiLevel = 800;
				break;
			}
		}
	}*/

	if (pBot->GetKi() >= MinKiLevel)
	{
		TaskState = DONE; // we're done now :)
		return;
	}

	pBot->ResetMoveSpeed();

	if (!pBot->m_fPowerUp)
	{
		pBot->StartPowerUp();
	}
}

short CPowerUpTask::GetKiTarget()
{
	if (pBot->m_sKiGainTarget)
		return pBot->m_sKiGainTarget;

	if ((BotManager.IsDBMode()) &&
		((pBot->m_iDragonball) || ((pBot->m_eBotDBType == DB_DEFENDER) && (pEnemy))))
		return 800;

	return 990;
}


// Teleport task
CTeleportTask::CTeleportTask(CBot *pB) : CBaseTask(pB)
{
	if (!pBot->m_iTeleportDir)
		pBot->m_iTeleportDir = GetRandomTeleDir();

	if (pBot->m_fTeleportDelay < gpGlobals->time)
		pBot->m_fTeleportDelay = Delay + 0.2;

	if (pBot->BotShouldFallDown())
	{
		if (pBot->m_fFlying)
			pBot->StopFly();		

		pBot->m_bShouldFly = false;
		pBot->m_fCheckFlyDelay = gpGlobals->time + RANDOM_FLOAT(0.8, 1.2);
	}
}

CTeleportTask::~CTeleportTask()
{
	pBot->m_iTeleportDir = 0;
	pBot->m_fTeleportDelay = 0.0f;
}

void CTeleportTask::TaskThink()
{
	pBot->m_vAimVec.x = 0;

	pBot->ResetMoveSpeed();
	pBot->SetMoveDir(pBot->m_iTeleportDir);

	if (pBot->m_fTeleportDelay <= gpGlobals->time)
	{
		pBot->Teleport();
		TaskState = DONE;
	}
}

int CTeleportTask::GetRandomTeleDir()
{
	TMultiChoice<int> TeleDirChoices;

	// Less chance for left & right since this doesn't confuse the enemy very much
	if (pBot->GetCollisionDist(dir_left, 350.0f) >= 350.0f)
		TeleDirChoices.Insert(dir_left, 30);

	if (pBot->GetCollisionDist(dir_right, 350.0f) >= 350.0f)
		TeleDirChoices.Insert(dir_right, 30);

	// More chance for up & down teleport because it confuses the enemy more :)
	if (pBot->GetCollisionDist(dir_up, 650.0f) >= 350.0f)
		TeleDirChoices.Insert(dir_up, 75);
	
	if (pBot->GetCollisionDist(dir_down, 650.0f) >= 350.0f)
		TeleDirChoices.Insert(dir_down, 75);

	if (pBot->GetCollisionDist(dir_left_back, 650.0f) >= 350.0f)
		TeleDirChoices.Insert((dir_back | dir_left), 40);

	if (pBot->GetCollisionDist(dir_right_back, 650.0f) >= 350.0f)
		TeleDirChoices.Insert((dir_back | dir_right), 40);

	if (pBot->GetCollisionDist(dir_back, 650.0f) >= 350.0f)
		TeleDirChoices.Insert(dir_back, 40);

	int TeleChoice = 0;
	TeleDirChoices.GetSelection(TeleChoice);
	return TeleChoice;
}

// Block task
CBlockTask::~CBlockTask()
{
	pBot->m_pEntToBlock = NULL;

	if (pBot->m_fBlock)
		pBot->StopBlock();
}

void CBlockTask::TaskThink()
{
	pBot->ResetMoveSpeed();

	// If block time is over AND not blockstruggling AND not block prepunches AND not its
	// not very dangerous OR bot almost hasn't got any ki left
	if (((BlockTime < gpGlobals->time) && (pBot->m_fBlockStruggling == false) &&
		(pBot->m_Melee.m_eAdvMeleeStatus != PREPUNCH_DEFEND) &&
		(pBot->m_sDangerLevel < DANGER_HEAVY_MIN_VAL)) || (pBot->GetKi() < 8))
	{
		if (pBot->m_fBlock)
			pBot->StopBlock();
		TaskState = DONE;
		return;
	}

	if (pBot->m_fBlockStruggling)
	{
		if (pBot->m_pBotSkill->bUsesTurbo)
		{
			pBot->ToggleTurbo(true);
			pBot->m_fTurboTime = gpGlobals->time + 1.5f;
		}

		edict_t *pEnt = (pBot->m_pTouchEnt) ? pBot->m_pTouchEnt->edict() : NULL;

		// Get the attack the bot is block struggling with
		if ((IsNull(pEnt) == false) && (IsAttack(pEnt)))
		{
//			lcout << "BS'ing with: " << STRING(pBot->pTouchEnt->v.classname) << "\n";

			if (IsNull(pEnt->v.owner) == false)
			{
				float AttackY = pEnt->v.angles.y;
				float AttackX = pEnt->v.angles.x;

				Vector VecToAimAt = pBot->GetAimAngles(pEnt->v.owner->v.origin);

				float YDiff = fabs(UTIL_WrapAngle(AttackY + 180) - VecToAimAt.y);
				float XDiff = fabs(UTIL_WrapAngle(AttackX + 180) - VecToAimAt.x);

				if ((YDiff < 20) && (XDiff < 20))
				{
					pBot->SetIdealAngles(VecToAimAt);
				}
				else
					pBot->AimToVec(pEnt->v.origin);
			}
			else
				pBot->AimToVec(pEnt->v.origin);
		}
	}
	else if (pBot->m_pEntToBlock)
	{
		// Aim at the dangerous entity(player, attack)
		pBot->AimToVec(pBot->m_pEntToBlock->pev->origin);
	}

	if (!pBot->m_fBlock)
		pBot->StartBlock();
}

// Kick enemy with melee task
void CKickEnemyTask::TaskThink()
{
	if (pBot->m_pBotEnemy)
	{
		if (pEnemy->pev->movetype == MOVETYPE_PLAYERSWOOP)
		{
			// Don't try to kick the enemy while he swoops..
			TaskState = DONE;
			return;
		}

		if (!pBot->CanAttackPlayer(pBot->m_pBotEnemy))
		{
			TaskState = DONE;
			return;
		}

		if (pBot->CanMeleeKickPlayer(pEnemy))
			m_fCanKickTime = gpGlobals->time;
		else if ((gpGlobals->time - m_fCanKickTime) > 0.2f)
		{
			// Couldn't (safely) kick the enemy for a while, break
			TaskState = DONE;
			return;
		}

		float dist = pBot->GetDistance(pBot->m_pBotEnemy->pev->origin);

		if (dist < 30.0f)
		{
			pBot->SetMoveDir(dir_back);
		}
		else if (dist < 350.0f)
		{
			pBot->SetMoveDir(dir_forward);

			if (dist > 150.0f)
			{
				// Check if the bot isn't facing the enemy and the enemy isn't facing the bot
				if ((EntIsInFieldOfView(pBot, pBot->m_pBotEnemy) == false) &&
					(EntIsInFieldOfView(pBot->m_pBotEnemy, pBot) == false))
				{
					pBot->ResetMoveSpeed(); // Give the bot some time to aim at his enemy.
				}
			}
		}
		else if (dist < 750.0f)
		{
			if ((pBot->GetKi() > 450) &&
				(RANDOM_LONG(1, 100) <= pBot->m_pPersonality->teleport_percent))
				pBot->Teleport();
			else
			{
				TaskState = DONE;
				return;
			}
		}
		else
		{
			// Enemy is to far away
			TaskState = DONE;
			return;
		}

		if ((pBot->m_pActiveItem) && (pBot->m_pActiveItem->m_iId != WEAPON_MELEE) && 
			(pBot->m_pActiveItem->m_iId != WEAPON_SWORD))
		{
			if (pEdict->v.playerclass == 7)
			{
				pBot->SelectItem("weapon_sword");
			}
			else
			{
				pBot->SelectItem("weapon_melee");
			}
		}

		if ((pBot->m_pBotSkill->bUsesTurbo) && (pBot->GetKi() > 200))
		{
			pBot->ToggleTurbo(true);
			pBot->m_fTurboTime = gpGlobals->time + 1.5;
		}
		pEdict->v.button |= IN_ATTACK2;
	}
	else
	{
		TaskState = DONE;
	}
}

// Power struggle task
void CPowerStruggleTask::TaskThink()
{
	if (pBot->m_fPowerStruggling || pBot->m_fBlockStruggleAttacker)
	{
		pEdict->v.button |= IN_ATTACK;

		if ((pBot->m_pBotSkill->bUsesTurbo) && (pBot->GetKi() > 20))
		{
			pBot->ToggleTurbo(true);
			pBot->m_fTurboTime = gpGlobals->time + 1.5;
		}
	}
	else
	{
		TaskState = DONE;
	}
}

// Ascend task (called while a bot is ascending)
void CAscendTask::TaskThink()
{
	if (!pBot->m_fAscending)
	{
		TaskState = DONE;
		return;
	}
	pBot->ResetMoveSpeed();
}

// Blinded task(made when a bot is....blinded)
void CBlindedTask::TaskThink()
{
    if (!pBot->m_fBlinded)
	{
		TaskState = DONE;
		return;
	}

	// Randomly move back
    pBot->SetMoveDir(dir_back);
    switch (RANDOM_LONG(0, 2))
	{
    case 0:
        pBot->SetMoveDir(dir_left);
        break;
    case 1:
        pBot->SetMoveDir(dir_right);
        break;
	}

	// Randomly teleport away(if skilled enough)
    if ((RANDOM_LONG(1, 1000) < 40) && (pBot->m_bSkillNr < 3) &&
		(RANDOM_LONG(1, 100) < pBot->m_pPersonality->teleport_percent))
        pBot->Teleport();
}

// Look around a bit task
void CLookAroundTask::TaskThink()
{
	// Time to look to another direction?
	if (LookAroundDelay < gpGlobals->time)
	{
		Stage++;
		if (Stage > 4)
		{
			float curdist = 0.0;
			float dist = 0.0;

			Vector DesiredSpot;

			for (int i=0;i<4;i++)
			{
				dist = UTIL_Distance(pEdict->v.origin, LookAroundSpots[i]);
				if (dist > curdist)
				{
					curdist = dist;
					DesiredSpot = LookAroundSpots[i];
				}
			}

			pBot->AimToVec(DesiredSpot);
			TaskState = DONE;
			return;
		}
		pBot->m_vAimVec.y = UTIL_WrapAngle(pBot->m_vAimVec.y+RANDOM_LONG(80, 100));

		UTIL_MakeVectors(pEdict->v.v_angle);

		Vector vecEnd = pEdict->v.origin + (gpGlobals->v_forward*10000);
        TraceResult tr;

		UTIL_TraceHull (pEdict->v.origin, vecEnd, ignore_monsters, human_hull, pEdict, &tr);
		
		LookAroundSpots[Stage-1] = tr.vecEndPos;

		LookAroundDelay = gpGlobals->time + RANDOM_FLOAT(0.4, 0.8);
		LookAroundDelay += (tr.flFraction*2);
	}

	pBot->ResetMoveSpeed(); // Dont move while looking around

	// Randomly look a bit up or down
	if (RANDOM_LONG(0,5) > 4)
		pBot->m_vAimVec.x = UTIL_WrapAngle(pBot->m_vAimVec.x+RANDOM_LONG(-10, 10));

	// Randomly look a bit left or right
	if (RANDOM_LONG(0,5) > 4)
		pBot->m_vAimVec.y = UTIL_WrapAngle(pBot->m_vAimVec.y+RANDOM_LONG(-10, 10));
}

// Recover health task (for buu bots)
void CRecoverHPTask::TaskThink()
{
	if ((pBot->GetKi() < 200) && (pBot->GetHealth() > 20))
	{
		TaskState = DONE;
		return;
	}

	if (pBot->GetKi() < 25)
	{
		TaskState = DONE;
		return;
	}

    // select this 'weapon' if it isn't already selected
    if ((pBot->m_pActiveItem) && (pBot->m_pActiveItem->m_iId != WEAPON_REGENERATION))
       pBot->SelectItem("weapon_regeneration");

	pEdict->v.button |= IN_ATTACK;
	pBot->ResetMoveSpeed();
}

// Wall jump task
CWallJumpTask::CWallJumpTask(CBot *pB) : CBaseTask(pB)
{
	if (!pBot->m_fWallJump)
		Delay = gpGlobals->time + 1.0;

	LifeTime = Delay + 3.0f;
	StartChargingTime = 0.0f;
}

void CWallJumpTask::TaskThink()
{
	if ((pBot->m_fWallJump) ||
		((pBot->m_fMelee) && (pBot->m_Melee.m_eThrowStatus == THROW_WALLSTICK)))
	{
		pBot->ResetMoveSpeed();
		pEdict->v.button |= IN_JUMP;
	}
	else
	{
		TaskState = DONE;
		return;
	}
}

// Idle task, made when a bot can't do anything
void CIdleTask::TaskThink()
{
	bool IsIdle = (BotManager.BotsAreIdle() || pBot->m_fBecomeCandy);

	if (!IsIdle)
		IsIdle = ((pBot->m_Melee.m_eAdvMeleeStatus == STRUGGLE_ATTACK) ||
				  (pBot->m_Melee.m_eAdvMeleeStatus == STRUGGLE_BURST_ATTACK) ||
				  (pBot->m_Melee.m_eThrowStatus == THROW_LAY) && (pBot->m_fMelee));

	if (IsIdle)
		pBot->ResetMoveSpeed();
	else
		TaskState = DONE;
}

// Recover melee task, gets made when a bot should recover after thrown or something
void CRecoverMeleeTask::TaskThink()
{
    if ((pBot->m_fMelee) && ((pBot->m_Melee.m_eAdvMeleeStatus == GRAB_DEFEND) ||
		 (pBot->m_Melee.m_eThrowStatus != THROW_DISABLE)))
		pEdict->v.button |= (IN_ATTACK | IN_JUMP);
	else
		TaskState = DONE;
}

// Jump of the ground task(after being thrown)
CJumpOffGroundTask::CJumpOffGroundTask(CBot *pB) : CBaseTask(pB)
{
	short LeftVal = 50, RightVal = 50, BackVal = 50, ForwardVal = 50;

	if (pEnemy)
	{
		int iEntDir = UTIL_EntDir(pBot, pEnemy->pev->origin);
	
		if (iEntDir & dir_left)
			LeftVal = 15;
		if (iEntDir & dir_right)
			RightVal = 15;
		if (iEntDir & dir_back)
			BackVal = 15;
		if (iEntDir & dir_forward)
			ForwardVal = 15;
	}

	TMultiChoice<int> JumpDirChoices;
	JumpDirChoices.Insert(dir_forward, ForwardVal);
	JumpDirChoices.Insert(dir_back, BackVal);
	JumpDirChoices.Insert(dir_left, LeftVal);
	JumpDirChoices.Insert(dir_right, RightVal);
	JumpDirChoices.Insert((dir_forward | dir_left), (LeftVal + ForwardVal) / 2);
	JumpDirChoices.Insert((dir_forward | dir_right), (RightVal + ForwardVal) / 2);
	JumpDirChoices.Insert((dir_back | dir_left), (LeftVal + BackVal) / 2);
	JumpDirChoices.Insert((dir_back | dir_right), (RightVal + BackVal) / 2);

	MoveDir = 0; // Incase no choice found....
	JumpDirChoices.GetSelection(MoveDir);
}

void CJumpOffGroundTask::TaskThink()
{
	pEdict->v.button |= IN_JUMP;

	pBot->ResetMoveSpeed();
	pBot->SetMoveDir(MoveDir);

	if ((pBot->m_Melee.m_eThrowStatus != THROW_LANDRECOVER) || (!pBot->m_fMelee))
	{
		TaskState = DONE;
		return;
	}
}

// Normal jump task, to avoid danger
void CNormalJumpTask::TaskThink()
{
	pEdict->v.button |= IN_JUMP;

	if (LifeTime <= gpGlobals->time)
	{
		TaskState = DONE;
		return;
	}

	if (pEdict->v.velocity.z < 0) // Going down?
	{
		TaskState = DONE;
		return;
	}

	if (pEdict->v.gravity < 0.1)
	{
		if (pEdict->v.flags & FL_ONGROUND)
			pBot->StopFly();
		else
		{
			TaskState = DONE;
			return;
		}
	}

	pBot->SetMoveDir(pBot->m_sJumpDir);

	if (TeleCheckDelay < gpGlobals->time)
	{
		TeleCheckDelay = gpGlobals->time + RANDOM_FLOAT(0.25, 0.4);

		if (RANDOM_LONG(1, 100) <= pBot->m_pPersonality->teleport_percent)
			pBot->Teleport();
	}
}

// Combo task
CComboTask::~CComboTask()
{
	if (pBot->m_pBotCombo)
		pBot->DeleteCombos();
}

void CComboTask::TaskThink()
{
	if (pBot->m_pBotCombo)
	{
		if (pBot->m_pBotCombo->m_eComboState == NOT_STARTED)
			pBot->m_pBotCombo->Activate();

		pBot->m_pBotCombo->ExcuteCombo();

		if (pBot->m_pBotCombo->m_eComboState == DONE)
		{
			pBot->m_eTaskAfterTask = pBot->m_pBotCombo->m_eTaskAfterCombo;

			pBot->DeleteCombos();
			TaskState = DONE;
			return;
		}
	}
	else
	{
		TaskState = DONE;
	}
}

// Counter melee task, used by bots to reteliate and defend a combo
CCounterMeleeTask::~CCounterMeleeTask()
{
	pBot->AddShootDelay();
	pBot->m_MeleeDefendButtons.DeleteAllNodes();
	pBot->ClearMeleeComboList();
}

// Beam jump task
void CBeamJumpTask::TaskThink()
{
	pEdict->v.button |= IN_JUMP;
	if (LifeTime <= gpGlobals->time)
	{
		TaskState = DONE;
		return;
	}

	bool BotBeamJumps = ((pBot->m_fControl) && (pEdict->v.movetype == MOVETYPE_RIDEBEAM));
	if ((pBot->m_fCharging == false) && (BotBeamJumps == false) &&
		(pBot->m_fControl == false))
	{
		TaskState = DONE;
		return;
	}

	if (!BotBeamJumps) // Bot doesn't beamjump yet?
	{
		// Aim at the spot where the beam should collide to
		pBot->AimToVec(pBot->m_vBeamJumpSpot);

		if ((pBot->m_iChargePercent) && ((CreateTime + 0.2f) >= gpGlobals->time))
		{
			pEdict->v.button |= IN_ATTACK;
		}
	}

	CBaseEntity *pAttack = UTIL_GetLastAttackFromPlayer(pBot);

	if (IsNull(pAttack))
	{
		if (LifeTime < (gpGlobals->time+0.4))
		{
			TaskState = DONE;
			return;
		}
	}

	// Does the bot (already) beamjumps and has an enemy
	if ((pBot->m_fControl) && (pEdict->v.movetype == MOVETYPE_RIDEBEAM) && (pBot->m_pBotEnemy))
	{
		float EnemyDist = pBot->GetDistance(pBot->m_pBotEnemy->pev->origin);
		float EnemyToAttackDist = UTIL_Distance(pBot->m_pBotEnemy->pev->origin, pAttack->pev->origin);
		float BotToAttackDist = pBot->GetDistance(pAttack->pev->origin);
		float ExplodeDistForBot = pAttack->pev->dmg * 11;
		float ExplodeDistForEnemy = pAttack->pev->dmg * 9;

		// If the the bot isn't close to his enemy AND not close to his own attack AND
		// the enemy is close to the bot's attack...
		if ((EnemyDist > 950.0f) && (EnemyToAttackDist < ExplodeDistForEnemy) &&
			(BotToAttackDist > ExplodeDistForBot))
		{
			TaskState = DONE; // Explode the beam >:)
			return;
		}
	}

	if (TraceDelay <= gpGlobals->time)
	{
		if (BotBeamJumps)
		{
			TraceDelay = gpGlobals->time + 0.1;
			if (pBot->GetCollisionDist(dir_back, 50.0) < 50.0f)
			{
				TaskState = DONE;
			}
		}
	}
}

// Sensu bean task, simply heads to a sensu bean(bag)
void CGetSensuBeanTask::TaskThink()
{
	if (IsNull(pBot->m_pTargetSensuBean))
	{
		TaskState = DONE;
		return;
	}

	if (pBot->m_pTargetSensuBean->pev->effects & EF_NODRAW)
	{
		TaskState = DONE;
		return;
	}

	if (pBot->m_iSensuBeans >= 5)
	{
		TaskState = DONE;
		return;
	}

	if (!pBot->FVisible(pBot->m_pTargetSensuBean->pev->origin))
	{
		TaskState = DONE;
		return;
	}

	if (pBot->m_fYawSpeed <= 25.0f)
		m_fLastTurnLessTime = gpGlobals->time;

	// Bot had to turn much for a while?
	if (m_fLastTurnLessTime < (gpGlobals->time - 1.0f))
		m_fStopMoveTime = gpGlobals->time + 0.25f; // Give the bot some time to aim

	if (m_fStopMoveTime >= gpGlobals->time)
		pBot->ResetMoveSpeed(); // Stop moving, so the bot can aim to the sensu beans

	pBot->AimToVec(pBot->m_pTargetSensuBean->pev->origin);

	if (pBot->m_pBotSkill->bUsesTurbo)
		pBot->ToggleTurbo(true);

	pBot->m_fTurboTime = gpGlobals->time + 0.25f;
}

// Get Candy task, simply heads to a candy
void CGetCandyTask::TaskThink()
{
	if (IsNull(pBot->m_pCandyTarget))
	{
		TaskState = DONE;
		return;
	}

	if (!pBot->FVisible(pBot->m_pCandyTarget->pev->origin))
	{
		TaskState = DONE;
		return;
	}

	if (pBot->m_fYawSpeed <= 25.0f)
		m_fLastTurnLessTime = gpGlobals->time;

	// Bot had to turn much for a while?
	if (m_fLastTurnLessTime < (gpGlobals->time - 1.0f))
		m_fStopMoveTime = gpGlobals->time + 0.25f; // Give the bot some time to aim

	if (m_fStopMoveTime >= gpGlobals->time)
		pBot->ResetMoveSpeed(); // Stop moving, so the bot can aim to the candy

	pBot->AimToVec(pBot->m_pTargetSensuBean->pev->origin);

	if (pBot->m_pBotSkill->bUsesTurbo)
		pBot->ToggleTurbo(true);

	pBot->m_fTurboTime = gpGlobals->time + 0.25f;
}

void CMoveToGoalTask::TaskThink()
{
	if (pBot->GetDistance(BotManager.m_vBotGoal) < 75.0f)
	{
		TaskState = DONE;
		return;
	}

	if (!pBot->m_pCurrentGoalFloodWP)
	{
		pBot->m_pCurrentGoalFloodWP = WaypointFlooder.GetNearestFloodWP(BotManager.m_vBotGoal, 750.0f);

		if (pBot->m_pCurrentGoalFloodWP == NULL)
		{
			TaskState = DONE;
			return;
		}
	}

	if (!pBot->HeadToGoal())
		TaskState = DONE;
}


// Shoot Health laser task(for super bots)
CShootLaser1Task::~CShootLaser1Task()
{
	if (pBot->m_pBotEnemy)
		pBot->m_pBotEnemy->pev->flags &= ~FL_FROZEN;
	pBot->AddShootDelay();
}

void CShootLaser1Task::TaskThink()
{
	if (pEnemy == NULL)
	{
		TaskState = DONE;
		return;
	}

	if (ShootTime < gpGlobals->time)
	{
		TaskState = DONE;
		return;
	}

	if (pEdict->v.health >= pBot->m_iMaxHealth)
	{
		TaskState = DONE;
		return;
	}

	if (pEdict->v.fuser4 < 150.0)
	{
		TaskState = DONE;
		return;
	}

	if (!pBot->CanAttackPlayer(pEnemy))
	{
		pEnemy->pev->flags &= ~FL_FROZEN;
		TaskState = DONE;
		return;
	}

	pBot->ResetMoveSpeed();

	pEnemy->pev->flags |= FL_FROZEN;

	/* UNDONE : need to keep the frozen time of each player
	pClient->FrozenTime = gpGlobals->time + 0.5;

	//if (pBot->pBotEnemy->v.maxspeed > 0.0)
		//	(*g_engfuncs.pfnSetClientMaxspeed)(pBot->pBotEnemy, 0.0);
*/
	pBot->ShootHealthBeam(pEnemy);

	if (pBot->m_fLastBeamDrawTime <= gpGlobals->time)
		pBot->m_fLastBeamDrawTime = gpGlobals->time + 0.1;
}

void CShootLaser1Task::ManageEnemies()
{
	// This task will manage enemies by his own...

	CBasePlayer *pTemp = pBot->m_pBotEnemy; // Store current enemy
	pBot->m_pBotEnemy = pEnemy = pBot->GetBestEnemyFromList();

	// Did the bot lost his enemy ?
	if (pBot->m_pBotEnemy == NULL)
	{
		if (UTIL_IsValidPlayer(pTemp, false))
		{
			// Add enemy searchy delay
			float MinDelay = pBot->m_pBotSkill->flMinEnemySearchDelay;
			float MaxDelay = pBot->m_pBotSkill->flMaxEnemySearchDelay;

			pBot->m_fSearchEnemyDelay = gpGlobals->time + RANDOM_FLOAT(MinDelay, MaxDelay);
		}
	}
	else if (pBot->m_pBotEnemy != pTemp) // else does the bot has now a different enemy
	{
		if (UTIL_IsValidPlayer(pTemp, false, false))
			pTemp->pev->flags &= ~FL_FROZEN; // Otherwise player keeps frozen
	}
}

// Superbot task, absorb health from multiple players
CAbsorbHealthTask::~CAbsorbHealthTask()
{
	pEdict->v.renderfx = kRenderFxNone;
  	pEdict->v.rendermode = kRenderTransTexture;
	pEdict->v.renderamt = 255;
}

void CAbsorbHealthTask::TaskThink()
{
	if (pEdict->v.health >= pBot->m_iMaxHealth)
	{
		TaskState = DONE;
		return;
	}

	if (pBot->GetKi() < 100.0)
	{
		TaskState = DONE;
		return;
	}

	if (pBot->m_fMelee)
	{
		TaskState = DONE;
		return;
	}

	pBot->ToggleTurbo(false); // turn turbo off when its on, otherwise we get crappy things...

    pEdict->v.rendermode = kRenderNormal;
    pEdict->v.renderfx = kRenderFxGlowShell;
  
    pEdict->v.renderamt = 25;  // glow shell distance from entity
    pEdict->v.rendercolor.x = 125;
    pEdict->v.rendercolor.y = 255;
    pEdict->v.rendercolor.z = 255;

	pBot->ResetMoveSpeed();

	if (!ShootAbsorbHealthBeams())
	{
		// couldn't absorb someone :(
		TaskState = DONE;
		return;
	}
}
// ==================================================================
// Code of Bot Tasks - End
// ==================================================================
