/**
	Boom Attack
	An evil rocket which attacks you, can be ridden as well.

	@authors Randrian, Newton, Sven2
*/


public func Construction()
{
	SetAction("Fly");
	SetComDir(COMD_None);
	// Notify friendly fire rule.
	GameCallEx("OnCreationRuleNoFF", this);
	// Add flight effects.
	CreateEffect(FxFlightRotation, 100, 1);
	CreateEffect(FxFlight, 100, 2);
	return;
}


/*-- Flight --*/

// Rotates the boom attack slowly around its axis.
local FxFlightRotation = new Effect
{
	Construction = func()
	{
		this.rotation = 0;	
	},
	Timer = func(int time)
	{
		if (Target->GetRider())
			return FX_Execute_Kill;

		this.rotation += 2;
		if (this.rotation >= 360)
			this.rotation = 0;

		Target.MeshTransformation = Trans_Rotate(this.rotation, 0, 1, 0);
		return FX_OK;
	}
};

// Controls the boom attack flight by flying through the given waypoints or to the target.
// The way points is a list of coordinates as [{X = ??, Y = ??}, ...]. The waypoints are
// dealt with first and then the target is aimed for.
local FxFlight = new Effect
{
	Construction = func()
	{
		// Add AI for logging purposes.
		this.fx_ai = DefenseAI->AddAI(Target);
		this.fx_ai->SetActive(false);
		// Get a target.
		this.target = GetRandomAttackTarget(Target);
		// Get the boom attack waypoints from the scenario or make an array.
		this.waypoints = GameCall("GetBoomAttackWaypoints", Target) ?? [];
		this.current_waypoint = nil;
		this.attack_on_way_point_flight = false;
		if (this.target)
		{
			var dx = this.target->GetX() - Target->GetX();
			var dy = this.target->GetY() + this.target->GetBottom() - Target->GetY();
			Target->SetR(Angle(0, 0, dx, dy));
		}
		// Immediately start flying unless we don't have a target set yet
		// If there's no target, it may be set directly after creation so let the rocket survive for one frame
		// by not calling the timer yet.
		if (this.target || GetLength(this.waypoints))
		{
			this->Timer(0);	
		}
	},
	Timer = func(int time)
	{
		// Find target and if not explode.
		if (!this.target)
		{
			this.target = GetRandomAttackTarget(Target);
			DefenseAI->LogAI_Info(this.fx_ai, Format("BoomAttack lost target and updated it to %v.", this.target));
			if (!this.target && !this.current_waypoint && GetLength(this.waypoints) == 0)
			{
				Target->DoFireworks(NO_OWNER);
				return FX_Execute_Kill;	
			}
		}
		
		// Check if reached current waypoint.
		if (this.current_waypoint && Distance(this.current_waypoint.X, this.current_waypoint.Y, Target->GetX(), Target->GetY()) < 8)
		{
			DefenseAI->LogAI_Info(this.fx_ai, Format("BoomAttack reached waypoint (%d, %d).", this.current_waypoint.X, this.current_waypoint.Y));
			this.current_waypoint = nil;
		}
		
		// Get relative coordinates to target.
		var dx, dy;
		
		// Handle waypoints and get coordinates to move to.
		if (this.current_waypoint || GetLength(this.waypoints) > 0)
		{
			if (!this.current_waypoint)
				this.current_waypoint = PopFront(this.waypoints);
			// Set relative coordinates to new waypoint.
			dx = this.current_waypoint.X - Target->GetX();
			dy = this.current_waypoint.Y - Target->GetY();		
		}
		
		if (this.target)
		{
			// Explode if close enough to target.
			if (ObjectDistance(Target, this.target) < 12)
			{
				DefenseAI->LogAI_Info(this.fx_ai, Format("BoomAttack is in reach of enemy %v and explodes now.", this.target));
				Target->DoFireworks(NO_OWNER);				
				return FX_Execute_Kill;	
			}
			
			// Move to a nearby target if path is free and attacking is allowed.
			var target_on_path = GetAttackTargetOnWaypointPath();
			if (target_on_path && this.current_waypoint)
			{
				// Give up current and future waypoints and set to new target.
				this.current_waypoint = nil;
				this.waypoints = [];
				this.target = target_on_path;
				DefenseAI->LogAI_Info(this.fx_ai, Format("BoomAttack found new target %v on waypoint path.", target_on_path));
			}
			
			// Get relative coordinates to target.
			if (!this.current_waypoint)
			{
				dx = this.target->GetX() - Target->GetX();
				dy = this.target->GetY() + this.target->GetBottom() - Target->GetY();
				// Check if path is free to target, if not try to find a way around using waypoints.
				if (!PathFree(this.target->GetX(), this.target->GetY(), Target->GetX(), Target->GetY())/* && !Target->GBackSolid(dx, dy)*/)
				{
					// Try to set a waypoint half way on a line orthogonal to the current direction.
					for (var attempts = 0; attempts < 40; attempts++)
					{
						var d = Sqrt(dx**2 + dy**2);
						var try_dist = Max(20 + 2 * attempts, d * attempts / 80) + RandomX(-10, 10);
						var line_dist = (2 * Random(2) - 1) * try_dist;
						var way_x = Target->GetX() + dx / 2 + dy * line_dist / d;
						var way_y = Target->GetY() + dy / 2 - dx * line_dist / d;
						// Path to new waypoint must be free and inside the landscape borders.
						if (!PathFree(Target->GetX(), Target->GetY(), way_x, way_y) || !PathFree(this.target->GetX(), this.target->GetY(), way_x, way_y))
							continue;
						if (!Inside(way_x, 0, LandscapeWidth()) || !Inside(way_y, 0, LandscapeHeight()))
							continue;
						DefenseAI->LogAI_Info(this.fx_ai, Format("BoomAttack at (%d, %d) is aiming for %v at (%d, %d) takes a new route through (%d, %d).", Target->GetX(), Target->GetY(), this.target, this.target->GetX(), this.target->GetY(), way_x, way_y));
						this.current_waypoint = {X = way_x, Y = way_y};
						break;
					}
				}
			}
			
			// At this distance, fly horizontally. When getting closer, gradually turn to direct flight into target.
			if (!this.current_waypoint)
			{
				var aim_dist = 600;
				dy = dy * (aim_dist - Abs(dx)) / aim_dist;
			}
		}
		var angle_to_target = Angle(0, 0, dx, dy);
		var angle_rocket = Target->GetR();
		if (angle_rocket < 0)
			angle_rocket += 360;
		// Gradually update the angle.
		var angle_delta = angle_rocket - angle_to_target;
		var angle_step = BoundBy(Target.FlySpeed / 25, 4, 10);
		if (Inside(angle_delta, 0, 180) || Inside(angle_delta, -360, -180))
			Target->SetR(Target->GetR() - Min(angle_step, Abs(angle_delta)));
		else if (Inside(angle_delta, -180, 0) || Inside(angle_delta, 180, 360))
			Target->SetR(Target->GetR() + Min(angle_step, Abs(angle_delta)));

		// Update velocity according to angle.
		Target->SetXDir(Sin(Target->GetR(), Target.FlySpeed), 100);
		Target->SetYDir(-Cos(Target->GetR(), Target.FlySpeed), 100);
	
		// Create exhaust fire.
		var x = -Sin(Target->GetR(), 15);
		var y = +Cos(Target->GetR(), 15);
		var xdir = Target->GetXDir() / 2;
		var ydir = Target->GetYDir() / 2;
		Target->CreateParticle("FireDense", x, y, PV_Random(xdir - 4, xdir + 4), PV_Random(ydir - 4, ydir + 4), PV_Random(16, 38), Particles_Thrust(), 5);
		return FX_OK;
	},
	
	AddWaypoint = func(proplist waypoint)
	{
		PushBack(this.waypoints, waypoint);	
	},
	
	SetWaypoints = func(array waypoints)
	{
		this.waypoints = waypoints;
	},
	
	SetTarget = func(object target)
	{
		this.target = target;
	},
	
	SetAttackOnWaypointPath = func(bool on)
	{
		this.attack_on_way_point_path = on;
	},
	
	GetAttackTargetOnWaypointPath = func()
	{
		var attack_target = GameCall("GiveAttackTargetOnWaypointPath", Target);
		if (attack_target && PathFree(Target->GetX(), Target->GetY(), attack_target->GetX(), attack_target->GetY()))
			return attack_target;
		if (!this.attack_on_way_point_path)
			return nil;
		return Target->FindObject(Find_Category(C4D_Structure | C4D_Living | C4D_Vehicle), Find_Hostile(Target->GetController()), Find_Distance(100), Target->Find_PathFree(), Sort_Distance());	
	}
};


