/*
	Squid
	Author: Zapper
	
	The squid will head towards moving objects and try to avoid solid objects.
	To achieve this, force-field attractors and repulsors are placed based on Monte-Carlo sampling of the landscape.
*/

#include Library_ForceField
#include Library_Edible


static const SQUID_SWIM_MAX_SPEED = 30;
static const SQUID_VISION_MAX_RANGE = 100;

local walking, swimming;

local swim_animation, idle_animation, walk_animation, movement_animation_node;
local custom_bone_transform_slot, custom_bone_transform_r;
local base_transform;

// this is a 3D vector with the current direction the squid is facing
local current_orientation, current_speed;
local is_in_idle_animation;
local current_angle; // for smoother turning

local ink_level;

// Whether the squid is friendly or will actively chase Clonks.
local is_friendly;

/*
	Places squid.
	"settings" can contain a boolean property "friendly" which defines whether the squid will be harmless or harmful. Defaults to being harmless.
*/
public func Place(int amount, proplist rectangle, proplist settings)
{
	settings = settings ?? {};
	var friendly = settings.friendly ?? true;
	var max_tries = 2 * amount;
	var loc_area = nil;
	if (rectangle) loc_area = Loc_InRect(rectangle);
	var f;
	
	while ((amount > 0) && (--max_tries > 0))
	{
		var spot = FindLocation(Loc_Material("Water"), Loc_Space(50, false), loc_area);
		if (!spot) continue;
		
		f = CreateObject(this, spot.x, spot.y, NO_OWNER);
		if (!f) continue;
		
		if (!friendly)
			f->SetFriendly(false);
		
		// Randomly add some large/slim squid
		if (Random(3))
		{
			// there are naturally smaller and larger squid
			f->SetCon(RandomX(75, 125));
			// make sure the smaller ones don't grow larger any more
			f->StopGrowth(); 
		}
		
		if (f->Stuck())
		{
			f->RemoveObject();
			continue;
		}
		--amount;
	}
	return f; // return last created fish
}

public func IsAnimal() { return true; }

public func Construction()
{
	current_orientation = [0, -2, 1];
	current_angle = 0;
	
	SetYZScale(1000);
	
	// general stuff	
	StartGrowth(15);
	
	var len = GetAnimationLength("Float");
	idle_animation = PlayAnimation("Float", 5, Anim_Linear(0, 0, len, 180 + Random(40), ANIM_Loop), Anim_Const(1000));
	var len = GetAnimationLength("Swim");
	swim_animation = PlayAnimation("Swim", 5, Anim_Linear(0, 0, len, 90 + Random(20), ANIM_Loop), Anim_Const(0), idle_animation);
	movement_animation_node = swim_animation + 1;
	is_in_idle_animation = true;
	UpdateSwim();
	
	ink_level = 1000;
	
	SetAction("Swim");
	SetComDir(COMD_None);
	ScheduleCall(this, this.InitActivity,  1 + Random(10), 0);
	AddTimer(this.UpdateSwim, 2);
	
	// The squid is friendly by default.
	SetFriendly(true);
	
	_inherited(...);
	
	// setup of the force fields after the call to inherited(...)
	SetDefaultForceFieldMaxDistance(SQUID_VISION_MAX_RANGE);
	SetDefaultForceFieldTTD(36 * 4);
	SetMaxEmitterNumber(7);
}

public func SetFriendly(bool friendly)
{
	is_friendly = friendly ?? true;
	
	if (is_friendly)
	{
		SetMeshMaterial("SquidMaterialFriendly");
	}
	else
	{
		SetMeshMaterial("SquidMaterial");
	}
}

private func InitActivity()
{
	if (GetAlive()) AddTimer(this.Activity, 10);
}

public func SetYZScale(int new_scale)
{
	base_transform = Trans_Mul(Trans_Rotate(90, 0, 1, 0), Trans_Scale(1000, new_scale, new_scale));
	return true;
}

public func Death()
{
	RemoveTimer(this.UpdateSwim);
	RemoveTimer(this.Activity);
	Decay();
	this.Collectible = true;
	this.MeshTransformation = base_transform;
	
	// fade current animations into death animation
	var len = GetAnimationLength("Die");
	PlayAnimation("Die", 7, Anim_Linear(0, 0, len, 36, ANIM_Hold), Anim_Linear(0, 0, 1000, 10, ANIM_Hold));
		
	return _inherited(...);
}

