forked from Mirrors/openclonk
521 lines
13 KiB
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);
|
|
}
|
|
|