/*-- Waypoints & Target --*/

public func AddWaypoint(proplist waypoint)
{
	var fx = GetEffect("FxFlight", this);
	if (fx)
		fx->AddWaypoint(waypoint);
	return;
}

public func SetWaypoints(array waypoints)
{
	var fx = GetEffect("FxFlight", this);
	if (fx)
		fx->SetWaypoints(waypoints);
	return;
}

public func SetTarget(object target)
{
	var fx = GetEffect("FxFlight", this);
	if (fx)
		fx->SetTarget(target);
	return;
}


/*-- Riding --*/

local riderattach;
local rider;

public func SetRider(object to)
{
	rider = to;
	return;
}

public func GetRider() { return rider; }

public func OnMount(object clonk)
{
	SetRider(clonk);
	var dir = -1;
	if (GetX() > LandscapeWidth() / 2)
		dir = 1;
	clonk->PlayAnimation("PosRocket", CLONK_ANIM_SLOT_Arms, Anim_Const(0));
	riderattach = AttachMesh(clonk, "main", "pos_tool1", Trans_Translate(-1000, 2000 * dir, 2000));
	return true;
}

public func OnUnmount(object clonk)
{
	clonk->StopAnimation(clonk->GetRootAnimation(10));
	DetachMesh(riderattach);
	return;
}