public func CatchBlow()
{
	if (ink_level >= 1000)
		DoInk();
	else if (ink_level > 500 && !Random(5))
		DoInk();
}

public func NutritionalValue() { if (!GetAlive()) return 15; else return 0; }

private func Activity()
{
	if (swimming)
	{
		UpdateVision();
		DoActions();
	}
	else if (walking)
	{
		// PANIC WHERE THE WATER AT
		var rx = RandomX(10, 25);
		if (!Random(2)) rx *= -1;
		
		var has_water = false;
		for (var y = 0; y <= 12; y += 4)
		{
			if (!GBackLiquid(rx, y)) continue;
			has_water = true;
			break;
		}
		
		if (has_water)
		{
			if (rx < 0) SetComDir(COMD_Left);
			else SetComDir(COMD_Right);
			
			if (!Random(2))
				DoJump();
			return true;
		}
		else
		{
			if (!Random(2))
			{
				if (!Random(2)) SetComDir(COMD_Left);
				else SetComDir(COMD_Right);
			}
			if (!Random(3))
				DoJump();
		}
	}
	return 1;
}

private func UpdateVision()
{
	var playful_distance = SQUID_VISION_MAX_RANGE;
	if (!is_friendly)
		playful_distance /= 2;
	UpdateVisionFor(FindObjects(Find_Distance(playful_distance), Find_Category(C4D_Object | C4D_Living | C4D_Vehicle), Find_OCF(OCF_HitSpeed2), Find_Exclude(this), Find_NoContainer(), Sort_Distance()));
	
	if (!is_friendly)
	{
		UpdateVisionFor(FindObjects(Find_Distance(SQUID_VISION_MAX_RANGE), Find_OCF(OCF_Alive), Find_Func("IsClonk"), Find_NoContainer(), Sort_Distance()), true);
	}
	else
	{
		// Suddenly find a random object surprisingly interesting.
		if (!Random(10))
			UpdateVisionFor(FindObjects(Find_Distance(playful_distance), Find_Category(C4D_Object | C4D_Living | C4D_Vehicle), Find_Exclude(this), Find_NoContainer(), Sort_Reverse(Sort_Distance()))); 
	}
	UpdateWallVision();
}

private func UpdateVisionFor(array objects, bool is_food)
{
	// get first object from array that is in a certain angle
	for (var obj in objects)
	{
		if (!PathFree(GetX(), GetY(), obj->GetX(), obj->GetY())) continue;
		if ((obj->GetMaterial() != GetMaterial())) continue;
		
		AddAttractor(obj, nil, 250);
		
		if (is_food && ink_level > 800 && ObjectDistance(this, obj) < 20)
		{
			DoInk();
		}
		return true;
	}
	return false;
}

private func UpdateWallVision()
{
	var vision_range = SQUID_VISION_MAX_RANGE / 2;
	// just do a random ray cast and see whether we hit landscape
	var angle = Angle(0, 0, GetXDir(), GetYDir()) + RandomX(-120, +120);
	var distance = 3;
	while (distance < vision_range)
	{
		var x = Sin(angle, distance);
		var y = -Cos(angle, distance);
		if (GetMaterial(x, y) != Material("Water"))
		{
			AddRepulsor(GetX() + x, GetY() + y, 1000, vision_range / 2);
			return;
		}
		distance += 5;
	}
}

private func DoActions()
{
	var result = CalculateForce();
	current_orientation = [result[0], result[1]];
	var speed = Sqrt(result[0]**2 + result[1]**2);
	var max_speed = BoundBy((1000 - ink_level) * SQUID_SWIM_MAX_SPEED / 1000, 10, 2 * SQUID_SWIM_MAX_SPEED);
	current_speed = BoundBy(speed / 50, 2, max_speed);
}

