//
// 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/Q2/Q2_Init.txt"
#include "scripts/Q2/Q2_Gibs.txt"
#include "scripts/Q2/Q2_Proj.txt"
#include "scripts/Q2/Q2_Quad.txt"
#include "scripts/Q2/Q2_Misc.txt"
#include "scripts/Q2/q2debug.txt"

enum eQ2Weapons
{
	Q2_Wep_Blaster      = TW_WEAPON_PISTOL,
	Q2_Wep_Shotgun      = TW_WEAPON_SHOTGUN,
	Q2_Wep_SShotgun     = TW_WEAPON_ASHOTGUN,
	Q2_Wep_MachineGun   = TW_WEAPON_RIFLE,
	Q2_Wep_Chaingun     = TW_WEAPON_MINIGUN,
	Q2_Wep_GLauncher    = TW_WEAPON_GRENADE,
	Q2_Wep_RLauncher    = TW_WEAPON_MISSILE,
	Q2_Wep_HyperBlaster = TW_WEAPON_PULSERIFLE,
	Q2_Wep_Railgun      = TW_WEAPON_ACCELERATOR,
	Q2_Wep_BFG          = TW_WEAPON_CANNON,
	Q2_Wep_HandGrenade  = TW_WEAPON_CHRONO
}

float WeaponBobAmp = 0;

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

class TurokWeapon : ScriptObjectWeapon
{
	kWeapon@ self;
	bool bFlash = false;
	int IdleTick = -1;

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

	~TurokWeapon()
	{
	}

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

	void AnimProperties( int&out first, int&out last, bool&out bLoop )
	{
		first = 0; last = 0; bLoop = false;
	}

	void MuzzleFlash()
	{
	}

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

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

	bool FirePressed()
	{
		return self.Owner().Buttons() & BC_ATTACK != 0;
	}

	bool OwnerDying()
	{
		// dead or falling
		return OwnerP().Flags() & AF_DEAD != 0 || OwnerP().PlayerFlags() & (1<<12) != 0;
	}

	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( amt, true ) )
			return false;
		self.Owner().ConsumeAmmo( amt );
		// normally when ammo reaches 0, weapon switches automatically
		// so trigger a switch if ammo drops below min to fire
		HasAmmo( amt, true );
		return true;
	}
	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 );
		}
	}

	void BlockFireState()
	{
		// idle anim prevents entering fire state
		if ( !IsPlaying(anim_weaponIdle) )
			self.AnimState().Set( anim_weaponIdle, 4, ANF_LOOP );
	}

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

	// check position from eye, rather than from foot
	kVec3 CheckPosition( kVec3 v )
	{
		float r = OwnerP().Radius();
		float h = OwnerP().Height();
		OwnerP().Radius() = OwnerP().Height() = 0;
		kVec3 origin = OwnerP().Origin();
		OwnerP().Origin().z = v.z;
		if ( !OwnerP().CheckPosition(v) )
			v = CModel.InterceptVector();
		OwnerP().Origin() = origin;
		OwnerP().Radius() = r;
		OwnerP().Height() = h;
		return v;
	}

	void SpawnProj( const kStr&in type, kVec3&in offset, const kQuat&in q )
	{
		kVec3 v = CheckPosition( EyePos() + offset * OwnerP().Rotation() );
		Game.SpawnFx( type, OwnerP(), v, q );
	}
	void SpawnProj( const kStr&in type, kVec3&in offset, const kVec3&in dir )
	{
		SpawnProj( type, offset, ToQuat(dir) );
	}

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

	void Animate()
	{
		self.AnimState().flags |= ANF_LOOP;
		self.AnimState().flags &= ~ANF_CYCLECOMPLETED;
		int first, last, v;
		bool bLoop;

		AnimProperties( first, last, bLoop );

		switch ( PlayingID() )
		{
			// prevent idle anim from restarting when moving/stopping with generic weapon bob turned off
			case anim_weaponWalk: case anim_weaponRun: case anim_weaponIdle:
				if ( IdleTick == -1 ) IdleTick = self.GameTicks();
				v = first + self.GameTicks() - IdleTick;
				break;
			default:
				IdleTick = -1;
				v = first + int( PlayTime() * 60 + 0.5f );
		}
		if ( bLoop )
			v = first + ( (v-first) % (last-first+1) );
		else if ( v >= last )
		{
			v = last;
			AnimEnd();
		}
		self.ModelVariation() = v;
		RunAnim();
	}
	void AnimEnd()
	{
		self.AnimState().flags |= ANF_CYCLECOMPLETED | ANF_STOPPED;
	}

	float FakeRoot( float f, float r )
	{
		f -= 1;
		return 1 - Math::Pow( -f, r );
	}
	void RunAnim()
	{
		kStr generic;
		if ( Sys.GetCvarValue( "g_weaponbobbing", generic ) && generic.Atoi() == 1 )
		{
			self.RenderModel().Offset() = Math::vecZero;
			self.RenderModel().SetRotationOffset( 0, 0, 0,0,1 );
			return;
		}
		float t = self.GameTicks() / 9.0f;
		float amp = 0;
		if ( OwnerP().OnGround() && !OwnerP().InWater() )
		{
			amp = ( OwnerP().Movement() * kVec3(1,1,0) ).Unit();
			float max, fric;
			OwnerP().Definition().GetFloat( "player.groundForwardSpeed", max, 12.8f );
			OwnerP().Definition().GetFloat( "friction", fric, 0.5f );
			max *= fric;
			if ( amp > max ) amp = max;
		}
		WeaponBobAmp = Math::Lerp( WeaponBobAmp, amp, 0.1f );
//		self.RenderModel().Offset().x = -( self.RenderModel().Offset().y = amp * Math::Sin(t) );
//		self.RenderModel().Offset().z = amp/2 * Math::Cos(2*t);
		amp = WeaponBobAmp * 1.25f;
		self.RenderModel().SetRotationOffset( 0, amp/200 * Math::Sin( t ), 0,1,1 );
		self.RenderModel().Offset().z = amp/3 * Math::Cos(2*t);
/*		float f = Math::Sin( t );
		f = FakeRoot( Math::Fabs(f), 1.25f ) * (f > 0 ? 1.0f : -1.0f);
		self.RenderModel().SetRotationOffset( 0, amp/200.0f * f, 0,1,1 );
		f = Math::Cos( 2*t );
		f = FakeRoot( Math::Fabs(f), 1.25f ) * (f > 0 ? 1.0f : -1.0f);
		self.RenderModel().Offset().z = amp/3.0f * f;
*/	}

	void OnTick()
	{
		// prevent being given 3 grenades with "chronoscepter" message
		// if you collect all 8 chrono piece stand-ins without T+ installed
		GameVariables.SetValue( "chronoPieceFlags", "0" );
		Animate();
		QuadTick( self );
		UpdateWeaponSound();
		if ( bFlash )
		{
			MuzzleFlash();
			bFlash = false;
		}
	}

	void OnBeginFire() {}
	void OnFire() {}
	void OnEndFire() {}

	void OnRaise() {}
	void OnLower() {}
	void OnHoldster() {}
}

//==============================================================================
//
//    Blaster
//
//==============================================================================

final class TurokPistol : TurokWeapon
{
	TurokPistol( kWeapon@ a )
	{
		super( a );

		bool bNewGame;
		if ( GameVariables.GetBool("g_newgame",bNewGame) )
			Q2_NewGame();
		Q2_PreBeginLevel();
	}

	void OnPostBeginLevel()
	{
		Q2_PostBeginLevel();
	}

	void OnEndLevel()
	{
		Q2_EndLevel();
	}

	void AnimProperties( int&out first, int&out last, bool&out bLoop )
	{
		switch ( PlayingID() )
		{
//			case anim_weaponSwapIn:  first =  0; last =  4; bLoop = false; break;
//			case anim_weaponSwapOut: first = 53; last = 55; bLoop = false; break;
//			case anim_weaponFire:    first =  5; last =  8; bLoop = false; break;
//			default: /* idle */      first =  9; last = 52; bLoop =  true; break;
			case anim_weaponSwapIn:  first =   0; last =  24; bLoop = false; break;
			case anim_weaponSwapOut: first = 415; last = 430; bLoop = false; break;
			case anim_weaponFire:    first =  25; last =  56; bLoop = false; break;
			default: /* idle */      first =  57; last = 415; bLoop =  true; break;
		}
	}

	void MuzzleFlash()
	{
		self.FireProjectile( "fx/Blaster_Flash.kfx", 0,0,0 );
		self.RunFxEvent( "BlasterFlash" );
	}

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

