//
// 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:
//      Weapon Actions
//

#include "scripts/animations.txt"
#include "scripts/jk.txt"

enum eJKWeapons
{
	jk_Weapon_Fists            = TW_WEAPON_KNIFE,
	jk_Weapon_Bryar            = TW_WEAPON_BOW,
	jk_Weapon_DL44             = TW_WEAPON_PISTOL,
	jk_Weapon_STRifle          = TW_WEAPON_RIFLE,
	jk_Weapon_Bowcaster        = TW_WEAPON_SHOTGUN,
	jk_Weapon_Sniper           = TW_WEAPON_ASHOTGUN,
	jk_Weapon_Repeater         = TW_WEAPON_MINIGUN,
	jk_Weapon_ConcRifle        = TW_WEAPON_ACCELERATOR,
	jk_Weapon_Sequencers       = TW_WEAPON_ALIENGUN,
	jk_Weapon_ThermalDetonator = TW_WEAPON_GRENADE,
	jk_Weapon_RailDetonator    = TW_WEAPON_MISSILE,
	jk_Weapon_Carbonite        = TW_WEAPON_CANNON,
	jk_Weapon_Lightsaber       = TW_WEAPON_CHRONO
}

// recoil:
// pulse    -0.01963495463132858f
// pistol   -0.02094395086169243f
// minigun  -0.02094395086169243f
// rifle    -0.02416609786450863f
// ashotgun -0.02617993950843811f
// grenade  -0.03490658476948738f
// shotgun  -0.03926990926265717f

float Recoil_Fist   =  0.02f; // roll
float Recoil_Bry    = -0.02f;
float Recoil_DL44   = -0.025f;
float Recoil_STR    = -0.02f;
float Recoil_Bow    = -0.03f;
float Recoil_Sniper = -0.03f;
float Recoil_Rpt    = -0.01f; // roll is half this
float Recoil_Conc   = -0.05f;
float Recoil_Det    =  0.03f; // roll
float Recoil_Rld    = -0.04f;

//==============================================================================
//
//    TurokWeapon
//
//==============================================================================

class TurokWeapon : ScriptObjectWeapon
{
	float ShotPeriod;  // time in seconds to wait between shots
	int   MinAmmo = 1; // min ammo to fire (swap out when trying to fire with less)
	bool bIsGun = true;

	kWeapon @self;
	int  ShotTick  = -600; // self.GameTicks() when we last fired
	int  FlashTime = -1;   // how many ticks after ShotTick to do muzzle flash (negative means no flash pending)
	bool bEmptyFired = false;

	TurokWeapon( kWeapon @actor )
	{
		@self = actor;
	}

	~TurokWeapon()
	{
	}

	//==========================================================================
	// Smoke39

	void MuzzleFlash()
	{
	}

	void PlayHolsterSound()
	{
		if ( bIsGun ) self.PlaySound( "sounds/jk/holster.ksnd" );
	}

	void PlayEmptySound()
	{
		if ( bEmptyFired ) return;
		switch ( self.Owner().CurrentWeapon() )
		{
			case jk_Weapon_Bryar:
			case jk_Weapon_DL44:
				self.PlaySound( "sounds/jk/bry_empty.ksnd" );
				break;
			case jk_Weapon_STRifle:
			case jk_Weapon_Bowcaster:
			case jk_Weapon_Sniper:
			case jk_Weapon_Repeater:
				self.PlaySound( "sounds/jk/str_empty.ksnd" );
				break;
			case jk_Weapon_RailDetonator:
				self.PlaySound( "sounds/jk/rld_empty.ksnd" );
				break;
			case jk_Weapon_ConcRifle:
			case jk_Weapon_Carbonite:
				self.PlaySound( "sounds/jk/conc_empty.ksnd" );
			default:
		}
		bEmptyFired = true;
	}

	// speed up swap-in anim (swap-out already seems to be 3, but no harm in making sure)
	void SwapSpeedup()
	{
		switch ( self.AnimState().PlayingID() )
		{
			case anim_weaponSwapIn:
			case anim_weaponSwapOut:
				ChangeSpeed( 3 );
		}
	}

	//==========================================================================
	// Helpers

	kPuppet@ OwnerP()
	{
		return self.Owner().Actor();
	}
	kActor@ OwnerA()
	{
		return self.Owner().Actor().CastToActor();
	}

	int GetAmmo()
	{
		return self.Owner().GetAmmo( self.Owner().CurrentWeapon() );
	}

	bool HasAmmo( int amt=1, bool bSwitch=false )
	{
		if ( GetAmmo() >= amt )
			return true;
		if ( bSwitch )
			ForceSwitch();
		return false;
	}

	bool UseAmmo( int amt )
	{
		if ( HasAmmo(MinAmmo,true) )
		{
			self.Owner().ConsumeAmmo( amt );
			// normally when ammo reaches 0, weapon switches automatically
			// so trigger a switch if ammo drops below min to fire
			HasAmmo( MinAmmo, true );
			return true;
		}
		return false;
	}

	void ForceSwitch()
	{
		int ammo = GetAmmo();
		if ( ammo > 0 )
		{
			// use up ammo to trigger weapon change
			self.Owner().ConsumeAmmo( ammo );
			// then give it right back (unless infinite ammo is on and we took none)
			if ( GetAmmo() < ammo )
				self.Owner().GiveWeapon( self.Owner().CurrentWeapon(), ammo );
		}
	}

	kVec3 EyePos()
	{
		kPuppet @src = self.Owner().Actor();
		kVec3 v = src.Origin();
		v.z += src.ViewHeight() + src.LandingViewOffset();
		return v;
	}

	void BlockFireState()
	{
		// idle anim prevents entering fire state
		if ( !self.AnimState().IsPlaying(anim_weaponIdle) )
			self.AnimState().Blend( anim_weaponIdle, 4.0f, 8.0f, ANF_LOOP );
	}

	bool ShotPeriodComplete()
	{
		return (self.GameTicks() - ShotTick) * BoostFactor() * GAME_DELTA_TIME >= ShotPeriod;
	}

	void StopAutoFire()
	{
		PlayAnim( anim_weaponIdle );
	}

	bool IsBoosted( int dt )
	{
		return bIsGun && dt < 30*60;
	}
	bool IsBoosted()
	{
		return IsBoosted( Player.Actor().GameTicks() - PowerBoostTick );
	}
	int BoostFactor()
	{
		return IsBoosted() ? 2 : 1;
	}

