
enum eSniperFrames
{
	Sniper_SwapIn1  =   0, Sniper_SwapIn2  =  32,
	Sniper_SwapOut1 = 212, Sniper_SwapOut2 = 225,
	Sniper_Fire1_1  =  32, Sniper_Fire1_2  =  68,
	Sniper_Fire2_1  =  68, Sniper_Fire2_2  = 104,
	Sniper_Fire3_1  = 104, Sniper_Fire3_2  = 140,
	Sniper_Fire4_1  = 140, Sniper_Fire4_2  = 176,
	Sniper_Fire5_1  = 176, Sniper_Fire5_2  = 212,
	Sniper_Idle     =  32
}

final class TurokRifle : TurokWeapon
{
	float MinZoom = 3, MaxZoom = 9;
	float ZoomSpeed = 1.015f;

	float scopeMag;
	kAngle oldPlayerPitch, oldPlayerYaw, oldPlayerRoll;
	int lastScopeTick, scopeDeflicker;

	TurokRifle( kWeapon @a )
	{
		super( a );
		BobDamping = 0.98f;
	}

	void AnimProperties( int&out first, int&out last, float&out rate, bool&out bLoop )
	{
		rate = 60; bLoop = false;
		switch ( PlayingID() )
		{
			case anim_weaponSwapIn:      first = Sniper_SwapIn1;  last = Sniper_SwapIn2;  break;
			case anim_weaponSwapOut:     first = Sniper_SwapOut1; last = Sniper_SwapOut2; break;
			case anim_weaponAttack1:     first = Sniper_Fire1_1;  last = Sniper_Fire1_2;  break;
			case anim_weaponAttack2:     first = Sniper_Fire2_1;  last = Sniper_Fire2_2;  break;
			case anim_weaponAttack3:     first = Sniper_Fire3_1;  last = Sniper_Fire3_2;  break;
			case anim_weaponFire:        first = Sniper_Fire4_1;  last = Sniper_Fire4_2;  break;
			case anim_weaponFireCharged: first = Sniper_Fire5_1;  last = Sniper_Fire5_2;  break;
			default: /* idle */          first = last = Sniper_Idle; bLoop = true; break;
		}
	}

	bool FireAnim()
	{
		switch ( PlayingID() )
		{
			case anim_weaponAttack1: case anim_weaponAttack2: case anim_weaponAttack3:
			case anim_weaponFire: case anim_weaponFireCharged:
				return true;
		}
		return false;
	}

	bool Refire()
	{
		return FireAnim();
	}

	void OnBeginFire()
	{
		if ( !bFire1 )
		{
			BlockFireState();
			return;
		}

		PlayAnim( Math::RandMax(5) + anim_weaponAttack1 );
		self.PlaySound( "UT/sounds/Rifle/Fire.ksnd" );
		self.FireProjectile( "UT/fx/Rifle/Shell.kfx", 10, 28, -10, true );
		kVec3 offset( 1/0.7f, 5, -1 );
		if ( PlayerScoped() )
		{
			offset.x = 0;
			UpdateScopeRotation();
		}
		else
			self.FireProjectile( "UT/fx/Rifle/Flash.kfx", 0,0,0 );
		kStr proj = "UT/fx/Rifle/Bullet";
		if ( !bTurokPlusInstalled ) proj += "_Minus";
		if ( UDamage() ) proj += "_Amp";
		self.FireProjectile( proj + ".kfx", offset.x, offset.y, offset.z );
//		self.FireProjectile( "UT/fx/Tracer.kfx", offset.x, offset.y, offset.z );
		self.RunFxEvent( "GunFire" );
		OwnerP().LoudNoiseAlert();
		UseAmmo( 1 );
	}

	void Recoil()
	{
		if ( !FireAnim() || PlayTime() > 0.5f )
			return;

		float f = 1 - PlayTime() / 0.5f;
		f *= f * f;
		float p = Math::Sin( Math::pi * f ) * f;// * -0.03f;
		float r = Math::Sin( 2*Math::pi * f ) * f;// * 0.02f;
		switch ( PlayingID() )
		{
			case anim_weaponAttack1:      p *= -0.04;  r *=  0.03f;   break;
			case anim_weaponAttack2:      p *= -0.04;  r *=  0.01f;   break;
			case anim_weaponAttack3:      p *= -0.04;  r *= -0.015f;  break;
			case anim_weaponFire:         p *= -0.03;  r *=  0.02f;   break;
			case anim_weaponFireCharged:  p *= -0.03;  r *= -0.02f;   break;
		}
		OwnerP().RecoilPitch() = p;
		OwnerP().Roll() = r;
	}

	void OnTick()
	{
		bool bOldFire2 = bFire2;

		TurokWeapon::OnTick();

		Recoil();

		// pressed fire during gameplay
		if ( bFire2 && !bOldFire2 && !self.Owner().Locked() )
		{
			if ( Camera.Active() )
				CancelScope();
			else if ( CanScope() && (!IsPlaying(anim_weaponSwapIn)
			|| (self.ModelVariation() - Sniper_SwapIn1) > (Sniper_SwapIn2 - Sniper_SwapIn1) / 2) )
				StartScope();
		}

		ScopeTick();
	}