		if ( IsPlaying( anim_weaponFire ) && PlayTime() < 0.1f )
			self.Owner().Actor().RecoilPitch() = Math::Lerp( self.Owner().Actor().RecoilPitch(), -0.025f, 0.25f );
//			self.Owner().Actor().RecoilPitch() -= 0.005f;
	}

	void OnBeginFire()
	{
		kStr proj = "fx/Blaster_Proj";
		if ( QuadDamage() ) proj += "_Quad";
		if ( OwnerP().InWater() && OwnerP().Pitch() < Math::Deg2Rad(-20) ) proj += "_Up";
		proj += ".kfx";
		self.FireProjectile( proj, 5, FOVOffset( array<float>={ 45, 25.6f, 19.2f, 13.5f, 11 } ), -4 );
		self.PlaySound( "sounds/wep/Blaster/Fire.ksnd" );
		bFlash = true;
		self.AnimState().Set( anim_weaponFire, 4.0f, 0 );
		self.Owner().Actor().LoudNoiseAlert();
	}
}

//==============================================================================
//
//    Shotgun
//
//==============================================================================

final class TurokShotgun : TurokWeapon
{
	bool bReload=false, bEject=false;

	TurokShotgun( kWeapon@ a )
	{
		super( a );
	}

	void AnimProperties( int&out first, int&out last, bool&out bLoop )
	{
		switch ( PlayingID() )
		{
			case anim_weaponSwapIn:  first =   0; last =  31; bLoop = false; break;
			case anim_weaponSwapOut: first = 314; last = 328; bLoop = false; break;
			case anim_weaponFire:    first =  32; last =  97; bLoop = false; break;
			default: /* idle */      first =  98; last = 313; bLoop =  true; break;
		}
	}

	void MuzzleFlash()
	{
//		self.FireProjectile( "fx/Shotgun_Flash.kfx", 6.656f-1.0f, 29.69f, -2.7648f+0.85f );
		self.FireProjectile( "fx/Shotgun_Flash.kfx", 0,0,0 );
		self.RunFxEvent( "ShotgunFlash" );
	}

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

		if ( IsPlaying( anim_weaponFire ) )
		{
			if ( PlayTime() <= 0.05f )
				self.Owner().Actor().RecoilPitch() = Math::Lerp( self.Owner().Actor().RecoilPitch(), -0.1f, 0.25f );
			if ( bReload && self.ModelVariation() == 56 )
			{
				self.PlaySound( "sounds/wep/Shotgun/Cock.ksnd" );
				bReload = false;
			}
			if ( bEject && self.ModelVariation() == 68 )
			{
				self.FireProjectile( "fx/Shotshell_Right.kfx", 14, 28, -18, true );
				bEject = false;
			}
		}
	}

	void OnBeginFire()
	{
		self.FireProjectile( QuadDamage() ? "fx/Shotgun_Proj_Quad.kfx" : "fx/Shotgun_Proj.kfx",
			1.75f, 25.6f, -1.25f );
		self.PlaySound( "sounds/wep/Shotgun/Fire.ksnd" );
		bFlash = true;
		bReload = true;
		bEject = true;
		self.AnimState().Set( anim_weaponFire, 4.0f, 0 );
		self.Owner().Actor().LoudNoiseAlert();
		self.Owner().ConsumeAmmo( 1 );
	}
}

//==============================================================================
//
//    Super Shotgun
//
//==============================================================================

const int SSG_Eject = 72;

final class TurokAutoShotgun : TurokWeapon
{
	bool bEject=false;

	TurokAutoShotgun( kWeapon@ a )
	{
		super( a );
	}

	void AnimProperties( int&out first, int&out last, bool&out bLoop )
	{
		switch ( PlayingID() )
		{
			case anim_weaponSwapIn:  first =   0; last =  29; bLoop = false; break;
			case anim_weaponSwapOut: first = 416; last = 430; bLoop = false; break;
			case anim_weaponFire:    first =  30; last =  95; bLoop = false; break;
			default: /* idle */      first =  96; last = 415; bLoop =  true; break;
		}
	}

	void MuzzleFlash()
	{
//		self.FireProjectile( "fx/SShotgun_Flash.kfx", 6.656f-1.0f, 29.69f, -2.7648f+0.85f );
		self.FireProjectile( "fx/SShotgun_Flash.kfx", 0,0,0 );
		self.RunFxEvent( "SShotgunFlash" );
	}

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

		if ( IsPlaying( anim_weaponFire ) )
		{
			if ( PlayTime() < 0.1f )
				OwnerP().RecoilPitch() = Math::Lerp( OwnerP().RecoilPitch(), -0.15f, 0.25f );
			if ( bEject && self.ModelVariation() == SSG_Eject )
			{
				// in view
//				self.FireProjectile( "fx/Shotshell_Left.kfx",   6, 28, -16, true );
//				self.FireProjectile( "fx/Shotshell_Right.kfx", 24, 28, -16, true );
				// de-emphasized
				self.FireProjectile( "fx/Shotshell_Left.kfx",   9, 28, -18, true );
				self.FireProjectile( "fx/Shotshell_Right.kfx", 21, 28, -18, true );
				bEject = false;
			}
		}
	}

	void OnBeginFire()
	{
		if ( !UseAmmo(2) )
		{
			BlockFireState();
			return;
		}
		self.FireProjectile( QuadDamage() ? "fx/SShotgun_Proj_Quad.kfx" : "fx/SShotgun_Proj.kfx",
			2, 0, -0.25f );
		self.PlaySound( "sounds/wep/SShotgun/Fire.ksnd" );
		bFlash = true;
		bEject = true;
		self.AnimState().Set( anim_weaponFire, 4.0f, 0 );
		OwnerP().LoudNoiseAlert();
	}
}

//==============================================================================
//
//    Machine Gun
//
//==============================================================================

enum eMachinGunFrames
{
// 	MG_SwapIn1  =   0, MG_SwapIn2  =   3,
// 	MG_SwapOut1 =  47, MG_SwapOut2 =  50,
// 	MG_Fire1    =   4, MG_Fire2    =   6, MG_Refire =  6,
// 	MG_Idle1    =   7, MG_Idle2    =  46
	MG_SwapIn1  =   0, MG_SwapIn2  =  23,
	MG_SwapOut1 = 355, MG_SwapOut2 = 370,
	MG_Fire1    =  24, MG_Fire2    =  35, MG_Refire = 31,
	MG_Idle1    =  35, MG_Idle2    = 354
};

final class TurokRifle : TurokWeapon
{
	int ShotTick = -60;
	float SpreadPct = 0;
	float RecoilPitch, RecoilRoll; // camera shake
	array<kVec3> ShellOffsets = {
		// old recoil
/*		kVec3( 7,    27.64f, -6.5f  ), //  47.5
		kVec3( 7,    27.64f, -7.5f  ), //  74
		kVec3( 6.5f, 27.64f, -7.5f  ), //  90
		kVec3( 5.5f, 27.64f, -7     ), // 110
		kVec3( 5,    27.64f, -6.75f )  // 120
*/		// new recoil
		kVec3( 7.75f, 27.64f, -7     ), //  47.5
		kVec3( 7.75f, 27.64f, -8     ), //  74
		kVec3( 7,     27.64f, -7.75f ), //  90
		kVec3( 5.75f, 27.64f, -7.25f ), // 110
		kVec3( 5.25f, 27.64f, -7     )  // 120
	};

	TurokRifle( kWeapon@ a )
	{
		super( a );
	}

	void MuzzleFlash()
	{
//		self.FireProjectile( "fx/MachineGun_Flash.kfx", 5.12f+0.35f, 29.69f, -3.584f-0.5f );
		self.FireProjectile( "fx/MachineGun_Flash.kfx", 0, 0, 0 );
		self.RunFxEvent( "MachineGunFlash" );
	}

	void AnimProperties( int&out first, int&out last, bool&out bLoop )
	{
		switch ( PlayingID() )
		{
			case anim_weaponSwapIn:  first = MG_SwapIn1;  last = MG_SwapIn2;  bLoop = false; break;
			case anim_weaponSwapOut: first = MG_SwapOut1; last = MG_SwapOut2; bLoop = false; break;
			case anim_weaponFire:    first = MG_Fire1;    last = MG_Fire2;    bLoop = false;
			            if ( HasAmmo() && FirePressed() ) last = MG_Refire;   break;
			default: /* idle */      first = MG_Idle1;    last = MG_Idle2;    bLoop =  true; break;
		}
	}

	void OnTick()
	{
		TurokWeapon::OnTick();
		if ( IsPlaying( anim_weaponFire ) && PlayTime() <= 0.1f )
		{
			float f = 1 - PlayTime() / 0.1f;
			f *= f;
			OwnerP().RecoilPitch() = f * RecoilPitch;
			OwnerP().Roll()        = f * RecoilRoll;
		}
	}