	void MeleeDamage( kActor@ target, int damage )
	{
		int health = target.Health();
		target.InflictGenericDamage( OwnerA(), damage );
		if ( target.Health() <= 0 && health > 0 && target.ScriptObject() !is null )
		{
			TurokEnemy@ E;
			if ( ( @E = cast<TurokEnemy@>(target.ScriptObject().obj) ) !is null )
				E.m_bMortallyWounded = true;
		}
	}

	bool Hitscan( kVec3&in start, kVec3&in end )
	{
		Dummy1.SetPosition( start );
		Dummy2.SetPosition( end );
		if ( Dummy1.CanSee( Dummy2, CF_IGNOREBLOCKERS|CF_NOCLIPSTATICS ) )
			return false;
		// ceiling collision occurs partially up the wall, and returns screwed up z-coord
		// so interpolate its position between start/end points based on its x and y coordinates
		if ( CModel.ClipResult() & (1<<1) == 0 )
			return true;
		kVec3 rayDif = end - start;
		rayDif.z = 0;
		float rayLen = rayDif.Unit();
		if ( rayLen == 0 )
			CModel.InterceptVector().z = Dummy1.GetCeilingHeight();
		else
		{
			kVec3 hitDif = CModel.InterceptVector() - start;
			hitDif.z = rayDif.z = 0;
			CModel.InterceptVector().z = Math::Lerp( start.z, end.z, hitDif.Unit() / rayLen );
		}
		return true;
	}

	//==========================================================================
	// Anim Helpers

	void PlayAnim( int ID, float rate=4.0f, float tween=4.0f, int flags=0 )
	{
		if ( tween > 0 ) self.AnimState().Blend( ID, rate, tween, flags );
		else             self.AnimState().Set(   ID, rate, flags );
	}
	void ChangeSpeed( float rate )
	{
		self.AnimState().ChangeSpeed( rate );
	}

	bool IsPlaying( int animID )
	{
		return self.AnimState().IsPlaying( animID );
	}
	float PlayTime()
	{
		return self.AnimState().PlayTime();
	}
	float TrackTime()
	{
		return self.AnimState().TrackTime();
	}

	//==========================================================================
	// OnTick

	void OnTick()
	{
		SwapSpeedup();
		if ( FlashTime >= 0 && (self.GameTicks()-ShotTick) >= FlashTime )
		{
			MuzzleFlash();
			FlashTime = -1;
		}
		UpdatePowerBoost();

		if ( self.Owner().Buttons() & BC_ATTACK == 0 )
			bEmptyFired = false;
		else if ( !HasAmmo(MinAmmo,true) && ShotPeriodComplete()
			&& (self.AnimState().PlayingID() != anim_weaponSwapOut || TrackTime() < 1) )
		{
			PlayEmptySound();
			// signal that looping fire anims should stop to exit fire state
			if ( IsPlaying(anim_weaponFireLoop) )
				StopAutoFire();
		}

		if ( IsPlaying(anim_weaponSwapOut) && TrackTime() == 0 && !self.Owner().Locked() )
			PlayHolsterSound();
	}

	void UpdatePowerBoost()
	{
		int dt = Player.Actor().GameTicks() - PowerBoostTick;

		if ( IsBoosted(dt) &&
			(dt < 1736 // before and during first beep
			|| (dt >= 1751 && dt < 1768) // during second beep
			|| dt >= 1783) ) // during last beep
		{
			self.RunFxEvent( "PowerBoost" );
			bPowerBoostEffect = true;
		}
		else if ( bPowerBoostEffect )
		{
			self.RunFxEvent( "UnPowerBoost" );
			bPowerBoostEffect = false;
		}

		switch ( dt )
		{
			// (30 - 0.283*i - 0.25*(i-1)) * 60; i=1,2,3, starting from last
			case 1719: if ( PowerBoostBeep > 0 ) return; break;
			case 1751: if ( PowerBoostBeep > 1 ) return; break;
			case 1783: if ( PowerBoostBeep > 2 ) return; break;
			default: return;
		}
		Player.Actor().PlaySound( "sounds/jk/scalarm.ksnd" );
		++PowerBoostBeep;
	}

	//==========================================================================

	void OnSpawn()
	{
	}

	// Smoke39 - check that enough time has passed since last shot
	void OnBeginFire()
	{
		if ( ShotPeriodComplete() && HasAmmo(MinAmmo,true) )
			BeginFire();
		else
			BlockFireState();
	}

	// Smoke39 - actually begin fire sequence
	void BeginFire()
	{
	}

	void OnFire()
	{
		if ( ShotPeriodComplete() )
		{
			if ( self.Owner().Buttons() & BC_ATTACK != 0 && HasAmmo(MinAmmo,true) )
				BeginFire();
			// signal that looping fire anims should stop to exit fire state
			else if ( IsPlaying(anim_weaponFireLoop) )
				StopAutoFire();
		}
	}

	void OnEndFire()
	{
	}

	void OnRaise()
	{
	}

	void OnHoldster()
	{
		bEmptyFired = false;
	}

	void OnLower()
	{
		bEmptyFired = false;
		if ( bPowerBoostEffect )
		{
			self.RunFxEvent( "UnPowerBoost" );
			bPowerBoostEffect = false;
		}
	}
}

//==============================================================================
//
//    Fists
//
//==============================================================================

final class TurokKnife : TurokWeapon
{
	int LastAnim;
	array<kActor@> HitActors;

	TurokKnife( kWeapon@ actor )
	{
		super( actor );
		bIsGun = false;
		jkPreBeginLevel();
	}

	void OnPostBeginLevel()
	{
		jkPostBeginLevel();
	}

	void OnTick()
	{
		UpdatePowerBoost();
		int dt = self.GameTicks() - ShotTick;
		float f;
		switch ( LastAnim )
		{
			// right punch - 2 anim frames of windup, 2 of punch
			case anim_weaponAttack1:
				if ( dt >= 8 && dt < 16 )
				{
					f = float( dt - 7 ) / 8;
					OwnerP().Roll() = Math::Lerp( OwnerP().Roll(), Recoil_Fist, f );
				}
				break;
			// left punch - 1 anim frame of windup, 2 of punch
			case anim_weaponAttack2:
				if ( dt >= 4 && dt < 12 )
				{
					f = float( dt - 3 ) / 8;
					OwnerP().Roll() = Math::Lerp( OwnerP().Roll(), -Recoil_Fist, f );
				}
				break;
		}
	}

