/***********************************************************************

tdm_player_tools.script

Functions related to player tools like flashbomb, mines, lockpicks, etc.

***********************************************************************/

#ifndef __DARKMOD_PLAYER_TOOLS__
#define __DARKMOD_PLAYER_TOOLS__

#define HOLD_TIME_MIN_VELOCITY 200		// The minimum hold time (a threshold)
#define HOLD_TIME_MAX_VELOCITY 1200		// The maximum hold time to achieve MAX_VELOCITY
#define MIN_VELOCITY 0					// The velocity for holdTime <= HOLD_TIME_MIN_VELOCITY
#define MAX_VELOCITY 900				// The velocity for holdTime > HOLD_TIME_MAX_VELOCITY

// The clipmask of the projectiles right after launch
#define PROJECTILE_LAUNCH_CLIPMASK CONTENTS_SOLID|CONTENTS_PROJECTILE|CONTENTS_MONSTERCLIP|CONTENTS_MOVEABLECLIP
// The time that has to pass after launch until the clipmask is reset to normal
#define LAUNCH_CLIPMASK_TIMEOUT 500 // msec

// Base class of the player_tools
object player_tools {
	// Any shared methods and member variables can go here
	float useCount;	// dummy variable, objects may not be empty 
					// (can be removed when other variables are added)
};

/*
* SPYGLASS scriptobject
*/
object playertools_spyglass : player_tools {
	/**
	 * greebo: Sets up this script object at spawn time.
	 */
	void init();

	/**
	* greebo: This is the method that gets called from within the SDK
	*		  when the player "uses" this tool as inventory item.
	*
	* @userEntity: the entity that is using this item, usually $player1
	* @frobbedEntity: the entity the player is currently frobbing, may be $null_entity
	* @buttonState: a float telling about the current button state (pressed, released)
	*/
	void inventoryUse(entity userEntity, entity frobbedEntity, float buttonState);

	/**
	 * greebo: Performs the zoom out transition
	 */
	void zoomOut();

	// Set to TRUE, if the spyglass is currently being used.
	float zoomedIn;

	// When this is set to TRUE, no zoom commands are allowed
	float zoomMutex;

	// The GUI overlay handle
	float overlayHandle;
};

/*
* COMPASS scriptobject
*/
object playertools_compass : player_tools {
	// greebo: Gets called upon addition to the inventory
	void inventory_item_init(entity userEntity, float overlayHandle, string overlayName);	

	// Gets called when the player switches to another inventory item
	void inventory_item_unselect(entity userEntity, float overlayHandle);

	// Gets called when this item is selected in the inventory
	void inventory_item_select(entity userEntity, float overlayHandle);

	// Gets called to update the HUD (not each frame!)
	void inventory_item_update(entity userEntity, float overlayHandle);

	// Updates the HUD variables
	void updateCompass(entity userEntity);

	// greebo: Call this function in a new thread. It will update the compass HUD each frame.
	void updateLoop(entity userEntity);

	// Member variables
	vector 	_playerAngles;
	float	_compcwise;
	float	_compccwise;

	// The GUI overlay handle (containing the compass model)
	float	_overlayHandle;

	// The thread number of the update loop
	float	_updateThreadNum;
};

/*
* HEALTH POTION scriptobject
*/
object playertools_healthpotion : player_tools {
	/**
	* greebo: This is the method that gets called from within the SDK
	*		  when the player "uses" this tool as inventory item.
	*
	* @userEntity: the entity that is using this item, usually $player1
	* @frobbedEntity: the entity the player is currently frobbing, may be $null_entity
	* @buttonState: a float telling about the current button state (pressed, released)
	*/
	void inventoryUse(entity userEntity, entity frobbedEntity, float buttonState);
};

/**
 * HOLY WATER scriptobject
 */
object playertools_holywater : player_tools
{
	// Constructor
	void init();

	/**
	* greebo: This is the method that gets called from within the SDK
	*		  when the player "uses" this tool as inventory item.
	*
	* @userEntity: the entity that is using this item, usually $player1
	* @frobbedEntity: the entity the player is currently frobbing, may be $null_entity
	* @buttonState: a float telling about the current button state (pressed, released)
	*/
	void inventoryUse(entity userEntity, entity frobbedEntity, float buttonState);

