
enum ePulseAnims
{
	anim_Pulse_Fire       = anim_weaponFireLoop,
	anim_Pulse_Beam_Start = anim_weaponAttack1,
	anim_Pulse_Beam_Loop  = anim_weaponAttack2,
	anim_Pulse_Beam_End   = anim_weaponAttack3
}

enum ePulseFrames
{
	Pulse_SwapIn1      =   0, Pulse_SwapIn2      =  31,
	Pulse_SwapOut1     =  31, Pulse_SwapOut2     =  47,
	Pulse_Fire1        = 137, Pulse_Fire2        = 189,
	Pulse_Beam_Start_1 =  97, Pulse_Beam_Start_2 = 107,
	Pulse_Beam_Loop_1  = 107, Pulse_Beam_Loop_2  = 126,
	Pulse_Beam_End_1   = 127, Pulse_Beam_End_2   = 137,
	Pulse_SpinDown1    =  48, Pulse_SpinDown2    =  96,
	Pulse_Idle         = 137
}

final class TurokPulseRifle : TurokWeapon
{
	int PulsePeriod = 11;
//	int BeamPeriod  =  7; // 1.57x ammo use
//	float BeamDPS = 1.25f * 12 * 60 / PulsePeriod; // 1.25x of 12 damage proj DPS
	int BeamPeriod  =  8; // 1.375x ammo use
//	int BeamPeriod  =  9; // 1.222x ammo use
	float BeamDPS = 12.0f * 60 / PulsePeriod; // match 12 damage proj DPS
	//int BeamPeriod  = 11;
	//float BeamDPS = 0.75f * 12.0f * 60 / PulsePeriod; // 75% of 12 damage proj DPS

	int fireState, shotTick;
	float recoilPitch, recoilRoll;
	kAngle angle;
	float damage;
	bool bSpinDown = false;
	array<kActor@> beam(10);
//	kVec3 altOffset( 8, 30, -5 );
//	kVec3 altOffset( 7.5f, 15, -5.5f );
	kVec3 altOffset( 7.3f, 30, -5.6f );

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

	void OnPostBeginLevel()
	{
		kStr str = "PulseBeam";
		for ( uint i=0; i<beam.length(); ++i )
			@beam[i] = ActorFactory.Spawn( i > 0 ? str : (str + "_Start"), Player.Actor().Origin().x, Player.Actor().Origin().y, Player.Actor().Origin().z, 0, Player.Actor().SectorIndex() );
		beam[0].Scale().y *= 2.0f/3;
	}

	void AnimProperties( int&out first, int&out last, float&out rate, bool&out bLoop )
	{
		rate = 60; bLoop = false;
		switch ( PlayingID() )
		{
			case anim_weaponSwapIn:     first = Pulse_SwapIn1;      last = Pulse_SwapIn2;      break;
			case anim_weaponSwapOut:    first = Pulse_SwapOut1;     last = Pulse_SwapOut2;     break;
			case anim_Pulse_Fire:       first = Pulse_Fire1;        last = Pulse_Fire2;        bLoop = true; break;
			case anim_Pulse_Beam_Start: first = Pulse_Beam_Start_1; last = Pulse_Beam_Start_2; break;
			case anim_Pulse_Beam_Loop:  first = Pulse_Beam_Loop_1;  last = Pulse_Beam_Loop_2;  bLoop = true; break;
			case anim_Pulse_Beam_End:   first = Pulse_Beam_End_1;   last = Pulse_Beam_End_2;   break;
			default: /* idle */
			         if ( bSpinDown ) { first = Pulse_SpinDown1;    last = Pulse_SpinDown2; }
			                     else { first =                     last = Pulse_Idle;      }  bLoop = true; break;
		}
	}

	int ModFrame( int v )
	{
		switch ( PlayingID() )
		{
			case anim_weaponWalk: case anim_weaponRun: case anim_weaponIdle:
				if ( bSpinDown && v > Pulse_SpinDown2 )
					return Pulse_SpinDown2;
		}
		return v;
	}

	void AnimEnd()
	{
		if ( IsPlaying( anim_Pulse_Beam_Start ) )
			PlayAnim( anim_Pulse_Beam_Loop );
		else
			TurokWeapon::AnimEnd();
	}

	bool Refire()
	{
		return IsPlaying( anim_Pulse_Beam_End );
	}

	void OnBeginFire()
	{
		StopWeaponSound(); // cut spin-down sound short
		self.StopLoopingSounds(); // stop other mode's fire sound when switching between them
		// primary takes precedence if both are pressed
		if ( bFire1 )
		{
			PlayAnim( anim_Pulse_Fire );
			fireState = 1;
			angle = Math::Deg2Rad( 45 );
		}
		else
		{
			PlayAnim( anim_Pulse_Beam_Start );
			fireState = 2;
		}
		shotTick = -60;
	}