	void OnBeginFire()
	{
		// full recovery after an extra shot period
		float elapsed = ( (self.GameTicks() - ShotTick) - (MG_Refire - MG_Fire1) )
		              / float( MG_Refire - MG_Fire1 );
		// scale rather than decrement, and do so non-linearly, both to prevent rapid taps from recovering too fast, while still allowing brief pauses between bursts to recover a useful amount
		SpreadPct *= 1 - Math::Pow( elapsed, 3 - 2 * SpreadPct );
		if ( SpreadPct < 0 ) SpreadPct = 0;
		float spread = 0.03f * SpreadPct;
		SpreadPct += 1.0f / 3;
		if ( SpreadPct > 1 ) SpreadPct = 1;
		ShotTick = self.GameTicks();
		kVec3 dir = kVec3(0,1,0).Randomize( spread ) * OwnerP().Rotation();
		SpawnProj( QuadDamage() ? "fx/Bullet_Quad.kfx" : "fx/Bullet.kfx", kVec3(1,25.6f,-1), dir );

		kVec3 offset = FOVOffset( ShellOffsets );
		self.FireProjectile( "fx/Bullet_Shell.kfx", offset.x, offset.y, offset.z, true );
		kStr str = "sounds/wep/MachineGun/Fire";
		self.PlaySound( str + (1+Math::RandMax(5)) +".ksnd" );
		bFlash = true;
		self.AnimState().Set( anim_weaponFire, 4.0f, 0 );
		self.Owner().Actor().LoudNoiseAlert();
		self.Owner().ConsumeAmmo( 1 );

		RecoilPitch = Math::RandRange( 0.5f, 1.0f ) * -0.015f;
		float r     = Math::RandRange( 0.5f, 1.0f ) *  0.015f;
		RecoilRoll  = RecoilRoll < 0 ? r : -r;
	}
}

//==============================================================================
//
//    Chaingun
//
//==============================================================================

enum eChaingunAnims {
	anim_CG_SpinUp   = anim_weaponAttack1,
	anim_CG_SpinDown = anim_weaponAttack2
}

// shot frames are 1 frame before barrel aligns with chamber, to account for 1-frame actorfx delay
enum eChaingunFrames {
	                 CG_Align1_0 = 25,
	CG_Shot1_1 = 32, CG_Align1_1,
	CG_Shot1_2 = 41, CG_Align1_2,
	CG_Shot1_3 = 48, CG_Align1_3,
	CG_Shot1_4 = 54, CG_Align1_4,
	CG_Shot1_5 = 59, CG_Align1_5,
	CG_Shot1_6 = 63,
	                 CG_Align2_0,
	CG_Shot2_1 = 67, CG_Align2_1,
	CG_Shot2_2 = 70, CG_Align2_2,
	CG_Shot2_3 = 73, CG_Align2_3,
	CG_Shot2_4 = 77, CG_Align2_4,
	CG_Shot2_5 = 80, CG_Align2_5,
	CG_Shot2_6 = 84,
	                  CG_Align3_0,
	CG_Shot3_1 =  89, CG_Align3_1,
	CG_Shot3_2 =  96, CG_Align3_2,
	CG_Shot3_3 = 105, CG_Align3_3,
	CG_Shot3_4 = 114, CG_Align3_4,
	CG_Shot3_5 = 124, CG_Align3_5,
	CG_Shot3_6 = 135, CG_Align3_6
}

final class TurokMinigun : TurokWeapon
{
	int LastShot;
	bool bSpinning = false;
	int SpinFrame;

	TurokMinigun( kWeapon@ a  )
	{
		super( a );
	}

	void AnimProperties( int&out first, int&out last, bool&out bLoop )
	{
		switch ( PlayingID() )
		{
			case anim_weaponSwapIn:   first =         0; last =  24; bLoop = false; break;
			case anim_weaponSwapOut:  first =       376; last = 390; bLoop = false; break;
			case anim_CG_SpinUp:      first = SpinFrame; last =  64; bLoop = false; break;
			case anim_weaponFireLoop: first =        64; last =  84; bLoop =  true; break;
			case anim_CG_SpinDown:    first = SpinFrame; last = 136; bLoop = false; break;
			default: /* idle */       first =       136; last = 375; bLoop =  true; break;
		}
	}

	void AnimEnd()
	{
		if ( IsPlaying(anim_CG_SpinUp) )
			self.AnimState().Set( anim_weaponFireLoop, 4, 0 );
		else
			TurokWeapon::AnimEnd();
	}

	void MuzzleFlash()
	{
		self.FireProjectile( "fx/Chaingun_Flash.kfx", 0,0,0 );
		self.FireProjectile( IsPlaying(anim_weaponFireLoop) ?
			"fx/Chaingun_Smoke_Fast.kfx" : "fx/Chaingun_Smoke_Slow.kfx", 0,0,0 );
		self.RunFxEvent( "MachineGunFlash" );
		// recenter recoil the frame after setting it
		OwnerP().RecoilPitch() = 0;
		OwnerP().Roll() = 0;
	}

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

		if ( self.Owner().Locked() )
		{
			if ( bSpinning && OwnerDying() )
				SpinDown();
			return;
		}

		if ( IsPlaying(anim_weaponFireLoop) )
			self.PlaySound( "sounds/wep/Chaingun/Whir.ksnd" );
		// spin down if climbing/swimming forced us down
		else if ( !IsPlaying(anim_CG_SpinUp) && bSpinning )
			SpinDown();

		int b;
		switch ( self.ModelVariation() )
		{
			case CG_Shot1_1: case CG_Shot2_1: case CG_Shot3_1: b=1; break;
			case CG_Shot1_2: case CG_Shot2_2: case CG_Shot3_2: b=2; break;
			case CG_Shot1_3: case CG_Shot2_3: case CG_Shot3_3: b=3; break;
			case CG_Shot1_4: case CG_Shot2_4: case CG_Shot3_4: b=4; break;
			case CG_Shot1_5: case CG_Shot2_5: case CG_Shot3_5: b=5; break;
			                                  case CG_Shot3_6:      if ( !FirePressed() ) return;
			case CG_Shot1_6: case CG_Shot2_6:                  b=0; break;

			case CG_Align1_1: SpinDown( CG_Align3_5 ); return;
			case CG_Align1_2: SpinDown( CG_Align3_4 ); return;
			case CG_Align1_3: SpinDown( CG_Align3_3 ); return;
			case CG_Align1_4: SpinDown( CG_Align3_2 ); return;
			case CG_Align1_5: SpinDown( CG_Align3_1 ); return;
			case CG_Align2_0: case CG_Align2_1: case CG_Align2_2:
			case CG_Align2_3: case CG_Align2_4: case CG_Align2_5:
				SpinDown( CG_Align3_0 );
				return;
			case CG_Align3_1: SpinDown_Interrupt( CG_Align1_5 ); return;
			case CG_Align3_2: SpinDown_Interrupt( CG_Align1_4 ); return;
			case CG_Align3_3: SpinDown_Interrupt( CG_Align1_3 ); return;
			case CG_Align3_4: SpinDown_Interrupt( CG_Align1_2 ); return;
			case CG_Align3_5: SpinDown_Interrupt( CG_Align1_1 ); return;

			// no alignment
			default: return;
		}

