/*
	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 fireball. Fireballs are not owned by anything and are always created first
///				on the server.
//=====================================================================================================================

#include "Precompiled.h"
#include "GameObjects/GameFireball.h"
#include "Game/GameCore.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 fireballs move

const float GameFireball::SPEED = 700.0f;

/// Amount of damage that the fireball does

const float GameFireball::DAMAGE = 10.0f;

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

const float GameFireball::DAMAGE_RADIUS = 150.0f;

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

const float GameFireball::COLLISION_RADIUS = 4.0f;

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

const float GameFireball::SPLASH_FORCE = 400.0f;

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

const float GameFireball::SPLASH_DAMAGE = 50.0f;

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

const float GameFireball::SPLASH_Y_SCALE = 0.35f;

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

GameFireball::GameFireball
(
	const Vec3f &	position	, 
	const Vec3f &	direction	, 
	unsigned		net_id
)
:
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"			)
	
	// Create the animation:
	
	m_sprite = new SpriteAnimation( GameCore::GetSingleton()->GetSpriteBank()->GetSprite( GameSpriteBank::SPRITE_FIREBALL ) );
	
	// 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
//=====================================================================================================================

GameFireball::~GameFireball()
{
	// Cleanup this
	
	delete m_sprite;
}

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

void GameFireball::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();
}

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

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

	// Call base function:
	
	GameObjectBase::OnGameRemoved();
	
	// Remove the reference to this
	
	m_map->RemoveReference();
}

//=====================================================================================================================
/// @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 GameFireball::OnUpdate( float time_delta )
{
	// The fireball 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 fireball collides against the world or against players:
			
			if ( CheckWorldCollision() )
			{	
				// Remove us from the world
				
				GetManager()->RemoveObject(this);
				
				// Done:
				
				break;
			}
		}
		else
		{
			// See if the fireball collides against the world
			
			if ( CheckWorldCollision() || CheckPlayerCollision() )
			{
				// Do player splash damage
			
				SplashDamagePlayers();
			
				// Remove us from the world
				
				GetManager()->RemoveObject(this);
				
				// Done:
				
				break;				
			}
		}
	}
}

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

void GameFireball::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 fireball collides with the world: returns true if it is
//=====================================================================================================================

bool GameFireball::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 fireball collides with players: returns true if it is
/// @returns	True if the fireball collides with players
/// @pre		This can only be called on the server
//=====================================================================================================================

bool GameFireball::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 )
	{
		// 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		Server only: does splash damage against players
/// @pre		This can only be called on the server
//=====================================================================================================================

void GameFireball::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 ) );
			}
		}
	}
}
