//
// Copyright(C) 2014-2015 Samuel Villarreal
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// DESCRIPTION:
//      Player Script Object Class
//

#include "scripts/common.txt"

/*
==============================================================
Global vars for Knife
==============================================================
*/

const array<int> g_KnifeRaptorHits          = { 3, 3, 3 };
const array<int> g_KnifeHumanHits           = { 15, 15, 15 };
const array<int> g_knifeDimetrondonHits     = { 1, 1, 1 };
const array<int> g_KnifeTriceratopsHits     = { 1, 1, 1 };
const array<int> g_KnifeSubterraneanHits    = { 1, 1, 1 };
const array<int> g_KnifeStalkerHits         = { 7, 7, 7 };
const array<int> g_KnifeAlienHits           = { 1, 1, 1 };
const array<int> g_KnifePurlinHits          = { 3, 3, 3 };
const array<int> g_KnifeRobotHits           = { -1, -1, -1 };
const array<int> g_KnifeSewerCrabHits       = { 1, 1, 1 };
const array<int> g_KnifePlantHits           = { 1, 1, 1 };

const int g_KnifeMortalWoundHits    = 30;
const int g_KnifeMortalDeathHits    = 999;
const int g_KnifeTrexHits           = 25;
const int g_KnifeCampaignerHits     = 20;
const int g_KnifeLonghunterHits     = 15;
const int g_KnifeMantisHits         = 25;

// Smoke39
const array<array<kStr>> PlayerStepSounds = {
	{ "fx/playerStepRight1.kfx", "fx/playerStepRight2.kfx", "fx/playerStepRight3.kfx" },
	{ "fx/playerStepLeft1.kfx", "fx/playerStepLeft2.kfx", "fx/playerStepLeft3.kfx" }
};

/*
==============================================================
TurokPlayer
==============================================================
*/

class TurokPlayer : ScriptObjectPlayer
{
    kPuppet @self;
    kActor @m_pWarpBuzzActor;
    kVec3 m_vBloodVector;
    kVec3 m_vStabVector;
    kVec3 m_vShoveVector;
    float m_damageFloorTime;
    float m_initialFriction;
	float starTimer;							  
    float m_nBubbles;
    float m_bubblesSfxTimer;
    float m_shoveTime;
    // Smoke39 - kludge; KnifeAttack() seems to get called ~1-3 times per swipe
    // so the knife sets this to false, initiates the damage, then checks this to see if anything was hit
    bool bKnifeHit;
	float footstep;
	bool bWasOnGround, bLeftFoot;
	int DamFlashTick; // tick when last scope damage flash occured
	bool bWasInSaveSector;

    TurokPlayer(kPuppet @actor)
    {
        @self = actor;
        @m_pWarpBuzzActor = null;
        m_damageFloorTime = 0;
        m_initialFriction = 0;
        m_nBubbles = 0;
        m_bubblesSfxTimer = 0;
        m_shoveTime = 0;
		// Smoke39
		footstep = 0;
		bWasOnGround = true;
		DamFlashTick = -30;
		bWasInSaveSector = false;
    }

    // Smoke39
	void OnEndLevel()
	{
		@EnemyList = null;
		@TraceStartActor = @TraceEndActor = null;
		@WeaponSound = null;
		TauntSystem::LevelEnd();
	}

    /*
    ==============================================================
    InSpiritWorld
    ==============================================================
    */

    bool InSpiritWorld(void)
    {
        if(Game.GetCurrentMapID() < 26 || Game.GetCurrentMapID() > 41)
        {
            return false;
        }

        return true;
    }

    /*
    ==============================================================
    DoSplashBubbles
    ==============================================================
    */

    void DoSplashBubbles(void)
    {
        float cnt;

        if(m_nBubbles <= 0)
        {
            return;
        }

        m_nBubbles -= GAME_FRAME_TIME;

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

        if(m_nBubbles < m_bubblesSfxTimer)
        {
            self.PlaySound("sounds/shaders/underwater_swim_1.ksnd");
            m_bubblesSfxTimer = 0;
        }

        cnt = m_nBubbles / 6;
        if(cnt < 1)
        {
            cnt = 1;
        }

        for(int i = 0; i < int(cnt); ++i)
        {
            self.SpawnFx("fx/water_bubble.kfx", Math::vecZero);
        }
    }

    /*
    ==============================================================
    DoWarpSounds
    ==============================================================
    */

    void DoWarpSounds(void)
    {
        if(m_pWarpBuzzActor is null)
        {
            return;
        }

        float time = float(PlayLoop.Ticks());

        m_pWarpBuzzActor.Origin() = self.Origin();

        m_pWarpBuzzActor.Origin().x += (GAME_SCALE*15) * Math::Sin(Math::Deg2Rad(time * 6.0f)) + self.Yaw();
        m_pWarpBuzzActor.Origin().y += (GAME_SCALE*15) * Math::Cos(Math::Deg2Rad(time * 7.0f)) + self.Yaw();

        m_pWarpBuzzActor.PlaySound("sounds/shaders/generic_185.ksnd");
    }