	void AmmoLED()
	{
		int ammo = GetAmmo();
		int red = ammo < 10 ? 10 : 0;
		for ( int pos=1; pos<=3; ++pos )
		{
			int digit = (ammo % 10) + red;
			for ( int i = pos == 1 ? 19 : 10; i >= 0; --i )
				self.RenderModel().HideSection( pos, i, i != digit );
			ammo /= 10;
		}
		// surface 0 corresponds to 1 ammo, surface 24 corresponds to 25 ammo
		ammo = GetAmmo() - 1;
		if ( ammo > 24 ) ammo = 24;
		for ( int i=0; i<25; ++i )
			self.RenderModel().HideSection( 4, i, i != ammo );
	}

	void OnTick()
	{
		TurokWeapon::OnTick();
		AmmoLED();
		Recoil();
		if ( self.Owner().Locked() )
		{
			self.StopLoopingSounds(); // stop fire sound
			HideBeam();
			return;
		}
		switch ( PlayingID() )
		{
			// OnFire() isn't called when ammo is empty and fire is held, so call it manually in such a case
			// could just always call NormalFire()/AltFire() from here, but that causes projectiles to not render until a tick later
			case anim_Pulse_Fire:
			case anim_Pulse_Beam_Start:
			case anim_Pulse_Beam_Loop:
				if ( !HasAmmo() && Player.Buttons() & BC_ATTACK != 0 )
					OnFire();
				break;
			// check if a forced swap-out (climbing, swimming) interrupted firing
			case anim_weaponSwapOut:
			// jumping off a wall at just the wrong time can prevent forced swap-out from playing
			// so cancel everything during swap-in, too
			case anim_weaponSwapIn:
				if ( fireState > 0 ) StopFiring();
				bSpinDown = false;
		}
	}

	void Recoil()
	{
		float f = self.GameTicks() - shotTick;
		int period = IsPlaying(anim_Pulse_Fire) ? PulsePeriod : BeamPeriod;
		if ( f > period ) return;
		f = 1 - f / period;
		if ( IsPlaying(anim_Pulse_Fire) )
			f *= f*f;
		f = Math::Sin( Math::pi * f );
		OwnerP().RecoilPitch() = f * recoilPitch;
		OwnerP().Roll()        = f * recoilRoll;
	}

	void SetRecoil()
	{
//		recoilPitch = -0.008f * (1 + Math::RandFloat()) / 2;
//		float r     = -0.004f * (1 + Math::RandFloat()) / 2;
		float p = -0.009f * (1 + Math::RandFloat()) / 2;
		float r = -0.006f * (1 + Math::RandFloat()) / 2;
		if ( !IsPlaying(anim_Pulse_Fire) )
		{
			if ( recoilPitch < 0 ) p = -p;
			p /= 2;
			r *= 1.5f;
		}
		recoilPitch = p;
		recoilRoll = recoilRoll < 0 ? -r : r;
	}

	void OnFire()
	{
		switch ( PlayingID() )
		{
			case anim_Pulse_Fire:       NormalFire(); break;
			case anim_Pulse_Beam_Start:
			case anim_Pulse_Beam_Loop:  AltFire();    break;
		}
	}

	void NormalFire()
	{
		self.PlaySound( "UT/sounds/Pulse/Fire1.ksnd" );
		if ( self.GameTicks() - shotTick < PulsePeriod )
			return;
		if ( !bFire || !HasAmmo() )
		{
			StopFiring();
			return;
		}
		if ( bFire2 && !bFire1 )
		{
			OnBeginFire();
			return;
		}
		shotTick = self.GameTicks();
		kVec3 v( 8, 30, -5 );
		v += kVec3( 0, 0, 2 ) * kQuat( angle, 0,1,0 );
		angle += 1.8f;
		kStr proj = "UT/fx/Pulse/Proj";
		if ( UDamage() ) proj += "_Amp";
		self.FireProjectile( proj + ".kfx", v.x, v.y, v.z );
		self.RunFxEvent( "UT_PulseFire" );
		OwnerP().LoudNoiseAlert();
		SetRecoil();
		UseAmmo( 1 );
	}