private func UpdateSwim()
{
	ink_level = Min(ink_level + 5, 1000);
	
	if (!swimming) return;
		
	// the movement angle is an interpolation between the current real movement direction and a default "upwards" direction
	
	var velocity = Sqrt(GetXDir() ** 2 + GetYDir() ** 2);
	if (velocity == 0) velocity = 1;
	
	var current = current_orientation;
	var angle = Angle(0, 0, current[0], current[1]);
	if (!current[0] && !current[1]) angle = 180; // drift down
	
	var target_x = BoundBy(Sin(angle, current_speed), GetXDir() - 2, GetXDir() + 2);
	var target_y = BoundBy(-Cos(angle, current_speed), GetYDir() - 2, GetYDir() + 2);
	
	SetXDir(target_x);
	SetYDir(target_y);
	
	// the animation to play depends on the speed of the squid
	var is_fast = velocity >= SQUID_SWIM_MAX_SPEED/3;
	if (is_fast && !is_in_idle_animation) {} // no change needed
	else if (!is_fast && is_in_idle_animation) {} // ok, too
	else
	{
		var current_weight = GetAnimationWeight(movement_animation_node);
		var start = 1000;
		var end = 0;
		
		if (is_in_idle_animation)
		{
			start = 0;
			end = 1000;
		}
		
		SetAnimationWeight(movement_animation_node, Anim_Linear(current_weight, start, end, 30, ANIM_Hold));
		is_in_idle_animation = !is_in_idle_animation;
	}
	
	// Where do we want to go?
	var target_angle = Angle(0, 0, GetXDir(1), GetYDir(1));
	var turn_direction = BoundBy(GetTurnDirection(current_angle, target_angle), -4, +4);
	
	// Move head a bit to simulate water resistance.
	custom_bone_transform_r = BoundBy(custom_bone_transform_r + turn_direction, -45, 45);
	// The head wants to stand upright, though.
	if (custom_bone_transform_r > 0) custom_bone_transform_r -= 1;
	else custom_bone_transform_r += 1;
	// Need to remove the old bone transformation?
	if (custom_bone_transform_slot != nil)
		StopAnimation(custom_bone_transform_slot);
		
	if (custom_bone_transform_r != 0)
		custom_bone_transform_slot = TransformBone("shell", Trans_Rotate(custom_bone_transform_r, 0, 1, 0), 6, Anim_Const(800)); 
	else
		custom_bone_transform_slot = nil;

	
	// And finally, slowly turn to target.
	current_angle = current_angle + turn_direction;
	this.MeshTransformation = Trans_Mul(Trans_Rotate(current_angle, 0, 0, 1), base_transform);
}


private func DoJump()
{
	SetAction("Jump");
	Sound("Hits::SoftTouch*");
	
	var x_dir = RandomX(ActMap.Jump.Speed/2, ActMap.Jump.Speed);
	if (GetComDir() == COMD_Left) x_dir *= -1;
	var y_dir = -RandomX(ActMap.Jump.Speed/3, ActMap.Jump.Speed);
	SetSpeed(x_dir, y_dir);
}

private func StartWalk() 
{
	if (GBackLiquid())
	{
		SetAction("Swim");
		return;
	}
	walking = true;
	swimming = false;
	
	if (walk_animation == nil)
	{
		this.MeshTransformation = base_transform;
		
		var len = GetAnimationLength("Walk");
		walk_animation = PlayAnimation("Walk", 6, Anim_Linear(0, 0, len, 36, ANIM_Loop), Anim_Linear(0, 0, 1000, 10, ANIM_Hold));
	}
}

private func StartSwim()
{
	swimming = true;
	walking = false;
	
	if (walk_animation != nil)
	{
		StopAnimation(walk_animation);
		walk_animation = nil;
	}
	
	// make sure we go down when entering the water
	current_orientation = [RandomX(-10, 10), RandomX(2, 10), 1];
	
	UpdateSwim();
}

private func StartJump()
{
	if (GBackLiquid())
	{
		SetAction("Swim");
		return;
	}
	
	swimming = false;
	walking = false;
}

private func DoInk()
{
	ink_level = 0;
	
	var particles =
	{
		CollisionVertex = 500,
		OnCollision = PC_Stop(),
		ForceY = PV_Random(-2, 2),
		ForceX = PV_Random(-2, 2),
		DampingX = 900, DampingY = 900,
		Alpha = PV_Linear(255, 0),
		R = 50, G = 50, B = 100,
		Size = PV_Linear(PV_Random(10, 20), PV_Random(40, 60)),
		Phase = PV_Random(0, 15)
	};
	CreateParticle("SmokeThick", 0, 0, PV_Random(-40, 40), PV_Random(-40, 40), PV_Random(36, 100), particles, 64);
	// Make squid invisible for some time. Also inverse behavior during invisibility.
	AddEffect("Invisibility", this, 1, 2, this, nil, particles);
	// Drain Clonks' breath a bit faster when in ink.
	AddEffect("IntInkBlob", nil, 1, 5, nil, GetID(), GetX(), GetY());
}