	void ScopeTick()
	{
		if ( !PlayerScoped() )
			return;
		if ( !CanScope() )
		{
			CancelScope();
			return;
		}

		if ( scopeMag < MinZoom || bFire2 || (self.Owner().Buttons() & BC_MAPZOOMIN != 0 && !bFire) )
		{
			scopeMag *= ZoomSpeed;
			if ( scopeMag > MaxZoom ) scopeMag = MaxZoom;
		}
		else if ( self.Owner().Buttons() & BC_MAPZOOMOUT != 0 && !bFire )
		{
			scopeMag /= ZoomSpeed;
			if ( scopeMag < MinZoom ) scopeMag = MinZoom;
		}

		kStr str;
		Camera.fov = ( Sys.GetCvarValue( "r_fov", str ) ? str.Atof() : 74 ) / scopeMag;

		ApplyScopeSensitivity();
		Camera.origin    = EyePos();
		Camera.pitch     = oldPlayerPitch;
		Camera.yaw       = oldPlayerYaw;
		Camera.roll      = oldPlayerRoll;

		float f = (1 + scopeMag) / 2;
		Camera.pitch += OwnerP().RecoilPitch() / f;
		Camera.origin.z += OwnerP().RecoilPitch() * 100 / f;

		ScopeView();
	}

	void ScopeView()
	{
		if ( self.AnimState().CycleCompleted() && self.GameTicks() - scopeDeflicker > 2 )
			scopeDeflicker = self.GameTicks();
		// skip first call, except for last tick of CycleCompleted, which only has one call
		// GameTicks is the same for tick after CycleCompleted, so also don't update lastScopeTick,
		// so that the tick after CycleCompleted won't spawn twice
		if ( self.GameTicks() != lastScopeTick && (!self.AnimState().CycleCompleted() || self.GameTicks() - scopeDeflicker != 2) )
		{
			lastScopeTick = self.GameTicks();
			return;
		}

		float distScale = 1.0f / Math::Tan( Math::Deg2Rad( Camera.fov ) / 2.0f );

		float p = self.RenderModel().Offset().z * 0.02f + OwnerP().Velocity().z * 0.005f;
		float y = self.RenderModel().Offset().x * 0.01f;
		float t = 0;
		if ( FireAnim() )
		{
			float f = PlayTime();
			if ( f < 0.3f )
			{
				f = 1 - f / 0.3f;
				f = f * f;
				p -= Math::Sin( Math::pi * f ) * f*f * 0.1f;
				y += Math::Sin( Math::pi * f*f ) * f * 0.025f;
				t -= Math::Sin( Math::pi * f*f*f ) * distScale * 0.1f;
			}
		}
		p /= distScale;
		y /= distScale;

		p = 0;
		y = 0;
		t = 0;

		kQuat rot = kQuat( Camera.yaw   + y, 0,0,1 ) *
		            kQuat( Camera.pitch + p, 1,0,0 ) *
		            kQuat( Camera.roll,      0,1,0 );
		kVec3 loc = Camera.origin + kVec3( 0, distScale + t, 0 ) * rot;
		Game.SpawnFx( "UT/fx/Rifle/Reticle.kfx", OwnerP(), loc, rot );
	}

	//----------------------------------

	// conditions common to both allowing zooming in, and zooming out if not met
	// additional conditions specific to one or the other are in OnTick()
	bool CanScope()
	{
		return !IsPlaying(anim_weaponSwapOut);
	}

	void StartScope()
	{
		Camera.StartCinematic( CMF_NO_LETTERBOX | CMF_SHOW_HIDDEN_OBJECTS | CMF_NO_INITIAL_FADEOUT );
		// at least one of these allows camera position to be updated in OnTick() after key acquisition cutscene
		Camera.ClearLookAtActor();
		Camera.ClearFinalView();
		Camera.ClearViewTracks();
		oldPlayerPitch = self.Owner().Actor().Pitch();
		oldPlayerYaw   = self.Owner().Actor().Yaw();
		oldPlayerRoll  = self.Owner().Actor().Roll();
		// spend 9 ticks zooming in to min level, so there'll be a grace period for the player to let go of alt fire before zooming in further
		scopeMag = MinZoom / Math::Pow( ZoomSpeed, 9*2 );
	}

	void ApplyScopeSensitivity()
	{
		kPuppet @owner = OwnerP();
		float f = 1.0f / scopeMag;
		owner.Pitch() = oldPlayerPitch = oldPlayerPitch.Interpolate( owner.Pitch(), f );
		owner.Yaw()   = oldPlayerYaw   = oldPlayerYaw.Interpolate(   owner.Yaw(),   f );
		// kill turn bob if enabled (also kills screen shake, so leave it alone if turn bob is off)
		kStr str;
		if ( !Sys.GetCvarValue( "g_turnbob", str ) || str.Atoi() != 0 )
			owner.Roll() = 0;
		oldPlayerRoll = owner.Roll();
	}
	// apply scope sensitivity to owner rotation (use this when spawning projectile)
	void UpdateScopeRotation()
	{
		ApplyScopeSensitivity();
		self.Owner().Actor().Rotation() =
			kQuat( oldPlayerYaw, 0,0,1 ) * kQuat( oldPlayerPitch, 1,0,0 ) * kQuat( oldPlayerRoll, 0,1,0 );
	}
}