/*-- Explosion --*/

// Don't get hit by projectiles shot from own rider.
public func IsProjectileTarget(object projectile, object shooter) { return (!shooter) || (shooter->GetActionTarget() != this); }
public func OnProjectileHit(object shot) { return DoFireworks(shot->GetController()); }

public func ContactBottom() { return Hit(); }
public func ContactTop() { return Hit(); }
public func ContactLeft() { return Hit(); }
public func ContactRight() { return Hit(); }

public func Hit() { return DoFireworks(NO_OWNER); }
public func HitObject(object ) { return DoFireworks(NO_OWNER); }

public func Damage(int change, int cause, int cause_plr)
{
	if (change > 0)
		return DoFireworks(cause_plr);
	return;	
}

public func Incineration(int caused_by)
{
	if (OnFire())
		return DoFireworks(caused_by);
	return;
}

private func DoFireworks(int killed_by)
{
	if (rider)
	{
		rider->Fling(RandomX(-5, 5), -5);
		rider->SetAction("Walk");
		SetRider(nil);
	}
	SetKiller(killed_by);
	Fireworks();
	Explode(40);
	return;
}

public func Destruction()
{
	// Notify defense goal for reward and score.
	GameCallEx("OnRocketDeath", this, GetKiller());
	// Notify friendly fire rule.
	GameCallEx("OnDestructionRuleNoFF", this);
}

public func HasNoNeedForAI() { return true; }


/*-- Enemy spawn registration --*/

public func Definition(def)
{
	if (def == DefenseBoomAttack)
	{
		var spawn_editor_props = { Type="proplist", Name=def->GetName(), EditorProps= {
			Rider = new EnemySpawn->GetAICreatureEditorProps(nil, "$NoRiderHelp$") { Name="$Rider$", EditorHelp="$RiderHelp$" },
			FlySpeed = { Name="$FlySpeed$", EditorHelp="$FlySpeedHelp$", Type="int", Min=5, Max=10000 },
		} };
		var spawn_default_values = {
			Rider = nil,
			FlySpeed = def.FlySpeed,
		};
		EnemySpawn->AddEnemyDef("BoomAttack", { SpawnType=DefenseBoomAttack, SpawnFunction=def.SpawnBoomAttack, OffsetAttackPathByPos=true, GetInfoString=def.GetSpawnInfoString }, spawn_default_values, spawn_editor_props);
	}
}

private func SpawnBoomAttack(array pos, proplist enemy_data, proplist enemy_def, array attack_path, object spawner)
{
	// Spawn the boomattack
	var boom = CreateObject(DefenseBoomAttack, pos[0], pos[1], g_enemyspawn_player);
	if (!boom) return;
	// Boomattack settings
	boom.FlySpeed = enemy_data.FlySpeed;
	var wp0 = attack_path[0];
	boom->SetR(Angle(0, 0, wp0.X - pos[0], wp0.Y - pos[1]) + Random(11) - 5);
	boom->SetWaypoints(attack_path);
	// Rider?
	var clonk = EnemySpawn->SpawnAICreature(enemy_data.Rider, pos, enemy_def, [attack_path[-1]], spawner);
	if (clonk)
	{
		clonk->SetAction("Ride", boom);
		return [boom, clonk];
	}
	// Return rider-less boom attack
	return boom;
}

private func GetSpawnInfoString(proplist enemy_data)
{
	if (enemy_data.Rider && enemy_data.Rider.Type == "Clonk")
	{
		return Format("{{DefenseBoomAttack}}%s", EnemySpawn->GetAIClonkInfoString(enemy_data.Rider.Properties));
	}
	else
	{
		return "{{DefenseBoomAttack}}";
	}
}


/*-- Properties --*/

local ActMap = {
	Fly = {
		Prototype = Action,
		Name = "Fly",
		Procedure = DFA_FLOAT,
		Length = 1,
		Delay = 0,
		Wdt = 15,
		Hgt = 27,
	}
};

local Name = "$Name$";
local Description = "$Description$";
local ContactCalls = true;
local FlySpeed = 100;
local BlastIncinerate = 8;
local ContactIncinerate = 8;
local HasNoFriendlyFire = true;
