
class Q2_Projectile : ScriptObject
{
	kActor@ self;
	float timeScale = 1;

	Q2_Projectile( kActor@ a )
	{
		@self = a;
	}

	void OnSpawn()
	{
		// CF_COLLIDEFLOORS - prevents proj from jumping up slopes on collision?
		// CF_NOCEILINGADJUST results in slightly more consistent ceiling adjustment
		self.ClipFlags() = CF_CLIPEDGES | CF_DROPOFF | CF_ALLOWCRAWL | CF_COLLIDECORPSES | CF_NOCEILINGADJUST | CF_COLLIDEFLOORS;
		self.Rotation() = Player.Actor().Rotation();
		// make sure it doesn't freeze in mid air at long range
		self.Flags() |= AF_ALWAYSACTIVE;
	}

	void OnTick()
	{
		timeScale = self.Movement().Unit() / self.Velocity().Unit();
	}

	void OnCollide( kCModel@ cModel )
	{
		if ( cModel.ContactActor() is Player.Actor().CastToActor() )
			return;
		if ( cModel.ClipResult() & (1<<2) != 0 ) // wall
		{
			// where we were
			Dummy1.SetPosition( self.PrevOrigin() );
			// where we should be
			Dummy2.SetPosition( self.PrevOrigin() + self.Velocity() * timeScale );
			// check if there's really an obstacle
			// if not, that means we really just hit the edge of an AAF_RESTRICTED sector
			if ( Dummy1.CanSee( Dummy2, CF_IGNOREBLOCKERS | CF_NOCLIPSTATICS | CF_NOCLIPACTORS ) )
			{
				self.SetPosition( Dummy2.Origin() );
				return;
			}
		}
		Collision( cModel.ContactActor(), cModel.InterceptVector(), cModel.ContactNormal(), cModel.ClipResult() );
	}

	void Collision( kActor@ other, kVec3&in hitLoc, kVec3&in hitNormal, uint clipResult )
	{
	}
}

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

const float BFG_BeamRadius   =  50*GAME_SCALE;
const float BFG_DirectRadius =  20*GAME_SCALE;
const float BFG_BlastRadius  = 125*GAME_SCALE;

class BFG_Projectile : Q2_Projectile
{
	bool bQuad, bExplode = false;
	int SpawnTick, LastTick;
	array<kActor@> checked, hits, beams, cumulative;

	BFG_Projectile( kActor@ a )
	{
		super( a );
	}

	// clean up our beams any time we're removed
	~BFG_Projectile()
	{
		for ( uint i=0; i<beams.length(); ++i )
			if ( beams[i] !is null && !beams[i].IsStale() )
				beams[i].Remove();
		beams.resize( 0 );
	}
	// beams are already being removed, so prevent destructor from trying to remove stale handles
	void OnEndLevel()
	{
		beams.resize( 0 );
	}

	void OnSpawn()
	{
		Q2_Projectile::OnSpawn();

		bQuad = QuadDamage();
		SpawnTick = LastTick = PlayLoop.Ticks();
		self.Velocity() = kVec3( -0.0075f, 1, 0.005f ).Normalize(); // spawn offset / 2000
		self.Velocity() = self.Velocity() * 10 * self.Rotation();
	}

	void InteractRadius( float radius, int rings, const kStr&in callback,
		float f1=0, float f2=0, float f3=0, float f4=0 )
	{
		checked.resize( 0 );
		self.InteractActorsAtPosition( self.Origin(), callback, f1, f2, f3, f4 );
		int r2 = 4;
		for ( int r=1; r<=rings; ++r )
		{
			kVec3 offset( 0, r*radius/rings, 0 );
			int yMax = 1 + 4*r; // yaws when p == 0
			for ( int p=-r; p<=r; ++p )
			{
				kQuat pitch( Math::Deg2Rad( 90.0f * p/r ), 1,0,0 );
				int yaws = yMax - 4*Math::Abs(p);
				for ( int y=0; y<yaws; ++y )
				{
					kQuat q = kQuat( Math::Deg2Rad( 360.0f*y/yaws ), 0,0,1 ) * pitch;
					self.InteractActorsAtPosition( self.Origin() + offset*q, callback, f1, f2, f3, f4 );
//					if ( rings > 3 )
//						Game.SpawnFx( "fx/spark.kfx", self, self.Origin() + offset*q, self.Rotation() );
				}
			}
		}
	}

	void OnTick()
	{
		if ( bExplode || self.InWater() )
		{
			InteractRadius( BFG_BlastRadius, 5, "SplashDamage" );
			for ( uint a=0; a<cumulative.length(); ++a )
				if ( cumulative[a] !is null && !cumulative[a].IsStale() )
					SplashDamage( cumulative[a], 0,0,0,0 );
			self.SpawnFx( "fx/BFG_Explosion.kfx", Math::vecZero );
			self.PlaySound( "sounds/wep/BFG/Explode.ksnd" );
			self.Velocity() = Math::vecZero; // prevent sound from flying off if we hit water
			self.Remove();
			return;
		}

		Q2_Projectile::OnTick();

		// make sure none of the actors we've beamed have been removed
		for ( uint a=0; a<cumulative.length(); ++a )
			if ( cumulative[a] is null || cumulative[a].IsStale() )
				cumulative.removeAt(a--);

		float bBeamDamage = 0;
		if ( PlayLoop.Ticks() != LastTick )
		{
			// 10s life span
			if ( PlayLoop.Ticks() - SpawnTick >= 10*60 )
			{
				self.Remove();
				return;
			}
			// orbiting particles
			self.SpawnFx( "fx/BFG_Particle.kfx", Math::vecZero );
			if ( PlayLoop.Ticks() % 6 == 0 )
				bBeamDamage = 1;
			LastTick = PlayLoop.Ticks();
		}

		// orbiting particles follow a frame behind, but main proj appears exactly in place
		// so spawn the main particle a frame behind to match
		kStr str = "fx/BFG";
		str = str + ( ((PlayLoop.Ticks()/5) % 2) + 1 ) + ".kfx";
		Game.SpawnFx( str, self, self.PrevOrigin(), self.Rotation() );
		self.PlaySound( "sounds/wep/BFG/Fly.ksnd" );

		hits.resize( 0 );
		// beams also don't lag, so send prev origin so they can match everything else
		InteractRadius( BFG_BeamRadius, 2, "BeamDamage", self.PrevOrigin().x, self.PrevOrigin().y, self.PrevOrigin().z, bBeamDamage );
		// hide any leftover beams that aren't currently in use
		for ( uint i=hits.length(); i<beams.length(); ++i )
			beams[i].Flags() |= AF_HIDDEN;
	}