	/**
	 * greebo: Is responsible for updating the HUD while the holy water effect is active.
	 * Resets the weapon after the given time has passed.
	 *
	 * Note: start this function in a new thread.
	 *
	 * @userEntity: the entity using the holy water (usually the player).
	 */
	void update(entity userEntity);

	// The thread number for counting down the seconds and updating the inventory name
	float _updateThreadNum;
};

/**
* FLASHBOMB scriptobject
*/
object playertools_flashbomb : player_tools {
	/**
	* greebo: This is the method that gets called from within the SDK
	*		  when the player "uses" this tool as inventory item.
	*
	* @userEntity: the entity that is using this item, usually $player1
	* @frobbedEntity: the entity the player is currently frobbing, may be $null_entity
	* @buttonState: a float telling about the current button state (pressed, released)
	*/
	void inventoryUse(entity userEntity, entity frobbedEntity, float buttonState);

	/**
	* greebo: This is the method that gets called from within the SDK
	*		  when the player releases the inventory use key
	*
	* @userEntity: the entity that is using this item, usually $player1
	* @holdTime: The time the key has been held down in ms.
	*/
	void inventoryUseKeyRelease(entity userEntity, entity eFrobbed, float holdTime);
};

/**
* FLASHMINE scriptobject
*/
object playertools_flashmine : player_tools {
	/**
	* greebo: This is the method that gets called from within the SDK
	*		  when the player "uses" this tool as inventory item.
	*
	* @userEntity: the entity that is using this item, usually $player1
	* @frobbedEntity: the entity the player is currently frobbing, may be $null_entity
	* @buttonState: a float telling about the current button state (pressed, released)
	*/
	void inventoryUse(entity userEntity, entity frobbedEntity, float buttonState);

	/**
	* greebo: This is the method that gets called from within the SDK
	*		  when the player releases the inventory use key
	*
	* @userEntity: the entity that is using this item, usually $player1
	* @holdTime: The time the key has been held down in ms.
	*/
	void inventoryUseKeyRelease(entity userEntity, entity eFrobbed, float holdTime);
};

/**
* MINE scriptobject
*/
object playertools_mine : player_tools {
	/**
	* greebo: This is the method that gets called from within the SDK
	*		  when the player "uses" this tool as inventory item.
	*
	* @userEntity: the entity that is using this item, usually $player1
	* @frobbedEntity: the entity the player is currently frobbing, may be $null_entity
	* @buttonState: a float telling about the current button state (pressed, released)
	*/
	void inventoryUse(entity userEntity, entity frobbedEntity, float buttonState);

	/**
	* greebo: This is the method that gets called from within the SDK
	*		  when the player releases the inventory use key
	*
	* @userEntity: the entity that is using this item, usually $player1
	* @holdTime: The time the key has been held down in ms.
	*/
	void inventoryUseKeyRelease(entity userEntity, entity eFrobbed, float holdTime);
};

/*
* PLAYER LANTERN scriptobject
*/
object playertools_lantern : player_tools {

	// Constructor
	void init();

	/**
	* greebo: This is the method that gets called from within the SDK
	*		  when the player "uses" this tool as inventory item.
	*
	* @userEntity: the entity that is using this item, usually $player1
	* @frobbedEntity: the entity the player is currently frobbing, may be $null_entity
	* @buttonState: a float telling about the current button state (pressed, released)
	*/
	void inventoryUse(entity userEntity, entity frobbedEntity, float buttonState);

	/**
	 * The entity that gets spawned when the lantern is switched on.
	 */
	entity	lanternLight;
};

/**
* greebo: This returns the velocity factor for a given hold time.
*		  the factor is clamped to MIN_VELOCITY and MAX_VELOCITY.
*		  Below the threshold HOLD_TIME_MIN_VELOCITY, the factor is MIN_VELOCITY.
*		  Above HOLD_TIME_MAX_VELOCITY the factor is MAX_VELOCITY.
*		  Between those two values, the factor is interpolated.
*/
float getVelocityFactorForHoldTime(float holdTime) {
	float velocityFactor;
	
	if (holdTime > HOLD_TIME_MIN_VELOCITY) {
		velocityFactor = (holdTime - HOLD_TIME_MIN_VELOCITY) / (HOLD_TIME_MAX_VELOCITY - HOLD_TIME_MIN_VELOCITY);
		velocityFactor *= MAX_VELOCITY;
	}
	else {
		velocityFactor = MIN_VELOCITY;
	}

	return velocityFactor;
}