    /*
    ==============================================================
    IntroCinematicEvent
    ==============================================================
    */

    void IntroCinematicEvent(kActor @instigator, const float w, const float x, const float y, const float z)
    {
        if(Game.GetCurrentMapID() != 43)
        {
            return;
        }

        Game.CallDelayedMapScript(3, instigator, 0);
    }

    /*
    ==============================================================
    OnTick
    ==============================================================
    */

    void OnTick(void)
    {
	if (starTimer > 00.0f)
																 
		{
	starTimer -= GAME_DELTA_TIME;
	if (starTimer <= 0.0f)
		{
	m_initialFriction = 0.50f; 
		
								   
	
	   
			  
		}
			}
		// Smoke39 - update enemy linked list
		TurokEnemy@ prev;
		for ( TurokEnemy@ E = EnemyList; E !is null; @E = E.NextEnemy )
		{
			if ( E.self.IsStale() )
			{
				if ( prev is null )
					@EnemyList = E.NextEnemy;
				else
					@prev.NextEnemy = E.NextEnemy;
			}
			else
				@prev = E;
		}

		// Smoke39
		TurokPlusTitle::Tick();

		// Smoke39 - open Turok+ menu in save sectors
		bool bInSaveSector = World.GetAreaFlags(self.AreaID()) & AAF_SAVEGAME != 0;
		if ( bInSaveSector != bWasInSaveSector )
		{
			if ( bInSaveSector )
				Spawn( "TurokPlusMenu", self.Origin() );
			bWasInSaveSector = bInSaveSector;
		}

		// Smoke39
		UpdateGameSpeedometer();
		UpdateWeaponSound();
		// allow this to keep recharging while down
		if ( Player.CurrentWeapon() != TW_WEAPON_ALIENGUN )
			RechargeFlareGun();

		// Smoke39
		TauntSystem::Tick();
		if ( GetAreaFlags(self) & AAF_SECRET != 0 )
		{
			// there's no way to tell if a secret's been found already,
			// so use a GameVariable to keep track of them
			kStr secrets = GetGameVarS( "secrets" );
			kStr secretID = "s"+ Game.GetCurrentMapID() +"."+ self.AreaID() +";";
			if ( secrets.IndexOf(secretID) == -1 )
			{
				TauntSystem::SecretFound();
				SetGameVarS( "secrets", secrets + secretID );
			}
		}

        // Smoke39
        if ( self.PlayerFlags() & PF_FALLINGDEATH != 0 )
			FallingToDeath();
//        self.Flags() |= AF_CASTSHADOW; // doesn't work
        // only cast shadow outside of cutscenes, unless Turok is actually visible
        if ( !Camera.Active() || self.Flags() & AF_NODRAW == 0 || !Player.Locked() )
            Game.SpawnFx( "fx/PlayerShadowProjector.kfx", self, self.Origin(), kVec3(0,0,-1).ToQuat() );
		UpdateFootsteps();

        const int area = self.AreaID();
        float damageDelay =  float(World.GetAreaArg(area, 5)) / 1024.0f;

        if(self.AnimState().PlayingID() == anim_campaingerRage)
        {
            return;
        }

        if(InSpiritWorld())
        {
            DoWarpSounds();
        }

        if(self.GetWaterLevel() == WLT_UNDER)
        {
            DoSplashBubbles();
        }

        if((World.GetAreaFlags(area) & AAF_DAMAGE) != 0 && self.OnGround())
        {
            m_damageFloorTime += GAME_DELTA_TIME;

            if(m_damageFloorTime >= damageDelay)
            {
                m_damageFloorTime = 0;
                self.InflictGenericDamage(self.CastToActor(), World.GetAreaArg(area, 4));
            }

            if((World.GetAreaFlags(area) & AAF_LAVA) != 0)
            {
                self.Friction() = m_initialFriction * 0.5f;
            }
        }
        else
        {
            m_damageFloorTime = damageDelay;
            self.Friction() = m_initialFriction;
        }

        if(m_shoveTime > 0)
        {
            kVec3 vDir = m_vShoveVector - self.Origin();
            float anYaw = vDir.ToYaw();

            self.Yaw() = self.Yaw().Interpolate(anYaw, 0.2f);
            m_shoveTime -= GAME_DELTA_TIME;
        }
    }