	void AltFire()
	{
		// don't stop until beamStart finishes
		if ( (!bFire2 || !HasAmmo()) && IsPlaying(anim_Pulse_Beam_Loop) )
		{
			StopFiring();
			return;
		}
		self.PlaySound( "UT/sounds/Pulse/Fire2.ksnd" );
		self.RunFxEvent( "PulseBeam" );
		if ( self.GameTicks() - shotTick >= BeamPeriod )
		{
			UseAmmo( 1 );
			UDamage(); // play amped fire sound
			shotTick = self.GameTicks();
			SetRecoil();
		}

		kVec3 dir = kVec3( 0, 1, 0 ) * OwnerP().Rotation();
		kVec3 start = CheckPosition( EyePos() + altOffset * OwnerP().Rotation() );
		kVec3 end = EyePos() + dir*60*GAME_SCALE;

		// no hit
		kStr fx = "UT/fx/Pulse/Beam_";
		int frame = self.GameTicks() / 2 % 4;
		if ( !Hitscan( start, end ) )
		{
			Game.SpawnFx( fx + "End_" + frame + ".kfx", OwnerP(), end, ToQuat(-dir) );
			UpdateBeam( start, end );
			return;
		}
		if ( CModel.ContactActor() is OwnerA() )
		{
			HideBeam();
			return;
		}

		// save this stuff, since CModel gets overwritten when spawning fx
		kActor@ a = CModel.ContactActor();
		kVec3 hitLocation = CModel.InterceptVector();
		kVec3 hitNormal = CModel.ContactNormal();
		bool bHitMesh = CModel.ClipResult() & (1<<4) != 0;

		UpdateBeam( start, hitLocation );

		// general hit
		Game.SpawnFx( fx + "Hit_" + frame + ".kfx", OwnerP(), hitLocation - dir*8, ToQuat(hitNormal) );

		// hit level
		if ( CModel.ContactActor() is null )
		{
			if ( !bHitMesh )
				Game.SpawnFx( "UT/fx/Pulse/Beam_Scorch.kfx", OwnerP(), hitLocation, ToQuat(hitNormal) );
			return;
		}

		// hit actor
		float inc = BeamDPS * GAME_DELTA_TIME;
		if ( UDamRemaining() > 0 ) inc *= 3;
		if ( a.InstanceOf("kexAI") )
		{
			switch ( Game.GetDifficulty() )
			{
				case DIFFICULTY_EASY:     inc *= 1.4f; break;
				case DIFFICULTY_HARD:
				case DIFFICULTY_HARDCORE: inc *= 0.7f; break;
			}
		}
		for ( damage += inc; damage >= 1; damage -= 1 )
			a.InflictGenericDamage( OwnerA(), 1 );
	}

	void StopFiring()
	{
		self.StopLoopingSounds(); // stop fire sound
		bSpinDown = fireState == 1;
		// normal fire
		if ( fireState == 1 )
		{
			PlayWeaponSound( "UT/sounds/Pulse/SpinDown.ksnd" );
			if ( IsPlaying(anim_Pulse_Fire) )
				PlayAnim( anim_weaponIdle );
		}
		// alt fire
		else
		{
			if ( IsPlaying(anim_Pulse_Beam_Loop) )
				PlayAnim( anim_Pulse_Beam_End );
			HideBeam();
		}
		fireState = 0;
	}

	void UpdateBeam( kVec3 start, kVec3&in end )
	{
		kVec3 offset = kVec3( altOffset.x, 0, altOffset.z ).Normalize() * 2;
		// hitscan is less likely to hit player if it starts further forward
		// so move beam back from actual hitscan origin to its proper position
		offset.y = 11 - altOffset.y;
		offset += self.RenderModel().Offset() * 0.16f;
		start += offset * OwnerP().Rotation();
		kVec3 dif = end - start;
		float dist = dif.Unit();
		if ( dist < 21 )
		{
			HideBeam();
			return;
		}
		kVec3 dir = dif / dist;
		float delta = 81;
		kVec3 vDelta = dir * delta;
		kQuat q = ToQuat( dif );
		q = q * kQuat( self.GameTicks() / 10.0f, 0,1,0 );
		int frame = (self.GameTicks()/3) % 5;
		uint i = 0;
		while ( i < beam.length() && dist > 0 )
		{
			beam[i].Origin() = start;
			beam[i].Rotation() = q;
			beam[i].ModelVariation() = frame;
			beam[i].Flags() &= ~AF_HIDDEN;
			if ( i == 0 )
			{
				start += vDelta * 2/3;
				dist -= delta * 2/3;
			}
			else
			{
				start += vDelta;
				dist -= delta;
			}
			++i;
		}
		while ( i < beam.length() )
			beam[i++].Flags() |= AF_HIDDEN;
	}

	void HideBeam()
	{
		for ( uint i=0; i<beam.length(); ++i )
			beam[i].Flags() |= AF_HIDDEN;
	}

	bool Hitscan( kVec3&in start, kVec3&in end )
	{
		// fallbacks if we hit nothing
		kVec3 hitLocation = end, hitNormal = (start-end).Normalize();
		bool bHit = false;
		Dummy1.SetPosition( start );
		Dummy2.SetPosition( end );
		// CanSee() skips actors/statics if it hits the level, so check level collision by itself first
		if ( !Dummy1.CanSee( Dummy2, CF_IGNOREBLOCKERS ) )
		{
			bHit = true;
			// 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 )
			{
				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 );
				}
			}
			hitLocation = CModel.InterceptVector();
			hitNormal = CModel.ContactNormal();
			end = hitLocation;
		}
		// now check for actors
		Dummy2.SetPosition( end );
		if ( !Dummy1.CanSee( Dummy2, CF_CLIPEDGES|CF_COLLIDEFLOORS|CF_NOCLIPSTATICS|CF_NOCLIPACTORS ) )
			return true;
		// retore results of first hitscan if we hit no actors
		CModel.InterceptVector() = hitLocation;
		CModel.ContactNormal() = hitNormal;
		return bHit;
	}
}