void playertools_flashbomb::inventoryUse(entity userEntity, entity frobbedEntity, float buttonState)
{
	// This is called on impulse keyDown event - nothing to do
}

void playertools_flashbomb::inventoryUseKeyRelease(entity userEntity, entity eFrobbed, float holdTime)
{
	entity projectile = sys.spawn(getKey("def_projectile"));

	vector angles = $player1.getViewAngles();
	vector direction = sys.angToForward(angles);

	vector velocity = direction;
	velocity *= getVelocityFactorForHoldTime(holdTime);

	vector origin = $player1.getEyePos();
	origin += direction * 30;
	projectile.setOrigin(origin);

	string soundShader = projectile.getKey("snd_throw");
	sys.cacheSoundShader(soundShader);

	// If the soundchannel argument is empty, the float defaults to 0, which is SND_CHANNEL_ANY
	projectile.startSoundShader(soundShader, SND_CHANNEL_BODY);

	// Callback to decrease the inventory stack count
	$player1.changeInvItemCount(getKey("inv_name"), getKey("inv_category"), -1);

	projectile.launch( origin, direction, velocity );

	// Clear the projectile's CONTENT_PROJECTILE flag so that it doesn't collide with the player in the first few hundred msec.
	float savedClipMask = projectile.getClipMask();
	projectile.setClipMask(PROJECTILE_LAUNCH_CLIPMASK);

	// Reset the clipmask back to normal after a certain amount of time
	sys.wait(LAUNCH_CLIPMASK_TIMEOUT/1000);
	projectile.setClipMask(savedClipMask);
}

/**
* =============== FLASHMINE ===========================================
*/
void playertools_flashmine::inventoryUse(entity userEntity, entity frobbedEntity, float buttonState)
{
	// This is called on impulse keyDown event - nothing to do
}

void playertools_flashmine::inventoryUseKeyRelease(entity userEntity, entity eFrobbed, float holdTime)
{
	
	entity projectile = sys.spawn(getKey("def_projectile"));

	vector viewAngles = $player1.getViewAngles();
	vector direction = sys.angToForward(viewAngles);

	vector velocity = direction;
	velocity *= getVelocityFactorForHoldTime(holdTime);

	vector origin = $player1.getEyePos();
	origin += direction * 30;
	projectile.setOrigin(origin);

	string soundShader = projectile.getKey("snd_throw");
	sys.cacheSoundShader(soundShader);

	// If the soundchannel argument is empty, the float defaults to 0, which is SND_CHANNEL_ANY
	projectile.startSoundShader(soundShader, SND_CHANNEL_BODY);

	// Callback to decrease the inventory stack count
	$player1.changeInvItemCount(getKey("inv_name"), getKey("inv_category"), -1);

	projectile.launch( origin, direction, velocity );

	// Clear the projectile's CONTENT_PROJECTILE flag so that it doesn't collide with the player in the first few hundred msec.
	float savedClipMask = projectile.getClipMask();
	projectile.setClipMask(PROJECTILE_LAUNCH_CLIPMASK);

	// Reset the clipmask back to normal after the arm time
	sys.wait(projectile.getFloatKey("delay"));
	projectile.setClipMask(savedClipMask);
}

/**
* =============== EXPLOSIVE MINE ===========================================
*/

void playertools_mine::inventoryUse(entity userEntity, entity frobbedEntity, float buttonState)
{
	// This is called on impulse keyDown event - nothing to do
}