		// check if we can fire
		if ( !HasAmmo() || LastShot == self.ModelVariation() )
			return;
		LastShot = self.ModelVariation();
		SpawnProj( b );
		self.FireProjectile( "fx/Bullet_Shell.kfx", 15, 15, -10, true );
		kStr str = "sounds/wep/MachineGun/Fire";
		self.PlaySound( str + (1+Math::RandMax(5)) +".ksnd" );
		bFlash = true;
		OwnerP().LoudNoiseAlert();
		OwnerP().RecoilPitch() = Math::RandRange( -0.005f, -0.01f );
		OwnerP().Roll() = Math::RandRange( 0.005f, 0.01f );
		if ( b % 2 == 0 ) OwnerP().Roll() = -OwnerP().Roll();
		self.Owner().ConsumeAmmo( 1 );
	}

	// check if spin-down should be cancelled
	void SpinDown_Interrupt( int f )
	{
		// out of ammo - exit fire state so weapon can be lowered
		if ( !HasAmmo() )
			self.AnimState().Set( anim_weaponIdle, 4, ANF_LOOP );
		// pulled trigger - spin back up
		else if ( FirePressed() )
			SpinUp( f );
	}

	void SpinDown( int f )
	{
		// out of ammo - exit fire state so weapon can be lowered without waiting for spin-down
		if ( !HasAmmo() )
			self.AnimState().Set( anim_weaponIdle, 4, ANF_LOOP );
		// released trigger - spin down
		else if ( !FirePressed() )
		{
			self.AnimState().Set( anim_CG_SpinDown, 4, 0 );
			SpinFrame = f;
		}
		// still firing
		else
			return;
		SpinDown();
	}

	void SpinDown()
	{
		self.StopLoopingSounds(); // stop whir sound
		StopWeaponSound(); // stop spin-up sound
		switch ( LastShot )
		{
			case CG_Shot1_1: PlayWeaponSound( "sounds/wep/Chaingun/Stop5.ksnd" ); break;
			case CG_Shot1_2: PlayWeaponSound( "sounds/wep/Chaingun/Stop4.ksnd" ); break;
			case CG_Shot1_3: PlayWeaponSound( "sounds/wep/Chaingun/Stop3.ksnd" ); break;
			case CG_Shot1_4: PlayWeaponSound( "sounds/wep/Chaingun/Stop2.ksnd" ); break;
			case CG_Shot1_5: PlayWeaponSound( "sounds/wep/Chaingun/Stop1.ksnd" ); break;
			default:         PlayWeaponSound( "sounds/wep/Chaingun/Stop.ksnd" ); break;
		}
		bSpinning = false;
	}

	void SpawnProj( int barrel )
	{
		// when tapping fire (before spin-up could be interrupted), the middle 3 shots will have medium spread
		float spread;
		switch ( PlayingID() )
		{
			case anim_CG_SpinUp:       spread = barrel == 5 || barrel == 0 ? 0.5f : 0;  break;
			case anim_weaponFireLoop:  spread = 1;  break;
			case anim_CG_SpinDown:     spread = barrel == 1 ? 0.5f : 0;  break;
		}
		kVec3 offset( 8, 25.6f, -8 );
		kVec3 dir = kVec3( -0.01f, 1, 0.01f ).Randomize( Math::Lerp( 0.035f, 0.055f, spread ) ) * OwnerP().Rotation();
		kQuat q = ToQuat( dir );
		SpawnProj( QuadDamage() ? "fx/Bullet_Quad.kfx" : "fx/Bullet.kfx", offset, q );
		if ( spread > 0 || barrel % 2 == 1 )
			SpawnProj( "fx/Chaingun_Tracer.kfx", offset, q );
	}

	void SpinUp( int f )
	{
		self.AnimState().Set( anim_CG_SpinUp, 4.0f, 0 );
		SpinFrame = f;
		LastShot = f - 1;
		StopWeaponSound(); // stop spin-down sound
		switch ( f )
		{
			case CG_Align1_0: PlayWeaponSound( "sounds/wep/Chaingun/Start.ksnd" ); LastShot = CG_Shot3_6; break;
			case CG_Align1_1: PlayWeaponSound( "sounds/wep/Chaingun/Start1.ksnd" ); break;
			case CG_Align1_2: PlayWeaponSound( "sounds/wep/Chaingun/Start2.ksnd" ); break;
			case CG_Align1_3: PlayWeaponSound( "sounds/wep/Chaingun/Start3.ksnd" ); break;
			case CG_Align1_4: PlayWeaponSound( "sounds/wep/Chaingun/Start4.ksnd" ); break;
			case CG_Align1_5: PlayWeaponSound( "sounds/wep/Chaingun/Start5.ksnd" ); break;
		}
		bSpinning = true;
	}

	void OnBeginFire()
	{
		SpinUp( CG_Align1_0 );
	}
}

//==============================================================================
//
//    Hand Grenade
//
//==============================================================================

enum eHandGrenadeAnims {
	anim_HG_Prime = anim_weaponAttack1,
	anim_HG_Hold  = anim_weaponAttack2,
	anim_HG_Throw = anim_weaponAttack3
}
enum eHandGrenadeFrames {
	HG_SwapIn1  =   0, HG_SwapIn2  =  12,
	HG_SwapOut1 = 371, HG_SwapOut2 = 383,
	HG_Prime_1  =  13, HG_Prime_2  =  73, HG_Opened = 49,
	HG_Hold     =  74,
	HG_Throw_1  =  75, HG_Throw_2  = 107, HG_Thrown = 93,
	HG_Idle1    = 107, HG_Idle2    = 370
}

final class TurokChrono : TurokWeapon
{
	int LastFuse;
	bool bThrow;

	TurokChrono( kWeapon@a )
	{
		super( a );
	}

	void AnimProperties( int&out first, int&out last, bool&out bLoop )
	{
		switch ( PlayingID() )
		{
			case anim_weaponSwapIn:   first = HG_SwapIn1;  last = HG_SwapIn2;  bLoop = false; break;
			case anim_weaponSwapOut:  first = HG_SwapOut1; last = HG_SwapOut2; bLoop = false;
				if ( !HasAmmo() )     first = last; // short-circuit when out of ammo
				break;
			case anim_HG_Prime:       first = HG_Prime_1;  last = HG_Prime_2;  bLoop = false; break;
			case anim_HG_Hold:        first = HG_Hold;     last = HG_Hold;     bLoop =  true; break;
			case anim_HG_Throw:       first = HG_Throw_1;  last = HG_Throw_2;  bLoop = false;
				if ( LastFuse == 19 ) first = HG_Thrown; // skip throw anim if it blew up in our hands
				if ( !HasAmmo() )     last  = HG_Thrown; // stop as soon as off-screen when out of ammo
				break;
			default: /* idle */       first = HG_Idle1;    last = HG_Idle2;    bLoop =  true; break;
		}
	}
	void AnimEnd()
	{
		if ( IsPlaying(anim_HG_Prime) )
			self.AnimState().Set( anim_HG_Hold, 4, 0 );
		else
			TurokWeapon::AnimEnd();
	}

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

		// camera shake
		if ( IsPlaying(anim_HG_Throw) && PlayTime() < 0.15f && LastFuse < 19 )
		{
			float f = PlayTime() / 0.15f;
			if ( World.GetAreaFlags( OwnerP().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
				OwnerP().RecoilPitch() = Math::Lerp( OwnerP().RecoilPitch(), -0.0075f, f );
			}
			OwnerP().Roll() = Math::Lerp( OwnerP().Roll(), 0.03f, f );
		}

		// hide when out of ammo
		if ( HasAmmo() )
		{
			self.Flags() &= ~AF_NODRAW;
			// only play swap-in click if we're actually holding something to make the sound
			if ( IsPlaying(anim_weaponSwapIn) && PlayTime() == 0 )
				self.PlaySound( "sounds/wep/HGrenade/SwapIn.ksnd" );
		}
		else if ( !IsPlaying(anim_HG_Throw) )
			self.Flags() |=  AF_NODRAW;
	}

	void OnBeginFire()
	{
		self.AnimState().Set( anim_HG_Prime, 4, 0 );
		self.PlaySound( "sounds/wep/HGrenade/Prime.ksnd" );
		bThrow = false;
		LastFuse = -1;
	}

	void OnFire()
	{
		int ticks, fuse;
		switch ( PlayingID() )
		{
			// start ticking once grenade is opened
			case anim_HG_Prime:
				ticks = self.ModelVariation() - HG_Opened;
				if ( ticks < 0 || ticks % 12 != 0 ) break;
				Fuse( ticks/12, true );
				break;
			case anim_HG_Hold:
				// fuse ran out in our hand, waiting for player to release fire to raise another grenade
				if ( LastFuse == 19 && !FirePressed() )
				{
					self.AnimState().Set( anim_HG_Throw, 4, 0 );
					break;
				}
				ticks = int( PlayTime() * 60 + 0.5f ) + HG_Prime_2 - HG_Opened;
				fuse = (ticks+6) / 12;
				if ( !FirePressed() )
				{
//					Sys.Print( ""+ (ticks/12.0f) +" "+ fuse );
					bThrow = true;
				}
				Fuse( fuse, ticks % 12 == 0 );
				break;
		}
	}

	void Fuse( int i, bool bExact )
	{
		if ( i <= LastFuse || i >= 4*5 )
			return;

		if ( !bExact )
		{
			if ( bThrow )
			{
				Throw( i );
				LastFuse = i;
			}
			return;
		}

		if ( i == 19 ) // fuse ran out in our hand
			Throw( i, true );
		else if ( bThrow )
			Throw( i );
		else if ( i % 5 == 4 )
			self.PlaySound( "sounds/wep/HGrenade/Fuse_Clunk.ksnd" );
		else
			self.PlaySound( "sounds/wep/HGrenade/Fuse_Ting.ksnd" );
		LastFuse = i;
	}