	void OnBeginFire()
	{
		LastAnim = LastAnim == anim_weaponAttack1 ? anim_weaponAttack2 : anim_weaponAttack1;
		PlayAnim( LastAnim );
		if ( OwnerP().InWater() )
			self.PlaySound( "sounds/shaders/underwater_swim_2.ksnd" );
		else
		{
			// mix in unused sound randomly, but favor the one actually used in-game in JK
			switch ( Math::RandMax(3) )
			{
				case 0:  self.PlaySound( "sounds/jk/fist_swing2.ksnd" ); break;
				default: self.PlaySound( "sounds/jk/fist_swing1.ksnd" );
			}
		}
		ShotTick = self.GameTicks();
	}

	void MeleeAttack1( kActor@ instigator, const float w, const float x, const float y, const float z )
	{
		float range = w*GAME_SCALE;
		kVec3 eye = EyePos();
		kVec3 origin = eye + kVec3( 0, range, 0 ) * OwnerP().Rotation();
		self.InteractActorsAtPosition( origin, "PunchHit", range, origin.x, origin.y, origin.z );

		if ( HitActors.length() < 1 )
		{
			origin = eye + kVec3( 0, 2*range, 0 ) * OwnerP().Rotation();
			if ( Hitscan( eye, origin ) )
			{
				self.PlaySound( "sounds/jk/fist_wall.ksnd" );
				Game.SpawnFx( "fx/jk/fistx.kfx", OwnerP(), CModel.InterceptVector(), CModel.ContactNormal().ToQuat() );
			}
			return;
		}

		origin = ( origin + eye ) / 2;

		kVec3 bestV;
		float bestDist = 0;
		kActor@ best;
		for ( uint i=0; i<HitActors.length(); ++i )
		{
			kActor@ a = HitActors[i];
			kVec3 v = a.Origin();
			v += ( eye - v ).Normalize() * a.Radius();
			v.z = origin.z;
			if ( v.z > a.Origin().z + a.Height() ) v.z = a.Origin().z + a.Height();
			else if ( v.z < a.Origin().z ) v.z = a.Origin().z;
			float dist = (eye-v).UnitSq();
			if ( best !is null && dist >= bestDist ) continue;
			@best = a;
			bestDist = dist;
			bestV = v;
		}
		// clear the list now that we've chosen the best
		HitActors.resize(0);

		// half damage to metallic enemies
		if ( best.ImpactType() == IT_METAL ) MeleeDamage( best,  4 );
		// allow beetles to be 1-shot on Hard+
		else if ( best.Type() == AT_INSECT ) MeleeDamage( best, 15 );
		else                                 MeleeDamage( best,  8 );

		switch ( best.ImpactType() )
		{
			case IT_DEFAULT: // trees
			case IT_METAL:
				best.PlaySound( "sounds/jk/fist_wall.ksnd" );
				break;
			case IT_FORCEFIELD:
				best.PlaySound( "sounds/jk/shield_damage.ksnd" );
				break;
			default: // body
				best.PlaySound( "sounds/jk/fist_flesh.ksnd" );
		}
		origin = best.Origin();
		origin.z = bestV.z;
		kQuat q = ( bestV - origin ).ToQuat();
		Game.SpawnFx( "fx/jk/fistx.kfx", best, bestV, q );
	}

	void PunchHit( kActor@ a, const float range, const float x, const float y, const float z )
	{
		if ( a is null || a.IsStale() || a.Flags() & AF_HOLDTRIGGERANIM != 0 )
			return;
		if ( a is OwnerA() )
			return;
		if ( !OwnerP().CanSee( a, CF_IGNOREBLOCKERS ) )
			return;
		if ( Math::Sqrt( a.DistanceToPoint( kVec3(x,y,z) ) ) - a.Radius() > range )
			return;
		HitActors.insertLast( a );
	}
}

//==============================================================================
//
//    Bryar Pistol
//
//==============================================================================

final class TurokBow : TurokWeapon
{
	TurokBow( kWeapon@ actor )
	{
		super( actor );
		ShotPeriod = 0.5f;
	}

	void MuzzleFlash()
	{
		kStr fx = "fx/jk/bry_muzzle";
		if ( IsBoosted() ) fx += "_boost";
		else               FlashTime--;
		self.FireProjectile( fx + FlashTime + ".kfx", 0,0,0 );
		self.RunFxEvent( "BryFlash" );
	}

	void BeginFire()
	{
		FlashTime = 2;
		if ( IsBoosted() )
		{
			// flash a little earlier before the muzzle flips over projectile
			if ( IsPlaying(anim_weaponFire) )
				--FlashTime;
			PlayAnim( anim_weaponFire, 3, IsPlaying(anim_weaponFire) ? 1 : 3, ANF_CYCLECOMPLETED );
		}
		else
		{
			// flash a little later to account for longer tween time slowing down the muzzle flip
			if ( IsPlaying(anim_weaponFire) )
				FlashTime += 1;
			PlayAnim( anim_weaponFire, 4, IsPlaying(anim_weaponFire) ? 4 : 2, ANF_CYCLECOMPLETED );
		}
		self.PlaySound( "sounds/jk/bry_shot.ksnd" );
		self.FireProjectile( "fx/jk/bry.kfx", 5, 25, -4 );
		self.Owner().Actor().LoudNoiseAlert();
		self.Owner().Actor().RecoilPitch() = Recoil_Bry;
		self.Owner().ConsumeAmmo( 1 );
		ShotTick = self.GameTicks();
	}
}

//==============================================================================
//
//    DL-44 Pistol
//
//==============================================================================

final class TurokPistol : TurokWeapon
{
	TurokPistol( kWeapon@ actor )
	{
		super( actor );
		ShotPeriod = 0.65f;
	}

	void MuzzleFlash()
	{
		kStr fx = "fx/mots/bls_muzzle";
		if ( IsBoosted() ) fx += "_boost";
		else               FlashTime--;
		self.FireProjectile( fx + FlashTime + ".kfx", 0,0,0 );
		self.RunFxEvent( "BlsFlash" );
	}

	void BeginFire()
	{
		FlashTime = 2;
		if ( IsBoosted() )
		{
			// flash a little earlier before the muzzle flips away from the projectile
			if ( IsPlaying(anim_weaponFire) )
				--FlashTime;
			PlayAnim( anim_weaponFire, 3, IsPlaying(anim_weaponFire) ? 1 : 3, ANF_CYCLECOMPLETED );
		}
		else
		{
			// flash a little later to account for longer tween time slowing down the muzzle flip
			if ( IsPlaying(anim_weaponFire) )
				++FlashTime;
			PlayAnim( anim_weaponFire, 4, IsPlaying(anim_weaponFire) ? 4 : 2, ANF_CYCLECOMPLETED );
		}
		self.PlaySound( "sounds/mots/bls_shot.ksnd" );
		self.FireProjectile( "fx/mots/bls.kfx", 7, 25, -4 );
		self.Owner().Actor().LoudNoiseAlert();
		self.Owner().Actor().RecoilPitch() = Recoil_DL44;
		self.Owner().ConsumeAmmo( 2 );
		ShotTick = self.GameTicks();
	}
}