	// Smoke39
	void UpdateFootsteps()
	{
		if ( !Opt::bPlayerFootsteps )
			return;

		// keep footsteps from droning on during cutscenes
		if ( Player.Locked() )
			return;

		bool bOnGround = self.OnGround() && self.PlayerFlags() & (PF_FLY|PF_NOCLIP) == 0
			&& World.GetAreaFlags(self.AreaID()) & (AAF_CLIMB|AAF_LADDER|AAF_WATER|AAF_CLIFF) == 0;

		// landed
		if ( !bWasOnGround && bOnGround )
		{
			footstep = 1.0;
			bLeftFoot = false;
		}
		// running
		else if ( bOnGround && self.PlayerFlags() & PF_CRAWLING == 0 )
		{
			float maxSpeed=6.4, backSpeed=4.864, freq=3;
			// we could let the slower movement speed give us lower percentages for slower footsteps
			// but then running diagonally would have faster footsteps when capping to 1.0 (since each axis is only getting up to 0.5), which we don't want
			// so we change the scale to match lava speed, and instead reduce freq
			// (check both lava and damage flag, since lava flag does nothing without damage flag)
			if ( World.GetAreaFlags(self.AreaID()) & (AAF_DAMAGE|AAF_LAVA) == (AAF_DAMAGE|AAF_LAVA) )
			{
				maxSpeed /= 2;
				backSpeed /= 2;
				freq = 2;
			}
			// setting backSpeed to maxSpeed would cause footsteps to match actual speed, but this sounds too slow in practice
			// leaving backSpeed at actual max back speed would make running backward sound as fast as forward, but that doesn't reflect the speed reduction
			// so we average them to slow the step speed a bit, but not too much
			backSpeed = (maxSpeed + backSpeed) / 2;

//			kVec3 X = self.GetTransformedVector( kVec3(1/self.Scale().x,0,0) ) - self.Origin();
//			kVec3 Y = self.GetTransformedVector( kVec3(0,-1/self.Scale().y,0) ) - self.Origin();
			kQuat horizontalRot = kQuat( self.Yaw(), 0,0,1 );
			kVec3 X = kVec3(1,0,0) * horizontalRot;
			kVec3 Y = kVec3(0,1,0) * horizontalRot;
			float speedX = Math::Fabs( self.Movement().Dot( X ) / maxSpeed );
			float speedY = self.Movement().Dot( Y );
			if ( speedY >= 0 )
				speedY /= maxSpeed;
			else
				speedY /= backSpeed;

			footstep += Math::OLDMin( Math::Sqrt( speedX*speedX + speedY*speedY ), 1.0 )
				* freq * GAME_DELTA_TIME;
		}

		if ( footstep >= 1 )
		{
			bLeftFoot = !bLeftFoot;
			const array<kStr>@ sounds = PlayerStepSounds[ Int(bLeftFoot) ];
			Game.SpawnFx( sounds[ Math::RandMax(sounds.length) ],
				self, self.Origin(), kVec3(0,0,-1).ToQuat() );
			footstep -= 1;
		}

		bWasOnGround = bOnGround;
	}

    /*
    ==============================================================
    OnBeginLevel
    ==============================================================
    */

	void OnBeginLevel(void)
	{
		if ( InSpiritWorld() )
			@m_pWarpBuzzActor = ActorFactory.Spawn( "DummyActor", 0, 0, 0, 0, self.SectorIndex() );
	}

    /*
    ============================================================================
    Smoke39 - OnPostBeginlevel
    ============================================================================
    */

    // check and try to fix discrepancies with loading unmodded saves
	void OnPostBeginLevel()
	{
        Opt::LoadSettings();

		// give hand grenade weapon if player has grenade ammo, but not the weapon
		if ( Opt::bHandGrenades && GrenadeCount() > 0 && !Player.HasWeapon(TW_WEAPON_GRENADE) )
		{
			SetSmoke39Flag( SMOKE39_HAS_GL, false );
			Player.GiveWeapon( TW_WEAPON_GRENADE, 0 );
		}

		// try to guess if the player has a backpack
		for ( int i=0; i<NUMTUROKWEAPONS; ++i )
		{
			if ( UsingBackpack(i) )
			{
				GameVariables.SetValue( "bHasBackpack", "1" );
				break;
			}
		}
	}

    // check if this weapon has more than unmodded max ammo
    // if any of these ammo capacities are increased, this will cause false positives
    // to fix that, these values could be increased to match, but that would then cause false negatives
    // the better solution would be to ONLY call this when first loading an unmodded save
    // ideally, you'd keep checking the new values in OnPickup(), too
    bool UsingBackpack( int slot )
    {
		switch ( slot )
		{
			case TW_WEAPON_PISTOL:
			case TW_WEAPON_RIFLE:     return Player.GetAmmo(slot) > 100;
			case TW_WEAPON_SHOTGUN:
			case TW_WEAPON_ASHOTGUN:  return Player.GetAmmo(slot) > 20;
			case TW_WEAPON_GRENADE:   return Player.GetAmmo(slot) > 20;
			case TW_WEAPON_MISSILE:   return Player.GetAmmo(slot) > 6;
			case TW_WEAPON_CANNON:    return Player.GetAmmo(slot) > 2;
		}
		return false;
    }

    /*
    ==============================================================
    OnSpawn
    ==============================================================
    */