private func FxIntInkBlobStart(object target, effect fx, temp, int x, int y)
{
	if (temp) return;
	fx.x = x;
	fx.y = y;
}

private func FxIntInkBlobTimer(object target, effect fx, int time)
{
	if (time > 30 + Random(20)) return -1;
	var max_distance = 40;
	for (var clonk in FindObjects(Find_Distance(max_distance, fx.x, fx.y), Find_OCF(OCF_Alive), Find_Func("IsClonk")))
	{
		if (!PathFree(fx.x, fx.y, clonk->GetX(), clonk->GetY())) continue;
		var distance = Distance(fx.x, fx.y, clonk->GetX(), clonk->GetY());
		var breath_penalty = 2 * (max_distance - distance);
		clonk->DoBreath(-breath_penalty);
	}
	return FX_OK;
}

private func FxInvisibilityStart(object target, effect fx, temp, particles)
{
	if (temp) return;
	fx.old_visibility = this.Visibility;
	this.Visibility = VIS_None;
	fx.particles = particles;
	SetInverseForceFields(true);
}

private func FxInvisibilityTimer(object target, effect fx, int time)
{
	if (time > 35 * 5) return FX_Execute_Kill;
	if (time > 35 * 2) return FX_OK;
	var size = 2 * 35 - time;
	fx.particles.Size = size/2;
	CreateParticle("SmokeThick", 0, 0, PV_Random(-5, 5), PV_Random(-5, 5), PV_Random(20, 36), fx.particles, 4);
	return FX_OK;
}

private func FxInvisibilityStop(object target, effect fx, int reason, int temp)
{
	if (temp) return;
	fx.particles.Size = PV_Linear(PV_Random(15, 20), PV_Random(0, 5));
	CreateParticle("SmokeThick", 0, 0, PV_Random(-30, 30), PV_Random(-30, 30), PV_Random(20, 36), fx.particles, 32);
	this.Visibility = fx.old_visibility;
	SetInverseForceFields(false);
}

// Only bleeding Squid will be eaten by other predators.
public func IsPrey() { return GetEnergy() < this.MaxEnergy / 3000; }

public func SaveScenarioObject(props)
{
	if (!is_friendly) props->AddCall("Hostile", this, "SetFriendly", false);
	return true;
}

local ActMap = {

Swim = {
	Prototype = Action,
	Name = "Swim",
	Procedure = DFA_SWIM,
	Speed = 100,
	Accel = 16,
	Decel = 16,
	Length = 1,
	Delay = 0,
	FacetBase=1,
	NextAction = "Swim",
	StartCall = "StartSwim"
},
Walk = {
	Prototype = Action,
	Name = "Walk",
	Procedure = DFA_WALK,
	Speed = 30,
	Accel = 16,
	Decel = 16,
	Length = 1,
	Delay = 0,
	FacetBase=1,
	Directions = 2,
	FlipDir = 1,
	NextAction = "Walk",
	StartCall = "StartWalk",
	InLiquidAction = "Swim",
},
Jump = {
	Prototype = Action,
	Name = "Jump",
	Procedure = DFA_FLIGHT,
	Speed = 30,
	Accel = 16,
	Decel = 16,
	Length = 1,
	Delay = 0,
	FacetBase=1,
	Directions = 2,
	FlipDir = 1,
	NextAction = "Jump",
	StartCall = "StartJump",
	InLiquidAction = "Swim",
},
Dead = {
	Prototype = Action,
	Name = "Dead",
	Procedure = DFA_NONE,
	Speed = 10,
	Length = 1,
	Delay = 0,
	FacetBase=1,
	Directions = 2,
	FlipDir = 1,
	NextAction = "Hold",
	NoOtherAction = 1,
	ObjectDisabled = 1,
}
};
local Name = "$Name$";
local Description = "$Description$";
local MaxEnergy = 80000;
local MaxBreath = 360; // 360 =ten seconds
local Placement = 1;
local NoBurnDecay = true;
local BreatheWater = 1;
local BorderBound = C4D_Border_Sides | C4D_Border_Top | C4D_Border_Bottom;
local ContactCalls = true;

public func Definition(proplist def)
{
	def.PictureTransformation = Trans_Mul(Trans_Translate(0, -1600, 0), Trans_Rotate(20, 1, 0, 0), Trans_Rotate(70, 0, 1, 0));
}