//==============================================================================
//
//    E-11 Rifle
//
//==============================================================================

final class TurokRifle : TurokWeapon
{
	TurokRifle( kWeapon@ actor )
	{
		super( actor );
		ShotPeriod = 0.2f;
	}

	void MuzzleFlash()
	{
		self.FireProjectile( "fx/jk/str_muzzle.kfx", 0,0,0 );
		self.RunFxEvent( "StrFlash" );
	}

	void BeginFire()
	{
		self.PlaySound( "sounds/jk/str_shot.ksnd" );
		self.FireProjectile( "fx/jk/str.kfx", 6, 25, -6 );
		FlashTime = 0;
		PlayAnim( anim_weaponFire, 4, IsPlaying(anim_weaponFire) ? 0 : 2 );
		self.Owner().Actor().LoudNoiseAlert();
		self.Owner().Actor().RecoilPitch() = Recoil_STR;
		self.Owner().ConsumeAmmo( 2 );
		ShotTick = self.GameTicks();
	}
}

//==============================================================================
//
//    Bowcaster
//
//==============================================================================

// this needs more custom logic than thermal detonator because charge can begin before fire anim finishes, and we don't want to interrupt it with our static "charge" anim

final class TurokShotgun : TurokWeapon
{
	bool bCharging = false;
	bool bBoostCharge; // charge began while power boost was active

	TurokShotgun( kWeapon@ actor )
	{
		super( actor );
		ShotPeriod = 0.6f;
		MinAmmo = 2;
	}

	void PlayHolsterSound()
	{
		TurokWeapon::PlayHolsterSound();
		if ( bCharging )
		{
			self.StopLoopingSounds();
			bCharging = false;
		}
	}

	void BeginCharge()
	{
		if ( bCharging ) return;
		bCharging = true;
		bBoostCharge = IsBoosted();
		ShotTick = self.GameTicks();
	}

	void OnBeginFire()
	{
		if ( !HasAmmo(2,true) )
		{
			BlockFireState();
			return;
		}

		PlayAnim( anim_weaponFire, 4, 4, ANF_LOOP | ANF_CYCLECOMPLETED );
		BeginCharge();
	}

	void OnFire()
	{
		if ( bCharging )
		{
			if ( self.Owner().Buttons() & BC_ATTACK == 0 )
				Fire();
			else if ( bBoostCharge )
				self.PlaySound( "sounds/jk/bow_charge_boosted.ksnd" );
			else
				self.PlaySound( "sounds/jk/bow_charge.ksnd" );
		}
		else if ( IsPlaying(anim_weaponFireCharged) )
		{
			if ( ShotPeriodComplete() && self.Owner().Buttons() & BC_ATTACK != 0 && HasAmmo(2,true) )
			{
				if ( TrackTime() >= 1 )
					OnBeginFire();
				else
					BeginCharge();
			}
		}
	}

	void MuzzleFlash()
	{
		self.FireProjectile( "fx/jk/bow_muzzle.kfx", 0,0,0 );
		self.RunFxEvent( "BowFlash" );
	}

	void Fire()
	{
		self.FireProjectile( "fx/jk/bow1.kfx", 9, 25, -9 );
		// spawning too early causes barrel to be left of flash
		// spawning too late causes barrel to be below flash
		// 2 is the earliest that gives decent horizontal alignment
		FlashTime = 2;
		UseAmmo( 2 );
		if ( bCharging )
		{
			float t = (self.GameTicks()-ShotTick) * BoostFactor() * GAME_DELTA_TIME;
			if ( t >= ShotPeriod && UseAmmo(2) )
				self.FireProjectile( "fx/jk/bow2.kfx", 9, 25, -9 );
			if ( t >= 2*ShotPeriod && UseAmmo(2) )
				self.FireProjectile( "fx/jk/bow3.kfx", 9, 25, -9 );
			bCharging = false;
		}
		self.StopLoopingSounds();
		self.PlaySound( "sounds/jk/bow_shot.ksnd" );
		PlayAnim( anim_weaponFireCharged, 4, 2, ANF_CYCLECOMPLETED );
		self.Owner().Actor().LoudNoiseAlert();
		self.Owner().Actor().RecoilPitch() = Recoil_Bow;
		ShotTick = self.GameTicks();
	}
}

//==============================================================================
//
//    Sniper Rifle
//
//==============================================================================

final class TurokAutoShotgun : TurokWeapon
{
	TurokAutoShotgun( kWeapon@ actor )
	{
		super( actor );
		ShotPeriod = 0.9f;
		MinAmmo = 3;
	}

	void MuzzleFlash()
	{
		self.FireProjectile( "fx/jk/rep_muzzle.kfx", 0,0,0 );
		self.RunFxEvent( "BlsFlash" );
	}

	void BeginFire()
	{
		PlayAnim( anim_weaponFire, 4, 2 );
		self.PlaySound( "sounds/jk/rep_shot.ksnd" );
		self.FireProjectile( "fx/jk/rep.kfx", 3.5f, 25, -3 );
		FlashTime = 0;
		self.Owner().Actor().LoudNoiseAlert();
		self.Owner().Actor().RecoilPitch() = Recoil_Sniper;
		UseAmmo( 3 );
		ShotTick = self.GameTicks();
	}
}

//==============================================================================
//
//    Repeater
//
//==============================================================================

final class TurokMinigun : TurokWeapon
{
	float RecoilPitch, RecoilRoll;

	TurokMinigun( kWeapon@ actor )
	{
		super( actor );
		ShotPeriod = 0.11f;
	}

	void OnTick()
	{
		TurokWeapon::OnTick();
		float f = (self.GameTicks() - ShotTick) * GAME_DELTA_TIME;
		if ( f >= ShotPeriod ) return;
		f = 1 - (f + GAME_DELTA_TIME) / (ShotPeriod + GAME_DELTA_TIME);
		f = Math::Sin( Math::pi * f*f );
		self.Owner().Actor().RecoilPitch() = f * RecoilPitch;
		self.Owner().Actor().Roll()        = f * RecoilRoll;
	}