    void OnSpawn(void)
    {
		// Smoke39
		InitGameSpeedometer( self.CastToActor() );
		TurokPlusTitle::Init();
		InitTraceActors( self.CastToActor() );
		StuckArrowTick = -60;
		// should normally be true, but some mods may handle it themselves,
		// and we don't want to spawn a redundant actor
		if ( WeaponSound is null )
			@WeaponSound = Spawn( -1, self.Origin(), 0, self.SectorIndex() );

        m_initialFriction = self.Friction();
    }

    /*
    ==============================================================
    OnEnterWater
    ==============================================================
    */

    void OnEnterWater(void)
    {
        // what speed is the player entering the water?
        if(self.Velocity().z < 0)
        {
            m_nBubbles = -self.Velocity().z * 2;

            if(m_nBubbles < 0)
            {
                m_nBubbles = 0;
                m_bubblesSfxTimer = m_nBubbles / 2;
            }
        }
    }

    /*
    ==============================================================
    OnArmorDamage
    ==============================================================
    */

    void OnArmorDamage(kActor @instigator, kDictMem @damageDef, const int damage)
    {
		// Smoke39 - scope view damage flash
        if ( damage > 0 )
			DamageFlash( true );

        // note: threshold is actually 5 but actual armor is divided by 3
        if(Player.Armor() > 15 && Player.Armor() - damage <= 15)
			Game.PrintLine( "$str_180", 0 );
    }

    /*
    ==============================================================
    DoViewShake
    ==============================================================
    */

    void DoViewShake(float velocity, float angle, float duration)
    {
        kActor @actor = ActorFactory.Spawn("QuakeSource", 0, 0 ,0 ,0, self.SectorIndex());
        TurokQuakeSource @quake;

        if(actor is null)
        {
            return;
        }

        @quake = cast<TurokQuakeSource@>(actor.ScriptObject().obj);

        if(quake is null)
        {
            return;
        }

        quake.SetupShake(self.Origin(), velocity, angle, duration);
    }

    /*
    ==============================================================
    DoShoveWithCamera
    ==============================================================
    */

    void DoShoveWithCamera(kActor @instigator)
    {
        if(instigator is null)
        {
            return;
        }

        kVec3 org;
        kVec3 pos;

        org.x = instigator.Origin().x;
        org.y = instigator.Origin().y;
        org.z = instigator.Origin().z + instigator.Height() * 0.5f;

        kVec3 dir = self.Origin() - org;

        if(dir.x*dir.x+dir.y*dir.y <= 0.0f)
        {
            dir.x = Math::RandCFloat();
            dir.y = Math::RandCFloat();
        }

        dir.Normalize();

        dir *= (1.75f*GAME_SCALE);
        dir.z = (0.875f*GAME_SCALE);

        self.Velocity() += dir;

        self.Origin().z += GAME_SCALE;
        self.PlayerFlags() |= PF_NOAIRFRICTION;

        m_shoveTime = 0.5f;
        m_vShoveVector = org;
    }

    /*
    ==============================================================
    OnDamage
    ==============================================================
    */

	void OnDamage(kActor @instigator, kDictMem @damageDef, const int damage)
	{
		// check for quake effect
		if ( damageDef !is null )
		{
			HandleArrowDamageSignal( damageDef );
			HandleDazeDamageSignal( damageDef );

			if ( GetBool( damageDef, "bQuake" ) )
			{
				float velocity = GetFloat( damageDef, "quakeVelocity",  0.5625f * GAME_SCALE );
				float angle    = GetFloat( damageDef, "quakeAngle",    -0.03f );
				float duration = GetFloat( damageDef, "quakeDuration",  2.75f );

				// Smoke39 - check for localized quake source
				float range;
				if ( damageDef.GetFloat("RangeLimit",range) && range > 0 )
					RangedQuake( velocity, angle, duration, ParticleOrigin(), range );
				else
					DoViewShake( velocity, angle, duration );
			}

			if ( GetBool( damageDef, "bShove" ) )
				DoShoveWithCamera( instigator );
		}

		if ( damage <= 0 )
			return;

		// killing blow
		if ( self.Health() > 0 && self.Health() - damage <= 0 )
			return;

		// Smoke39 - no low health message if the same hit kills us
		// also removed health>damage check, and instead just moved after killing blow check
		if ( self.Health() > 15 && self.Health() - damage <= 15 )//&& self.Health() > damage )
		{
			Game.PrintLine("$str_181", 0);
			// Smoke39
			TauntSystem::LowHealth();
		}

		// Smoke39
		DamageFlash();

		if ( self.GetWaterLevel() == WLT_UNDER )
		{
			switch ( Math::RandMax(2) )
			{
			case 0:
				self.PlaySound("sounds/shaders/generic_14_turok_water_injury_1.ksnd");
				break;
			case 1:
				self.PlaySound("sounds/shaders/generic_15_turok_water_injury_2.ksnd");
				break;
			}
		}
		else
		{
			switch ( Math::RandMax(5) & 3 )
			{
			case 0:
				self.PlaySound("sounds/shaders/generic_10_turok_injury_1.ksnd");
				break;
			case 1:
				self.PlaySound("sounds/shaders/generic_11_turok_injury_2.ksnd");
				break;
			case 2:
				self.PlaySound("sounds/shaders/generic_12_turok_injury_3.ksnd");
				break;
			case 3:
				self.PlaySound("sounds/shaders/generic_13_turok_injury_4.ksnd");
				break;
			}
		}
	}