void playertools_mine::inventoryUseKeyRelease(entity userEntity, entity eFrobbed, float holdTime)
{
	entity projectile = sys.spawn(getKey("def_projectile"));
	
	vector viewAngles = $player1.getViewAngles();
	vector direction = sys.angToForward(viewAngles);

	vector velocity = direction;
	velocity *= getVelocityFactorForHoldTime(holdTime);

	vector origin = $player1.getEyePos();
	origin += direction * 30;
	projectile.setOrigin(origin);

	string soundShader = projectile.getKey("snd_throw");
	sys.cacheSoundShader(soundShader);

	// If the soundchannel argument is empty, the float defaults to 0, which is SND_CHANNEL_ANY
	projectile.startSoundShader(soundShader, SND_CHANNEL_BODY);

	// Callback to decrease the inventory stack count
	$player1.changeInvItemCount(getKey("inv_name"), getKey("inv_category"), -1);

	projectile.launch( origin, direction, velocity );

	// Clear the projectile's CONTENT_PROJECTILE flag so that it doesn't collide with the player in the first few hundred msec.
	float savedClipMask = projectile.getClipMask();
	projectile.setClipMask(PROJECTILE_LAUNCH_CLIPMASK);

	// Reset the clipmask back to normal after a certain amount of time
	sys.wait(projectile.getFloatKey("delay"));
	projectile.setClipMask(savedClipMask);
}

/**
* =============== HEALTH POTION ===========================================
*/
void playertools_healthpotion::inventoryUse(entity userEntity, entity frobbedEntity, float buttonState)
{
	
	// Heal the entity using the second argument as healDefName
	float healed = userEntity.heal(getKey("def_heal"), 1.0);

	// If the entity could be healed (returnvalue != 0), play the sound, if there is one
	if (healed != 0) {
		// Do we have a non-empty soundshader string specified
		string soundShader = getKey("snd_swallow");
		if (soundShader != "") {
			sys.cacheSoundShader(soundShader);

			// If the soundchannel argument is empty
			userEntity.startSoundShader(soundShader, SND_CHANNEL_VOICE);
		}

		// Callback to decrease the inventory stack count
		userEntity.changeInvItemCount(getKey("inv_name"), getKey("inv_category"), -1);
	}
}

/**
* =============== HOLY WATER ===========================================
*/
void playertools_holywater::init()
{
	_updateThreadNum = -1;
}

void playertools_holywater::update(entity userEntity)
{
	string targetWeapon = getKey("weapon_name");

	// Change the name of the weapon in question
	string weaponName = getKey("weapon_display_name");
	userEntity.changeWeaponName(targetWeapon, weaponName);

	// Add a small padding to the end time, otherwise the countdown at the beginning feels too fast
	float endTime = sys.getTime() + getFloatKey("duration") + 0.5;

	string timeLeftStr = "";

	while (sys.getTime() < endTime)
	{
		if (userEntity.getCurWeaponName() == targetWeapon)
		{
			float timeLeft = endTime - sys.getTime();
			
			if (timeLeft < 0) timeLeft = 0;

			// Hack: cut off the fractional part
			setKey("___temp", timeLeft);
			timeLeft = getIntKey("___temp");
			
			timeLeftStr = timeLeft;

			// Add a leading zero, if necessary
			if (timeLeft < 10) {
				timeLeftStr = "0" + timeLeftStr;
			}

			timeLeftStr = "0:" + timeLeftStr + " ";
		}
		else
		{
			timeLeftStr = "";
		}

		// Push the time to the HUD
		userEntity.setGuiString(GUI_HUD, "WeaponTimeLeft", timeLeftStr);

		sys.wait(0.1);
	}

	// Reset the display name
	userEntity.changeWeaponName(targetWeapon, "");
	userEntity.resetWeaponProjectile(targetWeapon);

	// Clear the timer string
	userEntity.setGuiString(GUI_HUD, "WeaponTimeLeft", "");
}

void playertools_holywater::inventoryUse(entity userEntity, entity frobbedEntity, float buttonState)
{
	// If we have a previous thread still running, terminate it
	if (_updateThreadNum != -1)
	{
		sys.terminate(_updateThreadNum);
	}

	// Callback to decrease the inventory stack count
	userEntity.changeInvItemCount(getKey("inv_name"), getKey("inv_category"), -1);

	// Play the activate sound
	userEntity.startSoundShader(getKey("snd_activate"), SND_CHANNEL_ANY);

	// Swap the waterarrow projectiles
	userEntity.changeWeaponProjectile(getKey("weapon_name"), getKey("def_projectile_holy"));

	// Start a new script thread for updating the timer and resetting the projectile
	_updateThreadNum = thread update(userEntity);
}