	// projectile is hidden for first frame of existence
	// this allows us to cheat and spawn it with slightly less offset (not by much, but I'll take what I can get)
	// it also hides how it separates from the barrel as the gun moves when looking around
	// however, relying solely on the muzzle flash feels ever so slightly "light" and out of sync
	// so we spawn an un-delayed muzzle flash that mimics the projectile, which sticks to the barrel more closely than the actual projectile

	void MuzzleFlash()
	{
		self.FireProjectile( "fx/jk/rpt_muzzle2.kfx", 0,0,0 );
		self.RunFxEvent( "RptFlash" );
	}

	void BeginFire()
	{
		self.FireProjectile( "fx/jk/rpt.kfx", 6, 25, -9 );
		self.FireProjectile( "fx/jk/rpt_muzzle1.kfx", 0,0,0 );
		self.PlaySound( "sounds/jk/rpt_shot.ksnd" );
		FlashTime = 0;
		if ( !IsPlaying(anim_weaponFireLoop) )
			PlayAnim( anim_weaponFireLoop, 4, 2, ANF_LOOP );
		self.Owner().Actor().LoudNoiseAlert();
		self.Owner().ConsumeAmmo( 1 );
		ShotTick = self.GameTicks();

		RecoilPitch = Math::RandRange( 0.5f, 1.0f ) * Recoil_Rpt;
		float r     = Math::RandRange( 0.5f, 1.0f ) * Recoil_Rpt/2;
		if ( RecoilRoll < 0 )
			r = -r;
		RecoilRoll = r;
	}
}

//==============================================================================
//
//    Thermal Detonator
//
//==============================================================================

final class TurokGrenadeLauncher : TurokWeapon
{
	float tapDelay = 0.1f;

	TurokGrenadeLauncher( kWeapon@ actor )
	{
		super( actor );
		ShotPeriod = 0.8f;
		bIsGun = false;
	}

	// whiz through part of swap-in that's off-screen
	void SwapSpeedup()
	{
		switch ( self.AnimState().PlayingID() )
		{
			case anim_weaponSwapIn:
				ChangeSpeed( TrackTime() <= 0.5f ? 2 : 3 );
		}
	}

	void OnTick()
	{
		SwapSpeedup();
		UpdatePowerBoost();

		// hide when out of ammo
		if ( HasAmmo() )
			self.Flags() &= ~AF_NODRAW;
		else if ( !IsPlaying(anim_weaponFireCharged) )
		{
			self.Flags() |=  AF_NODRAW;
			// skip invisible swap-out anim so we don't feel unresponsive
			if ( IsPlaying(anim_weaponSwapOut) )
				self.AnimState().SetLastFrame();
		}

		// camera shake
		int dt = self.GameTicks() - ShotTick;
		if ( dt < 8 )
		{
			kPuppet@ owner = self.Owner().Actor();
			float f = float( dt + 1 ) / 8;
			if ( World.GetAreaFlags( owner.AreaID() ) & (AAF_CRAWL|AAF_ENTERCRAWL) == 0 )
			{
				float h;
				OwnerP().Definition().GetFloat( "player.viewHeight", h, 51.2f );
				OwnerP().ViewHeight() = Math::Lerp( OwnerP().ViewHeight(), h - GAME_SCALE/2, f );
				// look up very slightly to compensate for lowered viewpoint
				owner.RecoilPitch() = Math::Lerp( owner.RecoilPitch(), -0.0075f, f );
			}
			owner.Roll() = Math::Lerp( owner.Roll(), Recoil_Det, f );
		}
	}

	void BeginFire()
	{
		PlayAnim( anim_weaponFire, 4, 4, ANF_LOOP );
		self.PlaySound( "sounds/jk/det_pre.ksnd" );
	}

	float ChargePct( bool bCap = true )
	{
		float pct = (PlayTime() - tapDelay) / (1 - tapDelay);
		if ( pct < 0 ) pct = 0;
		if ( bCap && pct > 1 ) pct = 1;
		return pct;
	}

	// don't try to re-prime a grenade that we're already charging
	void OnFire()
	{
		if ( IsPlaying(anim_weaponFire) )
		{
			float pct = ChargePct( false );
			// allow to fade one tick after full bar is displayed
			if ( pct >= 1 + GAME_DELTA_TIME / (1-tapDelay) ) return;
			if ( pct > 1 ) pct = 1;
			kStr bar = 'I';
			int segments = int( 5 * pct );
			int i;
			for ( i=0; i<segments; ++i )
				bar = bar + '-';
			while ( i++ < 5 )
				bar = bar + ' ';
			bar = bar + 'I';
			Game.PrintLine( bar, 0, 2 );
		}
	}

	void OnEndFire()
	{
		float pct = ChargePct();
		pct = Math::Lerp( 0.3f, 1.0f, pct );

		kPuppet@ owner = self.Owner().Actor();
		kVec3 vel = kVec3( -0.003f * (4-3*pct), 1, 0 ).Normalize() * owner.Rotation();
		vel.z += 0.15f;
		vel.Normalize();
//		vel *= 2400 * pct * GAME_DELTA_TIME;
		vel *= 2000 * pct * GAME_DELTA_TIME;

		kVec3 start = EyePos() + kVec3( 10, 0, 0 ) * owner.Rotation();

		Game.SpawnFx( "fx/jk/det.kfx", owner, vel, start, owner.Rotation() );

		PlayAnim( anim_weaponFireCharged );
		self.PlaySound( "sounds/jk/det_throw.ksnd" );

		self.Owner().ConsumeAmmo( 1 );
		ShotTick = self.GameTicks();
	}
}

//==============================================================================
//
//    Sequencer Charges
//
//==============================================================================

final class TurokAlienRifle : TurokWeapon
{
	TurokAlienRifle( kWeapon @actor )
	{
		super( actor );
		ShotPeriod = 0.8f;
		bIsGun = false;
	}

	void OnTick()
	{
		TurokWeapon::OnTick();

		// hide when out of ammo
		if ( HasAmmo() )
		{
			self.Flags() &= ~AF_NODRAW;
			// stay offscreen for a bit longer after deploying mine, like we're grabbing another
			if ( IsPlaying(anim_weaponFire) && TrackTime() == 1 )
				ChangeSpeed( 16 );
		}
		else if ( !IsPlaying(anim_weaponFire) )
		{
			self.Flags() |=  AF_NODRAW;
			// skip invisible swap-out anim so we don't feel unresponsive
			if ( IsPlaying(anim_weaponSwapOut) )
				self.AnimState().SetLastFrame();
		}
	}

