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

521 lines
13 KiB
C

/*
Fish
Author: Zapper
*/
local SwimMaxAngle = 15;
local SwimMaxSpeed = 30;
local VisionMaxAngle = 140;
local VisionMaxRange = 200;
local walking, swimming;
local current_angle, current_speed, current_direction;
local swim_animation;
local base_transform;
local brain;
// Wall vision behaves slightly differently and it does not make much sense for other fishes to overload these values.
local wall_vision_range = 64;
local wall_vision_max_angle = 35;
func Place(int amount, proplist rectangle, proplist settings)
{
var max_tries = 2 * amount;
var loc_area = nil;
if (rectangle) loc_area = Loc_InArea(rectangle);
var f;
while ((amount > 0) && (--max_tries > 0))
{
var spot = FindLocation(Loc_Material("Water"), Loc_Space(20), loc_area);
if (!spot) continue;
f = CreateObjectAbove(this, spot.x, spot.y, NO_OWNER);
if (!f) continue;
// Randomly add some large/slim fish
if (Random(3))
{
// There are naturally smaller and larger fishes. Fishes above around 150 can be clipped, so stay below that.
f->SetCon(RandomX(75, 140));
// make sure the smaller ones don't grow larger any more
f->StopGrowth();
// Slim fish.
if (f->GetCon() > 100 || !Random(3))
f->SetYZScale(1000 - Random(300));
}
if (f->Stuck())
{
f->RemoveObject();
continue;
}
--amount;
}
return f; // return last created fish
}
func Construction()
{
// general stuff
StartGrowth(15);
current_angle = Random(360);
current_speed = RandomX(SwimMaxSpeed/5, SwimMaxSpeed);
var len = GetAnimationLength("Swim");
swim_animation = PlayAnimation("Swim", 5, Anim_Linear(0, 0, len, 100, ANIM_Loop), Anim_Const(500));
UpdateSwim();
SetAction("Swim");
SetComDir(COMD_None);
ScheduleCall(this, this.InitActivity, 1 + Random(10), 0);
AddTimer(this.UpdateSwim, 2);
InitFuzzyRules();
return _inherited(...);
}
func InitActivity()
{
if (GetAlive()) AddTimer(this.Activity, 10 + Random(5));
}
func SetYZScale(int new_scale)
{
base_transform = Trans_Scale(1000, new_scale, new_scale);
return true;
}
func Death()
{
RemoveTimer(this.UpdateSwim);
RemoveTimer(this.Activity);
this.MeshTransformation = Trans_Rotate(160 + Random(41), 1, 0, 0);
if (base_transform) this.MeshTransformation = Trans_Mul(base_transform, this.MeshTransformation);
StopAnimation(swim_animation);
AddTimer(this.Decaying, 500);
this.Collectible = true;
// maybe respawn a new fish if roe is near
var roe = FindObject(Find_Distance(200), Find_ID(FishRoe));
if (roe)
roe->Hatch(GetID());
return _inherited(...);
}
func Decaying()
{
if (GetCon()<20) RemoveObject(); else DoCon(-5);
return true;
}
protected func ControlUse(object clonk, int iX, int iY)
{
clonk->Eat(this);
}
public func NutritionalValue() { if (!GetAlive()) return 15; else return 0; }
func InitFuzzyRules()
{
brain = FuzzyLogic->Init();
// ACTION SETS
brain->AddSet("swim", "sharp_left", [[-2 * SwimMaxAngle, 1], [-SwimMaxAngle, 0], [SwimMaxAngle, 0]]);
brain->AddSet("swim", "left", [[-SwimMaxAngle, 1], [-SwimMaxAngle/2, 0], [SwimMaxAngle, 0]]);
brain->AddSet("swim", "straight", [[-5, 0], [0, 1], [5, 0]]);
brain->AddSet("swim", "right", [[-SwimMaxAngle, 0], [SwimMaxAngle/2, 0], [SwimMaxAngle, 1]]);
brain->AddSet("swim", "sharp_right", [[-SwimMaxAngle, 0], [SwimMaxAngle, 0], [2 * SwimMaxAngle, 1]]);
brain->AddSet("speed", "slow", [[0, 1], [SwimMaxSpeed/2, 0], [SwimMaxSpeed, 0]]);
brain->AddSet("speed", "fast", [[0, 0], [SwimMaxSpeed/2, 0], [SwimMaxSpeed, 1]]);
// RULE SETS
var directional_sets = ["friend", "enemy", "food"];
for (var set in directional_sets)
{
brain->AddSet(set, "left", [[-VisionMaxAngle, 1], [0, 0], [VisionMaxAngle, 0]]);
brain->AddSet(set, "straight", [[-5, 0], [0, 1], [5, 0]]);
brain->AddSet(set, "right", [[-VisionMaxAngle, 0], [0, 0], [VisionMaxAngle, 1]]);
}
var proximity_sets = ["friend_range", "enemy_range", "food_range"];
var middle = VisionMaxRange / 2;
for (var set in proximity_sets)
{
brain->AddSet(set, "far", [[middle, 0], [VisionMaxRange, 1], [VisionMaxRange, 1]]);
brain->AddSet(set, "medium", [[0, 0], [middle, 1], [VisionMaxRange, 0]]);
brain->AddSet(set, "close", [[0, 1], [0, 1], [middle, 0]]);
}
brain->AddSet("left_wall", "close", [[0, 1], [0, 1], [wall_vision_range/2, 0]]);
brain->AddSet("right_wall", "close", [[0, 1], [0, 1], [wall_vision_range/2, 0]]);
brain->AddSet("wall_range", "close", [[0, 1], [0, 1], [wall_vision_range, 0]]);
// RULES
brain->AddRule(brain->And(brain->Not("wall_range=close"), "enemy_range=close"), "speed=fast");
brain->AddRule(brain->Or("friend_range=close", "food_range=close", "wall_range=close"), "speed=slow");
brain->AddRule(brain->And(brain->Not("wall_range=close"), brain->Or("food=left", brain->And("friend=left", "enemy_range=far", "food_range=far"), "enemy=right")), "swim=left");
brain->AddRule(brain->And(brain->Not("wall_range=close"), brain->Or("food=right", brain->And("friend=right", "enemy_range=far", "food_range=far"), "enemy=left")), "swim=right");
brain->AddRule(brain->And("left_wall=close", brain->Not("right_wall=close")), "swim=sharp_right");
brain->AddRule("right_wall=close", "swim=sharp_left");
}
func Activity()
{
if (swimming)
{
UpdateVision();
var actions = brain->Execute();
DoActions(actions);
}
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;
}
func UpdateVision()
{
UpdateVisionFor("enemy", "enemy_range", FindObjects(Find_Distance(VisionMaxRange), Find_OCF(OCF_Alive), Find_Or(Find_Func("IsPredator"), Find_Func("IsClonk")), Find_NoContainer(), Sort_Distance()));
UpdateVisionFor("friend", "friend_range", FindObjects(Find_Distance(VisionMaxRange), Find_ID(GetID()), Find_Exclude(this), Find_NoContainer(), Sort_Distance()));
UpdateVisionFor("food", "food_range", FindObjects(Find_Distance(VisionMaxRange), Find_Func("NutritionalValue"), Find_NoContainer(), Sort_Distance()), true);
UpdateWallVision();
}
func UpdateVisionFor(string set, string range_set, 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;
var angle = Angle(GetX(), GetY(), obj->GetX(), obj->GetY());
var d = GetTurnDirection(current_angle, angle);
if (!Inside(d, -VisionMaxAngle, VisionMaxAngle) || d == 0) continue;
// prevent piranhas to jump out of the water to eat unsuspecting Clonks
if (is_food && (obj->GetMaterial() != GetMaterial())) continue;
//CreateParticle("MagicSpark", obj->GetX() - GetX(), obj->GetY() - GetY(), 0, 0, 60, RGB(0, 255, 0));
//this->Message("%s@%d (me %d, it %d)", obj->GetName(), d, current_angle, angle);
var angle = -VisionMaxAngle;
if (d > 0) angle = VisionMaxRange;
var distance = ObjectDistance(this, obj);
brain->Fuzzify(set, angle * distance / VisionMaxRange);
if (range_set != nil)
brain->Fuzzify(range_set, distance);
// now that we fuzzified our food - can we actually eat it, too???
if (is_food && distance < GetCon()/10)
DoEat(obj);
return true;
}
brain->Fuzzify(set, 0);
if (range_set != nil)
brain->Fuzzify(range_set, VisionMaxRange + 1);
return false;
}
func UpdateWallVision()
{
if (!Random(5))
{
// check for material in front, happens occasionally
var d = 10;
if (!GBackLiquid(Sin(current_angle, d), -Cos(current_angle, d)))
{
brain->Fuzzify("wall_range", d);
brain->Fuzzify("left_wall", d);
brain->Fuzzify("right_wall", wall_vision_range);
return;
}
}
// anything in this function, that appears weird, looks that way due to optimization..
var closest_wall = wall_vision_range;
for (var i = 0; i <= 1; ++i)
{
var what = "left_wall";
var angle = -wall_vision_max_angle;
if (i == 1)
{
angle = +wall_vision_max_angle;
what = "right_wall";
}
// quickly check solid point
var distance = wall_vision_range;
for (var d = 1; d <= wall_vision_range; d *= 2)
{
var x = Sin(current_angle + angle, d);
var y = -Cos(current_angle + angle, d);
if (GBackLiquid(x, y)) continue;
distance = Distance(0, 0, x, y);
if (distance < closest_wall) closest_wall = distance;
// CreateParticle("SphereSpark", x, y, 0, 0, 30, {Prototype = Particles_MagicRing(), Attach = nil}, 1);
break;
}
brain->Fuzzify(what, distance);
}
brain->Fuzzify("wall_range", closest_wall);
}
func DoActions(proplist actions)
{
current_speed = actions.speed;
if (current_speed == 0) current_speed = SwimMaxSpeed / 3;
current_direction = actions.swim;
if (current_direction == 0) current_direction = RandomX(-2, 2);
}
func UpdateSwim()
{
if (!swimming) return;
current_angle = (current_angle + current_direction) % 360;
if (current_angle < 0) current_angle = 360 + current_angle;
SetVelocity(current_angle, current_speed);
var len = GetAnimationLength("Swim");
var pos = GetAnimationPosition(swim_animation);
SetAnimationPosition(swim_animation, Anim_Linear(pos, 0, len, SwimMaxSpeed - current_speed + 1, ANIM_Loop));
var t = current_angle - 270;
var t2 = Cos(t, 90) - 90; // Expand the areas around 0deg and 180deg a bit so you see fish from the side more
this.MeshTransformation = Trans_Mul(Trans_Rotate(t, 0, 0, 1), Trans_Rotate(t2, 1, 0, 0), base_transform);
}
func DoEat(object obj)
{
// fishes can nom on food not only once - they will eat it piece for piece
var x = obj->GetX() - GetX();
var y = obj->GetY() - GetY();
Bubble(1, 0, 0);
Bubble(1, x, y);
//CreateParticle("MaterialParticle", x, y, RandomX(-5, 5), RandomX(-5, 5), 20, RGB(50, 25, 0));
var effect = GetEffect("IsBeingEaten", obj);
if (!effect)
effect = AddEffect("IsBeingEaten", obj, 1, 0, nil, Fish);
EffectCall(obj, effect, "Add");
DoEnergy(2);
// happy fishes can lay happy fish eggs
if (Random(2) && !GetEffect("PlacedRoe"))
AddEffect("PlaceRoe", this, 1, 30, this);
}
func FxPlaceRoeStart(target, effect, temp)
{
if (temp) return;
effect.placed = false;
}
func FxPlaceRoeTimer(target, effect, timer)
{
if (!effect.placed)
{
if (Random(4)) return FX_OK;
CreateObjectAbove(FishRoe, 0, 0, GetOwner());
effect.placed = true;
return FX_OK;
}
// cooldown for laying eggs!
if (timer < 35 * 60) return FX_OK;
return FX_Execute_Kill;
}
func FxIsBeingEatenStart(target, effect, temp)
{
if (temp) return;
effect.amount = 0;
}
func FxIsBeingEatenAdd(target, effect)
{
if (!target) return;
if (++effect.amount < target->~NutritionalValue()) return;
target->RemoveObject();
}
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);
}
func StartWalk()
{
if (GBackLiquid())
{
SetAction("Swim");
return;
}
walking = true;
swimming = false;
var len = GetAnimationLength("Swim");
var pos = GetAnimationPosition(swim_animation);
SetAnimationPosition(swim_animation, Anim_Linear(pos, 0, len, 10, ANIM_Loop));
this.MeshTransformation = Trans_Mul(Trans_Rotate(90 + RandomX(-10, 10), 1, 0, 0), base_transform);
}
func StartSwim()
{
swimming = true;
walking = false;
// make sure we go down when entering the water
current_direction = RandomX(170, 190);
UpdateSwim();
}
func StartJump()
{
if (GBackLiquid())
{
SetAction("Swim");
return;
}
swimming = false;
walking = false;
}
func SaveScenarioObject(props)
{
if (!inherited(props, ...)) return false;
// Avoid saving some stuff that's reinitialized anyway
props->Remove("Con"); props->Remove("XDir"); props->Remove("YDir"); props->Remove("ComDir");
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 = 40000;
local MaxBreath = 180; // 180 = five seconds
local Placement = 1;
local NoBurnDecay = 1;
local BreatheWater = 1;
func IsPrey() { return true; }
func Definition(def) {
SetProperty("PictureTransformation", Trans_Mul(Trans_Rotate(20,1,0,0),Trans_Rotate(70,0,1,0)), def);
}