	void Throw( int i, bool bTimeout=false )
	{
		if ( !bTimeout )
		{
			self.AnimState().Set( anim_HG_Throw, 4, 0 );
			self.PlaySound( "sounds/wep/HGrenade/Throw.ksnd" );
		}
		kStr gren = "fx/HGrenade/";
		if ( QuadDamage() )
			gren += "Quad_";
//		self.FireProjectile( gren + i +".kfx", 10, 0, 0 );
		kVec3 dir = kVec3(0,1,0) * OwnerP().Rotation();
		dir.z += 0.15f;
		if ( OwnerP().Pitch() < 0 )
			dir.z += 0.15f * OwnerP().Pitch() / Math::Deg2Rad(-90);
		// ToQuat() doesn't get yaw when dir is parallel to z-axis
		kQuat q = kQuat( OwnerP().Yaw(), 0,0,1 ) * kQuat( -dir.ToPitch(), 1,0,0 );
		SpawnProj( gren + i +".kfx", kVec3( 10, 0, 0 ), q );
		self.Owner().ConsumeAmmo( 1 );
	}
}

//==============================================================================
//
//    Grenade Launcher
//
//==============================================================================

enum eGLFrames {
	GL_Empty  = 65, // 10,
	GL_Loaded = 79  // 13
};

final class TurokGrenadeLauncher : TurokWeapon
{
//	bool bLauncher = true;

	TurokGrenadeLauncher( kWeapon@ a )
	{
		super( a );
	}
/*
	void CheckModel( bool bResetAnim )
	{
		int bits;
		GameVariables.GetInt( "Smoke39Flags", bits );
		bool bHasLauncher = bits & (1<<2) != 0;
		if ( bHasLauncher == bLauncher ) return;
		int animID = PlayingID();
		bLauncher = bHasLauncher;
		if ( bHasLauncher ) self.RenderModel().SetModel( "models/w_glauncher.bin", "anims/weapon_stubs.bin" );
		else                self.RenderModel().SetModel( "models/w_hgrenade.bin", "anims/weapon_stubs.bin" );
		if ( bResetAnim ) self.AnimState().Set( animID, 4, 0 );
	}

	void AnimProperties( int&out first, int&out last, bool&out bLoop )
	{
		if ( bLauncher ) AnimProperties_L( first, last, bLoop );
		else             AnimProperties_T( first, last, bLoop );
	}
	void AnimEnd()
	{
		switch ( PlayingID() )
		{
			case anim_weaponFire: case anim_weaponAttack3: CheckModel( false );
		}
		if ( bLauncher ) TurokWeapon::AnimEnd();
		else             AnimEnd_T();
	}
	void OnTick()
	{
		switch ( PlayingID() )
		{
			case anim_weaponSwapIn: case anim_weaponSwapOut:
			case anim_weaponIdle: case anim_weaponWalk: case anim_weaponRun:
				CheckModel( true );
		}
		if ( bLauncher ) OnTick_L();
		else             OnTick_T();
	}
	void OnBeginFire()
	{
		if ( bLauncher ) OnBeginFire_L();
		else             OnBeginFire_T();
	}
*/
	void AnimProperties( int&out first, int&out last, bool&out bLoop )
	{
		switch ( PlayingID() )
		{
// 			case anim_weaponSwapIn:  first =  0; last =  5; bLoop = false; break;
// 			case anim_weaponSwapOut: first = 59; last = 63; bLoop = false; break;
// 			case anim_weaponFire:    first =  6; last = 15; bLoop = false;
// 			                   if ( !HasAmmo() ) last = GL_Empty; break;
// 			default: /* idle */      first = 16; last = 58; bLoop =  true; break;
			case anim_weaponSwapIn:  first =   0; last =  29; bLoop = false; break;
			case anim_weaponSwapOut: first = 444; last = 463; bLoop = false; break;
			case anim_weaponFire:    first =  30; last =  99; bLoop = false;
			                    if ( !HasAmmo() ) last = GL_Empty; break;
			default: /* idle */      first = 100; last = 443; bLoop =  true; break;
		}
	}

	void OnTick()
	{
		TurokWeapon::OnTick();
		if ( IsPlaying( anim_weaponFire ) && PlayTime() < 0.2f )
			self.Owner().Actor().RecoilPitch() = Math::Lerp( self.Owner().Actor().RecoilPitch(), -0.1f, 0.2f*((0.2f-PlayTime())/0.2f) );
		// hide grenade if there isn't another one to load (and the last one isn't being loaded)
		self.RenderModel().HideSection( 0, 1, GetAmmo() < 2 && (!IsPlaying(anim_weaponFire) || self.ModelVariation() >= GL_Loaded || !HasAmmo()) );
	}

	void OnBeginFire()
	{
		self.AnimState().Set( anim_weaponFire, 4.0f, 0 );
		// no reload for last shot
		if ( GetAmmo() > 1 )
			self.PlaySound( "sounds/wep/GLauncher/Fire.ksnd" );
		else
			self.PlaySound( "sounds/wep/GLauncher/Fire_Last.ksnd" );

		kVec3 offset( 13.5f, FOVOffset( array<float>={ 69, 40, 30, 21, 17.5f } ), -20.5f );
		// account for bOffsetFromFloor
		float f = OwnerP().Pitch() / Math::Deg2Rad(90);
		offset += kVec3( 0, f, f*f ) * 9;
		kVec3 dir = kVec3( 0,1,0 ) * OwnerP().Rotation();
		dir.z += 0.2f;
		if ( OwnerP().Pitch() < 0 )
			dir.z += 0.1f * OwnerP().Pitch() / Math::Deg2Rad(-90);
		// ToQuat() doesn't get yaw when dir is parallel to z-axis
		kQuat q = kQuat( OwnerP().Yaw(), 0,0,1 ) * kQuat( -dir.ToPitch(), 1,0,0 );
		SpawnProj( QuadDamage() ? "fx/Grenade_Quad.kfx" : "fx/Grenade.kfx", offset, q );

		self.FireProjectile( "fx/GLauncher_Smoke.kfx", 0,0,0 );

		self.Owner().Actor().LoudNoiseAlert();
		self.Owner().ConsumeAmmo( 1 );
	}
}

//==============================================================================
//
//	Rocket Launcher
//
//==============================================================================

final class TurokRocketLauncher : TurokWeapon
{
	TurokRocketLauncher( kWeapon@ a )
	{
		super( a );
	}

	void AnimProperties( int&out first, int&out last, bool&out bLoop )
	{
		switch ( PlayingID() )
		{
			case anim_weaponSwapIn:  first =   0; last =  25; bLoop = false; break;
			case anim_weaponSwapOut: first = 392; last = 407; bLoop = false; break;
			case anim_weaponFire:    first =  26; last =  88; bLoop = false;
			                    if ( !HasAmmo() ) last =  46; break;
			default: /* idle */      first =  88; last = 391; bLoop =  true; break;
// 			case anim_weaponSwapIn:  first =  0; last =  4; bLoop = false; break;
// 			case anim_weaponSwapOut: first = 51; last = 54; bLoop = false; break;
// 			case anim_weaponFire:    first =  5; last = 13; bLoop = false;
// 			                   if ( !HasAmmo() ) last =  7; break;
// 			default: /* idle */      first = 13; last = 50; bLoop =  true; break;
		}
	}

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

		// hide rockets as ammo runs out
		self.RenderModel().HideSection( 0, 1, GetAmmo() < 1 );
		self.RenderModel().HideSection( 0, 2, GetAmmo() < 2 );
		self.RenderModel().HideSection( 0, 3, GetAmmo() < 3 );

		if ( IsPlaying( anim_weaponFire ) && PlayTime() < 0.1f )
			self.Owner().Actor().RecoilPitch() = Math::Lerp( self.Owner().Actor().RecoilPitch(), -0.075f, 0.15f*((0.1f-PlayTime())/0.1f) );
	}

	void OnBeginFire()
	{
		self.AnimState().Set( anim_weaponFire, 4.0f, 0 );
		// no reload for last shot
		if ( GetAmmo() > 1 )
			self.PlaySound( "sounds/wep/RLauncher/Fire.ksnd" );
		else
			self.PlaySound( "sounds/wep/RLauncher/Fire_Last.ksnd" );

		self.FireProjectile( QuadDamage() ? "fx/Rocket_Quad.kfx" : "fx/Rocket.kfx",
			10, FOVOffset( array<float>={ 68.5f, 40, 30, 21, 17.4f } ), -14 );

		self.Owner().Actor().LoudNoiseAlert();
		self.Owner().ConsumeAmmo( 1 );
	}
}

//==============================================================================
//
//    HyperBlaster
//
//==============================================================================