	void BeginFire()
	{
		PlayAnim( anim_weaponFire, 3 );
		ActorFactory.Spawn( "SequencerCharge", OwnerP().Origin().x, OwnerP().Origin().y, OwnerP().Origin().z, OwnerP().Yaw()+Math::pi, OwnerP().SectorIndex() );
		UseAmmo( 1 );
		ShotTick = self.GameTicks();
	}
}

enum SeqState
{
	seq_Falling,
	seq_Armed,
	seq_Triggered,
	seq_Taken
}

class SequencerCharge
{
	kActor@ self;
	int state = seq_Falling;
	int eventTick;
	bool bWasInWater = false;
	float yaw;

	SequencerCharge( kActor@ A )
	{
		@self = A;
	}

	void OnSpawn()
	{
		self.Flags() |= AF_SNAPTOFLOOR; // stick to movers
		yaw = self.Yaw();
	}

	bool InWater()
	{
		return self.InWater() || ( World.GetAreaFlags(self.AreaID()) & AAF_SHALLOWWATER != 0 && self.OnGround() );
	}

	void OnTick()
	{
		// prevent pickup spin
		self.Yaw() = yaw;
		// cycle lights
		self.ModelVariation() = (self.GameTicks()/15) % 2;
		// allow being picked once player steps away
		if ( self.Flags() & AF_CANBETOUCHED == 0 )
		{
			kVec3 dif = self.Origin() - Player.Actor().Origin();
			if ( dif.z < -1 || dif.z > Player.Actor().Height()+1
			|| (dif*kVec3(1,1,0)).Unit() > Player.Actor().Radius()+1 )
				self.Flags() |= AF_CANBETOUCHED;
		}
		switch ( state )
		{
			case seq_Falling:
				if ( InWater() )
				{
					if ( !bWasInWater )
					{
						self.SpawnFx( "fx/generic_96.kfx", Math::vecZero );
						self.PlaySound( "sounds/jk/water_small.ksnd" );
					}
					self.Velocity() *= 0.9f;
					bWasInWater = true;
				}
				else
					bWasInWater = false;

				self.Flags() &= ~AF_CANBETOUCHED; // prevent being picked up
				if ( self.OnGround() )
				{
					self.PlaySound( "sounds/jk/seq_arm.ksnd" );
					state = seq_Armed;
					eventTick = self.GameTicks();
				}
			break;
			case seq_Armed:
				if ( (self.GameTicks() - eventTick) * GAME_DELTA_TIME < 0.65f )
					return;
				self.InteractActorsAtPosition( self.Origin(), "ActorDetected" );
				if ( state == seq_Taken )
					self.Remove();
				else if ( state == seq_Triggered )
					self.PlaySound( "sounds/jk/seq_trig.ksnd" );
			break;
			case seq_Triggered:
				if ( (self.GameTicks() - eventTick) * GAME_DELTA_TIME < 0.3f )
					return;
				if ( InWater() )
				{
					self.SpawnFx( "fx/generic_93.kfx", kVec3( 0, 0, GAME_SCALE ) );
					self.PlaySound( "sounds/shaders/explosionWater.ksnd" );
				}
				else
				{
					self.SpawnFx( "fx/jk/seqx.kfx", kVec3( 0, 0, GAME_SCALE ) );
					self.PlaySound( "sounds/jk/rld_exp.ksnd" );
				}
				self.SpawnFx( "fx/jk/seqx_damage.kfx", kVec3( 0, 0, GAME_SCALE ) );
				self.Remove();
		}
	}

	void ActorDetected( kActor@ A, const float f1, const float f2, const float f3, const float f4 )
	{
		// skip if not an enemy
		if ( !A.InstanceOf("kexAI") )
		{
			if ( A.ScriptObject() is null || cast<TurokEnemy@>( A.ScriptObject().obj ) is null )
				return;
		}
		// skip if respawning, or non-thinking (hanging bodies on level 8)
		else if ( A.CastToAI().AIFlags() & ((1<<8)|AIF_NOTHINK) != 0 ) return;
		// skip if dead or waiting to ambush/come alive
		if ( A.Flags() & (AF_DEAD|AF_DISABLED|AF_HOLDTRIGGERANIM) != 0 ) return;
		// skip if obstructed
		if ( !self.CanSee( A, CF_IGNOREBLOCKERS ) ) return;

		kVec3 dif = A.Origin() - self.Origin();
		// 9.4 <= * < 9.5
		dif.z += A.Height()/2;
		if ( dif.Unit() - A.Radius() <= 30*GAME_SCALE/2 )
		{
			state = seq_Triggered;
			eventTick = self.GameTicks();
		}
	}
}

//==============================================================================
//
//    Rail Detonator
//
//==============================================================================

final class TurokRocketLauncher : TurokWeapon
{
	TurokRocketLauncher( kWeapon@ actor )
	{
		super( actor );
		ShotPeriod = 1;
	}

	void BeginFire()
	{
		PlayAnim( anim_weaponFire, 4, 4, ANF_CYCLECOMPLETED );
		self.PlaySound( "sounds/jk/rld_shot.ksnd" );

		self.FireProjectile( "fx/jk/rld.kfx", 9, 50, -8 );

		self.Owner().Actor().LoudNoiseAlert();
		self.Owner().Actor().RecoilPitch() = Recoil_Rld;
		self.Owner().ConsumeAmmo( 1 );
		ShotTick = self.GameTicks();
	}
}

//==============================================================================
//
//    Concussion Rifle
//
//==============================================================================

final class TurokAccelerator : TurokWeapon
{
	TurokAccelerator( kWeapon@ actor )
	{
		super( actor );
		ShotPeriod = 1.2f;
		MinAmmo = 8;
	}

	void MuzzleFlash()
	{
		self.FireProjectile( "fx/jk/conc_muzzle.kfx", 0,0,0 );
		self.RunFxEvent( "ConcFlash" );
	}

	void BeginFire()
	{
		PlayAnim( anim_weaponFire, 4, 2, ANF_CYCLECOMPLETED );
		self.PlaySound( "sounds/jk/conc_shot.ksnd" );
		self.FireProjectile( "fx/jk/conc.kfx", 5, 25, -8 );
		FlashTime = 0;
		self.Owner().Actor().LoudNoiseAlert();
		self.Owner().Actor().RecoilPitch() = Recoil_Conc;
		UseAmmo( 8 );
		ShotTick = self.GameTicks();
	}
}

//==============================================================================
//
//    Carbonite Gun
//
//==============================================================================