	void HandleArrowDamageSignal( kDictMem@ damageDef )
	{
		// eventually, different StickArrow values will indicate different arrow styles
		// if this method is implemented for more arrow types in place of smart projectiles
		if ( GetInt( damageDef, "StickArrow" ) != 1 ) return;
		// make sure this isn't a corpse sticker trying to spawn on a live enemy
		// or a wall being hit at the same time as an actor from the game's wonky collision
		if ( self.GameTicks() - StuckArrowTick < 2 ) return;
		kVec3 origin = ParticleOrigin();
//		kActor@ arrow = Spawn( "StuckArrow", origin, 0, -1 );
//		kActor@ arrow = Spawn( "StuckArrow", origin, 0, self.SectorIndex() );
		kActor@ arrow = Spawn( "StuckArrow", self.Origin(), 0, self.SectorIndex() );
		arrow.SetPosition( origin );
		kVec3 v = self.Origin();
		v.z += self.ViewHeight();
		v = origin - v;
		arrow.Velocity() = v.Normalize() * 7680;
		arrow.Pitch() = -v.ToPitch();
		arrow.Yaw()   =  v.ToYaw();
		StuckArrow@ SO = cast<StuckArrow@>( ActorScript(arrow) );
		if ( SO is null ) return;
		SO.CollisionCommon( null, origin, Math::vecZero, 0 );
		if ( GetBool( damageDef, "bFall" ) )
			SO.UnStick();
	}

	void HandleDazeDamageSignal( kDictMem@ damageDef )
	{
		float daze = GetFloat( damageDef, "Daze" );
		if ( daze <= 0 ) return;
		kVec3 origin = ParticleOrigin();
		float radius = Math::Sqr( GetFloat( damageDef, "radius" ) * GAME_SCALE );
		// InteractActorsAtPosition() is unreliable beyond short range
		for ( TurokEnemy@ E = EnemyList; E !is null; @E = E.NextEnemy )
		{
			if ( !E.CanBeDazed() ) continue;
			float dist = SqrDistToCylinder( E.self.Origin() - origin, E.self.Radius(), E.self.Height() );
			if ( dist >= radius ) continue;
			Trace( E.self.GetTransformedVector(E.DazeOffset()) , origin, CF_IGNOREBLOCKERS | CF_NOCLIPSTATICS );
			if ( (CModel.InterceptVector() - origin).UnitSq() > 25 ) continue;
			E.DazeTime = Math::OLDMax( daze * (1 - dist/radius), E.DazeTime );
			E.StunYaw = E.self.Yaw();
		}
	}

    // Smoke39
    kVec3 ParticleOrigin()
    {
		// known delta per tick of special trigger particle
		kVec3 delta( 0, 1000000000.0f*GAME_DELTA_TIME*GameSpeed(), 0 );
		// work backward from hit location to find origin
		return CModel.InterceptVector() - delta * CModel.Fraction();
    }

    // Smoke39 - flash the screen when scoped, since normal damage flash doesn't happen during cutscenes
    void DamageFlash( bool bArmor=false )
    {
		if ( !PlayerScoped() )
			return;

		int t = self.GameTicks() - DamFlashTick;
		if ( t < 3 )
			return;

		kStr flash = "fx/scope/DamageFlash";
		if ( bArmor )
			flash += "Armor";
		if ( t < 14 )
			flash += "Light";
		if ( GameSpeed() <= 0.6f )
			flash += "Slomo";
		flash += ".kfx";

		self.SpawnFx( flash, Math::vecZero );

		DamFlashTick = self.GameTicks();
    }

    // Smoke39
    void RangedQuake( float vel, float angle, float time, kVec3&in origin, float range )
    {
		kVec3 center = self.Origin();
		center.z += self.Height()/2;
		float power = 1 - ( origin - center ).Unit() / range;
		if ( power > 0 )
		{
			power = Math::Pow( power, 1.5f );
			DoViewShake( vel*power, angle*power, time );
		}
	}

    /*
    ==============================================================
    OnDeath
    ==============================================================
    */

    // Smoke39 - stuff to do for both normal and falling death
    void DeathCommon()
    {
		LostBackpack();
		TauntSystem::CancelTaunt();
    }

   	// Smoke39
	void FallingToDeath()
	{
		DeathCommon();
	}