enum eHyperBlasterFrames {
	HB_Barrel1  =  30,
	HB_Barrel2  =  36,
	HB_Barrel3  =  42,
	HB_Barrel4  =  48,
	HB_Barrel5  =  54,
	HB_Barrel6  =  60,
	HB_Barrel7  =  66, // spin-down
	HB_Barrel8  =  73,
	HB_Barrel9  =  81,
	HB_Barrel10 =  89,
	HB_Barrel11 =  96,
	HB_Barrel12 = 104,
}
const int HB_Period = 6; // ticks between shots

final class TurokPulseRifle : TurokWeapon
{
	int FireStart = -1;
	bool bSpinning = false;
	float RecoilPitch, RecoilRoll;
	int ShotTick = -HB_Period;

	TurokPulseRifle( kWeapon@ a )
	{
		super( a );
	}

	void MuzzleFlash()
	{
		self.FireProjectile( "fx/HyperBlaster_Flash.kfx", 0,0,0 );
		self.RunFxEvent( "HyperBlasterFlash" );
	}

	void AnimProperties( int&out first, int&out last, bool&out bLoop )
	{
		switch ( PlayingID() )
		{
			case anim_weaponSwapIn:  first =   0;       last =  29; bLoop = false; break;
			case anim_weaponSwapOut: first = 348;       last = 367; bLoop = false; break;
			default: if ( FireStart >= 0 )
			/* fire / spin-down */ { first = FireStart; last = 115; bLoop = false; } else
			/* idle */             { first = 116;       last = 347; bLoop =  true; }
		}
	}
	// make idle spin-down transition to actual idle anim
	void AnimEnd()
	{
		FireStart = -1;
		IdleTick = -1;
		TurokWeapon::AnimEnd();
	}

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

		// skip spin sounds during cutscenes
		// also skip entire animation process to avoid pressing fire during spin-down snapping barrel to firing position
		if ( self.Owner().Locked() )
		{
			if ( bSpinning && OwnerDying() )
				SpinDown();
			return;
		}

		// workaround for OnFire() not calling when ammo is empty and fire is held
		if ( IsPlaying(anim_weaponFire) && !HasAmmo() && FirePressed() )
			OnFire();

		int i = self.GameTicks() - ShotTick;
		if ( i < HB_Period )
		{
			float f = 1 - i / float(HB_Period-1);
			f *= f;
			OwnerP().RecoilPitch() = f * RecoilPitch;
			OwnerP().Roll()        = f * RecoilRoll;
		}

		if ( self.ModelVariation() >= HB_Barrel1 && self.ModelVariation() <= HB_Barrel7 )
		{
			self.PlaySound( "sounds/wep/HyperBlaster/Whir.ksnd" );
			bSpinning = true;
		}
		else if ( bSpinning )
			SpinDown();
	}

	void SpinDown()
	{
		self.StopLoopingSounds();
		PlayWeaponSound( "sounds/wep/HyperBlaster/Stop.ksnd" );
		bSpinning = false;
	}

	void OnBeginFire()
	{
		OnFire();
	}

	void OnFire()
	{
		if ( self.GameTicks() - ShotTick < HB_Period )
			return;
		if ( !HasAmmo() || !FirePressed() )
		{
			FireStart = self.ModelVariation(); // signal idle anim to continue fire anim through spin-down
			self.AnimState().Set( anim_weaponIdle, 4, ANF_LOOP );
			return;
		}

		// it's been long enough to fire another shot, so even if we're not aligned yet
		// (because we didn't fire on the last alignment, or we're firing from idle)
		// jump ahead to the next nearest alignment, rather than waiting for it to come up,
		// for better responsiveness
		if      ( self.ModelVariation() <= HB_Barrel1  ) FireStart = HB_Barrel1;
		else if ( self.ModelVariation() <= HB_Barrel2  ) FireStart = HB_Barrel2;
		else if ( self.ModelVariation() <= HB_Barrel3  ) FireStart = HB_Barrel3;
		else if ( self.ModelVariation() <= HB_Barrel4  ) FireStart = HB_Barrel4;
		else if ( self.ModelVariation() <= HB_Barrel5  ) FireStart = HB_Barrel5;
		else if ( self.ModelVariation() <= HB_Barrel6  ) FireStart = HB_Barrel6;
		else if ( self.ModelVariation() <= HB_Barrel7  ) FireStart = HB_Barrel1;
		else if ( self.ModelVariation() <= HB_Barrel8  ) FireStart = HB_Barrel2;
		else if ( self.ModelVariation() <= HB_Barrel9  ) FireStart = HB_Barrel3;
		else if ( self.ModelVariation() <= HB_Barrel10 ) FireStart = HB_Barrel4;
		else if ( self.ModelVariation() <= HB_Barrel11 ) FireStart = HB_Barrel5;
		else if ( self.ModelVariation() <= HB_Barrel12 ) FireStart = HB_Barrel6;
		else      /* rest of spin-down */                FireStart = HB_Barrel1;
		self.ModelVariation() = FireStart;
		self.AnimState().Set( anim_weaponFire, 4.0f, 0 );
		// stop spin-down sound
		StopWeaponSound();

		kVec3 offset;
		switch ( self.ModelVariation() )
		{
			case HB_Barrel7:
			case HB_Barrel1: offset.Set(  0, 0,  0 ); break;
			case HB_Barrel2: offset.Set( -1, 0, -1 ); break;
			case HB_Barrel3: offset.Set( -1, 0, -2 ); break;
			case HB_Barrel4: offset.Set(  0, 0, -3 ); break;
			case HB_Barrel5: offset.Set(  1, 0, -2 ); break;
			case HB_Barrel6: offset.Set(  1, 0, -1 ); break;
			default: return;
		}
		offset *= kVec3( 2, 1, 1.5f );
		offset += kVec3( 5, FOVOffset( array<float>={ 44, 25.6f, 19.2f, 13.5f, 11.2f } ), -4 );
		self.FireProjectile( QuadDamage() ? "fx/HyperBlaster_Proj_Quad.kfx" : "fx/HyperBlaster_Proj.kfx",
			offset.x, offset.y, offset.z );
		self.PlaySound( "sounds/wep/HyperBlaster/Fire.ksnd" );
		bFlash = true;
		self.Owner().Actor().LoudNoiseAlert();
		self.Owner().ConsumeAmmo( 1 );
		ShotTick = self.GameTicks();
		RecoilPitch = Math::RandRange( 0.5f, 1.0f ) * -0.01f;
		float r     = Math::RandRange( 0.5f, 1.0f ) *  0.01f;
		RecoilRoll  = RecoilRoll < 0 ? r : -r;
	}
}

//==============================================================================
//
//    Railgun
//
//==============================================================================

enum eRailgunFrames
{
	RG_SwapIn1  =   0, RG_SwapIn2  =  30,
	RG_SwapOut1 = 433, RG_SwapOut2 = 447,
	RG_Fire1    =  30, RG_Fire2    = 129, RG_Empty = 66, RG_Reload = 87,
	RG_Idle1    = 129, RG_Idle2    = 432
}

final class TurokAccelerator : TurokWeapon
{
	bool bReload;

	TurokAccelerator( kWeapon@ a )
	{
		super( a );
	}

	void AnimProperties( int&out first, int&out last, bool&out bLoop )
	{
		switch ( PlayingID() )
		{
			case anim_weaponSwapIn:  first = RG_SwapIn1;  last = RG_SwapIn2;  bLoop = false; break;
			case anim_weaponSwapOut: first = RG_SwapOut1; last = RG_SwapOut2; bLoop = false; break;
			case anim_weaponFire:    first = RG_Fire1;    last = RG_Fire2;    bLoop = false;
			                         if ( !HasAmmo() )    last = RG_Empty;    break;
			default: /* idle */      first = RG_Idle1;    last = RG_Idle2;    bLoop =  true; break;
		}
	}

	void OnTick()
	{
		TurokWeapon::OnTick();
		if ( !self.Owner().Locked() )
			self.PlaySound( "sounds/wep/Railgun/Hum.ksnd" );
//		if ( IsPlaying( anim_weaponFire ) && PlayTime() < 0.2f )
//			self.Owner().Actor().RecoilPitch() = Math::Lerp( self.Owner().Actor().RecoilPitch(), -0.075f, 0.35f );
		if ( IsPlaying( anim_weaponFire ) && PlayTime() < 0.3f )
			self.Owner().Actor().RecoilPitch() = Math::Lerp( self.Owner().Actor().RecoilPitch(), -0.1f, 0.25f*((0.3f-PlayTime())/0.3f) );
		// sound has 3 ticks of soft sound before the main chunky sound
		if ( bReload && self.ModelVariation() == RG_Reload-3 )
		{
			self.PlaySound( "sounds/wep/Railgun/Reload.ksnd" );
			bReload = false;
		}
	}