final class TurokFusionCannon : TurokWeapon
{
	int LastScaledTick = -1, ScaledTickCount = 0;
	float TimeScale = 1;
	kVec3 LastLoc, LastDir;
	bool bWasBoosted;

	TurokFusionCannon( kWeapon @actor )
	{
		super( actor );
		ShotPeriod = 0;
	}

	void OnTick()
	{
		TurokWeapon::OnTick();
		if ( LastScaledTick == PlayLoop.Ticks() )
			++ScaledTickCount;
		else
		{
			TimeScale = 2.0f / (1.0f + ScaledTickCount);
			ScaledTickCount = 0;
			LastScaledTick = PlayLoop.Ticks();
		}
	}

	// don't speed up swap-in, slow down swap-out to match
	void SwapSpeedup()
	{
		switch ( self.AnimState().PlayingID() )
		{
			case anim_weaponSwapIn:
			case anim_weaponSwapOut:
				ChangeSpeed( 4 );
		}
	}

	float FOVOffset( const array<float>&in offsets )
	{
		float FOV = 74;
		kStr str;
		if ( Sys.GetCvarValue( "r_fov", str ) )
			FOV = str.Atof();
		if ( FOV <= 74 )
			return Math::Lerp( offsets[0], offsets[1], (FOV-47.5f)/(74.0f-47.5f) );
		else if ( FOV <= 90 )
			return Math::Lerp( offsets[1], offsets[2], (FOV-74.0f)/(90.0f-74.0f) );
		else if ( FOV <= 110 )
			return Math::Lerp( offsets[2], offsets[3], (FOV-90.0f)/(110.0f-90.0f) );
		else
			return Math::Lerp( offsets[3], offsets[4], (FOV-110.0f)/(120.0f-110.0f) );
	}

	void OnBeginFire()
	{
		bool bWasBoosted = IsBoosted();
		TurokWeapon::OnBeginFire();
	}

	void BeginFire()
	{
		kVec3 offset( 5.75f, FOVOffset( array<float> = { 43, 25, 19, 13.25f, 11 } ), -7.5f );
		self.FireProjectile( "fx/mots/cbn.kfx", offset.x, offset.y, offset.z );

		kVec3 newLoc = EyePos() + offset * OwnerP().Rotation();
		kVec3 newDir = kVec3(0,1,0) * OwnerP().Rotation();

		bool bBoost = IsBoosted();
		if ( !IsPlaying(anim_weaponFireLoop) )
			PlayAnim( anim_weaponFireLoop, 4, 2, ANF_LOOP );
		// spawn cosmetic filler particles near the muzzle
		else if ( OwnerP().CheckPosition( newLoc ) )
		{
			// need fewer in slomo
			int num = (TimeScale < 1 || bBoost) ? 1 : 2;
			for ( int i=0; i<num; ++i )
			{
				float f = float(i+1) / (num+1);
				// skew a bit toward the muzzle
				f = Math::Pow( f, num == 2 ? 1.5f : 1.25f );
				kVec3 dir = LastDir;
				dir.Lerp( newDir, f );
				dir.Randomize( 0.075f );
				kQuat rot = dir.ToQuat();
				kVec3 loc = newLoc;
				loc.Lerp( LastLoc, f );
				loc += kVec3( 0, 1000*GAME_DELTA_TIME*TimeScale*f, 0 ) * rot;
				kStr fx = "fx/mots/cbn";
				if      ( bBoost )   fx += "_boosted";
				else if ( num == 1 ) fx += '3';
				else                 fx = fx + (i+1);
				Game.SpawnFx( fx + ".kfx", OwnerP(), loc, rot );
			}
		}

		LastLoc = newLoc;
		LastDir = newDir;

		if ( bBoost != bWasBoosted )
			self.StopLoopingSounds();
		if ( bBoost ) self.PlaySound( "sounds/mots/cbn_shot_boosted.ksnd" );
		else          self.PlaySound( "sounds/mots/cbn_shot.ksnd" );
		bWasBoosted = bBoost;

		// 100a/10s * 1s/60t = 1a/6t
		if ( self.GameTicks() % 6 == 0 )
			self.Owner().ConsumeAmmo( 1 );
		ShotTick = self.GameTicks();
	}

	void StopAutoFire()
	{
		PlayAnim( anim_weaponIdle );
		self.StopLoopingSounds();
	}
}

//==============================================================================
//
//    Lightsaber
//
//==============================================================================

enum eSaberColor
{
	jk_Saber_Green,
	jk_Saber_Yellow,
	jk_Saber_Orange,
	jk_Saber_DarkRed,
	jk_Saber_Red,
	jk_Saber_Purple,
	jk_Saber_Blue,
	jk_Saber_COUNT
}

int SaberColor = jk_Saber_Green;

void SetSaberColor( int color )
{
	SaberColor = color;
	if ( GameVariables.HasKey("jkSaberColor") )
		GameVariables.SetValue( "jkSaberColor", ""+ color );
	else
		GameVariables.Add( "jkSaberColor", ""+ color );
}

void sabercolor()
{
	SetSaberColor( (SaberColor+1) % jk_Saber_COUNT );
}

final class TurokChrono : TurokWeapon
{
	int LastAnim;
	bool bOn = false;

	TurokChrono( kWeapon@ actor )
	{
		super( actor );
		bIsGun = false;
	}

	// saber extension anim event
	void TriggerEvent( kActor@ a, const float w, const float x, const float y, const float z )
	{
		self.PlaySound( "sounds/jk/sab_on.ksnd" );
		self.RenderModel().SetVisibility( 7, true );
		bOn = true;
	}

	// make sure saber is considered on when entering a level with saber already out
	void OnPostBeginLevel()
	{
		if ( self.Owner().CurrentWeapon() == jk_Weapon_Lightsaber )
		{
			self.RenderModel().SetVisibility( 7, true );
			bOn = true;
		}
	}

	void PlayHolsterSound()
	{
		if ( bOn )
		{
			self.StopLoopingSounds();
			self.PlaySound( "sounds/jk/sab_off.ksnd" );
			bOn = false;
		}
		// if it hasn't been turned on yet, hide the blade so it doesn't tween into view for swap-out anim
		else
			self.RenderModel().SetVisibility( 7, false );
	}

	// don't speed up swap-in
	void SwapSpeedup()
	{
	}

	// prevent standard auto fire code from affecting one of our saber attack anims
	void StopAutoFire()
	{
	}

