/*
	Lan Wars.
	Copyright (c) 2009, Darragh Coy.
	All rights reserved.
	Original work(s), source code & author(s) details available at: http://www.darraghcoy.com

	Redistribution and use in source and binary forms, with or without modification, are permitted 
	provided that the following conditions are met:

		* 	Redistributions of source code must retain the above copyright notice, this list of 
			conditions and the following disclaimer.
		* 	Redistributions in binary form must reproduce the above copyright notice, this list of 
			conditions and the following disclaimer in the documentation.
		* 	Neither the name of the author nor the names of contributors may be used to endorse or 
			promote products derived from this software without specific prior written permission.
		* 	The use of any third party libraries, modules or data files provided with the source 
			form of the program must follow and respect the individual licensing terms of these 
			components. You do not have permission to modify or circumvent the licensing terms for 
			provided third party components unless the third party licenses specifically allow this. 
			If you do not agree to the terms of provided third party components then you do not have 
			permission to use them in your derived work(s).
		* 	The terms of this license do not apply to separate (but related) distributions if supplied 
			by the author(s), only to the contents of this source distribution, derived works, and the 
			binaries produced by either modified or unmodified versions of the source code. The terms 
			of this license do not also apply to any third party libraries or components provided with 
			the source distribution. Such third party elements are licensed under their own separate 
			agreements.

	THIS SOFTWARE IS PROVIDED BY DARRAGH COY ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 
	BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
	ARE DISCLAIMED. IN NO EVENT SHALL DARRAGH COY BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
	EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 
	OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 
	OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 
	IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

//=====================================================================================================================
/// @brief		This class represents a rocket. Rockets are not owned by anything and are always created first
///				on the server.
//=====================================================================================================================

#include "Precompiled.h"
#include "GameObjects/GameRocket.h"
#include "Game/GameCore.h"
#include "Game/GameObjectManager.h"
#include "Game/GameSpriteBank.h"
#include "GameObjects/GameMap.h"
#include "GameObjects/GamePlayer.h"
#include "GameObjects/GameEnemy.h"
#include "GameObjects/GameExplosion.h"
#include "Graphics/SpriteAnimation.h"
#include "Graphics/SpriteRenderer.h"
#include "Graphics/Camera3D.h"
#include "Doom/DoomLevel.h"
#include "Doom/DoomCollision.h"

using namespace Game;

//=====================================================================================================================
// Constants
//=====================================================================================================================

/// Speed at which rockets move

const float GameRocket::SPEED = 1000.0f;

/// Amount of damage that the rocket does

const float GameRocket::DAMAGE = 50.0f;

/// Damage radius: damage decreases as the rocket gets further away

const float GameRocket::DAMAGE_RADIUS = 150.0f;

/// Collision radius for the rocket: collides with walls and other objects when within this radius

const float GameRocket::COLLISION_RADIUS = 8.0f;

/// Damage force: force that is applied to objects that get damaged

const float GameRocket::SPLASH_FORCE = 400.0f;

/// Damage amount in health: amount of damage that a rocket does

const float GameRocket::SPLASH_DAMAGE = 50.0f;

/// How much to scale the y component of splash force

const float GameRocket::SPLASH_Y_SCALE = 0.35f;

//=====================================================================================================================
/// @brief		Constructor. Creates the rocket.
/// @param		position - position to create the rocket in
/// @param		direction - direction of travel for the rocket
/// @param		net_id - network id for the rocket. Not set on the server. NetObject (base class) decides this
///				on the server.
/// @param		owner - only used on the server. this is the owner of the rocket.
/// @pre		GameCore must exist
/// @pre		GameCore must have a GameSpriteBank
/// @pre		There must be a level in the game
/// @pre		On the server the owner must be given
/// @pre		On the client the owner must not be given
//=====================================================================================================================

GameRocket::GameRocket
(
	const Vec3f &	position	, 
	const Vec3f &	direction	, 
	unsigned		net_id		, 
	GamePlayer *	owner
)
:
PositionalObject(position,NetObject::OWNER_ID_NONE,net_id) , m_direction(direction)
{
	// Make sure ok
	
	ENSURE( GameCore::GetSingleton()					, "GameCore must exist"							)
	ENSURE( GameCore::GetSingleton()->GetSpriteBank()	, "GameCore must have a GameSpriteBank"			)
	ENSURE( IsClient() || owner != 0					, "On the server the owner must be given"		)
	ENSURE( IsServer() || owner == 0					, "On the client the owner must not be given"	)
	
	// Create the animation:
	
	m_sprite = new SpriteAnimation( GameCore::GetSingleton()->GetSpriteBank()->GetSprite( GameSpriteBank::SPRITE_ROCKET ) );
		
	// Save the owner
	
	m_owner	= owner;
	
	// Add a reference to the owner if there:
	
	if ( m_owner ) m_owner->AddReference();
	
	// Make sure the direction is normalized
	
	m_direction.Normalize();
	
	// Make sure the direction is not zero:
	
	if
	(
		Float::Equals( m_direction.x , 0.001f ) &&
		Float::Equals( m_direction.y , 0.001f ) &&
		Float::Equals( m_direction.z , 0.001f )
	)
	{
		// Unit x
		
		m_direction = Vec3f::UnitX();
	}	
}

//=====================================================================================================================
/// @brief		Destructor
//=====================================================================================================================

GameRocket::~GameRocket()
{
	// Remove reference to the owner if there:
	
	if ( m_owner ) m_owner->AddReference();

	// Cleanup this
	
	delete m_sprite;
}

//=====================================================================================================================
/// @brief		Called when the object is added into the game
/// @pre		The game map must exist
//=====================================================================================================================

void GameRocket::OnGameAdded()
{
	// Call base function:
	
	GameObjectBase::OnGameAdded();
	
	// Try and find the level:
	
	m_map = GetManager()->FindByType<GameMap>("GameMap");
	
	// Make sure ok:
	
	ENSURE( m_map , "The game map must exist" )
	
	// Add a reference to it
	
	m_map->AddReference();

	// On the server alert all nearby monsters of us:

	if ( IsServer() )
	{
		// Get all the enemys in the map

		std::vector<GameObjectBase*> enemies; GetManager()->FindByType<GameEnemy>("GameEnemy",enemies);

		// Run through all:

		for ( unsigned i = 0 ; i < enemies.size() ; ++i )
		{
			// Alert this enemy if it can hear us:

			if ( CanEnemyHearRocket((GameEnemy*)(enemies[i])) )
			{
				((GameEnemy*)(enemies[i]))->NoiseAlert(m_owner);
			}
		}
	}
}

//=====================================================================================================================
/// @brief		Called when the object is removed into the game
//=====================================================================================================================

void GameRocket::OnGameRemoved()
{
	// Make an explosion:
	
	GameExplosion * explosion = new GameExplosion( GetPosition() - m_direction * 20 );
	
	// Add to world:
	
	GetManager()->AddObject(explosion); explosion->RemoveReference();

	// On the server alert all nearby monsters of us:

	if ( IsServer() )
	{
		// Get all the enemys in the map

		std::vector<GameObjectBase*> enemies; GetManager()->FindByType<GameEnemy>("GameEnemy",enemies);

		// Run through all:

		for ( unsigned i = 0 ; i < enemies.size() ; ++i )
		{
			// Alert this enemy if it can hear us:

			if ( CanEnemyHearRocket((GameEnemy*)(enemies[i])) )
			{
				((GameEnemy*)(enemies[i]))->NoiseAlert(m_owner);
			}
		}
	}

	// Call base function:
	
	GameObjectBase::OnGameRemoved();
	
	// Remove the reference to this
	
	m_map->RemoveReference();
	
	// Remove reference to the owner if there:
	
	if ( m_owner ) m_owner->RemoveReference(); m_owner = 0;
}

//=====================================================================================================================
/// @brief		Update function. called once for every frame. 
/// @param		time_delta - amount of elapsed time between frames
/// @todo		Do explosions
/// @todo		Hurt monsters and players on the server
//=====================================================================================================================

void GameRocket::OnUpdate( float time_delta )
{
	// The rocket travels fast and is small, so split up it's collision over a number of iterations:
	
	for ( unsigned i = 0 ; i < 8 ; ++i )
	{
		// Move the object forward on both the client and server:
		
		SetPosition( GetPosition() + m_direction * SPEED * time_delta * 0.125f );

		// If we are on the client then only do predictive collision detection with the level: otherwise do full collision with the world
		
		if ( IsClient() )
		{
			// See if the rocket collides against the world or against enemies (enenmy collision is predicted)
			
			if ( CheckWorldCollision() || CheckEnemyCollision() )
			{	
				// Remove us from the world
				
				GetManager()->RemoveObject(this);
				
				// Done:
				
				break;
			}
		}
		else
		{
			// See if the rocket collides against the world
			
			if ( CheckWorldCollision() || CheckPlayerCollision() || CheckEnemyCollision() )
			{
				// Player and enemy splash damage
			
				SplashDamagePlayers();
				SplashDamageEnemies();
			
				// Remove us from the world
				
				GetManager()->RemoveObject(this);
				
				// Done:
				
				break;				
			}
		}
	}
}

//=====================================================================================================================
/// @brief		Draws the rocket
/// @param		c - camera to draw from
/// @pre		GameCore must exist
/// @pre		GameCore must have a sprite renderer
//=====================================================================================================================

void GameRocket::OnDraw( Camera3D * c )
{
	// Make sure ok
	
	ENSURE( GameCore::GetSingleton()						, "GameCore must exist"						)
	ENSURE( GameCore::GetSingleton()->GetSpriteRenderer()	, "GameCore must have a sprite renderer"	)
	
	// Set the position of our sprite:
	
	m_sprite->SetPosition(GetPosition());
	
	// Get our flattened direction
	
	Vec2f dir = Vec2f(m_direction.x,-m_direction.z); dir.Normalize();
	
	// Figure out the 2d angle of our sprite about the y axis:
	
	float angle = dir.AngleBetween( -Vec2f::UnitY() );
	
	// Set the angle of our sprite:
	
	m_sprite->SetDirection(c->GetPosition(),GetPosition(), - angle - Float::PI() );
	
	// Draw:
	
	m_sprite->Draw( c , GameCore::GetSingleton()->GetSpriteRenderer() );
}

//=====================================================================================================================
/// @brief		Checks if the rocket collides with the world: returns true if it is
//=====================================================================================================================

bool GameRocket::CheckWorldCollision()
{
	// Do collision against the world:
	
	Vec3f new_pos = m_map->GetLevel().GetCollision().Collide(GetPosition(),COLLISION_RADIUS,COLLISION_RADIUS);
	
	// See if there was a collision:
	
	if ( new_pos != GetPosition() )
	{
		// Save new position
		
		SetPosition(new_pos);
		
		// Collision
		
		return true;
	}
	else
	{
		// No collision:
		
		return false;
	}
}

//=====================================================================================================================
/// @brief		Checks if the rocket collides with players: returns true if it is
/// @returns	True if the rocket collides with players
/// @pre		This can only be called on the server
//=====================================================================================================================

bool GameRocket::CheckPlayerCollision()
{
	// Make sure ok
	
	ENSURE( IsServer() , "This can only be called on the server" )
	
	// Get all of the players in the game:
	
	std::vector<GameObjectBase*> players; GetManager()->FindByType<GamePlayer>("GamePlayer",players);
	
	//'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
	// Produce an y scaling that will make the bounding ellipsoid height of the player equal to it's xz radius.
	// We reduce the problem of bounding ellipsoid collision to bounding sphere collision in that way.
	//'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
	
	float y_scale = 1.0f / ( GamePlayer::COLLISION_HEIGHT / GamePlayer::COLLISION_WIDTH );
	
	// Get our position in this scaled coord system:
	
	Vec3f rpos = GetPosition(); rpos.y *= y_scale;
	
	// Run through all the players:
	
	for ( unsigned i = 0 ; i < players.size() ; ++i )
	{
		// Only if this is not the owner of the rocket:
		
		if ( players[i] == m_owner ) continue;
	
		// Get this player:
		
		GamePlayer & player = (GamePlayer&)(*players[i]);

		// Dead players don't block:

		if ( player.GetHealth() <= 0 ) continue;
		
		// Get it's position: scale y so the bounding ellipse becomes a bounding sphere
		
		Vec3f ppos = player.GetPosition(); ppos.y *= y_scale;
		
		// Get the distance between our position and the player's position:
		
		float dist = ppos.DistanceTo(rpos);
		
		// See if within radius:
		
		if ( dist < GamePlayer::COLLISION_WIDTH + COLLISION_RADIUS )
		{
			// Collision: 
			
			return true;
		}
	}
	
	// No collision:
	
	return false;
}

//=====================================================================================================================
/// @brief		Checks if the rocket collides with enemys: returns true if it is
/// @returns	True if the rocket collides with enemys
//=====================================================================================================================

bool GameRocket::CheckEnemyCollision()
{
	// Get all of the enemies in the game:
	
	std::vector<GameObjectBase*> enemies; GetManager()->FindByType<GameEnemy>("GameEnemy",enemies);
	
	//'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
	// Produce an y scaling that will make the bounding ellipsoid height of the enemy equal to it's xz radius.
	// We reduce the problem of bounding ellipsoid collision to bounding sphere collision in that way.
	//'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
	
	float y_scale = 1.0f / ( GameEnemy::COLLISION_HEIGHT / GameEnemy::COLLISION_WIDTH );
	
	// Get our position in this scaled coord system:
	
	Vec3f rpos = GetPosition(); rpos.y *= y_scale;
	
	// Run through all the enemies:
	
	for ( unsigned i = 0 ; i < enemies.size() ; ++i )
	{
		// Only if this is not the owner of the rocket:
		
		if ( enemies[i] == m_owner ) continue;
	
		// Get this enemy:
		
		GameEnemy & enemy = (GameEnemy&)(*enemies[i]);

		// Dead enemies don't block:

		if ( enemy.GetHealth() <= 0 ) continue;
		
		// Get it's position: scale y so the bounding ellipse becomes a bounding sphere
		
		Vec3f ppos = enemy.GetPosition(); ppos.y *= y_scale;
		
		// Get the distance between our position and the enemy's position:
		
		float dist = ppos.DistanceTo(rpos);
		
		// See if within radius:
		
		if ( dist < GameEnemy::COLLISION_WIDTH + COLLISION_RADIUS )
		{
			// Collision: 
			
			return true;
		}
	}
	
	// No collision:
	
	return false;
}

//=====================================================================================================================
/// @brief		Server only: does splash damage against players
/// @pre		This can only be called on the server
//=====================================================================================================================

void GameRocket::SplashDamagePlayers()
{
	// Make sure ok
	
	ENSURE( IsServer() , "This can only be called on the server" )
	
	// Get all of the players in the game:
	
	std::vector<GameObjectBase*> players; GetManager()->FindByType<GamePlayer>("GamePlayer",players);
	
	// Run through all the players:
	
	for ( unsigned i = 0 ; i < players.size() ; ++i )
	{
		// Get this player
		
		GamePlayer & player = (GamePlayer&)( * players[i] );
	
		// Get the position of the player:
		
		Vec3f player_pos = player.GetPosition();
		
		// Get the distance beween us and the player
		
		float dist = GetPosition().DistanceTo(player_pos);
		
		// See if close enough for splash damage:
		
		if ( dist < DAMAGE_RADIUS )
		{
			// Ok: can apply splash damage. Do a raycast to see if we got a line of sight to the player
			
			DoomCollision::RaycastResult result = m_map->GetLevel().GetCollision().Raycast(GetPosition(),player_pos);
			
			// See if there is anything in between:
			
			if ( result.m_hit == false )
			{
				// Splash damage applies: apply splash force to the player
				
				Vec3f force_dir = player_pos - GetPosition(); force_dir.Normalize();
				
				// Scale:
				
				force_dir *= SPLASH_FORCE * ( ( DAMAGE_RADIUS - dist ) / DAMAGE_RADIUS );
				
				// Scale y force differently:
				
				force_dir.y *= SPLASH_Y_SCALE;
				
				// Apply:
				
				player.ApplyForce( force_dir );
				player.SetHealth( player.GetHealth() - SPLASH_DAMAGE *  ( ( DAMAGE_RADIUS - dist ) / DAMAGE_RADIUS ) );
			}
		}
	}
}

//=====================================================================================================================
/// @brief		Server only: does splash damage against enemies
/// @pre		This can only be called on the server
//=====================================================================================================================

void GameRocket::SplashDamageEnemies()
{
	// Make sure ok
	
	ENSURE( IsServer() , "This can only be called on the server" )
	
	// Get all of the enemies in the game:
	
	std::vector<GameObjectBase*> enemies; GetManager()->FindByType<GameEnemy>("GameEnemy",enemies);
	
	// Run through all the enemies:
	
	for ( unsigned i = 0 ; i < enemies.size() ; ++i )
	{
		// Get this enemy
		
		GameEnemy & enemy = (GameEnemy&)( * enemies[i] );
		
		// Only if this enemy is not already dead: if so then do not move the enemy
		
		if ( enemy.GetHealth() <= 0 ) continue;		
	
		// Get the position of the enemy:
		
		Vec3f enemy_pos = enemy.GetPosition();
		
		// Get the distance beween us and the enemy
		
		float dist = GetPosition().DistanceTo(enemy_pos);		
		
		// See if close enough for splash damage:
		
		if ( dist < DAMAGE_RADIUS )
		{
			// Ok: can apply splash damage. Do a raycast to see if we got a line of sight to the enemy
			
			DoomCollision::RaycastResult result = m_map->GetLevel().GetCollision().Raycast(GetPosition(),enemy_pos);
			
			// See if there is anything in between:
			
			if ( result.m_hit == false )
			{
				// Splash damage applies: apply splash force to the enemy
				
				Vec3f force_dir = enemy_pos - GetPosition(); force_dir.Normalize();
				
				// Scale:
				
				force_dir *= SPLASH_FORCE * ( ( DAMAGE_RADIUS - dist ) / DAMAGE_RADIUS );
				
				// Scale y force differently:
				
				force_dir.y *= SPLASH_Y_SCALE;
				
				// Apply:
				
				enemy.ApplyForce( force_dir );
				enemy.SetHealth( enemy.GetHealth() - SPLASH_DAMAGE *  ( ( DAMAGE_RADIUS - dist ) / DAMAGE_RADIUS ) );

				// Alert the enemy of damage:

				enemy.DamageAlert(m_owner);
			}
		}
	}
}

//=====================================================================================================================
/// @brief		Server only: does a raycast to the given enemy and checks if the enemy can hear us from our current 
///				position
/// @param		enemy - enemy to raycast to
/// @returns	True if the enemy can hear us
/// @pre		This must only be called on the server
/// @pre		Given enemy cannot be null
//=====================================================================================================================

bool GameRocket::CanEnemyHearRocket( GameEnemy * enemy )
{
	// Make sure ok

	ENSURE( IsServer()		, "This must only be called on the server"	)
	ENSURE( enemy != 0		, "Given enemy cannot be null"				)

	// Get the subsectors we and the enemy are in:

	const DoomGLSubsectorV2 * subsec1 = m_map->GetLevel().GetSubsector(GetPosition());
	const DoomGLSubsectorV2 * subsec2 = m_map->GetLevel().GetSubsector(enemy->GetPosition());

	// Get the sectors each is in:

	if ( subsec1 && subsec2 )
	{
		// Get the sectors

		const DoomSector & sec1 = m_map->GetLevel().GetSector(*subsec1);
		const DoomSector & sec2 = m_map->GetLevel().GetSector(*subsec2);

		// See if potentially visible to each other:

		if ( m_map->GetLevel().GetCollision().IsPotentiallyVisible(sec1,sec2) )
		{
			// Raycast between the two positions

			DoomCollision::RaycastResult result = m_map->GetLevel().GetCollision().Raycast2D
			(
				GetPosition()			,
				enemy->GetPosition()
			);

			// Return if there is no hit

			return result.m_hit == false;
		}
		else
		{
			// Not potentially visible

			return false;
		}
	}
	else
	{
		// Raycast between the two positions

		DoomCollision::RaycastResult result = m_map->GetLevel().GetCollision().Raycast2D
		(
			GetPosition()			,
			enemy->GetPosition()
		);

		// Return if there is no hit

		return result.m_hit == false;
	}
}