	void Collision( kActor@ other, kVec3&in hitLoc, kVec3&in hitNormal, uint clipResult )
	{
		if ( bExplode ) return;
		// spawning gibs from here sometimes spawns from hit location
		// so instead delay until OnTick(), which the engine seems to like better
		bExplode = true;
		self.Velocity() = Math::vecZero;
	}

	void SplashDamage( kActor@ a, const float f1, const float f2, const float f3, const float f4 )
	{
		if ( a is null || a.IsStale() ) return;
		if ( a.Flags() & AF_HOLDTRIGGERANIM != 0 ) return;
		if ( checked.findByRef(a) >= 0 ) return;
		checked.insertLast( a );

		// can't see into bridge sectors, but can see out of bridge sectors
		// so require LOS to be blocked both ways to reject
		if ( !self.CanSee(a,CF_IGNOREBLOCKERS) && !a.CanSee(self,CF_IGNOREBLOCKERS) )
			return;

		kVec3 origin = a.Origin();
		origin.z += a.Height() / 2;

		kVec3 dif = origin - self.Origin();
		float dist = dif.Unit() - a.Radius();
		if ( dist > BFG_BlastRadius ) return;
		if ( dist < 0 ) dist = 0;

		if ( a is Player.Actor().CastToActor() )
		{
			if ( dist < BFG_DirectRadius )
			{
				int damage = 50;
				if ( bQuad ) damage *= 4;
				damage = int( damage * (1 - dist/BFG_DirectRadius) );
				a.InflictGenericDamage( Player.Actor().CastToActor(), damage );
			}
			return;
		}

		kVec3 hit = origin + (EyePos() - origin).Normalize() * a.Radius();
		Game.SpawnFx( "fx/BFG_Hit.kfx", self, hit, self.Rotation() );

		int damage = 500;
		if ( bQuad ) damage *= 4;
		damage = int( damage * (1 - dist/BFG_BlastRadius) );

		if ( a.ScriptObject() !is null && cast<TurokDestructible@>(a.ScriptObject().obj) !is null )
		{
			if ( a.AnimState().PlayingID() == anim_destructibleDeath )
				return;
			a.AnimState().Blend( anim_destructibleDeath, 4.0f, 4.0f, 0 );
			a.Flags() &= ~AF_SOLID;
			a.MarkPersistentBit( false );
		}
		else
		{
			if ( a.Flags() & AF_DEAD == 0 )
				a.InflictGenericDamage( Player.Actor().CastToActor(), damage );
			// dead actors ignore damage, but we still want to deal overkill damage to potentially gib them
			// so subtract health manually
			else
			{
				if ( a.InstanceOf("kexAI") )
				{
					switch ( Game.GetDifficulty() )
					{
						case DIFFICULTY_EASY:     damage = int( damage * 1.4f ); break;
						case DIFFICULTY_HARD:
						case DIFFICULTY_HARDCORE: damage = int( damage * 0.7f );
					}
				}
				a.Health() -= damage;
			}
			GibActor( a, dif.Normalize() );
		}
	}

	void BeamDamage( kActor@ a, const float x, const float y, const float z, const float bDamage )
	{
		if ( a is null || a.IsStale() ) return;
		if ( a is Player.Actor().CastToActor() ) return;
		if ( a.ScriptObject() !is null && cast<TurokDestructible@>(a.ScriptObject().obj) !is null ) return;
		if ( a.Flags() & AF_HOLDTRIGGERANIM != 0 ) return;
		if ( checked.findByRef(a) >= 0 ) return;
		checked.insertLast( a );

		// can't see into bridge sectors, but can see out of bridge sectors
		// so require LOS to be blocked both ways to reject
		if ( !self.CanSee(a,CF_IGNOREBLOCKERS) && !a.CanSee(self,CF_IGNOREBLOCKERS) )
			return;

		kVec3 origin = a.Origin();
		origin.z += a.Height() / 2;

		kVec3 dif = origin - self.Origin();
		if ( dif.Unit() - a.Radius() > BFG_BeamRadius ) return;
		hits.insertLast( a );
		if ( cumulative.findByRef(a) < 0 ) cumulative.insertLast( a );

		if ( beams.length() < hits.length() )
			beams.insertLast( ActorFactory.Spawn( "BFGBeam", self.Origin().x, self.Origin().y, self.Origin().z, 0, self.SectorIndex() ) );
		kActor@ beam = beams[ hits.length()-1 ];
		beam.Origin().Set( x, y, z );
		dif = origin - beam.Origin();
		beam.Rotation() = ToQuat( dif );
		beam.Scale().y = dif.Unit();
		beam.Flags() &= ~AF_HIDDEN;

		if ( bDamage > 0 )
		{
			int damage = 5;
			if ( bQuad ) damage *= 4;
			a.InflictGenericDamage( Player.Actor().CastToActor(), damage );
		}
	}
}