	bool Hitscan( kVec3&in start, kVec3&in end, uint flags )
	{
		Dummy1.SetPosition( start );
		Dummy2.SetPosition( end );
		if ( Dummy1.CanSee( Dummy2, flags ) )
			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;
	}

	void OnBeginFire()
	{
		self.AnimState().Set( anim_weaponFire, 4.0f, 0 );
		self.PlaySound( "sounds/wep/Railgun/Fire.ksnd" );
		self.Owner().Actor().LoudNoiseAlert();
		self.Owner().ConsumeAmmo( 1 );
		bReload = true;
		// 112.5 would match Q2 based on bullet/explosive damage
		// 120 is just enough to 3-shot yellow robots on hard
		int damage = 120;
		if ( QuadDamage() )
			damage *= 4;

		kVec3 dir = kVec3( 0, 1, 0 ) * OwnerP().Rotation();
		kVec3 start = CheckPosition( EyePos() + dir * FOVOffset( array<float>={ 25.7f, 15, 11.3f, 8, 6.5f } ) );
		kVec3 origin = start;
		kVec3 end = start + dir*10000;

		// CanSee() skips actors/statics if it hits the level, so check level collision by itself first
		if ( Hitscan( start, end, CF_IGNOREBLOCKERS ) )
		{
			end = CModel.InterceptVector();
			dir = (end - start).Normalize();
		}
		kVec3 hDir( dir.x, dir.y, 0 );

		// check actors/statics
		array<kActor@> hits;
		kActor@ lastHit;
		int count = 0;
		// infinite loop failsafe, and make sure start hasn't been pushed beyond end
		while ( count++ < 100 && (end-start).Dot(dir) > 0 && Hitscan(start,end,CF_CLIPEDGES|CF_COLLIDEFLOORS|CF_NOCLIPSTATICS|CF_NOCLIPACTORS) )
		{
			// hit static mesh (level), we're done
			if ( CModel.ContactActor() is null )
			{
				end = CModel.InterceptVector();
				break;
			}
			// hit the same actor - dot product didn't take us far enough, so inch forward and try again
			if ( CModel.ContactActor() is lastHit )
			{
				start += dir * 2;
				continue;
			}
			// hit new actor
			@lastHit = CModel.ContactActor();
			// move start past actor's cental axis so the next iteration won't hit it again
			kVec3 v = lastHit.Origin() - start;
			v.z = 0;
			float f = v.Dot( hDir ) + 1;
			start += dir*f;
			// should never happen, but just in case
			if ( lastHit is OwnerA() )
				continue;
			hits.insertLast( lastHit );
			lastHit.InflictGenericDamage( OwnerA(), damage );
			if ( !SpawnHitEffects( lastHit, CModel.InterceptVector(), -dir ) )
			{
				end = CModel.InterceptVector();
				break;
			}
		}
		// do another pass in the opposite direction - catches some cases the first pass can miss
		start = end;
		@lastHit = null;
		count = 0;
		while ( count++ < 100 && (start-origin).Dot(dir) > 0 && Hitscan(start,origin,CF_CLIPEDGES|CF_COLLIDEFLOORS|CF_NOCLIPSTATICS|CF_NOCLIPACTORS) )
		{
			// hit static mesh (level)
			// TODO: update end, defer damage from both passes, and ignore actors hit beyond this point
			if ( CModel.ContactActor() is null )
				continue;
			// hit the same actor - dot product didn't take us far enough, so inch forward and try again
			if ( CModel.ContactActor() is lastHit )
			{
				start -= dir * 2;
				continue;
			}
			// hit new actor
			@lastHit = CModel.ContactActor();
			// move start past actor's cental axis so the next iteration won't hit it again
			kVec3 v = lastHit.Origin() - start;
			v.z = 0;
			float f = v.Dot( hDir ) - 1;
			start += dir*f;
			// skip player, or if already hit by first pass
			if ( lastHit is OwnerA() || hits.findByRef(lastHit) >= 0 )
				continue;
			lastHit.InflictGenericDamage( OwnerA(), damage );
			// move hit location to front (roughly)
			CModel.InterceptVector() -= dir * 2 * lastHit.Radius();
			// TODO: same note as static mesh above
			if ( !SpawnHitEffects( lastHit, CModel.InterceptVector(), -dir ) )
			{
//				end = CModel.InterceptVector();
//				break;
			}
		}

		SpawnTrail( origin, end );
	}

	bool SpawnHitEffects( kActor@ a, kVec3&in hitLocation, kVec3&in hitNormal )
	{
		TurokEnemy@ E;
		switch ( a.ImpactType() )
		{
			case IT_METAL:
				a.PlaySound( "sounds/shaders/bullet_impact_5.ksnd" );
				Game.SpawnFx( "fx/generic_251.kfx", a, hitLocation, ToQuat(hitNormal) );
				return true;
			case IT_FLESH_HUMAN:
				a.PlaySound( "sounds/shaders/bullet_impact_13.ksnd" );
				Game.SpawnFx( "fx/blood.kfx", a, hitLocation, ToQuat(hitNormal) );
				GibActor( a, -hitNormal );
				return true;
			case IT_FLESH_CREATURE:
				a.PlaySound( "sounds/shaders/bullet_impact_14.ksnd" );
				Game.SpawnFx( "fx/greenblood.kfx", a, hitLocation, ToQuat(hitNormal) );
				GibActor( a, -hitNormal );
				return true;
			case IT_FORCEFIELD:
				a.PlaySound( "sounds/shaders/generic_219.ksnd" );
				Game.SpawnFx( "fx/generic_187.kfx", a, hitLocation, ToQuat(hitNormal) );
				return false;
		}
		return true;
	}

	void SpawnTrail( kVec3&in start, kVec3&in end )
	{
		// offset start from actual ray to line up with barrel
		start += kVec3( 4, 0, -2 ) * OwnerP().Rotation();
		kVec3 dir = (end - start).Normalize();
		kQuat q = ToQuat( dir );
		float dist = (end-start).Unit();
		float len = 1000;
		kActor@ a;
		RailSpiral@ spiral;
		RailBeam@ beam;
		bool bFirst = true;
		while ( true )
		{
			kStr spiralStr = "RailSpiral_", beamStr = "RailBeam_";
			spiralStr = spiralStr + int(len);
			beamStr   = beamStr   + int(len);
			while ( dist >= len )
			{
				@a = ActorFactory.Spawn( spiralStr, start.x, start.y, start.z, 0, -1 );
				if ( a !is null && a.ScriptObject() !is null
					&& (@spiral = cast<RailSpiral@>(a.ScriptObject().obj)) !is null )
						spiral.q = a.Rotation() = q;
				@a = ActorFactory.Spawn( beamStr, start.x, start.y, start.z, 0, -1 );
				if ( a !is null )
				{
					// hide soft endcap unless first segment
					a.RenderModel().HideSection( 0, 1, !bFirst );
					if ( a.ScriptObject() !is null
						&& (@beam = cast<RailBeam@>(a.ScriptObject().obj)) !is null )
							beam.q = a.Rotation() = q;
				}
				if ( len < 17 ) break;
				bFirst = false;
				start += dir*len;
				dist -= len;
			}
			if ( len > 100 ) len /= 10;
			else if ( len > 34 ) len /= 3;
			else if ( len > 17 ) len /= 2;
			else break;
		}
	}
}

//==============================================================================
//
//    BFG
//
//==============================================================================

enum eBFGFrames
{
/*	BFG_SwapIn1  =  0, BFG_SwapIn2  =  8,
	BFG_SwapOut1 = 55, BFG_SwapOut2 = 58,
	BFG_Fire1    =  9, BFG_Fire2    = 31, BFG_Shot = 17,
	BFG_Idle1    = 32, BFG_Idle2    = 54
*/	BFG_SwapIn1  =   0, BFG_SwapIn2  =  31,
	BFG_SwapOut1 = 377, BFG_SwapOut2 = 396,
	BFG_Fire1    =  32, BFG_Fire2    = 192, BFG_Shot = 88,
	BFG_Idle1    = 193, BFG_Idle2    = 376
}

final class TurokFusionCannon : TurokWeapon
{
	bool bFired;

	TurokFusionCannon( kWeapon@ a )
	{
		super( a );
	}

	void AnimProperties( int&out first, int&out last, bool&out bLoop )
	{
		switch ( PlayingID() )
		{
			case anim_weaponSwapIn:  first = BFG_SwapIn1;  last = BFG_SwapIn2;  bLoop = false; break;
			case anim_weaponSwapOut: first = BFG_SwapOut1; last = BFG_SwapOut2; bLoop = false; break;
			case anim_weaponFire:    first = BFG_Fire1;    last = BFG_Fire2;    bLoop = false; break;
			default: /* idle */      first = BFG_Idle1;    last = BFG_Idle2;    bLoop =  true; break;
		}
	}