/**
* =============== PLAYER LANTERN ==========================================
*/
void playertools_lantern::init() {
	lanternLight = $null_entity;
}

void playertools_lantern::inventoryUse(entity userEntity, entity frobbedEntity, float buttonState)
{
	if (lanternLight == $null_entity)
	{
		// No lantern light so far, spawn it
		lanternLight = sys.spawn(getKey("def_lantern_light"));	
		// Bind the lantern to the joint as defined in the entityDef file
		//userEntity.attach(lanternLight, "light_source");
		// Obsttorte: Bind the lantern to the players weapon object, to reflect leaning and other movements #4977
		lanternLight.bind($player1_weapon);
		lanternLight.setOrigin('10 0 0');
		// Set the icon to the "on" icon
		userEntity.changeInvIcon(getKey("inv_name"), getKey("inv_category"), getKey("inv_icon_on"));

		// Set the lightgem modifier value of this inventory item
		userEntity.setLightgemModifier("lantern", getIntKey("lgmodifier"));
	}
	else
	{
		// Lantern is already active, remove it
		lanternLight.remove();
		lanternLight = $null_entity;

		// Set the icon to the "on" icon
		userEntity.changeInvIcon(getKey("inv_name"), getKey("inv_category"), getKey("inv_icon_off"));

		// Set the lightgem modifier value of this inventory item
		userEntity.setLightgemModifier("lantern", 0);
	}
}

/**
* =============== PLAYER COMPASS ==========================================
*/

#define COMPASS_MIN_PITCH -75
#define COMPASS_MAX_PITCH 40

void playertools_compass::updateCompass(entity userEntity)
{
	_playerAngles = userEntity.getViewAngles();

	float yaw = _playerAngles_y - 90;
	
	// Clamp the pitch to [COMPASS_MIN_PITCH .. COMPASS_MAX_PITCH]
	float playerPitchClamped = _playerAngles_x;

	if (playerPitchClamped > 0)	{
		playerPitchClamped = COMPASS_MAX_PITCH * playerPitchClamped/90;
	}
	else {
		playerPitchClamped = COMPASS_MIN_PITCH * -playerPitchClamped/90;
	}

	float modelPitch = playerPitchClamped * sys.sin(yaw);
	float modelRoll = playerPitchClamped * sys.cos(yaw);

	userEntity.setGuiFloat(_overlayHandle, "modelYaw", -_playerAngles_y);
	userEntity.setGuiFloat(_overlayHandle, "modelPitch", modelPitch);
	userEntity.setGuiFloat(_overlayHandle, "modelRoll", modelRoll);
}

void playertools_compass::updateLoop(entity userEntity)
{
	// Update the compass GUI each frame
	eachFrame
	{
		updateCompass(userEntity);
	}
}

void playertools_compass::inventory_item_init(entity userEntity, float overlayHandle, string overlayName)
{
	_overlayHandle = overlayHandle;

	updateCompass(userEntity);
}

void playertools_compass::inventory_item_select(entity userEntity, float overlayHandle)
{
	_overlayHandle = overlayHandle;

	// Disable the default inventory
	userEntity.setGuiFloat(userEntity.getInventoryOverlay(), "Inventory_ItemVisible", 0);

	// Make the compass GUI visible
	userEntity.setGuiFloat(_overlayHandle, "CompassVisible", 1);

	// Start a new thread 
	_updateThreadNum = thread updateLoop(userEntity);
}

void playertools_compass::inventory_item_unselect(entity userEntity, float overlayHandle)
{
	_overlayHandle = overlayHandle;

	// Kill the update loop
	sys.terminate(_updateThreadNum);

	// Disable the compass
	userEntity.setGuiFloat(_overlayHandle, "CompassVisible", 0);

	// Enable the default inventory again
	userEntity.setGuiFloat(userEntity.getInventoryOverlay(), "Inventory_ItemVisible", 1);
}

void playertools_compass::inventory_item_update(entity userEntity, float overlayHandle)
{
	updateCompass(userEntity);
}

/**
* =============== SPYGLASS ==========================================
*/

#define ZOOM_GUI_LAYER 6
#define ZOOM_STARTFOV 90
#define ZOOM_ENDFOV 25
#define ZOOM_STEP 10
#define ZOOM_IN_DURATION 400 // in msec
#define ZOOM_OUT_DURATION 150 // in msec
#define ZOOM_STEP_DURATION 75 // in msec