	void OnTick()
	{
		TurokWeapon::OnTick();
		// resume hum in case game was paused
		if ( bOn && !self.Owner().Locked() )
			self.PlaySound( "sounds/jk/sab_hum.ksnd" );
		// blade color and fizzle anim
		self.ModelVariation() = 10*SaberColor + (self.GameTicks() % 10);
	}

	int PickAttackAnim()
	{
		int threshold = 6;
		switch ( LastAnim )
		{
			case anim_weaponAttack1: // 8 frames
			case anim_weaponFire: // 8 frames
				threshold += 32;
				break;
			case anim_weaponAttack2: // 7 frames (5 frames slowed down)
			case anim_weaponFireCharged: // 7 frames
				threshold += 28;
				break;
			case anim_weaponAttack3: // 11 frames
				threshold += 44;
				break;
			case anim_weaponFireLoop: // 11 frames, long recovery at the end
				threshold = 44;
				break;
		}
		// it's been a while since last attack - start combo over
		if ( self.GameTicks() - ShotTick >= threshold )
			return anim_weaponAttack1;

		switch ( LastAnim )
		{
			// right to left anim - play left to right anim
			case anim_weaponAttack1:
			case anim_weaponAttack2:
			case anim_weaponAttack3:
				switch ( Math::RandMax( 3 ) )
				{
					case 0: return anim_weaponFire;
					case 1: return anim_weaponFireCharged;
					case 2: return anim_weaponFireLoop;
				}
			// left to right anim - play right to left anim
			case anim_weaponFire:
			case anim_weaponFireCharged:
			case anim_weaponFireLoop:
				return anim_weaponAttack1 + Math::RandMax( 3 );
		}
		return anim_weaponAttack1;
	}

	void OnBeginFire()
	{
		LastAnim = PickAttackAnim();
		// slow 5 frames down to match left version's 7 frames
		if ( LastAnim == anim_weaponAttack2 )
			PlayAnim( LastAnim, 4.0f*7/5 + 0.4f );
		else
			PlayAnim( LastAnim );
		kStr snd = "sounds/jk/sab_swing";
		self.PlaySound( snd + (1 + Math::RandMax(8)) + ".ksnd" );
		ShotTick = self.GameTicks();
	}

	void KnifeAttack1( kActor@ instigator, const float w, const float x, const float y, const float z )
	{
		float range = w*GAME_SCALE;
		kVec3 v = EyePos() + kVec3( 0, range, 0 ) * self.Owner().Actor().Rotation();
		self.InteractActorsAtPosition( v, "SaberHit", range, v.x, v.y, v.z );
	}

	void SaberHit( kActor@ a, const float range, const float x, const float y, const float z )
	{
		if ( a is null || a.IsStale() || a.Flags() & AF_HOLDTRIGGERANIM != 0 )
			return;
		if ( a is OwnerA() )
			return;
		if ( !OwnerP().CanSee( a, CF_IGNOREBLOCKERS ) )
			return;
		kVec3 origin( x, y, z );
		if ( Math::Sqrt( a.DistanceToPoint( origin ) ) - a.Radius() > range )
			return;

		// anim_weaponAttack2 is 5 frames, but slowed to match left version's 7
		int n = IsPlaying(anim_weaponAttack2) ? 7 : self.AnimState().NumFrames();
		// 15, 18, 27 - just enough for slow + any other to kill warriors on hard+
		int dam = 15 + 3 * (n-7);
		// allow beetles to always be 1-shot on Hard+
		if ( a.Type() == AT_INSECT && dam < 15 )
			dam = 15;
		MeleeDamage( a, dam );

		kVec3 eye = EyePos();
		origin = ( origin + eye ) / 2;
		kVec3 v = a.Origin();
		v += ( eye - v ).Normalize() * a.Radius();
		v.z = origin.z;
		if ( v.z > a.Origin().z + a.Height() ) v.z = a.Origin().z + a.Height();
		else if ( v.z < a.Origin().z ) v.z = a.Origin().z;
//		kQuat q = ( eye - v ).ToQuat();
		origin = a.Origin();
		origin.z = v.z;
		kQuat q = ( v - origin ).ToQuat();

		kStr snd;
		switch ( a.ImpactType() )
		{
			case IT_FORCEFIELD:
				a.PlaySound( "sounds/jk/sab_ff.ksnd" );
				Game.SpawnFx( "fx/jk/sab_ff.kfx", a, v, q );
				break;
			case IT_DEFAULT: // trees
				snd = "sounds/jk/sab_wall";
				a.PlaySound( snd + (1+Math::RandMax(4)) + ".ksnd" );
				Game.SpawnFx( "fx/jk/sab_wall.kfx", a, v, q );
				break;
			default: // body
				a.PlaySound( "sounds/jk/sab_body.ksnd" );
				Game.SpawnFx( "fx/jk/sab_body.kfx", a, v, q );
		}
	}
}

//==============================================================================
//
//    Turok+ compatibility
//

kActor@ WeaponSound;
void UpdateWeaponSound()
{
	if ( WeaponSound !is null && WeaponSound.IsStale() )
		@WeaponSound = null;
}

void RechargeFlareGun( float scale=1.0f )
{
}

bool PlayerScoped()
{
	return false;
}

void CancelScope()
{
}

int MeleeWeaponHand;
enum EHandedness {
	HAND_Left,
	HAND_Right
}

//==============================================================================
//
//    Turok Leftovers
//

final class TurokPulseRifle : TurokWeapon
{
	float pulseRifleFireTime;

	TurokPulseRifle(kWeapon @actor)
	{
		pulseRifleFireTime = 0;
		super(actor);
	}

	void OnBeginFire(void)
	{
		self.AnimState().Blend(anim_weaponFireLoop, 4.0f, 4.0f, ANF_LOOP);
		pulseRifleFireTime = -0.08f;
	}

	void OnFire(void)
	{
		float playTime = self.AnimState().PlayTime();

		if((playTime - pulseRifleFireTime) >= 0.16f)
		{
			self.PlaySound("sounds/shaders/machine_gun_shot_2.ksnd");
			self.FireProjectile("fx/longhunter_gun_pulse.kfx", 10.24f, -40.96f, -9.216f);
			self.Owner().Actor().LoudNoiseAlert();
			self.Owner().Actor().RecoilPitch() = -0.01963495463132858f;
			self.Owner().ConsumeAmmo(1);

			pulseRifleFireTime = playTime;
		}
	}

	void OnEndFire(void)
	{
		self.AnimState().Blend(anim_weaponIdle, 4.0f, 4.0f, ANF_LOOP);
		pulseRifleFireTime = 0;
	}
}
