Game system managing 'cocaine sharks' in an ocean habitat. Spawns and updates SharkActor instances, handles AI hunting, movement, biting/eating fish, player actions to scare or fend sharks, and persistence of kill/eaten counters.
namespace NoChillquarium;
/// <summary>
/// Ocean hazard: cocaine sharks hunt fish in ocean habitats.
/// Scare with Ð + team power, or Fend with trained/jacked fish.
/// </summary>
public sealed class SharkActor
{
public string Id { get; init; }
public float X { get; set; }
public float Y { get; set; }
public float Vx { get; set; }
public float Vy { get; set; }
public float BiteCooldown { get; set; }
public float Life { get; set; } = 1f; // 0 = dead
public bool Alive => Life > 0.01f;
public int Facing => Vx >= 0f ? 1 : -1;
public float Width => 110f;
public float Height => 56f;
}
public static class SharkSystem
{
public const string OceanHabitatId = "ocean";
public const float SpawnIntervalMin = 22f;
public const float SpawnIntervalMax = 38f;
public const int MaxSharks = 2;
public const double ScareCost = 30;
public const double FendCost = 0; // free but needs power
public const float BiteRange = 38f;
public const float SharkSpeed = 95f;
static readonly Random _rng = new();
static readonly List<SharkActor> _sharks = new();
static float _spawnTimer = 12f;
static float _alertFlash;
static int _version;
static int _kills;
static int _eaten;
static bool _everSpawned;
public static int Version => _version;
public static IReadOnlyList<SharkActor> Sharks => _sharks;
public static int SharkCount => _sharks.Count( s => s.Alive );
public static bool HasSharks => SharkCount > 0;
public static float AlertFlash => _alertFlash;
public static int Kills => _kills;
public static int Eaten => _eaten;
public static bool EverSpawned => _everSpawned;
public static bool IsOceanActive =>
TankSim.IsActive
&& string.Equals( TankSim.HabitatId, OceanHabitatId, StringComparison.OrdinalIgnoreCase );
public static void Clear()
{
_sharks.Clear();
_spawnTimer = 12f;
_alertFlash = 0f;
_version++;
}
public static void ResetProgress()
{
_kills = 0;
_eaten = 0;
_everSpawned = false;
Clear();
}
public static void Load( int kills, int eaten, bool everSpawned )
{
_kills = Math.Max( 0, kills );
_eaten = Math.Max( 0, eaten );
_everSpawned = everSpawned;
Clear();
}
public static void WriteToSave( SaveData data )
{
if ( data is null )
return;
data.SharkKills = _kills;
data.SharkEaten = _eaten;
data.SharkEverSpawned = _everSpawned;
}
public static void Tick( float dt )
{
if ( !TankSim.IsActive || dt <= 0f )
return;
if ( dt > 0.05f )
dt = 0.05f;
if ( _alertFlash > 0f )
{
_alertFlash = MathF.Max( 0f, _alertFlash - dt );
_version++;
}
if ( !IsOceanActive )
{
// Leave the ocean → sharks swim off.
if ( _sharks.Count > 0 )
{
_sharks.Clear();
_version++;
}
return;
}
// Spawn cadence
_spawnTimer -= dt;
if ( _spawnTimer <= 0f )
{
_spawnTimer = SpawnIntervalMin + (float)_rng.NextDouble() * (SpawnIntervalMax - SpawnIntervalMin);
if ( SharkCount < MaxSharks && TankSim.FishCount > 0 )
TrySpawn();
}
for ( var i = _sharks.Count - 1; i >= 0; i-- )
{
var shark = _sharks[i];
if ( !shark.Alive )
{
_sharks.RemoveAt( i );
continue;
}
if ( shark.BiteCooldown > 0f )
shark.BiteCooldown -= dt;
Hunt( shark, dt );
Integrate( shark, dt );
if ( shark.BiteCooldown <= 0f )
TryBite( shark );
}
if ( _sharks.Count > 0 )
_version++;
}
static void TrySpawn()
{
var fromLeft = _rng.NextDouble() < 0.5;
var y = 60f + (float)_rng.NextDouble() * (TankSim.Height - 120f);
var x = fromLeft ? -40f : TankSim.Width + 40f;
var speed = SharkSpeed * (0.85f + (float)_rng.NextDouble() * 0.35f);
var vx = fromLeft ? speed : -speed;
_sharks.Add( new SharkActor
{
Id = Guid.NewGuid().ToString( "N" )[..6],
X = x,
Y = y,
Vx = vx,
Vy = ((float)_rng.NextDouble() - 0.5f) * 30f,
BiteCooldown = 1.2f,
Life = 1f
} );
_everSpawned = true;
_alertFlash = 1.4f;
Chaos.Add( 6f );
GameAudio.PlayUi( GameAudio.AlertWarning );
TankSim.ShowBanner( "COCAINE SHARK in the water. Your fish are snacks." );
Achievements.OnSharkSighted();
_version++;
}
static void Hunt( SharkActor shark, float dt )
{
if ( TankSim.FishCount == 0 )
{
// Patrol
shark.Vx += ((float)_rng.NextDouble() - 0.5f) * 40f * dt;
shark.Vy += ((float)_rng.NextDouble() - 0.5f) * 30f * dt;
return;
}
// Prefer weak / slow fish
FishActor prey = null;
var best = float.MaxValue;
foreach ( var f in TankSim.Fish )
{
var dx = f.X - shark.X;
var dy = f.DrawY - shark.Y;
var dist = MathF.Sqrt( dx * dx + dy * dy );
// Jacked / trained are harder targets (deprioritize)
var score = dist + f.Agility * 8f + f.SpeedStat * 6f + (f.Jacked ? 40f : 0f);
if ( f.IsSick || f.InWithdrawal )
score -= 30f;
if ( score < best )
{
best = score;
prey = f;
}
}
if ( prey is null )
return;
var pdx = prey.X - shark.X;
var pdy = prey.DrawY - shark.Y;
var len = MathF.Sqrt( pdx * pdx + pdy * pdy );
if ( len < 1f )
return;
var want = SharkSpeed * (0.9f + (1f - Math.Clamp( prey.TrainingNorm, 0f, 1f )) * 0.35f);
shark.Vx = pdx / len * want;
shark.Vy = pdy / len * want * 0.75f;
// Panic nearby fish slightly
if ( len < 160f )
{
prey.Vx += (pdx / len) * 40f * dt;
prey.Vy += (pdy / len) * 30f * dt;
}
}
static void Integrate( SharkActor shark, float dt )
{
shark.X += shark.Vx * dt;
shark.Y += shark.Vy * dt;
var minX = -20f;
var maxX = TankSim.Width + 20f;
var minY = 30f;
var maxY = TankSim.Height - 40f;
if ( shark.Y < minY )
{
shark.Y = minY;
shark.Vy = MathF.Abs( shark.Vy );
}
else if ( shark.Y > maxY )
{
shark.Y = maxY;
shark.Vy = -MathF.Abs( shark.Vy );
}
// Bounce horizontal within extended bounds
if ( shark.X < minX )
{
shark.X = minX;
shark.Vx = MathF.Abs( shark.Vx );
}
else if ( shark.X > maxX )
{
shark.X = maxX;
shark.Vx = -MathF.Abs( shark.Vx );
}
}
static void TryBite( SharkActor shark )
{
FishActor closest = null;
var bestDist = BiteRange;
foreach ( var f in TankSim.Fish )
{
var dx = f.X - shark.X;
var dy = f.DrawY - shark.Y;
var dist = MathF.Sqrt( dx * dx + dy * dy );
if ( dist < bestDist )
{
bestDist = dist;
closest = f;
}
}
if ( closest is null )
return;
// Escape chance from agility / speed / jacked / meth
var escape = 0.18f + closest.Agility * 0.04f + closest.SpeedStat * 0.03f;
if ( closest.Jacked )
escape += 0.22f;
if ( closest.OnMeth )
escape += 0.12f;
if ( closest.IsSick || closest.InWithdrawal )
escape -= 0.2f;
escape = Math.Clamp( escape, 0.08f, 0.78f );
shark.BiteCooldown = 1.6f + (float)_rng.NextDouble() * 0.8f;
if ( _rng.NextDouble() < escape )
{
// Dash away
var ang = (float)(_rng.NextDouble() * Math.PI * 2);
closest.Vx = MathF.Cos( ang ) * closest.Species.Speed * 2.4f;
closest.Vy = MathF.Sin( ang ) * closest.Species.Speed * 1.2f;
if ( _rng.NextDouble() < 0.35 )
TankSim.ShowBanner( $"{closest.Species.Name} dodged a shark. Barely." );
return;
}
// Eaten
var name = closest.Species.Name;
var ex = closest.X;
var ey = closest.DrawY;
TankSim.RemoveFish( closest.InstanceId );
ExplodeSystem.SpawnDeathPoof( ex, ey );
_eaten++;
Chaos.Add( 5f );
GameAudio.PlayUi( GameAudio.M80 );
TankSim.ShowBanner( $"Cocaine shark ate {name}. Insurance does not cover this." );
Achievements.OnFishEatenByShark();
_version++;
}
public static float TeamPower()
{
if ( TankSim.FishCount == 0 )
return 0f;
float sum = 0;
foreach ( var f in TankSim.Fish )
sum += BattleSystem.CombatPower( f );
return sum;
}
public static bool CanScare =>
IsOceanActive && HasSharks && Economy.Balance >= ScareCost;
public static bool CanFend =>
IsOceanActive && HasSharks && TankSim.FishCount > 0 && TeamPower() >= 8f;
/// <summary>Spend Ð — chance to drive a shark off based on team power.</summary>
public static bool TryScare()
{
if ( !HasSharks )
{
TankSim.ShowBanner( "No sharks. Ocean is... fine?" );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
if ( !Economy.TrySpend( ScareCost ) )
{
TankSim.ShowBanner( $"Scare costs Ð{Economy.FormatDoge( ScareCost )}." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
var power = TeamPower();
var chance = Math.Clamp( 0.25f + power * 0.02f, 0.25f, 0.9f );
if ( _rng.NextDouble() < chance )
{
// Remove weakest (first) shark
var shark = _sharks.FirstOrDefault( s => s.Alive );
if ( shark is not null )
{
ExplodeSystem.SpawnDeathPoof( shark.X, shark.Y );
_sharks.Remove( shark );
}
Chaos.Add( 2f );
Karma.Add( 3f );
GameAudio.PlayUi( GameAudio.Upgrade );
TankSim.ShowBanner( "Shark scared off. It'll tell its friends." );
Achievements.OnSharkScared();
}
else
{
Chaos.Add( 3f );
GameAudio.PlayUi( GameAudio.Error );
TankSim.ShowBanner( "Shark unimpressed. Your wallet is lighter." );
// Aggro: reduce bite cooldowns
foreach ( var s in _sharks )
s.BiteCooldown = MathF.Min( s.BiteCooldown, 0.4f );
}
SaveGame.TrySave();
_version++;
return true;
}
/// <summary>Fend with trained fish — can kill a shark for Ð loot.</summary>
public static bool TryFend()
{
if ( !HasSharks || TankSim.FishCount == 0 )
{
GameAudio.PlayUi( GameAudio.Error );
return false;
}
var power = TeamPower();
var chance = Math.Clamp( 0.15f + power * 0.025f, 0.15f, 0.92f );
// Jacked majority helps
var jacked = TankSim.Fish.Count( f => f.Jacked );
if ( jacked > 0 )
chance = Math.Clamp( chance + jacked * 0.06f, 0f, 0.95f );
var shark = _sharks.FirstOrDefault( s => s.Alive );
if ( shark is null )
return false;
if ( _rng.NextDouble() < chance )
{
ExplodeSystem.SpawnDeathPoof( shark.X, shark.Y );
// Extra paste bits
for ( var i = 0; i < 8; i++ )
{
// reuse death poof is enough
}
_sharks.Remove( shark );
_kills++;
var loot = 40 + _rng.NextDouble() * 50;
Economy.Add( loot );
Chaos.Add( 4f );
Karma.Add( 1.5f );
GameAudio.PlayDogeBark();
TankSim.ShowBanner( $"Shark defeated! +Ð{Economy.FormatDoge( loot )}. Cocaine not recovered." );
Achievements.OnSharkKilled( _kills );
// Tiny stat bump for survivors
foreach ( var f in TankSim.Fish )
{
f.Endurance = MathF.Min( 10f, f.Endurance + 0.1f );
f.Agility = MathF.Min( 10f, f.Agility + 0.12f );
}
}
else
{
Chaos.Add( 5f );
GameAudio.PlayUi( GameAudio.Error );
TankSim.ShowBanner( "Fend failed. Shark laughed (probably)." );
shark.BiteCooldown = 0.2f;
// Maybe eat one weak fish immediately
if ( _rng.NextDouble() < 0.4 && TankSim.FishCount > 0 )
{
var weak = TankSim.Fish.OrderBy( f => f.Agility + f.SpeedStat ).First();
var name = weak.Species.Name;
var ex = weak.X;
var ey = weak.DrawY;
TankSim.RemoveFish( weak.InstanceId );
ExplodeSystem.SpawnDeathPoof( ex, ey );
_eaten++;
TankSim.ShowBanner( $"Retaliation: shark ate {name}." );
Achievements.OnFishEatenByShark();
}
}
SaveGame.TrySave();
_version++;
return true;
}
}