    void OnDeath(kActor @killer, kDictMem @damageDef)
    {
        // Smoke39
        DeathCommon();

        self.PlaySound("sounds/shaders/generic_18_turok_normal_death.ksnd");

        //
        // we're going to be doing our own custom death cinematic for these specific maps
        //

        int currentMap = Game.GetCurrentMapID();

        switch(currentMap)
        {
        case 0: // campaigner boss map?
            self.PlayerFlags() |= PF_PREVENTDEATHCAM;
            Game.CallDelayedMapScript(2, self.CastToActor(), 0);
            break;
        case 3: // trex boss map?
            self.PlayerFlags() |= PF_PREVENTDEATHCAM;
            Game.CallDelayedMapScript(2, self.CastToActor(), 0);
            break;
        case 49: // mantis boss map?
            self.PlayerFlags() |= PF_PREVENTDEATHCAM;
            Game.CallDelayedMapScript(5, self.CastToActor(), 0);
            break;
        default:
			// Smoke39
			if ( PlayerScoped() )
				CancelScope();
            break;
        }
    }

    /*
    ==============================================================
    OnPickup
    ==============================================================
    */

	// Smoke39
	void LoadAmmo( const kStr&in ammoType )
	{
		Player.ConsumeAmmo( 1 - GetGameVarI(ammoType) );
		SetGameVarI( ammoType, 0 );
	}

    void OnPickup(kActor @pickup)
    {
        // Smoke39
        TauntSystem::Pickup(pickup);

        // Smoke39
        switch ( pickup.Type() )
        {
			case AT_PICKUP_KEY1:  case AT_PICKUP_KEY2:  case AT_PICKUP_KEY3:
			case AT_PICKUP_KEY4:  case AT_PICKUP_KEY5:  case AT_PICKUP_KEY6:
			case AT_PICKUP_FINALKEY1:  case AT_PICKUP_FINALKEY2:  case AT_PICKUP_FINALKEY3:
				SetGameVarF( "MeleePoints", GetGameVarF("MeleePoints") + MeleePointsPerKey );
				break;
        }

        switch(pickup.Type())
        {
        // Smoke39
        case AT_PICKUP_BACKPACK:
            GotBackpack();
			return;
        // Smoke39 - alt ammo loaders
        case AT_LOADER_TEKARROWS:
			LoadAmmo( TekArrows.GVK );
			return;
		case AT_LOADER_EXPSHELLS:
			LoadAmmo( ExpShells.GVK );
			return;

		// Smoke39 - check for generic chrono piece spawned by smart pickup, instead of original individual pieces
		case AT_CHRONO_PIECE:
			break;
        case AT_PICKUP_KEY1:
        case AT_PICKUP_KEY2:
        case AT_PICKUP_KEY3:
        case AT_PICKUP_KEY5:
        case AT_PICKUP_KEY6:
            self.RenderModel().SetTexture(21, pickup.SpawnParams(5));
            return;
        case AT_PICKUP_KEY4:
            self.RenderModel().SetTexture(21, pickup.SpawnParams(5));
            if(Game.GetCurrentMapID() == 48)
            {
                GameVariables.SetValue("bGotLonghunterKey", "1");
            }
            // fall through
        case AT_PICKUP_FINALKEY2:
            if(Game.GetCurrentMapID() == 49)
            {
                GameVariables.SetValue("bGotMantisKey", "1");
            }
            // fall through
        default:
            return;
        }

        // Smoke39 - no longer set chrono piece flags, just check them after smart pickup updates them
		if ( GetGameVarI("chronoPieceFlags") == 0xFF )
		{
			Player.GiveWeapon( TW_WEAPON_CHRONO, 3 );
			Game.PrintLine( Opt::ChronoType==CH_Chrono? "$str_156" : "nuke", 0 );
			TauntSystem::Weapon( TW_WEAPON_CHRONO );
		}
    }

    // Smoke39
    void GotBackpack()
	{
		SetGameVarB( "bHasBackpack", true );
	}
	void LostBackpack()
	{
		SetGameVarB( "bHasBackpack", false );
		// base game doesn't actually cap explosive shells when you lose the backpack,
		// so I guess we won't actually cap stored explosive shells either
		// (to fix this would also require an "explosive shells debt," to also cap loaded ammo)
//		SetGameVarI( ExpShells.GVK, Math::Min( GetGameVarI(ExpShells.GVK), MaxExpShells ) );
	}

    /*
    ==============================================================
    KnifeAttack
    ==============================================================
    */

    // Smoke39 - more permissive backstab detection when enemy is incapacitated
    // (full range, and check player position rather than facing)
    bool CheckStunnedBackstab( kActor@ other, float dist, float radius )
    {
		TurokEnemy@ script = cast<TurokEnemy@>( ActorScript(other) );
		if ( script is null || !script.IsIncapacitated() ) return false;
		if ( dist > radius + other.Radius() ) return false;
		return Math::Fabs( Math::Rad2Deg( other.GetTurnYaw(self.Origin()) ) ) > 135;
    }

