Game static system managing "porch crimes" and player-triggered misdemeanors. Tracks state (kicks, offers, robberies, doses, spikes, ransacks, loot), exposes getters for availability, persists to SaveData, advances chaos/karmic effects, plays audio/FX, and grants shop items and achievements.
namespace NoChillquarium;
/// <summary>
/// GTA-porch crimes that mash existing toys together: fight loot, janitor
/// caffeine refusal, kicking glass, robbing the funeral tin, dosing pipes,
/// spiking Brad, "gifting" Grandma. Charm is the felony. Comedy is the alibi.
/// </summary>
public static class CrimeSystem
{
const float KickCooldown = 1.15f;
const float KickWindowSeconds = 22f;
const int KicksBeforeCrack = 5;
static readonly Random _rng = new();
static int _version;
static int _kicks;
static int _janitorOffers;
static bool _robbedTin;
static bool _dosedPipes;
static bool _giftedGrandma;
static int _ransacks;
static int _spikes;
static bool _bradSpikedPending;
static int _fightLootM80;
static int _fightLootMeth;
static bool _hushM80Given;
static bool _caffeineCrackDone;
static float _kickCd;
static float _kickWindow;
static int _kicksInWindow;
static bool _ransackedThisResult;
static bool _sheetOpen;
public static int Version => _version;
public static bool SheetOpen => _sheetOpen;
public static int Kicks => _kicks;
public static int JanitorOffers => _janitorOffers;
public static bool RobbedTin => _robbedTin;
public static bool DosedPipes => _dosedPipes;
public static bool GiftedGrandma => _giftedGrandma;
public static int Ransacks => _ransacks;
public static int Spikes => _spikes;
public static bool BradSpikedPending => _bradSpikedPending;
/// <summary>How many porch felonies you've actually pulled (feeds Your Fault).</summary>
public static int Score =>
(_kicks >= 3 ? 1 : 0)
+ Math.Min( 2, _janitorOffers )
+ (_robbedTin ? 2 : 0)
+ (_dosedPipes ? 2 : 0)
+ (_giftedGrandma ? 2 : 0)
+ _ransacks
+ _spikes
+ (_fightLootM80 > 0 ? 1 : 0);
public static bool CanKick =>
TankSim.IsActive && GrandmaGifts.HasTank && _kickCd <= 0f
&& !MethRush.Open && !BattleSystem.IsOpen && !LotRunSystem.IsOpen;
public static bool CanRobTin =>
TankSim.IsActive && GrandmaGifts.HasReceived( "cold_wallet" ) && !_robbedTin;
public static bool CanOfferJanitor =>
TankSim.IsActive && Shop.MethBags > 0
&& (GrandmaGifts.HasReceived( "janitor_fusion" ) || StorySystem.HasCombinedOnce);
public static bool CanDosePipes =>
TankSim.IsActive && Shop.MethBags > 0 && LotRunSystem.Unlocked && !LotRunSystem.IsOpen;
public static bool CanGiftGrandma =>
TankSim.IsActive && Shop.MethBags > 0 && GrandmaGifts.HasTank
&& !GrandmaGifts.IsBusy && !GrandmaGifts.IsLookingAround && !_giftedGrandma;
public static bool CanSpikeBrad =>
Shop.MethBags > 0
&& BattleSystem.IsOpen
&& BattleSystem.Phase == BattlePhase.Lobby
&& !_bradSpikedPending;
public static bool CanRansack =>
BattleSystem.Phase == BattlePhase.Result
&& BattleSystem.PlayerWon
&& !_ransackedThisResult;
public static int SheetCount
{
get
{
var n = 0;
if ( CanRobTin ) n++;
if ( CanOfferJanitor ) n++;
if ( CanDosePipes ) n++;
if ( CanGiftGrandma ) n++;
return n;
}
}
public static string SheetMeta =>
SheetCount > 0 ? $"{SheetCount} ripe" : "ideas";
public static string JanitorSill =>
CanOfferJanitor ? "offer meth?" : "* tap tap *";
public static void ResetForNewGame()
{
_kicks = 0;
_janitorOffers = 0;
_robbedTin = false;
_dosedPipes = false;
_giftedGrandma = false;
_ransacks = 0;
_spikes = 0;
_bradSpikedPending = false;
_fightLootM80 = 0;
_fightLootMeth = 0;
_hushM80Given = false;
_caffeineCrackDone = false;
ClearSession();
_version++;
}
public static void Clear()
{
ClearSession();
_version++;
}
static void ClearSession()
{
_kickCd = 0f;
_kickWindow = 0f;
_kicksInWindow = 0;
_ransackedThisResult = false;
_sheetOpen = false;
}
public static void Load( SaveData data )
{
_kicks = Math.Max( 0, data?.TankKicks ?? 0 );
_janitorOffers = Math.Max( 0, data?.JanitorMethOffers ?? 0 );
_robbedTin = data?.RobbedCookieTin ?? false;
_dosedPipes = data?.DosedPipes ?? false;
_giftedGrandma = data?.GiftedGrandmaMeth ?? false;
_ransacks = Math.Max( 0, data?.GarageRansacks ?? 0 );
_spikes = Math.Max( 0, data?.BradSpikes ?? 0 );
_bradSpikedPending = data?.BradSpikedPending ?? false;
_fightLootM80 = Math.Max( 0, data?.FightLootM80 ?? 0 );
_fightLootMeth = Math.Max( 0, data?.FightLootMeth ?? 0 );
_hushM80Given = data?.JanitorHushM80 ?? false;
_caffeineCrackDone = data?.JanitorCaffeineCrack ?? false;
ClearSession();
_version++;
}
public static void WriteToSave( SaveData data )
{
if ( data is null )
return;
data.TankKicks = _kicks;
data.JanitorMethOffers = _janitorOffers;
data.RobbedCookieTin = _robbedTin;
data.DosedPipes = _dosedPipes;
data.GiftedGrandmaMeth = _giftedGrandma;
data.GarageRansacks = _ransacks;
data.BradSpikes = _spikes;
data.BradSpikedPending = _bradSpikedPending;
data.FightLootM80 = _fightLootM80;
data.FightLootMeth = _fightLootMeth;
data.JanitorHushM80 = _hushM80Given;
data.JanitorCaffeineCrack = _caffeineCrackDone;
}
public static void Tick( float dt )
{
if ( dt <= 0f )
return;
if ( _kickCd > 0f )
_kickCd = MathF.Max( 0f, _kickCd - dt );
if ( _kickWindow > 0f )
{
_kickWindow -= dt;
if ( _kickWindow <= 0f )
{
_kickWindow = 0f;
_kicksInWindow = 0;
}
}
}
public static void ToggleSheet()
{
if ( !TankSim.IsActive || !GrandmaGifts.HasTank )
return;
_sheetOpen = !_sheetOpen;
GameAudio.PlayUi( _sheetOpen ? GameAudio.Notice : GameAudio.Close );
_version++;
}
public static void CloseSheet()
{
if ( !_sheetOpen )
return;
_sheetOpen = false;
_version++;
}
public static void NotifyFightResult()
{
_ransackedThisResult = false;
_version++;
}
/// <summary>Fight purse side-loot. Returns a result-body fragment or empty.</summary>
public static string ApplyFightLoot( bool won, bool champFinal, bool champMid, float intensity )
{
var m80 = 0;
var meth = 0;
var chance = won
? (champFinal ? 1f : champMid ? 0.22f : Math.Clamp( 0.30f + intensity * 0.045f, 0.28f, 0.62f ))
: 0.12f;
if ( champFinal )
{
m80 = 1;
if ( _rng.NextDouble() < 0.55 )
m80++;
if ( _rng.NextDouble() < 0.28 )
meth = 1;
}
else if ( _rng.NextDouble() < chance )
{
m80 = 1;
if ( won && _rng.NextDouble() < 0.18 )
m80++;
if ( won && _rng.NextDouble() < 0.10 + intensity * 0.02f )
meth = 1;
}
if ( m80 <= 0 && meth <= 0 )
return "";
if ( m80 > 0 )
{
Shop.GrantM80( m80 );
_fightLootM80 += m80;
}
if ( meth > 0 )
{
Shop.GrantMeth( meth );
_fightLootMeth += meth;
}
Chaos.Add( 1.2f + m80 * 0.4f + meth );
_version++;
SaveGame.TrySave( quiet: true );
if ( !won )
return m80 > 0 ? $" Brad dropped a damp M80 running to the vending machine." : "";
var bits = new List<string>();
if ( m80 > 0 )
bits.Add( m80 == 1 ? "M80" : $"M80 ×{m80}" );
if ( meth > 0 )
bits.Add( "meth bag" );
var loot = string.Join( " + ", bits );
if ( champFinal )
return $" Garage locker: {loot}.";
return $" Swiped {loot} off the folding table.";
}
public static bool TryKickTank()
{
if ( !CanKick )
{
if ( _kickCd > 0f )
TankSim.ShowBanner( "Glass is still ringing. Wait a beat." );
return false;
}
_kickCd = KickCooldown;
_kicks++;
_kicksInWindow++;
_kickWindow = KickWindowSeconds;
var n = TankSim.KickAllFish();
BoomFeel.SoftPunch( 0.55f + Math.Min( 0.35f, _kicksInWindow * 0.06f ) );
WaterFx.Splash( TankSim.Width * 0.5f, TankSim.Height * 0.72f, 18, WaterLayer.Tank );
WaterFx.Squirt( TankSim.Width * 0.18f, TankSim.Height * 0.2f, -0.2f, -1f, 8, 180f, WaterLayer.Tank );
WaterFx.Squirt( TankSim.Width * 0.82f, TankSim.Height * 0.22f, 0.2f, -1f, 8, 180f, WaterLayer.Tank );
GameAudio.PlayUi( GameAudio.AlertWarning );
GameAudio.PulseMusic( 0.12f, 0.6f );
Chaos.Add( 1.6f );
Karma.Add( -0.9f );
string line = _kicksInWindow switch
{
1 => n > 0
? "You kicked the tank. The fish filed a complaint. You are HR."
: "You kicked empty glass. That's modern art. That's also a noise complaint.",
2 => "Second kick. The gravel resettled in fear. Comedy is a contact sport.",
3 => "Third kick. Grandma's porch has a pulse now. It is yours. It is wrong.",
4 => "Fourth. The water is taking notes. The notes are wet. The notes are evidence.",
_ => "You are beating up furniture. The furniture is winning on style points."
};
TankSim.ShowBanner( line );
if ( _kicksInWindow >= KicksBeforeCrack )
{
MethRush.ForceCrack( 1 );
_kicksInWindow = 0;
_kickWindow = 0f;
}
if ( _kicks == 1 )
GrandmaReact.OnFirstTankKick();
AchievementSystem.Unlock( "kick" );
JanitorPeep.On( "kick" );
StorySystem.Evaluate();
SaveGame.TrySave();
_version++;
return true;
}
public static bool TryOfferJanitorMeth()
{
if ( !CanOfferJanitor )
{
if ( Shop.MethBags <= 0 )
TankSim.ShowBanner( "No bag to offer. He already has a drug. It's Folgers." );
else
TankSim.ShowBanner( "The janitor isn't in the bit yet." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
// He refuses. The bag stays. That's the joke.
_janitorOffers++;
_sheetOpen = false;
Chaos.Add( 1.8f );
Karma.Add( -0.4f );
GameAudio.PlayUi( GameAudio.Notice );
JanitorPeep.SayNow( "caffeine" );
if ( !_hushM80Given && _janitorOffers >= 3 )
{
_hushM80Given = true;
Shop.GrantM80( 1 );
TankSim.ShowBanner( "He won't take the bag. He slides you a damp M80. \"Keep the noise away from the Folgers.\"" );
Chaos.Add( 2f );
}
else if ( !_caffeineCrackDone && _janitorOffers >= 5 )
{
_caffeineCrackDone = true;
MethRush.ForceCrack( 1 );
BoomFeel.SoftPunch( 0.8f );
WaterFx.Splash( TankSim.Width * 0.85f, TankSim.Height * 0.35f, 16, WaterLayer.Tank );
TankSim.ShowBanner( "He refused so hard the caffeine cracked the glass. That's a vibe. That's a lawsuit." );
}
else if ( _janitorOffers == 1 )
{
TankSim.ShowBanner( "You offered. He cited Folgers. Folgers has a lawyer. The lawyer is also Folgers." );
GrandmaReact.OnFirstJanitorMeth();
}
else
{
TankSim.ShowBanner( "Still a no. He is vibrating at a legal frequency. Your powder is off-key." );
}
AchievementSystem.Unlock( "janitor" );
StorySystem.Evaluate();
SaveGame.TrySave();
_version++;
return true;
}
public static bool TryPokeJanitor()
{
if ( !JanitorPeep.IsVisible || JanitorPeep.Leaving )
return false;
if ( CanOfferJanitor )
return TryOfferJanitorMeth();
JanitorPeep.SayNow( "poke" );
Chaos.Add( 0.4f );
GameAudio.PlayUi( GameAudio.Pop );
_version++;
return true;
}
public static bool TryRobCookieTin()
{
if ( !CanRobTin )
{
TankSim.ShowBanner( _robbedTin
? "Tin's empty. She counted. She can still count. That's worse."
: "No funeral tin yet. Wait for the wallet gift." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
_robbedTin = true;
_sheetOpen = false;
var swipe = Math.Round( 48 + _rng.NextDouble() * 42, 1 );
Economy.AddForced( swipe );
Chaos.Add( 3.5f );
Karma.Add( -8f );
GameAudio.PlayBigCash();
BoomFeel.SoftPunch( 0.45f );
TankSim.ShowBanner( $"You robbed the funeral tin. +Ð{Economy.FormatDoge( swipe )}. Grandpa's leftover hash. I mean cash. I mean both." );
GrandmaReact.OnRobbedTin();
JanitorPeep.On( "tin" );
AchievementSystem.Unlock( "tin" );
StorySystem.Evaluate();
SaveGame.TrySave();
_version++;
return true;
}
public static bool TryDosePipes()
{
if ( !CanDosePipes )
{
TankSim.ShowBanner( Shop.MethBags <= 0
? "No bag. The pipes are sober. The pipes hate that."
: "Adventure isn't open. The drain is just a drain." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
if ( !Shop.TryTakeMeth( 1 ) )
return false;
_dosedPipes = true;
_sheetOpen = false;
Chaos.Add( 5f );
Karma.Add( -3.2f );
BoomFeel.SoftPunch( 0.7f );
GameAudio.PlayUi( GameAudio.AlertWarning );
WaterFx.Squirt( TankSim.Width * 0.5f, TankSim.Height * 0.92f, 0f, -1f, 22, 260f, WaterLayer.Tank );
WaterFx.Splash( TankSim.Width * 0.5f, TankSim.Height * 0.88f, 16, WaterLayer.Tank );
TankSim.ShowBanner( "You flushed a bag. The pipes are doing homework. Rent just went up." );
JanitorPeep.ForceShow( "pipes" );
GrandmaReact.OnDosedPipes();
StorySystem.Evaluate();
SaveGame.TrySave();
_version++;
return true;
}
public static bool TryGiftGrandmaMeth()
{
if ( !CanGiftGrandma )
{
TankSim.ShowBanner( _giftedGrandma
? "She already grounded the soup. Don't test the soup."
: "Need a bag first. Shop → Shady." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
if ( !Shop.TryTakeMeth( 1 ) )
return false;
_giftedGrandma = true;
_sheetOpen = false;
Chaos.Add( 4f );
Karma.Add( -6.5f );
var pity = 12.0;
Economy.AddForced( pity );
GameAudio.PlayUi( GameAudio.Confirm );
TankSim.ShowBanner( $"Grandma: \"Sugar for tea.\" … \"This isn't sugar.\" Soup is grounded. She still tips Ð{Economy.FormatDoge( pity )}." );
GrandmaReact.OnGiftedMeth();
JanitorPeep.On( "grandma_meth" );
StorySystem.Evaluate();
SaveGame.TrySave();
_version++;
return true;
}
public static bool TrySpikeBrad()
{
if ( !CanSpikeBrad )
{
TankSim.ShowBanner( _bradSpikedPending
? "Bucket's already spicy. Fight while it's hot."
: "Need meth and Brad's lobby." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
if ( !Shop.TryTakeMeth( 1 ) )
return false;
_bradSpikedPending = true;
_spikes++;
Chaos.Add( 3.5f );
Karma.Add( -2.8f );
GameAudio.PlayUi( GameAudio.Inject );
TankSim.ShowBanner( "You dosed Brad's bucket. His fish are about to do homework with their gills." );
JanitorPeep.On( "spike" );
SaveGame.TrySave();
_version++;
return true;
}
/// <summary>Consumed when a fight actually starts.</summary>
public static bool ConsumeBradSpike()
{
if ( !_bradSpikedPending )
return false;
_bradSpikedPending = false;
_version++;
return true;
}
public static bool TryRansackGarage()
{
if ( !CanRansack )
return false;
_ransackedThisResult = true;
_ransacks++;
var cash = Math.Round( 16 + _rng.NextDouble() * 28, 1 );
Economy.AddForced( cash );
Shop.GrantM80( 1 );
var meth = _rng.NextDouble() < 0.22;
if ( meth )
Shop.GrantMeth( 1 );
Chaos.Add( 3f );
Karma.Add( -4.2f );
GameAudio.PlayBigCash();
var extra = meth ? " + a bag he hid in a tube sock" : "";
var line = $" Ransacked his drawer: +Ð{Economy.FormatDoge( cash )}, damp M80{extra}.";
BattleSystem.AppendResultNote( line );
TankSim.ShowBanner( $"Ransacked Brad's drawer. +Ð{Economy.FormatDoge( cash )}, damp M80{extra}. He laminated the theft." );
JanitorPeep.On( "ransack" );
if ( _ransacks == 1 )
GrandmaReact.OnFirstRansack();
SaveGame.TrySave();
_version++;
return true;
}
}