/**
	Lightning Bolt
	Launch a single bolt of lightning which splits up into smaller branches.
	
	@author Maikel
*/

local xdir, ydir;
local xdev, ydev;
local strength;
local launcher;

// Creates a lightning bolt at the specified location. 
// x: X coordinate, always global.
// y: Y coordinate, always global.
// strength: Strength of the bolt, 0 - 100.
// xdir: Average horizontal speed of the bolt.
// ydir: Average vertical speed of the bolt.
// xdev: Maximum deviation from the average horizontal speed.
// ydev: Maximum deviation from the average vertical speed.
// no_sound: Whether to not play a sound.
global func LaunchLightning(int x, int y, int strength, int xdir, int ydir, int xdev, int ydev, bool no_sound)
{
	var lightning = CreateObject(Lightning, x - GetX(), y - GetY());
	// Ignore the launching object if not called from effect, scenario, etc..
	var launching_object = nil;
	if (this && GetType(this) == C4V_C4Object)
		launching_object = this;
	return lightning && lightning->Launch(x, y, strength, xdir, ydir, xdev, ydev, no_sound, launching_object);
}

public func Launch(int x, int y, int to_strength, int to_xdir, int to_ydir, int to_xdev, int to_ydev, bool no_sound, to_launcher)
{
	xdir = to_xdir; ydir = to_ydir;
	xdev = to_xdev; ydev = to_ydev;
	strength = to_strength ?? 20;
	launcher = to_launcher;
	AddVertex(x - GetX(), y - GetY());
	AddEffect("LightningMove", this, 1, 1, this, nil, !no_sound);
	return true;
}

protected func FxLightningMoveStart(object target, effect fx, int temp, bool play_sound)
{
	if (temp)
		return FX_OK;
	fx.play_sound = play_sound;
	if (play_sound && strength > 30)
		Sound("Environment::Lightning::Thunder?", false, strength);
	return FX_OK;
}

protected func FxLightningMoveTimer(object target, effect fx, int time)
{
	// Calculate new coordinates to move to.
	var vertices = GetVertexNum();
	var oldx = GetVertex(vertices - 1, 0);
	var oldy = GetVertex(vertices - 1, 1);
	var newx = oldx + xdir + xdev - Random(2 * xdev);
	var newy = oldy + ydir + ydev - Random(2 * ydev);
	
	// Check if a lightning attractor is in range.
	var range = Distance(0, 0, xdir, ydir);
	var cone_angle = Angle(0, 0, xdir, ydir);
	var cone_width = 30 + Distance(0, 0, xdev, ydev);
	var attractor = FindObject(Find_Cone(3 * range / 2, cone_angle, cone_width, oldx, oldy), Find_Property("IsLightningAttractor"), Find_Exclude(launcher), Find_NoContainer(), Sort_Distance(oldx, oldy));
	if (attractor)
	{
		// Move to lightning attractor.
		newx = attractor->GetX() - GetX();
		newy = attractor->GetY() - GetY();
	}
	
	// Check if lightning hits landscape, and adapt new coordinates.
	// Open question: should it penetrate liquids?
	var strike_solid = PathFree2(oldx + GetX(), oldy + GetY(), newx + GetX(), newy + GetY());
	if (strike_solid)
	{
		newx = strike_solid[0] - GetX();
		newy = strike_solid[1] - GetY();
	}
	AddVertex(newx, newy);
	
	// Draw the new line with lightning particles.
	DrawLightningLine(oldx, oldy, newx, newy, strength / 10, Min(16, strength / 3));
	
	// Update the light range for this strike, it is set at the new position with the given strength.
	SetLightRange(strength / 2, strength);
	SetLightColor(RGB(60, 60, 192));
	this.LightOffset = [newx, newy];
	
	// Strike objects on the line: only objects that are vehicle, items, alive or structures.
	for (var obj in FindObjects(Find_OnLine(oldx, oldy, newx, newy), Find_Or(Find_Category(C4D_Object | C4D_Living | C4D_Vehicle | C4D_Structure), Find_Func("IsLightningStrikable", this)), Find_Exclude(launcher), Find_NoContainer()))
	{
		var damage = 3 + strength / 10;
		// Check if the object rejects a lightning strike, also check if object still exists because an object
		// at the same location may have been struck first and have removed nearby objects.
		if (obj && !obj->~RejectLightningStrike(this, damage))
		{
			var is_attractor = obj.IsLightningAttractor;
			// Perform the strike on the object.
			PerformLightningStrike(obj, damage);
			// Reduce strength of the lightning if an object is struck.
			strength -= damage;
			// Remove lightning if too weak or an attractor has been struck.
			if (strength <= 0 || is_attractor)
			{
				RemoveObject();
				return FX_OK;
			}
		}
	}
	
	// Remove lightning, if struck landscape or if strength is too low.
	if (strike_solid || (strength < 50 && !Random(strength / 4)))
	{
		RemoveObject();
		return FX_OK;
	}
	// Branch with chance inversely proportional to strength.
	if (strength > 24 && Random(Sqrt(10 * strength)) > 12)
	{
		var branch_strength = (strength + Random(strength)) / 4;
		var lightning = CreateObject(Lightning, newx, newy);
		if (lightning)
			lightning->Launch(newx + GetX(), newy + GetY(), branch_strength, xdir, ydir, xdev, ydev, !fx.play_sound);
		strength -= branch_strength / 3;
	}
	return FX_OK;
}

private func DrawLightningLine(int x1, int y1, int x2, int y2, int distance, int size)
{
	// Need at least two particles: start and end.
	distance = Max(distance, 1);
	var angle = Angle(x1, y1, x2, y2);
	var count = Max(2, Distance(x1, y1, x2, y2) / distance);
	var deltax = x2 - x1, deltay = y2 - y1;
	// Create a set of blue particles and smaller white particles on the line.
	var particle_blue = 
	{
		R = 0,
		G = 0,
		B = 240,
		Alpha = PV_Linear(128, 0),
		Size = size,
		Rotation = angle,
		BlitMode = GFX_BLIT_Additive
	};
	var particle_white = 
	{
		R = 240,
		G = 240,
		B = 240,
		Alpha = PV_Linear(192, 0),
		Size = 3 * size / 4,
		Rotation = angle,
		BlitMode = GFX_BLIT_Additive
	};
	for (var i = 1; i <= count; i++)
	{
		CreateParticle("LightningBolt", x1 + deltax * i / (count + 1), y1 + deltay * i / (count + 1), 0, 0, 20, particle_blue);
		CreateParticle("LightningBolt", x1 + deltax * i / (count + 1), y1 + deltay * i / (count + 1), 0, 0, 20, particle_white);
	}
	return;
}

private func PerformLightningStrike(object target, int damage)
{
	// Do a callback notifying the object that it has been struck by lightning.
	target->~OnLightningStrike(this, damage);
	// Damage or hurt objects. Lightning strikes may have a controller, thus pass this for kill tracing.
	if (target)
	{
		if (target->GetOCF() & OCF_Alive)
			Punch(target, damage);
		else
			target->DoDamage(damage, FX_Call_DmgScript, GetController());
	}
	return;
}

// Don't save in scenarios.
func SaveScenarioObject() { return false; }

local Name = "$Name$";