    void KnifeAttack( kActor @actor, const float arg1, const float arg2, const float arg3, const float arg4 )
    {
        float radius;
        float dist;
        int damage = 0;
        int angle1, angle2;
        int angDiff;
        int nKnife = int(arg1);
        int health;
        TurokEnemy @enemyObj;

        if(actor is null)
        {
            return;
        }

        if(actor.IsStale() || (actor.ScriptObject() is null))
        {
            // actor must be alive and has a script object
            return;
        }

        if(actor.ScriptObject().obj is null)
        {
            // ref handle to script object must exist
            return;
        }

        if((actor.Flags() & AF_HOLDTRIGGERANIM) != 0)
        {
            return;
        }

        // Smoke39 - LOS check
        if ( !self.CanSee( actor, CF_IGNOREBLOCKERS /*| CF_NOCLIPSTATICS | CF_NOCLIPACTORS*/ ) )
			return;

        // did actor get hit?
        radius = (arg2 * GAME_SCALE);

        dist = Math::Sqrt(actor.DistanceToPoint(m_vStabVector));

        angle1 = int(Math::Rad2Deg(actor.Yaw())) % 360;
        angle2 = int(Math::Rad2Deg( self.Yaw())) % 360;

        if(angle1 < 0) angle1 += 360;
        if(angle2 < 0) angle2 += 360;

        angDiff = (angle1 - angle2);

        // check for backstabs
        if ( CheckStunnedBackstab( actor, dist, radius ) // Smoke39
			|| dist <= ((radius * 0.5f) + actor.Radius()) && Math::Abs(angDiff) <= 40 )
        {
			// Smoke39
			if ( MeleeWeaponModel == MT_WARBLADE )
				damage = WarBladeDamage_Back( actor.Type(), nKnife );
			else
				damage = BackstabDamage( actor.Type() );
        }
        // do regular direct damage
        else if(dist <= (radius + actor.Radius()))
        {
			// Smoke39
			if ( MeleeWeaponModel == MT_WARBLADE )
				damage = WarBladeDamage_Front( actor.Type(), nKnife );
			else
				damage = KnifeDamage( actor.Type(), nKnife );
        }

        if(damage <= 0)
        {
            return;
        }

        health = actor.Health();
        actor.InflictGenericDamage(self.CastToActor(), damage);

        // very important that we know that all actors that we attack contains a pointer
        // to the TurokEnemy script object class
        if(!(actor.ScriptObject() is null) && !(actor.ScriptObject().obj is null))
        {
            @enemyObj = cast<TurokEnemy@>(actor.ScriptObject().obj);

            if(health > 0 && actor.Health() <= 0 && !(enemyObj is null))
            {
                enemyObj.m_bMortallyWounded = true;
                // Smoke39
                float meleePoints = GetGameVarF( "MeleePoints" ) + PointsPerMeleeKill[ MeleeWeaponModel ];
                SetGameVarF( "MeleePoints", meleePoints );
                if ( meleePoints >= PointsForWarBlade && !CheckSmoke39Flag(SMOKE39_WARBLADE) )
                {
					SetSmoke39Flag( SMOKE39_WARBLADE, true );
					if ( Opt::MeleeSetting == MS_TALON )
						ForcePickup( "WarBladePickup" );
				}
            }
        }

        // Smoke39 - play sound in KnifeParticles, so we handle all impact effects in one place
//        actor.PlaySound("sounds/shaders/tomahawk_impact_flesh.ksnd");
        KnifeParticles(actor, nKnife, damage);

        // Smoke39
        bKnifeHit = true;
    }

    // Smoke39 - moved out of KnifeAttack()
	int KnifeDamage( int ActorType, int nKnife )
	{
		switch ( ActorType )
		{
			case AT_RAPTOR:
				return g_KnifeRaptorHits[nKnife];

			case AT_DINOSAUR1:
				return g_knifeDimetrondonHits[nKnife];

			case AT_RIDER:
				return g_KnifeTriceratopsHits[nKnife];

			case AT_SANDWORM:
				return g_KnifeSubterraneanHits[nKnife];

			case AT_STALKER:
				return g_KnifeStalkerHits[nKnife];

			case AT_ALIEN:
				return g_KnifeAlienHits[nKnife];

			case AT_PURLIN:
				return g_KnifePurlinHits[nKnife];

			case AT_MECH:
				return g_KnifeRobotHits[nKnife];

			case AT_SEWERCRAB:
				return g_KnifeSewerCrabHits[nKnife];

			case AT_KILLERPLANT:
				return g_KnifePlantHits[nKnife];

			case AT_GRUNT:
			case AT_INSECT:
			case AT_DRAGONFLY:
			case AT_ANIMAL:
			case AT_BOAR:
				return g_KnifeHumanHits[nKnife];

			case AT_AIBOSS_TREX:
				return g_KnifeTrexHits;

			case AT_AIBOSS_CAMPAINGER:
				return g_KnifeCampaignerHits;

			case AT_AIBOSS_HUNTER:
				return g_KnifeLonghunterHits;

			case AT_AIBOSS_MANTIS:
				return g_KnifeMantisHits;
		}

		return 0;
	}

