openclonk/planet/Objects.ocd/Animals.ocd/Puka.ocd/Script.c

915 lines
19 KiB
C

/**
Puka
Cute little electronically active creature.
*/
local Name = "$Name$";
local Description = "$Description$";
// For the teleport trajectory.
local current_high_x, current_high_y;
local current_ground_x, current_ground_y;
// Animations.
local turn_angle;
// The closest enemy that has been found.
local enemy;
// Some positions near water that might be good teleport targets.
local water_positions;
local water_cycle;
public func Place(int amount, proplist rectangle, proplist settings)
{
var max_tries = 3 * amount;
var loc_area = nil;
if (rectangle) loc_area = Loc_InArea(rectangle);
var animal;
while ((amount > 0) && (--max_tries > 0))
{
// Try to find walkable ground near water.
var water_spot = FindLocation(Loc_Material("Water"));
var ground_spot = nil;
if (water_spot)
{
var water_rectangle = Shape->Rectangle(water_spot.x - 200, water_spot.y - 200, 400, 400);
// Make sure the position is inside the required target rectangle.
water_rectangle = Shape->Intersect(water_rectangle, rectangle)->GetBoundingRectangle();
ground_spot = FindLocation(Loc_Wall(CNAT_Bottom), Loc_Or(Loc_Sky(), Loc_Tunnel()), Loc_Space(20, CNAT_Top), Loc_InArea(water_rectangle));
}
// If no hip and cool spot found, just get some generic spot.
if (!ground_spot)
ground_spot = FindLocation(Loc_Wall(CNAT_Bottom), Loc_Or(Loc_Sky(), Loc_Tunnel()), Loc_Space(20, CNAT_Top), loc_area);
if (!ground_spot) continue;
animal = CreateObjectAbove(this, ground_spot.x, ground_spot.y, NO_OWNER);
if (!animal) continue;
if (animal->Stuck())
{
animal->RemoveObject();
continue;
}
--amount;
}
return animal;
}
public func Construction()
{
turn_angle = 0;
water_positions = [];
water_cycle = 0;
AddTimer("ExecuteActivity", 10);
AddTimer("UpdateEnemy", 30);
AddTimer("CheckTurn", 10);
SetAction("Walk");
SetComDir(COMD_Stop);
if (Random(2)) SetDir(DIR_Left);
else SetDir(DIR_Right);
// Two spot layouts are available.
if (!Random(2))
SetMeshMaterial("Puka2");
ScheduleCall(this, "RandomizeColor", 10);
return true;
}
private func RandomizeColor()
{
// Normally, Pukas are somewhat blue-ish. Rarely, you can find red ones, too!
var base_color = 170;
if (!Random(5)) base_color = 0;
SetColor(HSL(RandomX(base_color - 45, base_color + 45), 100 + Random(150), 50 + Random(50)));
}
public func CatchBlow(int damage, object from)
{
Schedule(this, "Sound(\"Animals::Puka::Hurt*\")", RandomX(5, 20));
}
private func CheckStuck()
{
}
private func StartSwim()
{
this.Activity = this.ActivitySwimming;
}
private func ClearActivity()
{
this.Activity = nil;
}
private func StartSleep()
{
this.Activity = this.ActivitySleeping;
}
private func StopSwim()
{
}
private func EndSwim()
{
}
private func StartJump()
{
}
private func EndJump()
{
RemoveEffect("JumpCheck", this);
}
private func StopWalk()
{
}
private func StartScale()
{
this.Activity = this.ActivityScaling;
}
private func StopScale()
{
}
private func StartHangle()
{
}
private func StopHangle()
{
}
public func Death()
{
AddEffect("IntDeathSparks", this, 1, 1, this);
SoundAt("Animals::Puka::Die");
}
private func FxIntDeathSparksStart(object target, effect fx, temp)
{
if (temp) return;
fx.particles =
{
Prototype = Particles_ElectroSpark1(),
G = 150, B = 150, Alpha = PV_Linear(255, 0),
Stretch = 2000,
DampingX = 999, DampingY = 999,
ForceY = -GetGravity()
};
}
private func FxIntDeathSparksTimer(object target, effect fx, int time)
{
CreateParticle("ElectroSpark", PV_Random(-5, 5), PV_Random(-5, 5),
PV_Random(-20, 20), PV_Random(-5, 10),
PV_Random(2, 10), fx.particles, 20);
if (time > this.ActMap.Dead.Length) return -1;
return 1;
}
private func FxIntDeathSparksStop(object target, effect fx, int reason, temp)
{
if (temp) return;
fx.particles.Stretch *= 2;
CreateParticle("ElectroSpark", PV_Random(-5, 5), PV_Random(-5, 5),
PV_Random(-60, 60), PV_Random(-60, 60),
PV_Random(2, 10), fx.particles, 800);
if (this) RemoveObject();
return 1;
}
private func Jump()
{
SetAction("Jump");
SetSpeed(GetXDir(), -10 - GetXDir());
}
private func StartWalk()
{
// Coming from a swim? Remember this location.
if (this.Activity == this.ActivitySwimming)
{
SetComDir(COMD_Stop);
water_positions[water_cycle] = [GetX(), GetY()];
water_cycle = (water_cycle + 1) % 5;
}
this.Activity = this.ActivityWalking;
}
private func ActivityScaling()
{
if (!Random(10)) return TryStartTeleport();
if (!Random(2))
{
if (Random(3)) SetComDir(COMD_Up);
else SetComDir(COMD_Down);
}
}
private func ActivitySwimming()
{
if (!Random(6))
{
Turn(nil, true);
if (GetComDir() == COMD_Left) SetComDir(COMD_UpLeft);
else if (GetComDir() == COMD_Right) SetComDir(COMD_UpRight);
}
if (!Random(20)) TryStartTeleport();
}
private func ActivityWalking()
{
var comd = GetComDir();
if (enemy)
{
if (enemy->GBackLiquid())
{
if (ShockWater()) return;
}
if (!Random(10)) SetComDir(COMD_Stop);
var distance = ObjectDistance(this, enemy);
if (distance < 50 && Random(2))
return TryStartTeleport();
}
// If not walking, randomly sleep.
if (comd == COMD_Stop)
{
if (!enemy)
{
if (!Random(15)) return SetAction("Sleep");
if (!Random(15)) // Idle?
{
if (Random(3))
{
Schedule(this, "Sound(\"Animals::Puka::Gulp\")", 40);
return SetAction("LookAround");
}
return SetAction("ScratchFace");
}
}
if (!Random(5))
{
if (Random(2)) SetComDir(COMD_Left);
else SetComDir(COMD_Right);
return;
}
if (!Random(20))
TryStartTeleport();
return;
}
if (!Random(5))
{
SetComDir(COMD_Stop);
return;
}
if (!Random(5))
{
Turn();
return;
}
// Anticipate holes in the landscape while walking.
}
private func ActivitySleeping()
{
if (!Random(20))
SetAction("Walk");
if (!Random(5))
AddEffect("IntDream", this, 1, 1, this);
}
private func FxIntDreamStart(object target, effect fx, temp)
{
if (temp) return;
fx.x = GetX();
fx.y = GetY() - 5;
fx.particles =
{
Prototype = Particles_ElectroSpark2(),
Size = PV_Linear(PV_Random(0, 1), PV_Random(1, 2)),
Rotation = PV_Random(360)
};
}
private func FxIntDreamTimer(object target, effect fx, int time)
{
if (time > RandomX(30, 200)) return -1;
fx.x += RandomX(-1, 1);
fx.y += RandomX(-1, 1);
CreateParticle("ElectroSpark", fx.x - GetX(), fx.y - GetY(), 0, 0, PV_Random(5, 20), fx.particles, 2);
}
private func ExecuteActivity()
{
if (this.Activity) this->Activity();
}
private func UpdateEnemy()
{
// Already disposed of the last one?
if (enemy && !enemy->GetAlive()) enemy = nil;
// Last one too far away now?
if (enemy && ObjectDistance(this, enemy) > 150) enemy = nil;
var x = GetX();
var y = GetY();
for (var obj in FindObjects(Find_Distance(100), Find_OCF(OCF_Alive), Find_AnimalHostile(GetOwner()), Sort_Distance()))
{
if (!PathFree(x, y, obj->GetX(), obj->GetY())) continue;
enemy = obj;
return;
}
}
private func DoElectroCircle()
{
for (var angle = 0; angle < 360; angle += 10)
{
var speed_x = Cos(angle, 15);
var speed_y = -Sin(angle, 15);
CreateParticle("ElectroSpark", PV_Random(-5, 5), PV_Random(-5, 5),
PV_Random(speed_x - 10, speed_x + 10), PV_Random(speed_y - 10, speed_y + 10),
PV_Random(5, 10),
Particles_ElectroSpark2(),
10);
}
var particles =
{
Prototype = Particles_ElectroSpark1(),
Stretch = 6000,
DampingX = 999, DampingY = 999
};
// Punish all close enemies (not allied animals, though).
for (var obj in FindObjects(Find_Distance(30), Find_OCF(OCF_Alive), Find_AnimalHostile(GetOwner()), Find_Exclude(this)))
{
var delta_x = 3 * (obj->GetX() - GetX());
var delta_y = 3 * (obj->GetY() - GetY());
obj->DoEnergy(-15, false, FX_Call_EngGetPunched, GetOwner());
CreateParticle("ElectroSpark", 0, 0,
PV_Random(delta_x - 10, delta_x + 10), PV_Random(delta_y - 10, delta_y + 10),
PV_Random(1, 5),
particles,
40);
}
}
private func FxIntTeleportStart(object target, effect fx, temp)
{
if (temp) return;
DoElectroCircle();
fx.start_x = GetX();
fx.start_y = GetY();
this.Visibility = VIS_None;
fx.particles =
{
Prototype = Particles_ElectroSpark1(),
Stretch = 6000,
DampingX = 999, DampingY = 999
};
var xdir, ydir;
if (current_high_x != nil)
{
xdir = current_high_x - fx.start_x;
ydir = current_high_y - fx.start_y;
}
else
{
xdir = 0;
ydir = -30;
}
CreateParticle("ElectroSpark", PV_Random(-5, 5), PV_Random(-5, 5),
PV_Random(xdir - 5, xdir + 5), PV_Random(ydir - 5, ydir + 5),
PV_Random(1, 5), fx.particles, 200);
SoundAt("Animals::Puka::TeleportOut");
}
private func FxIntTeleportTimer(object target, effect fx, int time)
{
// Instant teleport if no midpoint set.
if (current_high_x == nil)
{
var old_x = GetX();
var old_y = GetY();
// Try target position if not stuck.
SetPosition(current_ground_x, current_ground_y);
var counter = 3;
while (Stuck() && counter > 0)
{
SetPosition(GetX(), GetY() - 5);
}
// Otherwise abort!
if (Stuck())
SetPosition(old_x, old_y);
return -1;
}
// Actually fly the way if in "normal" mode.
time *= 40;
if (time >= 1000) return -1;
var half_time = 500;
var w0 = Max(0, half_time - time);
var w1 = half_time - Abs(time - half_time);
var w2 = Max(0, time - half_time);
var point_x = fx.start_x * w0 + current_high_x * w1 + current_ground_x * w2;
var point_y = fx.start_y * w0 + current_high_y * w1 + current_ground_y * w2;
point_x /= w0 + w1 + w2;
point_y /= w0 + w1 + w2;
var old_stuck = Stuck();
var x = GetX();
var y = GetY();
SetPosition(point_x, point_y);
if (Stuck() && !old_stuck)
SetPosition(x, y);
SetSpeed(0, 0);
}
private func FxIntTeleportStop(object target, effect fx, int reason, temp)
{
if (temp || !this) return;
this.Visibility = VIS_All;
SetAction("Jump");
DoElectroCircle();
var xdir, ydir;
if (current_high_x != nil)
{
xdir = GetX() - current_high_x;
ydir = GetY() - current_high_y;
}
else
{
xdir = 0;
ydir = 30;
}
var start_x = -xdir/2;
var start_y = -ydir/2;
CreateParticle("ElectroSpark", PV_Random(start_x - 5, start_x + 5), PV_Random(start_y - 5, start_y + 5),
PV_Random(xdir - 5, xdir + 5), PV_Random(ydir - 5, ydir + 5),
PV_Random(1, 5), fx.particles, 200);
SoundAt("Animals::Puka::TeleportIn");
}
public func TryStartTeleport()
{
// Already teleporting?
if (GetAction() == "Teleport" || GetEffect("IntTeleport", this)) return;
// Go to the most distant water?
var teleport_ok = false;
if (!Random(3))
{
var best_position = nil, best_distance = nil;
var x = GetX(), y = GetY();
for (var position in water_positions)
{
if (GBackSolid(AbsX(position[0]), AbsY(position[1]))) continue;
var distance = Distance(x, y, position[0], position[1]);
if (best_position == nil || distance > best_distance)
{
best_position = position;
best_distance = distance;
}
}
if (best_position != nil)
{
current_ground_x = best_position[0];
current_ground_y = best_position[1];
current_high_x = nil;
current_high_y = nil;
teleport_ok = true;
}
}
teleport_ok = teleport_ok || GetTeleportPosition();
// Go to a new random position?
if (teleport_ok)
{
SetComDir(COMD_Stop);
if (GetAction() == "Walk")
SetAction("Teleport");
else EndTeleport();
}
}
private func StartTeleport()
{
this.Activity = nil;
}
// When the teleport action has played.
private func EndTeleport()
{
AddEffect("IntTeleport", this, 1, 1, this);
}
private func GetTeleportPosition()
{
// Allow multiple tries to increase the chance of a good position.
for (var try = 0; try < 2; ++try)
{
// Project a line upwards and from the collision point (if any) project downwards at an angle.
var upwards_angle = RandomX(-45, 45);
if (GetAction() == "Scale")
{
if (GetDir() == DIR_Left) upwards_angle += 90;
else upwards_angle -= 90;
}
var upwards_length = 100;
var upwards_target_x = GetX() + Sin(upwards_angle, upwards_length);
var upwards_target_y = GetY() - Cos(upwards_angle, upwards_length);
var point = PathFree2(GetX(), GetY(), upwards_target_x, upwards_target_y);
var high_x, high_y;
if (point)
{
high_x = point[0];
high_y = point[1];
}
else
{
high_x = upwards_target_x;
high_y = upwards_target_y;
}
high_x = 2 * high_x / 3;
high_y = 2 * high_y / 3;
// Now project down again.
var downwards_angle = RandomX(135, 160);
if (Random(2)) downwards_angle *= -1;
var down_target_x = high_x + Sin(downwards_angle, 75 + upwards_length);
var down_target_y = high_y - Cos(downwards_angle, 75 + upwards_length);
point = PathFree2(high_x, high_y, down_target_x, down_target_y);
if (!point) continue;
// Don't stay at the same position..
if (Distance(GetX(), GetY(), point[0], point[1]) < 50) continue;
current_high_x = high_x;
current_high_y = high_y;
current_ground_x = point[0];
current_ground_y = point[1];
return true;
}
return false;
}
private func ShockWater()
{
// Already shocking? Most splendid.
if (GetAction() == "ShockWater") return true;
// Still reloading?
if (GetEffect("IntShockWaterCooldown", this)) return false;
// On which side is the water?
var dir = nil;
if (GBackLiquid(-15, 15)) dir = DIR_Right;
else if (GBackLiquid(+15, 15)) dir = DIR_Left;
// No water?
if (dir == nil)
{
return false;
}
// Turn to the right direction.
if (GetDir() != dir)
Turn(dir, false);
SetComDir(COMD_Stop);
this.Activity = nil;
SetAction("ShockWater");
AddEffect("Sparkle", this, 1, 1, this);
Sound("Animals::Puka::Hiss*");
return true;
}
private func EndShockWater()
{
// Randomly sample some points and - if water - fire lightning.
var x = 15;
if (GetDir() == DIR_Right) x *= -1;
var y = 15;
for (var obj in FindObjects(Find_Distance(120), Find_OCF(OCF_Alive), Find_AnimalHostile(this->GetOwner())))
{
if (!obj->GBackLiquid()) continue;
var angle = Angle(GetX(), GetY(), obj->GetX(), obj->GetY());
var xdir = +Sin(angle, 5);
var ydir = -Cos(angle, 5);
LaunchLightning(GetX() + x, GetY() + y, RandomX(50, 70), xdir, ydir, 10, 10, true);
}
// And some particles on the tail.
var particles =
{
Prototype = Particles_ElectroSpark1(),
Stretch = 2000,
DampingX = 950, DampingY = 950
};
CreateParticle("ElectroSpark", x, y, PV_Random(-30, 30), PV_Random(-30, 30), PV_Random(1, 5), particles, 30);
// Don't do that again immediately.
AddEffect("IntShockWaterCooldown", this, 1, 30);
}
private func FxSparkleStart(object target, effect fx, temp)
{
if (temp) return;
fx.particles =
{
Prototype = Particles_ElectroSpark2(),
Stretch = 1000,
DampingX = 999, DampingY = 999
};
}
private func FxSparkleTimer(object target, effect fx, int time)
{
if (time > 30) return -1;
var angle = RandomX(-90, 90);
var xdir = Sin(angle, 20);
var ydir = -Cos(angle, 20);
CreateParticle("ElectroSpark", xdir, ydir, xdir, ydir, 5, fx.particles, 1);
return;
}
private func CheckTurn()
{
if(GetXDir() < 0) if(GetDir() != DIR_Left) SetDir(DIR_Left);
else if(GetXDir() > 0) if(GetDir() != DIR_Right) SetDir(DIR_Right);
var t = false;
if(turn_angle == 0 && GetDir() == DIR_Left) t = true;
else if(turn_angle == 180 && GetDir() == DIR_Right) t = true;
if(t)
{
if(!GetEffect("IntTurning", this))
AddEffect("IntTurning", this, 1, 1, this);
}
}
private func Turn(int dir, bool move)
{
if (dir == nil)
{
if (GetDir() == DIR_Left)
dir = DIR_Right;
else
dir = DIR_Left;
}
if(move)
{
if(dir == DIR_Left) SetComDir(COMD_Left);
else SetComDir(COMD_Right);
}
if(GetDir() == dir) return;
SetXDir(0);
SetDir(dir);
CheckTurn();
}
private func FxIntTurningStart(object target, effect fx, temp)
{
if(temp)
return true;
}
private func FxIntTurningTimer(object target, effect fx, int time)
{
if(GetDir() == DIR_Left)
turn_angle += 15;
else turn_angle -= 15;
if(turn_angle < 0 || turn_angle > 180)
{
turn_angle = BoundBy(turn_angle, 0, 180);
this.MeshTransformation = Trans_Rotate(turn_angle + 180 + 30,0,1,0);
return -1;
}
this.MeshTransformation = Trans_Rotate(turn_angle + 180 + 30,0,1,0);
return 1;
}
// Immune to lightning.
public func RejectLightningStrike() { return true; }
local MaxEnergy = 30000;
local MaxBreath = 10000;
local NoBurnDecay = 1;
local BorderBound = C4D_Border_Sides;
local ContactCalls = true;
local ActMap = {
Walk = {
Prototype = Action,
Name = "Walk",
Procedure = DFA_WALK,
Accel = 4,
Decel = 22,
Speed = 100,
Directions = 2,
FlipDir = 0,
Length = 70,
Delay = 1,
Animation = "Walk",
StartCall = "StartWalk",
AbortCall = "StopWalk",
EndCall = "StopWalk",
InLiquidAction = "Swim",
},
Swim = {
Prototype = Action,
Name = "Swim",
Procedure = DFA_SWIM,
Accel = 16,
Decel = 22,
Speed = 100,
Directions = 2,
FlipDir = 0,
Length = 70,
Delay = 1,
Animation = "Swim",
StartCall = "StartSwim",
AbortCall = "StopSwim",
EndCall = "EndSwim",
},
Scale = {
Prototype = Action,
Name = "Scale",
Procedure = DFA_SCALE,
Accel = 16,
Decel = 22,
Speed = 50,
Directions = 2,
FlipDir = 0,
Length = 70,
Delay = 2,
Animation = "Scale",
StartCall = "StartScale",
AbortCall = "StopScale",
EndCall = "StopScale",
},
Jump = {
Prototype = Action,
Name = "Jump",
Procedure = DFA_FLIGHT,
Speed = 200,
Accel = 14,
Directions = 2,
FlipDir = 0,
Length = 20,
Delay = 2,
Animation = "RunJump",
NextAction = "Hold",
PhaseCall = "CheckStuck",
StartCall = "StartJump",
EndCall = "EndJump",
AbortCall = "EndJump",
InLiquidAction = "Swim",
},
Dead = {
Prototype = Action,
Name = "Dead",
Directions = 2,
FlipDir = 0,
Length = 30,
Delay = 1,
Animation = "Teleport",
NextAction = "Hold",
NoOtherAction = 1,
ObjectDisabled = 1
},
Teleport = {
Prototype = Action,
Name = "Teleport",
Procedure = DFA_THROW,
Directions = 2,
FlipDir = 0,
Length = 10,
Delay = 3,
Animation = "Teleport",
NextAction = "Fly",
StartCall = "StartTeleport",
EndCall = "EndTeleport",
},
Sleep = {
Prototype = Action,
Name = "Sleep",
Procedure = DFA_THROW,
Directions = 2,
FlipDir = 0,
Length = 30,
Delay = 1,
Animation = "RollSleep",
NextAction = "Hold",
StartCall = "StartSleep"
},
LookAround = {
Prototype = Action,
Name = "LookAround",
Procedure = DFA_THROW,
Directions = 2,
FlipDir = 0,
Length = 90,
Delay = 1,
Animation = "Idle",
NextAction = "Walk",
StartCall = "ClearActivity"
},
ScratchFace = {
Prototype = Action,
Name = "ScratchFace",
Procedure = DFA_THROW,
Directions = 2,
FlipDir = 0,
Length = 30,
Delay = 1,
Animation = "IdleHand",
NextAction = "Walk",
StartCall = "ClearActivity"
},
ShockWater = {
Prototype = Action,
Name = "ShockWater",
Procedure = DFA_THROW,
Directions = 2,
FlipDir = 0,
Length = 10,
Delay = 3,
Animation = "ShockWater",
NextAction = "Walk",
EndCall = "EndShockWater",
},
Fly = {
Prototype = Action,
Name = "Fly",
Procedure = DFA_FLOAT,
Speed = 200,
Accel = 14,
Directions = 2,
FlipDir = 0,
Length = 20,
Delay = 2,
Animation = "RunJump",
NextAction = "Hold",
InLiquidAction = "Swim",
}
};