forked from Mirrors/openclonk
557 lines
14 KiB
C
557 lines
14 KiB
C
/*
|
|
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));
|
|
}
|
|
|