	void MuzzleFlash()
	{
		self.FireProjectile( "fx/BFG_Flash.kfx", 0,0,0 );
		self.RunFxEvent( "BFGFlash" );
	}

	void OnTick()
	{
		TurokWeapon::OnTick();
		if ( !self.Owner().Locked() )
			self.PlaySound( "sounds/wep/BFG/Hum.ksnd" );

		if ( !IsPlaying(anim_weaponFire) ) return;
		float t = PlayTime() - (BFG_Shot - BFG_Fire1) / 60.0f;
		if ( t < -0.01f ) return; // test a little lower than 0 to account for imprecision

		if ( t < 0.3f )
			OwnerP().RecoilPitch() = Math::Lerp( OwnerP().RecoilPitch(), -0.4f, 0.1f*((0.3f-t)/0.3f) );

		if ( bFired ) return;
		kVec3 v = CheckPosition( EyePos() + kVec3( 15, 25, -10 ) * OwnerP().Rotation() );
		ActorFactory.Spawn( "BFGProj", v.x, v.y, v.z, OwnerP().Yaw(), OwnerP().SectorIndex() );
		bFlash = true;
		self.Owner().Actor().LoudNoiseAlert();
		UseAmmo( 50 );
		bFired = true;
	}

	void OnBeginFire()
	{
		if ( !HasAmmo(50,true) )
		{
			BlockFireState();
			return;
		}
		self.AnimState().Set( anim_weaponFire, 4.0f, 0 );
		self.PlaySound( "sounds/wep/BFG/Fire.ksnd" );
		bFired = false;
	}
}

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

// as long as this is being included for compatibility, it's actually being used by hyperblaster and chaingun
// in case Turok+ isn't installed, it's initialized/cleared in Q2_Init.txt, and updated in TurokWeapon::OnTick()
kActor@ WeaponSound;
void PlayWeaponSound( kStr&in sound )
{
	if ( WeaponSound !is null && !WeaponSound.IsStale() ) WeaponSound.PlaySound( sound );
}
void StopWeaponSound()
{
	if ( WeaponSound !is null && !WeaponSound.IsStale() ) WeaponSound.StopSound();
}
void UpdateWeaponSound()
{
	if ( WeaponSound !is null )
	{
		if ( !WeaponSound.IsStale() ) WeaponSound.Origin() = EyePos();
		else                          @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 TurokKnife : TurokWeapon
{
	TurokKnife(kWeapon @actor) { super(actor); }
	void OnBeginFire(void)
	{
		kPuppet @src = self.Owner().Actor();
		if(src.InWater())
		{
			self.AnimState().Blend(anim_weaponAttackUnderwater, 4.0f, 4.0f, 0);
			self.PlaySound("sounds/shaders/underwater_swim_2.ksnd");
			return;
		}
		int rnd = Math::RandMax(100);
		if(rnd <= 32)
		{
			self.AnimState().Blend(anim_weaponAttack1, 4.0f, 4.0f, 0);
			self.PlaySound("sounds/shaders/knife_swish_2.ksnd");
		}
		else if(rnd <= 64)
		{
			self.AnimState().Blend(anim_weaponAttack2, 4.0f, 4.0f, 0);
			self.PlaySound("sounds/shaders/knife_swish_1.ksnd");
		}
		else
		{
			self.AnimState().Blend(anim_weaponAttack3, 4.0f, 4.0f, 0);
			self.PlaySound("sounds/shaders/knife_swish_3.ksnd");
		}
	}
	void KnifeAttack1(kActor @instigator, const float w, const float x, const float y, const float z)
	{
		kPuppet @src = self.Owner().Actor();
		TurokPlayer @p = cast<TurokPlayer@>(src.ScriptObject().obj);
		p.m_vBloodVector = kVec3(0, (w * GAME_SCALE), 0);
		p.m_vBloodVector.z += (GAME_SCALE * 3);
		p.m_vStabVector = (kVec3(0, (w * GAME_SCALE), 0) * src.Rotation());
		p.m_vStabVector += src.Origin();
		p.m_vStabVector.z += (GAME_SCALE * 3);
		src.InteractActorsAtPosition(p.m_vStabVector, "KnifeAttack", 0, w);
	}
	void KnifeAttack2(kActor @instigator, const float w, const float x, const float y, const float z)
	{
		kPuppet @src = self.Owner().Actor();
		TurokPlayer @p = cast<TurokPlayer@>(src.ScriptObject().obj);
		p.m_vBloodVector = kVec3(0, (w * GAME_SCALE), 0);
		p.m_vBloodVector.z += (GAME_SCALE * 3);
		p.m_vStabVector = (kVec3(0, (w * GAME_SCALE), 0) * src.Rotation());
		p.m_vStabVector += src.Origin();
		p.m_vStabVector.z += (GAME_SCALE * 3);
		src.InteractActorsAtPosition(p.m_vStabVector, "KnifeAttack", 1, w);
	}
	void KnifeAttack3(kActor @instigator, const float w, const float x, const float y, const float z)
	{
		kPuppet @src = self.Owner().Actor();
		TurokPlayer @p = cast<TurokPlayer@>(src.ScriptObject().obj);
		p.m_vBloodVector = kVec3(0, (w * GAME_SCALE), 0);
		p.m_vBloodVector.z += (GAME_SCALE * 3);
		p.m_vStabVector = (kVec3(0, (w * GAME_SCALE), 0) * src.Rotation());
		p.m_vStabVector += src.Origin();
		p.m_vStabVector.z += (GAME_SCALE * 3);
		src.InteractActorsAtPosition(p.m_vStabVector, "KnifeAttack", 2, w);
	}
}
final class TurokBow : TurokWeapon
{
	bool m_bArrowFlashed;
	TurokBow(kWeapon @actor)
	{
		super(actor);
		m_bArrowFlashed = false;
	}
	void OnBeginFire(void)
	{
		self.PlaySound("sounds/shaders/bow_stretch.ksnd");
		self.AnimState().Blend(anim_weaponFire, 4.0f, 20.0f, ANF_LOOP);
		m_bArrowFlashed = false;
	}
	void OnFire(void)
	{
		float time = self.AnimState().PlayTime();
		if(time >= 1.4f && time <= 2.15f && !m_bArrowFlashed)
		{
			m_bArrowFlashed = true;
			self.FireProjectile("fx/super_arrow_flash.kfx", 0.07f*GAME_SCALE, 2.9f*GAME_SCALE, -0.6f*GAME_SCALE, true);
		}
	}
	void OnEndFire(void)
	{
		float time = self.AnimState().PlayTime();
		kPuppet @src = self.Owner().Actor();
		kVec3 origin = src.Origin();
		kQuat rotation = src.Rotation();
		kVec3 velocity;
		origin.z += (5*GAME_SCALE);
		origin += (kVec3(0.07f*GAME_SCALE, 2.9f*GAME_SCALE, -0.6f*GAME_SCALE) * rotation);
		m_bArrowFlashed = false;
		if(time >= 1.4f && time <= 2.15f)
			velocity = kVec3(0, 512 * 15, 0) * rotation;
		else
		{
			if(time > 0.7f) time = 0.7f;
			velocity = kVec3(0, (512 * time / 1.4f) * 15, 0) * rotation;
		}
		velocity *= GAME_DELTA_TIME;
		if(self.Owner().HasAltAmmo())
		{
			Game.SpawnFx("fx/arrow_explosive.kfx", src, velocity, origin, rotation);
			self.PlaySound("sounds/shaders/arrow_fly_tek.ksnd");
		}
		else
		{
			Game.SpawnFx("fx/arrow.kfx", src, velocity, origin, rotation);
			self.PlaySound("sounds/shaders/arrow_fly_normal.ksnd");
		}
		self.PlaySound("sounds/shaders/bow_twang.ksnd");
		self.AnimState().Set(anim_weaponFireCharged, 4.0f, ANF_NOINTERRUPT);
		self.Owner().ConsumeAmmo(1);
	}
}
final class TurokAlienRifle : TurokWeapon
{
	TurokAlienRifle(kWeapon @actor) { super(actor); }
	void OnBeginFire(void)
	{
		self.AnimState().Set(anim_weaponFire, 4.0f, 0);
		self.PlaySound("sounds/shaders/tek_weapon_1.ksnd");
		self.FireProjectile("fx/plasma1.kfx", 4.096f, 25.696f, -14.336f);
		self.Owner().ConsumeAmmo(5);
	}
}