#define SPYGLASS_IMMOBILIZATION IM_ATTACK|IM_ITEM_SELECT|IM_ITEM_DROP|IM_FROB|IM_WEAPON_SELECT
#define SPYGLASS "spyglass"

#define STATE_ZOOM_IN_REQUEST "ZoomInRequest"
#define STATE_ZOOM_OUT_REQUEST "ZoomOutRequest"
#define STATE_CLOSE_REQUEST "CloseRequest"

// grayman #3807
#define SPYGLASS_BACKGROUND "background"

void playertools_spyglass::init()
{
	zoomedIn = false;
	zoomMutex = false;
}

void playertools_spyglass::zoomOut()
{
	// Break the ongoing loop, if it hasn't already
	zoomedIn = false;

	// We're already zooming in or zoomed in, go back to normal
	$player1.endZoom(ZOOM_OUT_DURATION);

	zoomMutex = true; // disable commands during the zoom out transition

	sys.wait(ZOOM_OUT_DURATION/1000);
	$player1.destroyOverlay(overlayHandle);
	overlayHandle = 0;

	zoomMutex = false;

	// Mobilize the player again
	$player1.setImmobilization(SPYGLASS, 0);
}

void playertools_spyglass::inventoryUse(entity userEntity, entity frobbedEntity, float buttonState)
{
	// This is to avoid zoomIn events during zoomOut transitions, which look bad
	if (zoomMutex) {
		return;
	}

	if (!zoomedIn)
	{
		// Start zooming in
		zoomedIn = true;
		$player1.startZoom(ZOOM_IN_DURATION, ZOOM_STARTFOV, ZOOM_ENDFOV);

		// Create the GUI
		overlayHandle = $player1.createOverlay(getKey("gui"), ZOOM_GUI_LAYER);

		// Disable some player actions while zoomed in
		$player1.setImmobilization(SPYGLASS, SPYGLASS_IMMOBILIZATION);

		$player1.setGuiFloat(overlayHandle, STATE_ZOOM_IN_REQUEST, 0);
		$player1.setGuiFloat(overlayHandle, STATE_ZOOM_OUT_REQUEST, 0);

		// grayman #3807 - display the correct background based on aspect ratio
		$player1.setSpyglassOverlayBackground();

		eachFrame {
			// Check if the zoom has been ended >> break the loop
			if (!zoomedIn) {
				break;
			}

			float closeRequest = $player1.getGuiFloat(overlayHandle, STATE_CLOSE_REQUEST);
			if (closeRequest)
			{
				// Perform the zoomOut transition and break the loop
				zoomOut();
				break;
			}

			float zoomInRequest = $player1.getGuiFloat(overlayHandle, STATE_ZOOM_IN_REQUEST);
			float zoomOutRequest = $player1.getGuiFloat(overlayHandle, STATE_ZOOM_OUT_REQUEST);

			float curFov = $player1.getFov();
			float newFov = curFov;

			if (zoomInRequest)
			{
				newFov -= ZOOM_STEP;

				// Reset the GUI variable
				$player1.setGuiFloat(overlayHandle, STATE_ZOOM_IN_REQUEST, 0);
			}
			else if (zoomOutRequest)
			{
				// Zoom further out, if applicable
				newFov += ZOOM_STEP;

				// Reset the GUI variable
				$player1.setGuiFloat(overlayHandle, STATE_ZOOM_OUT_REQUEST, 0);
			}

			if (zoomInRequest || zoomOutRequest)
			{
				// Clamp the zoom value
				if (newFov < ZOOM_ENDFOV)
				{
					newFov = ZOOM_ENDFOV;
				}
				else if (newFov > ZOOM_STARTFOV) 
				{
					newFov = ZOOM_STARTFOV;
				}

				// Start the zoom transition from the current value to the the new one
				$player1.startZoom(ZOOM_STEP_DURATION, curFov, newFov);
			}
		}
	}
	else
	{
		// We're already zoomed in, so zoom out now.
		zoomOut();
	}
}

#endif //__DARKMOD_PLAYER_TOOLS__