    // Smoke39 - moved out of KnifeAttack()
	int BackstabDamage( int ActorType )
	{
		switch ( ActorType )
		{
			case AT_GRUNT:
			case AT_ANIMAL:
			case AT_BOAR:
				return g_KnifeMortalDeathHits;

			case AT_RAPTOR:
			case AT_DINOSAUR1:
			case AT_RIDER:
			case AT_SANDWORM:
			case AT_STALKER:
			case AT_ALIEN:
			case AT_PURLIN:
			case AT_MECH:
				return g_KnifeMortalWoundHits;

			case AT_AIBOSS_TREX:
				return g_KnifeTrexHits;

			case AT_AIBOSS_CAMPAINGER:
				return g_KnifeCampaignerHits;

			case AT_AIBOSS_HUNTER:
				return g_KnifeLonghunterHits;

			case AT_AIBOSS_MANTIS:
				return g_KnifeMantisHits;
		}

		return 0;
	}

	// Smoke39
	int WarBladeDamage_Front( int ActorType, int nKnife )
	{
		float dam = KnifeDamage( ActorType, nKnife );
		if ( dam == 0 )
			return 0;
		return int( Math::Ceil( (dam + g_KnifeMortalWoundHits) / 2.0f ) );
	}
	int WarBladeDamage_Back( int ActorType, int nKnife )
	{
		int WBFront = WarBladeDamage_Front( ActorType, nKnife ); // higher for bosses
		int KBack = BackstabDamage( ActorType ) + 5; // higher for normal enemies
		return Math::Max( WBFront, KBack );
	}

    /*
    ==============================================================
    KnifeParticles
    ==============================================================
    */

    void KnifeParticles(kActor @actor, const int nKnife, const int damage)
    {
        kStr redBlood, greenBlood;
        kStr particle;

        // Smoke39 - new effects for T2 talon, and off-hand
        switch(nKnife)
        {
        case 0:
			if ( MeleeWeaponHand == HAND_Right )
            {
				switch ( MeleeWeaponModel )
				{
					case MT_KNIFE:
						redBlood    = "fx/knifeleft_blood.kfx";
						greenBlood  = "fx/knifeleft_gblood.kfx";
						break;
					case MT_TALON:
						redBlood    = "fx/T2/TalonBlood_Red_Left.kfx";
						greenBlood  = "fx/T2/TalonBlood_Green_Left.kfx";
						break;
					case MT_WARBLADE:
						redBlood    = "fx/knifeleft_blood.kfx";
						greenBlood  = "fx/knifeleft_gblood.kfx";
						break;
				}
			}
            else
            {
				switch ( MeleeWeaponModel )
				{
					case MT_KNIFE:
						redBlood    = "fx/knifeOffhandBlood.kfx";
						greenBlood  = "fx/knifeOffhandGBlood.kfx";
						break;
					case MT_TALON:
						redBlood    = "fx/T2/TalonBlood_Red_Offhand.kfx";
						greenBlood  = "fx/T2/TalonBlood_Green_Offhand.kfx";
						break;
					case MT_WARBLADE:
						redBlood    = "fx/knifeOffhandBlood.kfx";
						greenBlood  = "fx/knifeOffhandGBlood.kfx";
						break;
				}
            }
            break;

        case 1:
            redBlood    = "fx/kniferight_blood.kfx";
            greenBlood  = "fx/kniferight_gblood.kfx";
            break;

        case 2:
			switch ( MeleeWeaponModel )
			{
				case MT_KNIFE:
					redBlood    = "fx/knifeforward_blood.kfx";
					greenBlood  = "fx/knifeforward_gblood.kfx";
					break;
				case MT_TALON:
					redBlood    = "fx/T2/TalonBlood_Red_Down.kfx";
					greenBlood  = "fx/T2/TalonBlood_Green_Down.kfx";
					break;
				case MT_WARBLADE:
					redBlood    = "fx/knifeforward_blood.kfx";
					greenBlood  = "fx/knifeforward_gblood.kfx";
					break;
			}
        }

		// Smoke39 - base effects on impact type, rather than actor type
		// metallic effects for metallic enemies, rather than stone spark and flesh sound
		// proper effects for force field, rather than flesh effects
		switch ( actor.ImpactType() )
		{
			case IT_METAL:
				actor.PlaySound( "sounds/shaders/bullet_impact_5.ksnd" );
				particle = "fx/generic_251.kfx";
				break;

			case IT_FORCEFIELD:
				actor.PlaySound( "sounds/shaders/generic_219.ksnd" );
				particle = "fx/generic_187.kfx";
				break;

			case IT_FLESH_HUMAN:
				actor.PlaySound( "sounds/shaders/tomahawk_impact_flesh.ksnd" );
				particle = redBlood;
				break;

			case IT_FLESH_CREATURE:
				actor.PlaySound( "sounds/shaders/tomahawk_impact_flesh.ksnd" );
				particle = greenBlood;
				break;

			default:
				actor.PlaySound( "sounds/shaders/tomahawk_impact_flesh.ksnd" );
				return;
		}

        actor.SpawnProjectile(particle, m_vBloodVector, self.Origin(), Math::Deg2Rad(360));
    }
};
