Razor UI component for the game's main shell. It renders all main screens (warning, main menu, achievements, settings, ending, playing) and the in-game HUD, shop, battle UI, fish/tank rendering, and many interactive panels and controls, wiring buttons to C# methods and reading game subsystem state.
@using System;
@using System.Globalization;
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace NoChillquarium
<root class="shell @(StrobeClass) @(GameSettings.ReduceStrobe ? "reduce-strobe" : "")">
@* Solid backdrop — menu is clean text, no wallpaper. *@
<div class="menu-bg"></div>
@if ( GameFlow.Screen == GameScreen.Warning )
{
<div class="screen warning">
<div class="warn-frame @(GameSettings.ReduceStrobe ? "" : "pulse")">
<div class="warn-banner">PHOTOSENSITIVE WARNING</div>
<div class="warn-body">
<div class="warn-line strong">STROBES · FLASHING · LOUD METAL</div>
<div class="warn-line dim">Epilepsy risk? Quit now, or enable Reduce Strobe in Settings.</div>
</div>
<div class="warn-parody">Dark-humor aquarium. Not chill.</div>
<div class="btn primary" @onclick="@OnAcceptWarning">I Understand</div>
</div>
</div>
}
else if ( GameFlow.Screen == GameScreen.MainMenu )
{
var achMenuLbl = "Achievements " + Achievements.UnlockedCount + "/" + Achievements.CatalogCount;
var newGameLbl = _newGameArmed ? "Erase save? Confirm" : "New Game";
<div class="screen menu">
<div class="menu-center">
<div class="menu-title">@GameInfo.Title</div>
<div class="menu-sub">@GameInfo.Tagline</div>
<div class="menu-links">
@if ( GameFlow.CanContinue )
{
<div class="menu-link primary" @onclick="@OnContinue" @onmouseover="@OnHover">CONTINUE</div>
<div class="menu-link quiet @(_newGameArmed ? "danger" : "")" @onclick="@OnNewGame" @onmouseover="@OnHover">@newGameLbl</div>
}
else
{
<div class="menu-link primary" @onclick="@OnNewGame" @onmouseover="@OnHover">NEW GAME</div>
}
<div class="menu-link quiet" @onclick="@OnAchievements" @onmouseover="@OnHover">@achMenuLbl</div>
<div class="menu-link quiet" @onclick="@OnSettings" @onmouseover="@OnHover">Settings</div>
<div class="menu-link quiet danger" @onclick="@OnQuit" @onmouseover="@OnHover">Quit</div>
</div>
</div>
</div>
}
else if ( GameFlow.Screen == GameScreen.Achievements )
{
<div class="screen settings">
<div class="menu-panel simple achievements-panel">
<div class="title small">Achievements</div>
<div class="achievements-meta">@Achievements.UnlockedCount / @Achievements.CatalogCount unlocked</div>
<div class="achievements-scroll">
@foreach ( var ach in Achievements.MenuOrder )
{
var got = Achievements.IsUnlocked( ach.Id );
var title = got || !ach.Hidden ? ach.Title : "???";
var desc = got ? ach.Description : (ach.Hidden ? "Hidden" : ach.Description);
<div class="achievements-row @(got ? "got" : "locked")">
<div class="achievements-star">@(got ? "★" : "☆")</div>
<div class="achievements-body">
<div class="achievements-row-title">@title</div>
<div class="achievements-row-desc">@desc</div>
</div>
</div>
}
</div>
<div class="btn" @onclick="@OnBackAchievements" @onmouseover="@OnHover">Back</div>
</div>
</div>
}
else if ( GameFlow.Screen == GameScreen.Settings )
{
<div class="screen settings">
<div class="menu-panel simple settings-panel">
<div class="title small">Settings</div>
<div class="setting-row">
<div class="setting-label">Music</div>
<div class="slider-row">
<div class="btn slim" @onclick="@(() => NudgeMusic(-0.1f))">−</div>
<div class="setting-value">@Pct(GameSettings.MusicVolume)</div>
<div class="btn slim" @onclick="@(() => NudgeMusic(0.1f))">+</div>
</div>
</div>
<div class="setting-row">
<div class="setting-label">SFX</div>
<div class="slider-row">
<div class="btn slim" @onclick="@(() => NudgeSfx(-0.1f))">−</div>
<div class="setting-value">@Pct(GameSettings.SfxVolume)</div>
<div class="btn slim" @onclick="@(() => NudgeSfx(0.1f))">+</div>
</div>
</div>
<div class="setting-row">
<div class="setting-label">Reduce Strobe</div>
<div class="btn slim @(GameSettings.ReduceStrobe ? "primary" : "")" @onclick="@ToggleStrobe">
@(GameSettings.ReduceStrobe ? "On" : "Off")
</div>
</div>
<div class="btn" @onclick="@OnBackSettings" @onmouseover="@OnHover">Back</div>
</div>
</div>
}
else if ( GameFlow.Screen == GameScreen.Ending )
{
var endClass = EndingSystem.Kind switch
{
EndingKind.GoodBoy => "end-good",
EndingKind.BadBoy => "end-bad",
_ => "end-mixed"
};
var strobeEnd = EndingSystem.Kind == EndingKind.BadBoy && !GameSettings.ReduceStrobe ? "strobe-end" : "";
<div class="screen ending @endClass @strobeEnd @(GameSettings.ReduceStrobe ? "reduce-strobe" : "")" @onclick="@OnEndingClick">
<div class="ending-frame">
<div class="ending-kicker">@EndingSystem.Subtitle</div>
<div class="ending-title">@EndingSystem.Title</div>
<div class="ending-line">@EndingSystem.CurrentLine</div>
<div class="ending-meta">Karma @Karma.ValueText · Chaos @Chaos.ValueText</div>
@if ( EndingSystem.LinesFinished )
{
<div class="btn primary" style="margin-top: 20px;" @onclick="@OnEndingDone" @onmouseover="@OnHover">Menu</div>
}
else
{
<div class="ending-hint">Click to continue… (@(EndingSystem.LineIndex + 1)/@EndingSystem.LineCount)</div>
}
</div>
</div>
}
else if ( GameFlow.Screen == GameScreen.Playing )
{
@{
var shakeX = (DeathFeel.ShakeX + BoomFeel.ShakeX).ToString( "0.##", CultureInfo.InvariantCulture );
var shakeY = (DeathFeel.ShakeY + BoomFeel.ShakeY).ToString( "0.##", CultureInfo.InvariantCulture );
var flashA = Math.Clamp( DeathFeel.Flash + BoomFeel.Flash, 0f, 1f ).ToString( "0.###", CultureInfo.InvariantCulture );
var whiteA = Math.Clamp( BoomFeel.WhiteFlash, 0f, 1f ).ToString( "0.###", CultureInfo.InvariantCulture );
var boomClass = BoomFeel.IsActive ? "booming" : "";
// Visual mood tracks music: calm = chill chrome, absurd/dark = no-chill chrome.
var moodClass = GameAudio.WantsNoChillMusic ? "nochill" : "chill";
var waterClass = TankSim.ViewingSalt ? "salt-view" : "fresh-view";
var habClass = TankSim.HabitatCssClass;
var dieClass = DeathFeel.PeakStage >= 3 ? "dying-hard" : DeathFeel.PeakStage >= 1 ? "dying" : "";
var sharkClass = SharkSystem.HasSharks ? "shark-alert" : "";
}
@* Game cabinet layout — screen-level move/up so hold survives leaving the prop *@
<div class="screen playing cq @moodClass @waterClass @dieClass @sharkClass @(ShopOpen ? "shop-open" : "") @(ExplodeSystem.ModeActive ? "remove-mode" : "") @(_decorDragging ? "decor-holding" : "")"
@onmousemove="@(() => OnDecorHoldMove())"
@onmouseup="@(() => OnDecorHoldUp())">
<div class="play-bezel">
<div class="play-bezel-inner">
@* HUD: wallet | census *@
<div class="cq-topbar">
@* Spacer keeps wallet centered against the fish census *@
<div class="cq-top-left"></div>
<div class="cq-top-center">
<div class="cold-chip @(Economy.WalletPulse ? "pulse" : "") @(EconomyHelpOpen ? "help-open" : "")"
@onclick="@OnToggleEconomyHelp" @onmouseover="@OnHover" title="How money works">
<div class="cold-chip-shell">
<div class="cold-chip-body">
<div class="cold-chip-brand">WALLET</div>
<div class="cold-chip-lcd @(Economy.WalletPulse ? "lit" : "")">
<div class="cold-chip-lcd-glass">
<div class="cold-chip-lcd-row bal">
<walletlcd class="wallet-lcd-line" text="@WalletFont.BalanceLine"></walletlcd>
</div>
<div class="cold-chip-lcd-row rate">
<walletlcd class="wallet-lcd-line sm" text="@WalletFont.RateLine" small="@true"></walletlcd>
</div>
</div>
</div>
<div class="cold-chip-controls">
<div class="cold-chip-btn"></div>
<div class="cold-chip-btn"></div>
<div class="cold-chip-led @(Economy.WalletPulse ? "on" : "")"></div>
</div>
<div class="cold-chip-hint">Ð help</div>
</div>
</div>
<div class="doge-float-layer">
@foreach ( var df in DogeFloat.Items )
{
var life = DogeFloat.LifeNorm( df );
var oy = df.OffsetY.ToString( "0.#", CultureInfo.InvariantCulture );
var ox = df.DriftX.ToString( "0.#", CultureInfo.InvariantCulture );
var op = life.ToString( "0.##", CultureInfo.InvariantCulture );
<div class="doge-float @(df.IsSpend ? "spend" : "earn") @(df.IsFunny ? "funny" : "")"
style="margin-left: @(ox)px; margin-top: @(oy)px; opacity: @op;">
@df.Label
</div>
}
</div>
</div>
</div>
<div class="cq-top-right">
<div class="cq-census">
<div class="cq-census-head">
@if ( SpriteCatalog.Has( "ui_fish_counter" ) )
{
<div class="cq-census-icon-box">
<spritebox class="cq-census-icon" sprite="ui_fish_counter"></spritebox>
</div>
}
<span class="cq-census-title">Fish</span>
<span class="cq-census-total">×@TankSim.FishCount</span>
</div>
@if ( SharkSystem.HasSharks )
{
<div class="cq-census-alert">Sharks ×@SharkSystem.SharkCount</div>
}
else if ( ExplodeSystem.ModeActive )
{
<div class="cq-census-alert">@ExplodeSystem.StatusText</div>
}
else if ( DeathFeel.PeakStage > 0 )
{
<div class="cq-census-alert">@DeathFeel.StageLabel</div>
}
<div class="cq-census-rows">
@{
var census = TankSim.CensusActive();
}
@if ( census.Count == 0 )
{
<div class="cq-census-empty">empty — shop for fish</div>
}
else
{
@foreach ( var entry in census )
{
var sp = entry.Species;
var n = entry.Count;
<div class="cq-census-row">
@if ( sp.HasSprite )
{
<div class="census-swatch has-sprite">
<spritebox class="swatch-sprite" sprite="@sp.SpriteId"></spritebox>
</div>
}
else
{
<div class="census-swatch" style="background-color: @sp.Color;"></div>
}
<span class="cq-census-name">@sp.Name</span>
<span class="cq-census-n">×@n</span>
</div>
}
}
</div>
</div>
</div>
</div>
@* Coach pill — always-reserved slot so show/hide never shifts the tank *@
<div class="cq-hint-row">
@if ( !string.IsNullOrEmpty( ContextHint ) )
{
<div class="cq-hint @ContextHintClass">@ContextHint</div>
}
</div>
@* Thin mood strip — fixed height, never reflows *@
<div class="cq-mood-bar @(Chaos.Value > 1f || DeathFeel.PeakStage > 0 ? "lit" : "dim")">
<div class="cq-mood-fill" style="width: @ChaosBarWidth%;"></div>
<div class="cq-mood-mark"></div>
</div>
@* Only show tank switcher once a second tank exists *@
@if ( TankSim.SaltUnlocked )
{
<div class="tank-switcher">
<div class="tank-switch @(TankSim.ViewingSalt ? "" : "on")" @onclick="@OnViewFresh" @onmouseover="@OnHover">Fresh</div>
<div class="tank-switch @(TankSim.ViewingSalt ? "on" : "")" @onclick="@OnViewSalt" @onmouseover="@OnHover">Salt</div>
</div>
}
<div class="cq-stage-row">
<div class="cq-stage">
<div class="tank-glass @habClass @(ExplodeSystem.ModeActive ? "m80-mode" : "") [email protected] @boomClass [email protected] @(MethRush.Open ? "meth-rush" : "")"
style="left: @(shakeX)px; top: @(shakeY)px;">
<div class="tank-rim top"></div>
<div class="tank-water" @ref="_tankWaterPanel">
@* Empty-water hit target — behind fish so fish clicks don't bubble-close the menu *@
<div class="tank-water-hit" @onclick="@OnTankBackgroundClick"></div>
<div class="tank-shine"></div>
<div class="tank-bubbles"></div>
<div class="hab-prop"></div>
<div class="tank-gravel"></div>
@* Glass stress from failed meth calms *@
@if ( MethRush.Cracks > 0 )
{
<div class="tank-cracks [email protected]">
<div class="crack c1"></div>
<div class="crack c2"></div>
@if ( MethRush.Cracks >= 2 )
{
<div class="crack c3"></div>
}
@if ( MethRush.Cracks >= 3 )
{
<div class="crack c4"></div>
<div class="crack c5"></div>
}
@if ( MethRush.Cracks >= 4 )
{
<div class="crack c6"></div>
<div class="crack c7"></div>
}
@if ( MethRush.Cracks >= 5 )
{
<div class="crack c8"></div>
<div class="crack spider"></div>
}
</div>
}
@foreach ( var piece in TankSim.ActiveDecor )
{
var def = piece.Def;
var pieceRef = piece;
var dl = piece.X.ToString( "0.##", CultureInfo.InvariantCulture );
var dtop = piece.Y.ToString( "0.##", CultureInfo.InvariantCulture );
var dw = def.Width.ToString( "0.##", CultureInfo.InvariantCulture );
var dh = def.Height.ToString( "0.##", CultureInfo.InvariantCulture );
var dragCls = piece.Held || ReferenceEquals( _decorDragPiece, piece ) ? "dragging" : "";
var flyCls = piece.IsMoving && !piece.Held ? "flying" : "";
// Press on prop to grab — hold is driven by screen mouseup / tick.
// Parameterless lambda avoids Razor PanelEvent vs MousePanelEvent mismatch.
if ( def.HasSprite )
{
<div class="decor has-sprite @def.CssClass @dragCls @flyCls"
style="left: @(dl)px; top: @(dtop)px; width: @(dw)px; height: @(dh)px;"
@onmousedown="@(() => BeginDecorDrag( pieceRef ))">
<spritebox class="decor-sprite-fill" sprite="@def.SpriteId"></spritebox>
</div>
}
else
{
<div class="decor @def.CssClass @dragCls @flyCls"
style="left: @(dl)px; top: @(dtop)px; width: @(dw)px; height: @(dh)px;"
@onmousedown="@(() => BeginDecorDrag( pieceRef ))"></div>
}
}
<div class="boom-white-flash" style="opacity: @whiteA;"></div>
<div class="death-flash" style="opacity: @flashA;"></div>
<div class="death-vignette [email protected]"></div>
@foreach ( var pellet in FeedSystem.Pellets )
{
var pl = pellet.X.ToString( "0.##", CultureInfo.InvariantCulture );
var pt = pellet.Y.ToString( "0.##", CultureInfo.InvariantCulture );
<div class="pellet" style="left: @(pl)px; top: @(pt)px; background-color: @pellet.Color;"></div>
}
@foreach ( var fish in TankSim.Fish )
{
var twitch = 0f;
var twHz = 10f;
if ( fish.OnMeth )
{
twitch = 2.4f + fish.AddictionNorm * 2.2f;
twHz = 26f + fish.AddictionNorm * 16f;
}
else if ( fish.InWithdrawal )
{
twitch = 1.2f + fish.AddictionNorm * 1.5f;
twHz = 7f + fish.AddictionNorm * 6f;
}
else if ( fish.IsSick )
{
twitch = fish.SickProgress * fish.SickProgress * 3.2f;
twHz = 10f + fish.SickProgress * 20f;
}
else if ( fish.IsFightInjured )
{
twitch = 0.8f + fish.FightInjuryProgress * 1.8f;
twHz = 6f + fish.FightInjuryProgress * 10f;
}
var tx = twitch * MathF.Sin( Time.Now * twHz + fish.BobPhase );
var ty = twitch * MathF.Cos( Time.Now * (twHz * 0.85f) + fish.BobPhase );
var left = (fish.X - fish.Species.Width * 0.5f + tx).ToString( "0.##", CultureInfo.InvariantCulture );
var top = (fish.DrawY - fish.Species.Height * 0.5f + ty).ToString( "0.##", CultureInfo.InvariantCulture );
var w = fish.Species.Width.ToString( "0.##", CultureInfo.InvariantCulture );
var h = fish.Species.Height.ToString( "0.##", CultureInfo.InvariantCulture );
// All sheets face right after load; flip when swimming left.
var flip = fish.Vx < 0f ? "flip" : "";
var taped = ExplodeSystem.IsSelected( fish.InstanceId );
var sel = taped ? "selected" : "";
var fresh = ExplodeSystem.IsFreshlyTaped( fish.InstanceId ) ? "just-taped" : "";
var sickClass = fish.LooksSick ? $"sick sick-{fish.DisplaySickStage}" : "";
var hurtClass = fish.IsFightInjured ? "fight-hurt" : "";
var meth = fish.OnMeth ? "meth" : "";
var stoned = fish.Stoned ? "stoned" : "";
var wd = fish.InWithdrawal ? "withdrawing" : "";
var hooked = fish.IsAddicted ? $"hooked hooked-{fish.AddictionStage}" : "";
var jacked = fish.Jacked ? "jacked" : "";
var fatClass = fish.FatStage > 0 ? $"fat fat-{fish.FatStage}" : "";
var spdClass = fish.SpeedStage > 0 ? $"spd spd-{fish.SpeedStage}" : "";
var agiClass = fish.AgilityStage > 0 ? $"agi agi-{fish.AgilityStage}" : "";
var endClass = fish.EnduranceStage > 0 ? $"end end-{fish.EnduranceStage}" : "";
var battlePick = BattleSystem.IsSelected( fish.InstanceId ) ? "battle-pick" : "";
var menuOn = fish.InstanceId == FishMenuId ? "menu-on" : "";
var id = fish.InstanceId;
// Jacked + overfeed chonk + trained shape: wider / longer / stockier.
var scaleX = fish.DrawScaleX;
var scaleY = fish.DrawScaleY;
// Stoned: a little puffy / floaty
if ( fish.Stoned )
{
scaleX *= 1.08f;
scaleY *= 1.06f;
}
// Extra wobble while high (layered on sick twitch)
if ( fish.Stoned )
{
tx += MathF.Sin( Time.Now * 3.1f + fish.BobPhase ) * 2.4f;
ty += MathF.Cos( Time.Now * 2.4f + fish.BobPhase * 1.3f ) * 3.2f;
}
// Agile fish lean into the turn a bit (visual pitch from Vy).
var lean = 0f;
if ( fish.AgilityStage > 0 )
{
var spdLen = MathF.Sqrt( fish.Vx * fish.Vx + fish.Vy * fish.Vy );
if ( spdLen > 1f )
lean = Math.Clamp( fish.Vy / spdLen * fish.AgilityNorm * 12f, -14f, 14f );
}
var wJ = (fish.Species.Width * scaleX).ToString( "0.##", CultureInfo.InvariantCulture );
var hJ = (fish.Species.Height * scaleY).ToString( "0.##", CultureInfo.InvariantCulture );
var leftJ = (fish.X - fish.Species.Width * scaleX * 0.5f + tx).ToString( "0.##", CultureInfo.InvariantCulture );
var topJ = (fish.DrawY - fish.Species.Height * scaleY * 0.5f + ty).ToString( "0.##", CultureInfo.InvariantCulture );
// Combine flip + lean so we don't fight CSS transform rules.
var leanStr = lean.ToString( "0.##", CultureInfo.InvariantCulture );
string xform;
if ( fish.Species.HasSprite )
xform = MathF.Abs( lean ) > 0.5f ? $"transform: rotate({leanStr}deg);" : "";
else if ( fish.Vx < 0f )
xform = MathF.Abs( lean ) > 0.5f
? $"transform: scaleX(-1) rotate({leanStr}deg);"
: "transform: scaleX(-1);";
else
xform = MathF.Abs( lean ) > 0.5f ? $"transform: rotate({leanStr}deg);" : "";
var fishStyle = $"left: {leftJ}px; top: {topJ}px; width: {wJ}px; height: {hJ}px; {xform}";
<div class="fish @flip @sel @fresh @sickClass @hurtClass @meth @stoned @wd @hooked @jacked @fatClass @spdClass @agiClass @endClass @battlePick @menuOn @(fish.Species.HasSprite ? "has-sprite" : "")"
style="@fishStyle"
@onclick="@(() => OnFishClick(id))">
@if ( fish.Species.HasSprite )
{
@* Mirror on the textured panel when swimming left *@
<spritebox class="fish-sprite" sprite="@fish.Species.SpriteId" mirror="@(fish.Vx < 0f)"></spritebox>
}
else
{
<div class="fish-body" style="background-color: @fish.Species.Color;">
<div class="fish-fin" style="background-color: @fish.Species.FinColor;"></div>
<div class="fish-eye"></div>
<div class="fish-belly"></div>
</div>
}
@* Trained-stat tells (readable at a glance in the tank) *@
@if ( fish.SpeedStage > 0 )
{
<div class="stat-spd-trail t1"></div>
@if ( fish.SpeedStage >= 2 )
{
<div class="stat-spd-trail t2"></div>
}
@if ( fish.SpeedStage >= 3 )
{
<div class="stat-spd-trail t3"></div>
}
}
@if ( fish.AgilityStage > 0 )
{
<div class="stat-agi-fin top"></div>
<div class="stat-agi-fin bot"></div>
@if ( fish.AgilityStage >= 2 )
{
<div class="stat-agi-fin mid"></div>
}
}
@if ( fish.EnduranceStage > 0 )
{
<div class="stat-end-plate"></div>
@if ( fish.EnduranceStage >= 2 )
{
<div class="stat-end-band"></div>
}
}
@if ( fish.Jacked )
{
<div class="jacked-veins"></div>
}
@if ( (fish.IsSick && fish.SickStage >= 2 && !fish.OnMeth)
|| (fish.IsFightInjured && fish.FightInjuryStage >= 2) )
{
<div class="sick-x"></div>
}
@if ( fish.IsFightInjured )
{
<div class="fight-hurt-badge">@fish.FightCooldownLeftText</div>
}
@if ( fish.Stoned )
{
<div class="stoned-aura"></div>
<div class="stoned-aura ring2"></div>
<div class="stoned-eyes"></div>
<div class="stoned-spark a"></div>
<div class="stoned-spark b"></div>
<div class="stoned-spark c"></div>
}
@if ( fish.OnMeth )
{
<div class="meth-eyes"></div>
<div class="meth-aura"></div>
<div class="meth-sweat"></div>
}
@if ( fish.InWithdrawal )
{
<div class="wd-shiver"></div>
}
@if ( taped )
{
<div class="m80-prop">
<div class="m80-tape"></div>
<div class="m80-stick">
<div class="m80-cap"></div>
<div class="m80-body"></div>
<div class="m80-label">M80</div>
</div>
<div class="m80-fuse"></div>
<div class="m80-spark"></div>
</div>
}
</div>
}
@foreach ( var shark in SharkSystem.Sharks )
{
if ( !shark.Alive )
continue;
var sl = (shark.X - shark.Width * 0.5f).ToString( "0.##", CultureInfo.InvariantCulture );
var st = (shark.Y - shark.Height * 0.5f).ToString( "0.##", CultureInfo.InvariantCulture );
var sw = shark.Width.ToString( "0.##", CultureInfo.InvariantCulture );
var sh = shark.Height.ToString( "0.##", CultureInfo.InvariantCulture );
var sflip = shark.Facing < 0 ? "flip" : "";
<div class="shark @sflip @(FishSpecies.SharkHasSprite ? "has-sprite" : "")" style="left: @(sl)px; top: @(st)px; width: @(sw)px; height: @(sh)px;">
@if ( FishSpecies.SharkHasSprite )
{
<spritebox class="shark-sprite" sprite="@FishSpecies.SharkSpriteId"></spritebox>
}
else
{
<div class="shark-body"></div>
<div class="shark-fin"></div>
<div class="shark-eye"></div>
<div class="shark-nose"></div>
}
<div class="shark-label">COKE</div>
</div>
}
@foreach ( var bit in ExplodeSystem.Particles )
{
var bl = bit.X.ToString( "0.##", CultureInfo.InvariantCulture );
var bt = bit.Y.ToString( "0.##", CultureInfo.InvariantCulture );
<div class="boom-bit" style="left: @(bl)px; top: @(bt)px; background-color: @bit.Color;"></div>
}
@foreach ( var shock in BoomFeel.Shocks )
{
var sx = shock.X.ToString( "0.##", CultureInfo.InvariantCulture );
var sy = shock.Y.ToString( "0.##", CultureInfo.InvariantCulture );
<div class="boom-shock" style="left: @(sx)px; top: @(sy)px;"></div>
}
@* Training / meth +stat pops over fish (tank-local coords) *@
@foreach ( var sf in StatFloat.Items )
{
if ( !sf.Visible )
continue;
var life = StatFloat.LifeNorm( sf );
if ( life <= 0.02f )
continue;
// Anchor without CSS transform (unreliable in S&box UI)
var sfl = (sf.X - 40f).ToString( "0.#", CultureInfo.InvariantCulture );
var sft = (sf.Y - 18f).ToString( "0.#", CultureInfo.InvariantCulture );
var sfo = life.ToString( "0.##", CultureInfo.InvariantCulture );
<div class="stat-float @sf.Kind"
style="left: @(sfl)px; top: @(sft)px; opacity: @sfo;">
@sf.Label
</div>
}
@* Fish context menu — Train / Fight / Sell · click name to rename *@
@if ( !string.IsNullOrEmpty( FishMenuId ) )
{
var menuFish = TankSim.FindFish( FishMenuId );
@if ( menuFish is not null )
{
// Prebuild labels — S&box Razor breaks on "Ð@expr"
var sellLbl = "Sell Ð" + Economy.FormatDoge( TankSim.SellPrice( menuFish ) );
string statusLine = "";
if ( menuFish.IsFightInjured ) statusLine = "Hurt · no fights " + menuFish.FightCooldownLeftText;
else if ( menuFish.Stoned && menuFish.Jacked ) statusLine = "Stoned · Jacked";
else if ( menuFish.Stoned ) statusLine = "Stoned";
else if ( menuFish.Jacked && menuFish.IsSick ) statusLine = "Jacked · Sick";
else if ( menuFish.Jacked ) statusLine = "Jacked";
else if ( menuFish.IsSick ) statusLine = "Sick";
else if ( menuFish.FatStage > 0 ) statusLine = "Chonk";
var endLbl = menuFish.Endurance.ToString( "0.#", CultureInfo.InvariantCulture );
var spdLbl = menuFish.SpeedStat.ToString( "0.#", CultureInfo.InvariantCulture );
var agiLbl = menuFish.Agility.ToString( "0.#", CultureInfo.InvariantCulture );
var statsLine = "END " + endLbl + " · SPD " + spdLbl + " · AGI " + agiLbl;
var fightLbl = menuFish.IsFightInjured ? ("Hurt " + menuFish.FightCooldownLeftText) : "Fight";
var canMove = TankSim.HasMultipleTanks;
var moveLbl = canMove ? ("Move → " + TankSim.OtherTankLabel) : "";
var menuW = FishRenameOpen ? 200f : 168f;
var menuH = FishRenameOpen ? 220f : (canMove ? 236f : 200f);
var ml = Math.Clamp( menuFish.X - menuW * 0.5f, 8f, TankSim.Width - menuW - 8f ).ToString( "0.#", CultureInfo.InvariantCulture );
var mt = Math.Clamp( menuFish.DrawY - (FishRenameOpen ? 130f : (canMove ? 120f : 110f)), 8f, TankSim.Height - menuH ).ToString( "0.#", CultureInfo.InvariantCulture );
<div class="fish-menu-layer">
<div class="fish-menu-backdrop" @onclick="@CloseFishMenu"></div>
<div class="fish-menu @(FishRenameOpen ? "renaming" : "")" style="left: @(ml)px; top: @(mt)px;">
@if ( FishRenameOpen )
{
<div class="fish-menu-sub">Name · Enter</div>
@* Text + Focus; Enter via onsubmit + Submit hotkey *@
<textentry @ref="_renameEntry" class="fish-rename-entry" onsubmit=@OnRenameConfirm></textentry>
<div class="fish-rename-row">
<div class="fish-menu-btn quiet slim" @onclick="@OnRenameRandom" @onmouseover="@OnHover">Random</div>
<div class="fish-menu-btn primary slim" @onclick="@OnRenameConfirm" @onmouseover="@OnHover">Save</div>
<div class="fish-menu-btn quiet slim" @onclick="@OnRenameCancel" @onmouseover="@OnHover">Cancel</div>
</div>
}
else
{
<div class="fish-menu-title clickable" @onclick="@OnFishMenuRename" @onmouseover="@OnHover" title="Rename">@menuFish.DisplayName</div>
@if ( !string.IsNullOrEmpty( statusLine ) )
{
<div class="fish-menu-sub">@statusLine</div>
}
<div class="fish-menu-stats-line" title="SPD · AGI · END">@statsLine</div>
<div class="fish-menu-stat-bars">
<div class="fish-stat-bar end"><div class="fish-stat-fill" style="width: @((menuFish.EnduranceNorm * 100f).ToString( "0", CultureInfo.InvariantCulture ))%;"></div></div>
<div class="fish-stat-bar spd"><div class="fish-stat-fill" style="width: @((menuFish.SpeedNorm * 100f).ToString( "0", CultureInfo.InvariantCulture ))%;"></div></div>
<div class="fish-stat-bar agi"><div class="fish-stat-fill" style="width: @((menuFish.AgilityNorm * 100f).ToString( "0", CultureInfo.InvariantCulture ))%;"></div></div>
</div>
<div class="fish-menu-btn primary" @onclick="@OnFishMenuTrain" @onmouseover="@OnHover">Train</div>
<div class="fish-menu-btn @(menuFish.IsFightInjured ? "quiet" : "")" @onclick="@OnFishMenuFight" @onmouseover="@OnHover">@fightLbl</div>
@if ( canMove )
{
<div class="fish-menu-btn" @onclick="@OnFishMenuMove" @onmouseover="@OnHover">@moveLbl</div>
}
<div class="fish-menu-btn" @onclick="@OnFishMenuSell" @onmouseover="@OnHover">@sellLbl</div>
}
</div>
</div>
}
}
</div>
<div class="tank-rim bottom"></div>
</div>
</div>
@* Right rail — tools + shop laptop *@
<div class="inv-panel">
<div class="inv-title">Tools</div>
<div class="inv-slots">
@* Icon/label feeds; Next only cycles type. Empty until Food tab purchase. *@
<div class="inv-slot food @(FeedSystem.IsBoosted ? "hot" : "") @(FeedSystem.HasAnyFood ? "" : "empty")"
@onmouseover="@OnHover"
title="@(FeedSystem.HasAnyFood ? "Feed" : "Shop → Food")">
<div class="inv-icon-wrap" @onclick="@OnFeed">
<spritebox class="inv-icon" sprite="inv_food"></spritebox>
</div>
<div class="inv-label" @onclick="@OnFeed">Feed</div>
<div class="inv-meta">@(FeedSystem.HasAnyFood && FeedSystem.Selected is not null ? FeedSystem.Selected.DisplayInv : "empty")</div>
@if ( FeedSystem.HasAnyFood )
{
<div class="inv-actions">
<div class="inv-chip quiet" @onclick="@OnCycleFoodClick" title="Cycle food">Next</div>
</div>
}
</div>
@if ( Shop.M80Bags > 0 )
{
<div class="inv-slot m80 @(ExplodeSystem.ModeActive ? "on" : "") @(ExplodeSystem.SelectedCount > 0 ? "armed" : "")"
@onclick="@OnRemoveOrDetonate" @onmouseover="@OnHover"
title="@(ExplodeSystem.SelectedCount > 0 ? $"Boom ×{ExplodeSystem.SelectedCount}" : "Tape, then boom")">
<div class="inv-icon-wrap">
<spritebox class="inv-icon" sprite="inv_m80"></spritebox>
<div class="inv-qty">@Shop.M80Bags</div>
</div>
<div class="inv-label">@(ExplodeSystem.SelectedCount > 0 ? $"×{ExplodeSystem.SelectedCount}" : "M80")</div>
<div class="inv-meta">@(ExplodeSystem.ModeActive ? "arm" : "ready")</div>
</div>
}
@if ( TrainingSystem.SteroidsUnlocked )
{
var juiceTitle = TrainingSystem.Syringes > 0
? "Inject"
: ("Buy · Ð" + Economy.FormatDoge( TrainingSystem.SyringeCost ));
<div class="inv-slot juice @(TrainingSystem.Syringes > 0 ? "" : "empty")"
@onclick="@OnJuiceTool" @onmouseover="@OnHover" title="@juiceTitle">
<div class="inv-icon-wrap">
<spritebox class="inv-icon" sprite="inv_syringe"></spritebox>
@if ( TrainingSystem.Syringes > 0 )
{
<div class="inv-qty">@TrainingSystem.Syringes</div>
}
</div>
<div class="inv-label">Juice</div>
<div class="inv-meta">@(TrainingSystem.Syringes > 0 ? "use" : "buy")</div>
</div>
}
@if ( Shop.MethBags > 0 )
{
<div class="inv-slot meth" @onclick="@OnFeedMeth" @onmouseover="@OnHover" title="Dose">
<div class="inv-icon-wrap">
<spritebox class="inv-icon" sprite="inv_meth"></spritebox>
<div class="inv-qty">@Shop.MethBags</div>
</div>
<div class="inv-label">Meth</div>
<div class="inv-meta">dose</div>
</div>
}
</div>
<div class="dw-laptop @(ShopOpen ? "on" : "")"
@onclick="@OnToggleShop" @onmouseover="@OnHover" title="Shop">
<div class="dw-laptop-box">
@if ( SpriteCatalog.Has( "ui_laptop" ) )
{
<spritebox class="dw-laptop-sprite" sprite="ui_laptop"></spritebox>
}
else
{
<div class="dw-laptop-fallback"></div>
}
</div>
<div class="dw-laptop-label">Shop</div>
</div>
</div>
</div>@* end cq-stage-row *@
@* Soft bottom dock — only situational actions (Escape → menu) *@
@if ( SharkSystem.IsOceanActive && SharkSystem.HasSharks
|| EndingSystem.CanGoodBoy || EndingSystem.CanBadBoy || EndingSystem.CanMixed )
{
<div class="cq-dock">
<div class="cq-dock-row">
@if ( SharkSystem.IsOceanActive && SharkSystem.HasSharks )
{
<div class="cq-btn warn @(SharkSystem.CanScare ? "" : "off")" @onclick="@OnScareShark" @onmouseover="@OnHover">Scare</div>
<div class="cq-btn danger @(SharkSystem.CanFend ? "" : "off")" @onclick="@OnFendShark" @onmouseover="@OnHover">Fend</div>
}
@if ( EndingSystem.CanGoodBoy )
{
<div class="cq-btn primary" @onclick="@OnEndingGood" @onmouseover="@OnHover">Sanctuary</div>
}
@if ( EndingSystem.CanBadBoy )
{
<div class="cq-btn danger" @onclick="@OnEndingBad" @onmouseover="@OnHover">WMD</div>
}
@if ( EndingSystem.CanMixed )
{
<div class="cq-btn warn" @onclick="@OnEndingMixed" @onmouseover="@OnHover">Mixed end</div>
}
</div>
</div>
}
</div>@* play-bezel-inner *@
</div>@* play-bezel *@
@* Wallet click → money help *@
@if ( EconomyHelpOpen )
{
<div class="shop-backdrop" @onclick="@CloseEconomyHelp"></div>
<div class="econ-help-panel">
<div class="econ-help-header">
<div class="econ-help-kicker">Ð</div>
<div class="econ-help-title">Money</div>
<div class="cq-btn slim shop-close" @onclick="@CloseEconomyHelp" @onmouseover="@OnHover">Close</div>
</div>
<div class="econ-help-scroll">
@foreach ( var sec in EconomyHelp.Sections )
{
<div class="econ-help-block">
<div class="econ-help-h">@sec.Title</div>
<div class="econ-help-p">@sec.Body</div>
</div>
}
</div>
<div class="econ-help-foot">@EconomyHelp.Footer</div>
</div>
}
@* Floating notices — absolute, never push tank / dock *@
@if ( !string.IsNullOrEmpty( TankSim.Banner ) || DeathFeel.PeakStage > 0 )
{
<div class="cq-notice @(DeathFeel.PeakStage >= 3 ? "urgent" : "") @(string.IsNullOrEmpty( TankSim.Banner ) ? "status" : "")">
@(string.IsNullOrEmpty( TankSim.Banner ) ? DeathFeel.StageLabel : TankSim.Banner)
</div>
}
@if ( Achievements.ToastVisible )
{
<div class="cq-ach-toast">
<div class="cq-ach-kicker">Achievement</div>
<div class="cq-ach-title">@Achievements.ToastTitle</div>
<div class="cq-ach-body">@Achievements.ToastBody</div>
</div>
}
@* Train — centered popup (same pattern as inject / meth rush) *@
@if ( TrainingSystem.TrainOpen )
{
<div class="shop-backdrop" @onclick="@OnCancelTrain"></div>
<div class="train-panel">
<div class="train-panel-kicker">Train · @TrainingSystem.TrainStatFocus</div>
<div class="train-panel-title">@TrainingSystem.TrainTargetName</div>
<div class="train-side-sub">@TrainingSystem.TrainRepLabel · Space or REP! in the green</div>
<div class="train-reps">
<div class="train-rep-dot @(TrainingSystem.TrainRep > 0 ? "done" : "") @(TrainingSystem.TrainRep == 0 ? "current" : "")"></div>
<div class="train-rep-dot @(TrainingSystem.TrainRep > 1 ? "done" : "") @(TrainingSystem.TrainRep == 1 ? "current" : "")"></div>
<div class="train-rep-dot @(TrainingSystem.TrainRep > 2 ? "done" : "") @(TrainingSystem.TrainRep == 2 ? "current" : "")"></div>
</div>
<div class="train-track">
<div class="train-zone"
style="left: @TrainingSystem.TrainZoneLeftPct.ToString( "0.##", CultureInfo.InvariantCulture )%; width: @TrainingSystem.TrainZoneWidthPct.ToString( "0.##", CultureInfo.InvariantCulture )%;"></div>
<div class="train-marker"
style="left: @((TrainingSystem.TrainMarker * 100f).ToString( "0.##", CultureInfo.InvariantCulture ))%;"></div>
</div>
<div class="train-msg @(string.IsNullOrEmpty( TrainingSystem.TrainFeedback ) && string.IsNullOrEmpty( StatFloat.LastLine ) ? "" : "hit")">
@if ( !string.IsNullOrEmpty( StatFloat.LastLine ) )
{
<span>@StatFloat.LastLine</span>
}
else if ( !string.IsNullOrEmpty( TrainingSystem.TrainFeedback ) )
{
<span>@TrainingSystem.TrainFeedback</span>
}
else
{
<span>Timing…</span>
}
</div>
<div class="train-actions">
<div class="cq-btn primary big" @onclick="@OnHitTrain" @onmouseover="@OnHover">REP! (Space)</div>
<div class="cq-btn quiet" @onclick="@OnCancelTrain" @onmouseover="@OnHover">Done</div>
</div>
</div>
}
else if ( TrainingSystem.HasSessionSummary )
{
<div class="train-panel train-result">
<div class="train-panel-kicker">Session done</div>
<div class="train-panel-title">Gains</div>
<div class="train-result-line">@TrainingSystem.SessionSummary</div>
@if ( !string.IsNullOrEmpty( StatFloat.LastLine ) )
{
<div class="train-msg hit"><span>@StatFloat.LastLine</span></div>
}
</div>
}
@if ( TrainingSystem.InjectOpen )
{
<div class="shop-backdrop" @onclick="@OnCancelInject"></div>
<div class="inject-panel">
<div class="inject-title">Steroid Inject</div>
<div class="inject-msg">Hit when the needle is in the green zone.</div>
<div class="inject-track">
<div class="inject-zone"></div>
<div class="inject-marker" style="left: @((TrainingSystem.Marker * 100f).ToString( "0.##", CultureInfo.InvariantCulture ))%;"></div>
</div>
<div class="train-actions">
<div class="cq-btn primary big" @onclick="@OnHitInject" @onmouseover="@OnHover">Inject!</div>
<div class="cq-btn quiet" @onclick="@OnCancelInject" @onmouseover="@OnHover">Cancel</div>
</div>
</div>
}
@if ( MethRush.Open )
{
<div class="shop-backdrop"></div>
<div class="meth-rush-panel">
<div class="meth-rush-kicker">METH RUSH</div>
<div class="meth-rush-title">@MethRush.RoundLabel</div>
<div class="meth-rush-sub">@MethRush.CrackLabel · misses crack the glass</div>
<div class="meth-rush-msg">
@if ( !string.IsNullOrEmpty( MethRush.Feedback ) )
{
<span>@MethRush.Feedback</span>
}
else
{
<span>Hit CALM! when the bar is green</span>
}
</div>
<div class="train-track meth-rush-track">
<div class="train-zone"
style="left: @MethRush.ZoneLeftPct.ToString( "0.##", CultureInfo.InvariantCulture )%; width: @MethRush.ZoneWidthPct.ToString( "0.##", CultureInfo.InvariantCulture )%;"></div>
<div class="train-marker"
style="left: @((MethRush.Marker * 100f).ToString( "0.##", CultureInfo.InvariantCulture ))%;"></div>
</div>
<div class="meth-rush-score">
<span>Hits @MethRush.Hits</span>
<span class="train-score-sep">·</span>
<span>Miss @MethRush.Misses</span>
</div>
<div class="train-actions">
<div class="cq-btn warn big" @onclick="@OnHitMethRush" @onmouseover="@OnHover">CALM!</div>
<div class="cq-btn quiet" @onclick="@OnBailMethRush" @onmouseover="@OnHover">Bail</div>
</div>
</div>
}
@if ( BattleSystem.IsOpen )
{
// NEVER put dogecoin symbol next to @ in Razor — build all labels in C#.
var battleTitle = "Fights W" + BattleSystem.Wins + " / L" + BattleSystem.Losses;
var fightBtnLbl = "Fight!";
if ( BattleSystem.Template is not null )
fightBtnLbl = "Fight! D" + Economy.FormatDoge( BattleSystem.Template.EntryFee );
<div class="shop-backdrop battle-dim" @onclick="@OnBattleBackdrop"></div>
<div class="battle-panel">
<div class="battle-head">
<div class="battle-head-title">@battleTitle</div>
<div class="cq-btn slim shop-close" @onclick="@OnCloseBattle" @onmouseover="@OnHover">Close</div>
</div>
@if ( BattleSystem.Phase == BattlePhase.Lobby )
{
@{
var fightReady = TankSim.Fish.Count( BattleSystem.CanFight );
var fightHurt = TankSim.Fish.Count( f => f.IsFightInjured );
var lobbyMsg = fightHurt > 0
? ("Pick a circuit · " + fightReady + " ready · " + fightHurt + " hurt")
: "Pick a circuit, pick fish, then Fight.";
}
<div class="battle-msg">@lobbyMsg</div>
<div class="battle-templates">
@foreach ( var t in BattleSystem.Catalog )
{
var pick = t;
var unlocked = BattleSystem.IsUnlocked( pick );
var sel = BattleSystem.Template?.Id == pick.Id;
var beaten = BattleSystem.IsBeaten( pick.Id );
// Build full line in C# — no inline @ with currency symbols
var fee = Economy.FormatDoge( pick.EntryFee );
var pMin = Economy.FormatDoge( pick.RewardMin );
var pMax = Economy.FormatDoge( pick.RewardMax );
string lineName;
string lineMeta;
if ( unlocked )
{
lineName = beaten ? (pick.Name + " done") : pick.Name;
lineMeta = "team " + pick.TeamSize + " entry D" + fee + " win D" + pMin + "-" + pMax;
var prize = BattleSystem.GetPrizeHabitat( pick );
if ( prize is not null && !BattleSystem.PrizeAlreadyOwned( pick ) )
lineMeta = lineMeta + " prize: " + prize.Name;
}
else
{
lineName = pick.Name;
lineMeta = BattleSystem.UnlockHint( pick );
}
<div class="battle-row @(sel ? "on" : "") @(unlocked ? "" : "locked")"
@onclick="@(() => OnSelectBattle(pick))" @onmouseover="@OnHover">
<div class="battle-row-name">@lineName</div>
<div class="battle-row-meta">@lineMeta</div>
</div>
}
</div>
@if ( BattleSystem.Template is not null )
{
var need = BattleSystem.Template.TeamSize;
var have = BattleSystem.SelectedCount;
var rosterLbl = "Team " + have + " / " + need + " (click a fish)";
<div class="battle-roster">
<div class="battle-roster-label">@rosterLbl</div>
<div class="battle-roster-row">
@foreach ( var fish in TankSim.Fish )
{
var on = BattleSystem.IsSelected( fish.InstanceId );
var hurt = fish.IsFightInjured;
var pow = BattleSystem.CombatPower( fish ).ToString( "0.#", CultureInfo.InvariantCulture );
var chipLbl = hurt
? (fish.DisplayName + " hurt " + fish.FightCooldownLeftText)
: (fish.DisplayName + " " + pow);
<div class="battle-chip @(on ? "on" : "") @(hurt ? "hurt" : "")" @onclick="@(() => OnBattlePickFish(fish.InstanceId))">
<div class="legend-swatch" style="background-color: @fish.Species.Color;"></div>
<span>@chipLbl</span>
</div>
}
</div>
<div class="battle-start-row">
<div class="cq-btn primary big @(BattleSystem.CanStart() ? "" : "off")" @onclick="@OnStartBattle" @onmouseover="@OnHover">@fightBtnLbl</div>
</div>
@if ( !BattleSystem.CanStart() )
{
string why;
if ( have < need )
why = fightReady < need
? ("Need " + need + " ready fish (" + fightReady + " healthy)")
: ("Need " + need + " fish on the team");
else if ( TankSim.Fish.Any( f => BattleSystem.IsSelected( f.InstanceId ) && f.IsFightInjured ) )
why = "Injured fish can't fight";
else
why = "Need D" + Economy.FormatDoge( BattleSystem.Template.EntryFee ) + " to enter";
<div class="battle-start-hint">@why</div>
}
</div>
}
}
else if ( BattleSystem.Phase == BattlePhase.Fighting || BattleSystem.Phase == BattlePhase.Result )
{
var flashOp = Math.Clamp( BattleSystem.ImpactFlash, 0f, 1f ).ToString( "0.##", CultureInfo.InvariantCulture );
var stageShakeX = (BattleSystem.ArenaShake * MathF.Sin( Time.Now * 40f )).ToString( "0.#", CultureInfo.InvariantCulture );
var stageShakeY = (BattleSystem.ArenaShake * 0.6f * MathF.Cos( Time.Now * 33f )).ToString( "0.#", CultureInfo.InvariantCulture );
<div class="battle-stage-wrap">
<div class="battle-stage @(BattleSystem.Phase == BattlePhase.Fighting ? "live" : "done") @(BattleSystem.VisKind)"
style="left: @(stageShakeX)px; top: @(stageShakeY)px;">
<div class="battle-water"></div>
<div class="battle-bubbles"></div>
<div class="battle-gravel"></div>
<div class="battle-lane you-lane">YOU</div>
<div class="battle-lane ai-lane">THEM</div>
@if ( BattleSystem.ImpactFlash > 0.05f )
{
<div class="battle-flash" style="opacity: @flashOp;"></div>
}
@foreach ( var f in BattleSystem.PlayerTeam.Concat( BattleSystem.AiTeam ) )
{
BattleSystem.GetDrawPos( f, out var dx, out var dy );
var left = (dx - f.Width * 0.5f).ToString( "0.#", CultureInfo.InvariantCulture );
var top = (dy - f.Height * 0.5f).ToString( "0.#", CultureInfo.InvariantCulture );
var w = f.Width.ToString( "0.#", CultureInfo.InvariantCulture );
var h = f.Height.ToString( "0.#", CultureInfo.InvariantCulture );
var hpPct = f.MaxHp > 0 ? (int)MathF.Round( f.Hp / f.MaxHp * 100f ) : 0;
var anim = f.Anim.ToString().ToLowerInvariant();
var side = f.Player ? "ally" : "foe";
var jacked = f.Jacked ? "jacked" : "";
var down = f.Alive ? "" : "down";
var mirror = f.Facing < 0;
<div class="battle-actor @side @anim @jacked @down"
style="left: @(left)px; top: @(top)px; width: @(w)px; height: @(h)px;">
@if ( f.HasSprite )
{
<spritebox class="battle-actor-sprite" sprite="@f.SpriteId" mirror="@mirror"></spritebox>
}
else
{
<div class="battle-actor-body" style="background-color: @f.Color;"></div>
<div class="battle-actor-fin" style="background-color: @(f.FinColor ?? f.Color);"></div>
}
@if ( f.Jacked )
{
<div class="battle-actor-jacked"></div>
}
<div class="battle-actor-name">@f.Name</div>
<div class="battle-actor-hp">
<div class="battle-actor-hp-fill @(f.Player ? "" : "ai")" style="width: @(hpPct)%;"></div>
</div>
</div>
}
@foreach ( var fx in BattleSystem.Fx )
{
var life = BattleSystem.FxLifeNorm( fx );
if ( life <= 0.02f )
continue;
var fxl = fx.X.ToString( "0.#", CultureInfo.InvariantCulture );
var fxt = (fx.Y + fx.OffsetY).ToString( "0.#", CultureInfo.InvariantCulture );
var fxo = life.ToString( "0.##", CultureInfo.InvariantCulture );
<div class="battle-fx @fx.Kind"
style="left: @(fxl)px; top: @(fxt)px; opacity: @fxo;">
@fx.Label
</div>
}
</div>
</div>
<div class="battle-log">
@foreach ( var line in BattleSystem.Log )
{
<div class="battle-log-line">@line</div>
}
</div>
@if ( BattleSystem.Phase == BattlePhase.Result )
{
<div class="battle-result @(BattleSystem.PlayerWon ? "win" : "lose")">
<div class="battle-result-title">@BattleSystem.ResultTitle</div>
<div class="battle-result-body">@BattleSystem.ResultBody</div>
<div class="cq-btn primary" style="margin-top: 10px;" @onclick="@OnAckBattle" @onmouseover="@OnHover">Continue</div>
</div>
}
else
{
<div class="battle-msg">Round @BattleSystem.Round · fins flying…</div>
}
}
</div>
}
@if ( ShopOpen )
{
// Prebuild Ð labels (S&box Razor chokes on "Ð@expr")
var dwBal = "Ð" + Economy.FormatDoge( Economy.Balance );
@* Sibling backdrop + absolute panel (same pattern as battle/inject — reliable hit-testing) *@
<div class="shop-backdrop shop-dim" @onclick="@OnCloseShop"></div>
<div class="shop-panel cq-shop darkweb">
<div class="dw-topbar">
<div class="dw-topbar-left">
<div class="dw-site">Shop</div>
</div>
<div class="dw-topbar-mid"></div>
<div class="dw-topbar-right">
<div class="dw-wallet">@dwBal</div>
<div class="cq-btn slim shop-close" @onclick="@OnCloseShop" @onmouseover="@OnHover">Close</div>
</div>
</div>
<div class="cq-shop-tabs">
<div class="cq-tab @(ShopTab == "fish" ? "on" : "")" @onclick="@(() => SetShopTab("fish"))" @onmouseover="@OnHover">Fish</div>
<div class="cq-tab @(ShopTab == "food" ? "on" : "")" @onclick="@(() => SetShopTab("food"))" @onmouseover="@OnHover">Food</div>
<div class="cq-tab @(ShopTab == "tank" ? "on" : "")" @onclick="@(() => SetShopTab("tank"))" @onmouseover="@OnHover">Tank</div>
<div class="cq-tab @(ShopTab == "decor" ? "on" : "")" @onclick="@(() => SetShopTab("decor"))" @onmouseover="@OnHover">Decor</div>
<div class="cq-tab @(ShopTab == "extra" ? "on" : "")" @onclick="@(() => SetShopTab("extra"))" @onmouseover="@OnHover">Shady</div>
</div>
<div class="shop-msg dw-msg">@Shop.LastMessage</div>
@if ( DarkWeb.CheckoutOpen )
{
var payLbl = DarkWeb.PayButtonLabel;
var priceLbl = DarkWeb.PriceLabel;
<div class="dw-checkout">
<div class="dw-checkout-kicker">@DarkWeb.StepLabel</div>
<div class="dw-checkout-title">@DarkWeb.ItemName</div>
<div class="dw-checkout-note">@DarkWeb.ItemNote</div>
<div class="dw-checkout-meta">
<span>@priceLbl</span>
</div>
<div class="dw-checkout-log">@DarkWeb.Status</div>
<div class="dw-checkout-actions">
@if ( DarkWeb.Step == 0 )
{
<div class="dw-spinner">Loading…</div>
<div class="cq-btn quiet" @onclick="@OnDarkWebCancel" @onmouseover="@OnHover">Cancel</div>
}
else if ( DarkWeb.Step == 1 )
{
<div class="cq-btn primary" @onclick="@OnDarkWebContinue" @onmouseover="@OnHover">Continue</div>
<div class="cq-btn quiet" @onclick="@OnDarkWebCancel" @onmouseover="@OnHover">Cancel</div>
}
else
{
<div class="cq-btn @(DarkWeb.CanAfford ? "warn" : "primary")" @onclick="@OnDarkWebPay" @onmouseover="@OnHover">@payLbl</div>
<div class="cq-btn quiet" @onclick="@OnDarkWebCancel" @onmouseover="@OnHover">Cancel</div>
}
</div>
</div>
}
else
{
<div class="shop-list">
@if ( ShopTab == "fish" )
{
@foreach ( var species in FishSpecies.ShopCatalog )
{
var can = Shop.CanBuyFish( species );
var waterTag = species.Water == WaterType.Salt ? "Salt" : "Fresh";
var mismatch = species.Water != TankSim.Water ? " · wrong water" : "";
var costLbl = "Ð" + Economy.FormatDoge( species.BuyCost );
var rateLbl = waterTag + " · +" + Economy.FormatDoge( species.IncomePerSecond ) + "/s" + mismatch;
var buySpecies = species;
<div class="shop-row buyable" @onclick="@(() => OnBuyFish(buySpecies))" @onmouseover="@OnHover">
<div class="shop-col name">
@if ( species.HasSprite )
{
<div class="legend-swatch has-sprite">
<spritebox class="swatch-sprite" sprite="@species.SpriteId"></spritebox>
</div>
}
else
{
<div class="legend-swatch" style="background-color: @species.Color;"></div>
}
@species.Name
</div>
<div class="shop-col rate">@rateLbl</div>
<div class="shop-col cost">@costLbl</div>
<div class="shop-col act">
<div class="cq-btn slim @(can ? "primary" : "off")">Buy</div>
</div>
</div>
}
}
else if ( ShopTab == "food" )
{
@foreach ( var food in FoodDef.Catalog )
{
var owned = FeedSystem.IsUnlocked( food );
var selected = FeedSystem.Selected is not null && FeedSystem.Selected.Id == food.Id;
var canUnlock = FeedSystem.CanUnlock( food );
var reason = FeedSystem.UnlockBlockedReason( food );
var costLbl = owned
? (selected ? "Active" : "Owned")
: "Ð" + Economy.FormatDoge( food.UnlockCost );
var buyFood = food;
<div class="shop-row buyable @(selected ? "owned" : "")"
@onclick="@(() => OnFoodRowClick(buyFood))" @onmouseover="@OnHover">
<div class="shop-col name">
<div class="legend-swatch" style="background-color: @food.PelletColor;"></div>
@food.DisplayInv
</div>
<div class="shop-col rate">
@food.Note · ×@food.BoostMultiplier.ToString( "0.##" )
@if ( !owned && !string.IsNullOrEmpty( reason ) )
{
<span> · @reason</span>
}
</div>
<div class="shop-col cost">@costLbl</div>
<div class="shop-col act">
@if ( owned )
{
<div class="cq-btn slim @(selected ? "primary" : "")">@(selected ? "Using" : "Use")</div>
}
else
{
<div class="cq-btn slim @(canUnlock ? "primary" : "off")">Unlock</div>
}
</div>
</div>
}
}
else if ( ShopTab == "tank" )
{
<div class="shop-row header">
<div class="shop-col name">@TankSim.ActiveHabitat.Name</div>
<div class="shop-col rate">cap @TankSim.Capacity · decor @TankSim.MaxDecor · ×@TankSim.ActiveHabitat.IncomeMult.ToString( "0.##" )</div>
<div class="shop-col cost"></div>
<div class="shop-col act"></div>
</div>
@{
var nextHab = Shop.NextHabitatOffer;
}
@if ( nextHab is not null )
{
var canHab = Shop.CanBuyHabitat( nextHab );
var timeOk = Economy.PlaytimeSeconds >= nextHab.UnlockPlaytime;
var habCost = "Ð" + Economy.FormatDoge( nextHab.Cost );
var buyHab = nextHab;
<div class="shop-row buyable" @onclick="@(() => OnBuyHabitat(buyHab))" @onmouseover="@OnHover">
<div class="shop-col name">→ @nextHab.Name</div>
<div class="shop-col rate">
@if ( timeOk )
{
<span>@nextHab.Note · +@nextHab.BaseCapacity base · decor @nextHab.MaxDecor</span>
}
else
{
<span>@Shop.HabitatUnlockText( nextHab )</span>
}
</div>
<div class="shop-col cost">@habCost</div>
<div class="shop-col act">
<div class="cq-btn slim @(canHab ? "primary" : "off")">Lease</div>
</div>
</div>
}
else
{
<div class="shop-row">
<div class="shop-col name">Max habitat</div>
<div class="shop-col rate">Ocean</div>
<div class="shop-col cost"></div>
<div class="shop-col act"></div>
</div>
}
@{
var capCost = "Ð" + Economy.FormatDoge( Shop.CapacityCost );
var saltCost = "Ð" + Economy.FormatDoge( Shop.SaltTankCost );
}
<div class="shop-row buyable" @onclick="@OnBuyCapacity" @onmouseover="@OnHover">
<div class="shop-col name">+@Shop.CapacityStep slots</div>
<div class="shop-col rate">@(TankSim.ViewingSalt ? "salt" : "fresh")</div>
<div class="shop-col cost">@capCost</div>
<div class="shop-col act">
<div class="cq-btn slim @(Shop.CanBuyCapacity() ? "primary" : "off")">Buy</div>
</div>
</div>
@if ( !TankSim.SaltUnlocked )
{
<div class="shop-row buyable" @onclick="@OnBuySaltTank" @onmouseover="@OnHover">
<div class="shop-col name">Salt tank</div>
<div class="shop-col rate">second aquarium</div>
<div class="shop-col cost">@saltCost</div>
<div class="shop-col act">
<div class="cq-btn slim @(Shop.CanBuySaltTank() ? "primary" : "off")">Buy</div>
</div>
</div>
}
}
else if ( ShopTab == "decor" )
{
<div class="shop-row header">
<div class="shop-col name">Props @TankSim.DecorCount/@TankSim.MaxDecor</div>
<div class="shop-col rate"></div>
<div class="shop-col cost"></div>
<div class="shop-col act"></div>
</div>
@foreach ( var decor in DecorDef.Catalog )
{
var can = Shop.CanBuyDecor( decor );
var decorCost = "Ð" + Economy.FormatDoge( decor.Cost );
var decorRate = decor.Note + " · +" + Economy.FormatDoge( decor.ComfortBonus ) + "/s";
var buyDecor = decor;
<div class="shop-row buyable" @onclick="@(() => OnBuyDecor(buyDecor))" @onmouseover="@OnHover">
<div class="shop-col name">
@if ( decor.HasSprite )
{
<div class="legend-swatch has-sprite">
<spritebox class="swatch-sprite" sprite="@decor.SpriteId"></spritebox>
</div>
}
@decor.Name
</div>
<div class="shop-col rate">@decorRate</div>
<div class="shop-col cost">@decorCost</div>
<div class="shop-col act">
<div class="cq-btn slim @(can ? "primary" : "off")">Buy</div>
</div>
</div>
}
}
else if ( ShopTab == "extra" )
{
@{
var methCost = "Ð" + Economy.FormatDoge( Shop.MethCost );
var m80Cost = "Ð" + Economy.FormatDoge( Shop.M80Cost );
var wmdCost = "Ð" + Economy.FormatDoge( EndingSystem.WmdCost );
var whaleCost = "Ð" + Economy.FormatDoge( EndingSystem.WhaleCost );
}
<div class="shop-row buyable shady" @onclick="@OnBuyM80" @onmouseover="@OnHover">
<div class="shop-col name">M80 @(Shop.M80Bags > 0 ? $"×{Shop.M80Bags}" : "")</div>
<div class="shop-col rate">tape · boom</div>
<div class="shop-col cost">@m80Cost</div>
<div class="shop-col act">
<div class="cq-btn slim @(Shop.CanBuyM80() ? "warn" : "off")">Buy</div>
</div>
</div>
<div class="shop-row buyable shady" @onclick="@OnBuyMeth" @onmouseover="@OnHover">
<div class="shop-col name">Meth</div>
<div class="shop-col rate">zoomies</div>
<div class="shop-col cost">@methCost</div>
<div class="shop-col act">
<div class="cq-btn slim @(Shop.CanBuyMeth() ? "warn" : "off")">Buy</div>
</div>
</div>
<div class="shop-row buyable shady" @onclick="@OnBuyWmd" @onmouseover="@OnHover">
<div class="shop-col name">WMD crate @(EndingSystem.HasWmd ? "✓" : "")</div>
<div class="shop-col rate">@EndingSystem.WmdShopNote</div>
<div class="shop-col cost">@wmdCost</div>
<div class="shop-col act">
@if ( EndingSystem.HasWmd )
{
<div class="cq-btn slim off">Owned</div>
}
else
{
<div class="cq-btn slim @(EndingSystem.CanBuyWmd() ? "danger" : "off")">Buy</div>
}
</div>
</div>
<div class="shop-row buyable shady" @onclick="@OnBuyWhale" @onmouseover="@OnHover">
<div class="shop-col name">Blue whale @(EndingSystem.HasWhale ? "✓" : "")</div>
<div class="shop-col rate">@EndingSystem.WhaleShopNote</div>
<div class="shop-col cost">@whaleCost</div>
<div class="shop-col act">
@if ( EndingSystem.HasWhale )
{
<div class="cq-btn slim off">Owned</div>
}
else
{
<div class="cq-btn slim @(EndingSystem.CanBuyWhale() ? "danger" : "off")">Buy</div>
}
</div>
</div>
<div class="shop-row header">
<div class="shop-col name">Endings</div>
<div class="shop-col rate">@EndingSystem.ArcTip</div>
<div class="shop-col cost"></div>
<div class="shop-col act"></div>
</div>
<div class="shop-row">
<div class="shop-col name">Chill</div>
<div class="shop-col rate">@EndingSystem.GoodHint</div>
<div class="shop-col cost"></div>
<div class="shop-col act"></div>
</div>
<div class="shop-row">
<div class="shop-col name">No-chill</div>
<div class="shop-col rate">@EndingSystem.BadHint</div>
<div class="shop-col cost"></div>
<div class="shop-col act"></div>
</div>
<div class="shop-row">
<div class="shop-col name">Mixed</div>
<div class="shop-col rate">@EndingSystem.MixedHint</div>
<div class="shop-col cost"></div>
<div class="shop-col act"></div>
</div>
}
</div>
}
<div class="shop-hint dw-hint">Esc closes</div>
</div>
}
</div>@* playing *@
}
</root>
@code
{
bool ShopOpen;
string ShopTab = "fish";
/// <summary>Fish context menu target (instance id), or null/empty when closed.</summary>
string FishMenuId;
bool EconomyHelpOpen;
/// <summary>Second click required to wipe a save from the main menu.</summary>
bool _newGameArmed;
string _fishClickGuardId;
float _fishClickGuardUntil;
bool FishRenameOpen;
string FishRenameDraft = "";
int _renamePoolIndex = -1;
bool _renameFocusPending;
TextEntry _renameEntry;
// Decor hold: mousedown grabs, stays held until mouseup (or Attack1 released).
Panel _tankWaterPanel;
DecorPiece _decorDragPiece;
float _decorLastLx;
float _decorLastLy;
float _decorRecentVx;
float _decorRecentVy;
float _decorGrabOx; // cursor offset inside piece on grab (tank px)
float _decorGrabOy;
bool _decorDragging;
float _decorLastScreenX;
float _decorLastScreenY;
float _decorGrabAt; // Time.Now when grabbed — ignore early release noise
bool _decorSawAttackDown; // true once Input saw Attack1 held this drag
/// <summary>Funny nicknames for Random while typing.</summary>
static readonly string[] FishNamePool =
{
"Mortimer", "Reginald", "Bartholomew", "Cornelius", "Percival",
"Winifred", "Agnes", "Beatrice", "Horatio", "Cecil",
"Chum Bucket", "Gutter Gary", "Draino", "Flush", "Sewer Steve",
"Tax Evasion", "Alimony", "Repo Ray", "Unpaid Fine", "W-2",
"Bubbles Jr", "Noodle", "Sashimi", "Miso", "Wasabi",
"Captain Crunch", "Gill Bates", "Fin Diesel", "Scales Obama", "Codzilla",
"Karen", "Chad", "Kyle", "Brenda", "Todd",
"Emo Support", "Therapy Debt", "Zoomies", "Final Boss", "HR",
"The Landlord", "HOA President", "Unit 4B", "Mister Fish", "Local Legend",
"Do Not Feed", "Science Project", "Oops", "Witness", "The Suspect",
"Blorbo", "Gloop", "Wigglesworth", "Sir Floats", "Count Chomp",
"Beelzebubbles", "Finneas", "Wet Doug", "Splash Gordon", "Lord Bubbles"
};
string StrobeClass =>
GameFlow.Screen == GameScreen.Warning && !GameSettings.ReduceStrobe
? "strobe-ready"
: "";
int ChaosBarWidth => (int)MathF.Round( Math.Clamp( Chaos.Value, 0f, 100f ) );
/// <summary>
/// Short coach line under the top bar. Empty = hidden (idle play stays clean).
/// Only shows for active modes / empty tank / fish menu.
/// </summary>
string ContextHint
{
get
{
if ( MethRush.Open )
return "Hit CALM! in the green";
if ( TrainingSystem.TrainOpen )
return "Space / REP! in the green";
if ( TrainingSystem.HasSessionSummary )
return TrainingSystem.SessionSummary;
if ( TrainingSystem.InjectOpen )
return "Inject! in the green";
if ( BattleSystem.Phase == BattlePhase.Fighting )
return "Fighting…";
if ( BattleSystem.Phase == BattlePhase.Lobby )
return "Pick circuit · pick fish · Fight";
if ( BattleSystem.Phase == BattlePhase.Result )
return "Continue";
if ( DarkWeb.CheckoutOpen )
return "Pay or cancel";
if ( ShopOpen || EconomyHelpOpen )
return "Esc closes";
if ( ExplodeSystem.ModeActive && ExplodeSystem.SelectedCount > 0 )
return "Click M80 again to boom";
if ( ExplodeSystem.ModeActive )
return "Tap fish to tape · Esc cancels";
if ( FishRenameOpen )
return "Name · Enter saves";
if ( !string.IsNullOrEmpty( FishMenuId ) )
{
var mf = TankSim.FindFish( FishMenuId );
if ( mf is not null && mf.IsFightInjured )
return "Hurt " + mf.FightCooldownLeftText;
return TankSim.HasMultipleTanks ? "Train · Fight · Move · Sell" : "Train · Fight · Sell";
}
if ( TankSim.FishCount == 0 )
return "Shop → buy a fish";
if ( !FeedSystem.HasAnyFood )
return "Shop → Food";
if ( _decorDragPiece is not null )
return "Hold · release to throw";
return "";
}
}
string ContextHintClass
{
get
{
if ( MethRush.Open || ExplodeSystem.ModeActive )
return "warn";
if ( _decorDragPiece is not null )
return "focus";
if ( TrainingSystem.TrainOpen || TrainingSystem.InjectOpen || TrainingSystem.HasSessionSummary )
return "focus";
if ( BattleSystem.IsOpen )
return "warn";
if ( TankSim.FishCount == 0 || !FeedSystem.HasAnyFood )
return "nudge";
if ( FishRenameOpen || !string.IsNullOrEmpty( FishMenuId ) )
return "focus";
if ( ShopOpen || DarkWeb.CheckoutOpen )
return "nudge";
return "";
}
}
protected override void OnStart()
{
GameSettings.Load();
GameFlow.Changed += OnFlowChanged;
SpriteCatalog.Warmup();
GameAudio.PlayUi( GameAudio.AlertWarning );
}
protected override void OnDestroy()
{
GameFlow.Changed -= OnFlowChanged;
if ( TankSim.IsActive )
SaveGame.TrySave();
GameAudio.StopMusic();
FeedSystem.Clear();
ExplodeSystem.Clear();
SharkSystem.Clear();
DeathFeel.Clear();
BoomFeel.Clear();
DogeFloat.Clear();
StatFloat.Clear();
DarkWeb.Clear();
MethRush.Clear();
TankSim.Clear();
}
protected override void OnUpdate()
{
GameAudio.TickMusic();
HandleEscape();
HandleTrainHotkeys();
HandleRenameHotkeys();
TryFocusRenameEntry();
TickDecorDrag();
if ( GameFlow.Screen == GameScreen.Playing && TankSim.IsActive )
{
TankSim.Tick( Time.Delta );
FeedSystem.Tick( Time.Delta );
ExplodeSystem.Tick( Time.Delta );
BattleSystem.Tick( Time.Delta );
SharkSystem.Tick( Time.Delta );
Economy.Tick( Time.Delta );
DogeFloat.Tick( Time.Delta );
StatFloat.Tick( Time.Delta );
MethRush.Tick( Time.Delta );
if ( ShopOpen )
DarkWeb.Tick( Time.Delta );
Chaos.TickCalm( Time.Delta );
Karma.TickCalmCare( Time.Delta );
DeathFeel.Tick( Time.Delta );
BoomFeel.Tick( Time.Delta );
TrainingSystem.Tick( Time.Delta );
Achievements.Tick( Time.Delta );
}
else if ( GameFlow.Screen == GameScreen.Ending )
{
EndingSystem.Tick( Time.Delta );
Achievements.Tick( Time.Delta );
}
else
{
Achievements.Tick( Time.Delta );
}
}
void TryFocusRenameEntry()
{
if ( !_renameFocusPending || _renameEntry is null )
return;
_renameFocusPending = false;
_renameEntry.Text = FishRenameDraft ?? "";
_renameEntry.Focus();
}
/// <summary>Space (Jump action) hits a training rep while the gym is open.</summary>
void HandleTrainHotkeys()
{
if ( GameFlow.Screen != GameScreen.Playing )
return;
if ( !TrainingSystem.TrainOpen )
return;
// Bound to space in ProjectSettings/Input.config
if ( !Input.Pressed( "Jump" ) )
return;
OnHitTrain();
}
/// <summary>Enter confirms fish rename (Submit action in Input.config).</summary>
void HandleRenameHotkeys()
{
if ( GameFlow.Screen != GameScreen.Playing )
return;
if ( !FishRenameOpen )
return;
if ( !Input.Pressed( "Submit" ) )
return;
OnRenameConfirm();
}
void OnFlowChanged()
{
if ( GameFlow.Screen != GameScreen.Playing )
{
ShopOpen = false;
FishMenuId = null;
EconomyHelpOpen = false;
ExplodeSystem.Clear();
}
StateHasChanged();
}
void OnToggleEconomyHelp()
{
EconomyHelpOpen = !EconomyHelpOpen;
if ( EconomyHelpOpen )
{
FishMenuId = null;
ShopOpen = false;
GameAudio.PlayUi( GameAudio.Notice );
}
else
{
GameAudio.PlayUi( GameAudio.Close );
}
StateHasChanged();
}
void CloseEconomyHelp()
{
if ( !EconomyHelpOpen )
return;
EconomyHelpOpen = false;
GameAudio.PlayUi( GameAudio.Close );
StateHasChanged();
}
void OnAcceptWarning() => GameFlow.AcceptWarning();
void OnNewGame()
{
// Protect against accidental wipe when a save exists.
if ( GameFlow.CanContinue && !_newGameArmed )
{
_newGameArmed = true;
GameAudio.PlayUi( GameAudio.Notice );
StateHasChanged();
return;
}
_newGameArmed = false;
GameFlow.NewGame();
}
void OnContinue()
{
_newGameArmed = false;
GameFlow.ContinueGame();
}
void OnSettings()
{
_newGameArmed = false;
GameFlow.OpenSettings();
}
void OnBackSettings() => GameFlow.CloseSettings();
void OnBackMenu()
{
ShopOpen = false;
DarkWeb.Clear();
if ( MethRush.Open )
MethRush.Clear();
if ( TrainingSystem.TrainOpen )
TrainingSystem.CancelTrain();
if ( TrainingSystem.InjectOpen )
TrainingSystem.CancelInject();
if ( BattleSystem.IsOpen && BattleSystem.Phase != BattlePhase.Fighting )
BattleSystem.Close();
FishMenuId = null;
EconomyHelpOpen = false;
GameFlow.ReturnToMenu();
}
/// <summary>
/// Escape closes the topmost panel first; on a clean tank, returns to main menu.
/// </summary>
void HandleEscape()
{
if ( !Input.EscapePressed )
return;
// Consume so the engine pause menu doesn't steal focus.
Input.EscapePressed = false;
if ( GameFlow.Screen == GameScreen.Settings )
{
GameFlow.CloseSettings();
StateHasChanged();
return;
}
if ( GameFlow.Screen == GameScreen.Achievements )
{
GameFlow.CloseAchievements();
StateHasChanged();
return;
}
if ( GameFlow.Screen == GameScreen.MainMenu && _newGameArmed )
{
_newGameArmed = false;
StateHasChanged();
return;
}
if ( GameFlow.Screen != GameScreen.Playing )
return;
// Layered dismiss — one level per press
if ( _decorDragPiece is not null )
{
EndDecorDrag( 0f, 0f );
return;
}
if ( MethRush.Open )
{
// Esc bails the rush (cracks the tank)
MethRush.Bail();
StateHasChanged();
return;
}
if ( DarkWeb.CheckoutOpen )
{
DarkWeb.Cancel();
StateHasChanged();
return;
}
if ( ShopOpen )
{
OnCloseShop();
return;
}
if ( EconomyHelpOpen )
{
CloseEconomyHelp();
return;
}
if ( BattleSystem.IsOpen && BattleSystem.Phase != BattlePhase.Fighting )
{
BattleSystem.Close();
StateHasChanged();
return;
}
if ( TrainingSystem.InjectOpen )
{
TrainingSystem.CancelInject();
StateHasChanged();
return;
}
if ( TrainingSystem.TrainOpen )
{
TrainingSystem.CancelTrain();
StateHasChanged();
return;
}
if ( FishRenameOpen )
{
OnRenameCancel();
return;
}
if ( !string.IsNullOrEmpty( FishMenuId ) )
{
CloseFishMenu();
return;
}
if ( ExplodeSystem.ModeActive )
{
ExplodeSystem.ModeActive = false;
GameAudio.PlayUi( GameAudio.Close );
StateHasChanged();
return;
}
// Nothing open → main menu (saves)
OnBackMenu();
}
void OnQuit() => GameFlow.Quit();
void OnHover() => GameAudio.PlayUi( GameAudio.Pop );
void OnFeed()
{
if ( ExplodeSystem.ModeActive )
return;
FeedSystem.TryFeed();
StateHasChanged();
}
void OnCycleFoodClick()
{
if ( ExplodeSystem.ModeActive )
return;
FeedSystem.CycleSelected();
StateHasChanged();
}
void OnSelectFood( FoodDef food )
{
FeedSystem.Select( food );
StateHasChanged();
}
void OnUnlockFood( FoodDef food )
{
FeedSystem.TryUnlock( food );
StateHasChanged();
}
void OnFoodRowClick( FoodDef food )
{
if ( food is null )
return;
if ( FeedSystem.IsUnlocked( food ) )
OnSelectFood( food );
else
OnUnlockFood( food );
}
void OnTankBackgroundClick()
{
// Empty water only (tank-water-hit sits behind fish). Feeding is inventory-only.
if ( !string.IsNullOrEmpty( FishMenuId ) )
CloseFishMenu();
}
void OnFishClick( string instanceId )
{
if ( string.IsNullOrEmpty( instanceId ) )
return;
// Guard double-fire if both mousedown + click bind in some hosts.
if ( _fishClickGuardId == instanceId && _fishClickGuardUntil > Time.Now )
return;
_fishClickGuardId = instanceId;
_fishClickGuardUntil = Time.Now + 0.12f;
// Battle lobby: click toggles team pick (fast).
if ( BattleSystem.Phase == BattlePhase.Lobby && BattleSystem.Template is not null )
{
FishMenuId = null;
BattleSystem.ToggleFish( instanceId );
StateHasChanged();
return;
}
// M80 tool mode: click tapes/untapes without menu.
if ( ExplodeSystem.ModeActive )
{
FishMenuId = null;
ExplodeSystem.ToggleSelect( instanceId );
StateHasChanged();
return;
}
// Default: open context menu on this fish (no scurry).
if ( FishMenuId == instanceId )
{
CloseFishMenu();
return;
}
FishMenuId = instanceId;
GameAudio.PlayUi( GameAudio.Pop );
StateHasChanged();
}
/// <summary>Click fish name → open text field (type freely, or Random).</summary>
void OnFishMenuRename()
{
var fish = TankSim.FindFish( FishMenuId );
if ( fish is null )
return;
FishRenameOpen = true;
// Prefill current nickname (or empty) so player can type over it
FishRenameDraft = string.IsNullOrWhiteSpace( fish.Nickname ) ? "" : fish.Nickname.Trim();
_renamePoolIndex = IndexInNamePool( FishRenameDraft );
_renameFocusPending = true;
GameAudio.PlayUi( GameAudio.Pop );
StateHasChanged();
}
/// <summary>Fill the field with the next funny pool name (still editable).</summary>
void OnRenameRandom()
{
if ( !FishRenameOpen )
return;
_renamePoolIndex++;
if ( _renamePoolIndex < 0 || _renamePoolIndex >= FishNamePool.Length )
_renamePoolIndex = 0;
FishRenameDraft = FishNamePool[_renamePoolIndex];
if ( _renameEntry is not null )
_renameEntry.Text = FishRenameDraft;
_renameFocusPending = true;
GameAudio.PlayUi( GameAudio.Pop );
StateHasChanged();
}
void OnRenameConfirm()
{
if ( !FishRenameOpen || string.IsNullOrEmpty( FishMenuId ) )
return;
// Read live text from the entry
if ( _renameEntry is not null )
FishRenameDraft = _renameEntry.Text ?? "";
var draft = FishRenameDraft?.Trim() ?? "";
var fish = TankSim.FindFish( FishMenuId );
// Empty or same as species → clear nickname
if ( fish?.Species is not null
&& string.Equals( draft, fish.Species.Name, StringComparison.OrdinalIgnoreCase ) )
draft = "";
var ok = TankSim.TryRenameFish( FishMenuId, draft );
GameAudio.PlayUi( ok ? GameAudio.Confirm : GameAudio.Error );
FishRenameOpen = false;
FishRenameDraft = "";
_renameFocusPending = false;
StateHasChanged();
}
void OnRenameCancel()
{
FishRenameOpen = false;
FishRenameDraft = "";
_renameFocusPending = false;
GameAudio.PlayUi( GameAudio.Close );
StateHasChanged();
}
static int IndexInNamePool( string name )
{
if ( string.IsNullOrWhiteSpace( name ) )
return -1;
for ( var i = 0; i < FishNamePool.Length; i++ )
{
if ( string.Equals( FishNamePool[i], name, StringComparison.OrdinalIgnoreCase ) )
return i;
}
return -1;
}
void CloseFishMenu()
{
FishRenameOpen = false;
FishRenameDraft = "";
_renameFocusPending = false;
FishMenuId = null;
StateHasChanged();
}
void OnFishMenuTrain()
{
var id = FishMenuId;
FishMenuId = null;
FishRenameOpen = false;
FishRenameDraft = "";
_renameFocusPending = false;
if ( string.IsNullOrEmpty( id ) )
return;
if ( ExplodeSystem.ModeActive )
{
TankSim.ShowBanner( "Finish removing fish first." );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
if ( BattleSystem.IsOpen )
{
TankSim.ShowBanner( "Close the fight panel first." );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
if ( TrainingSystem.InjectOpen )
{
TankSim.ShowBanner( "Finish inject first." );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
ShopOpen = false;
TrainingSystem.TryStartTrain( id );
StateHasChanged();
}
void OnFishMenuMove()
{
var id = FishMenuId;
FishMenuId = null;
FishRenameOpen = false;
FishRenameDraft = "";
_renameFocusPending = false;
if ( string.IsNullOrEmpty( id ) )
return;
if ( ExplodeSystem.ModeActive )
{
TankSim.ShowBanner( "Finish M80 mode first." );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
if ( BattleSystem.IsOpen )
{
TankSim.ShowBanner( "Close the fight panel first." );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
if ( TrainingSystem.TrainOpen || TrainingSystem.InjectOpen )
{
TankSim.ShowBanner( "Finish training first." );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
TankSim.TryMoveFishToOtherTank( id );
StateHasChanged();
}
void OnFishMenuSell()
{
var id = FishMenuId;
FishMenuId = null;
FishRenameOpen = false;
FishRenameDraft = "";
_renameFocusPending = false;
if ( !string.IsNullOrEmpty( id ) )
TankSim.TrySellFish( id );
StateHasChanged();
}
void OnFishMenuFight()
{
var id = FishMenuId;
var hurt = TankSim.FindFish( id );
if ( hurt is not null && hurt.IsFightInjured )
{
TankSim.ShowBanner( BattleSystem.FightBlockedReason( hurt ) );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
FishMenuId = null;
FishRenameOpen = false;
FishRenameDraft = "";
_renameFocusPending = false;
ShopOpen = false;
if ( ExplodeSystem.ModeActive )
ExplodeSystem.ModeActive = false;
if ( TrainingSystem.TrainOpen )
TrainingSystem.CancelTrain();
if ( !BattleSystem.IsOpen )
BattleSystem.TryOpen();
// Ensure a circuit is selected (auto-picks a full team).
if ( BattleSystem.Template is null )
{
var first = BattleSystem.Catalog.FirstOrDefault( BattleSystem.IsUnlocked );
if ( first is not null )
BattleSystem.SelectTemplate( first );
}
// Put THIS fish on the team without toggling them off if already auto-picked.
if ( !string.IsNullOrEmpty( id ) && BattleSystem.Phase == BattlePhase.Lobby )
BattleSystem.EnsureFishOnTeam( id );
GameAudio.PlayUi( GameAudio.Notice );
StateHasChanged();
}
void OnToggleBattle()
{
if ( BattleSystem.IsOpen )
{
if ( BattleSystem.Phase == BattlePhase.Fighting )
return;
BattleSystem.Close();
}
else
{
ShopOpen = false;
if ( ExplodeSystem.ModeActive )
ExplodeSystem.Clear();
BattleSystem.TryOpen();
}
StateHasChanged();
}
void OnCloseBattle()
{
if ( BattleSystem.Phase == BattlePhase.Fighting )
return;
BattleSystem.Close();
StateHasChanged();
}
void OnBattleBackdrop()
{
if ( BattleSystem.Phase == BattlePhase.Fighting )
return;
BattleSystem.Close();
StateHasChanged();
}
void OnSelectBattle( BattleTemplate t )
{
if ( t is null )
return;
BattleSystem.SelectTemplate( t );
StateHasChanged();
}
void OnBattlePickFish( string id )
{
BattleSystem.ToggleFish( id );
StateHasChanged();
}
void OnStartBattle()
{
BattleSystem.TryStart();
StateHasChanged();
}
void OnAckBattle()
{
BattleSystem.AcknowledgeResult();
StateHasChanged();
}
/// <summary>
/// One dock button: toggles tape mode (needs bought M80s); "Detonate ×N" booms taped fish.
/// </summary>
void OnRemoveOrDetonate()
{
ShopOpen = false;
if ( BattleSystem.IsOpen && BattleSystem.Phase != BattlePhase.Fighting )
BattleSystem.Close();
if ( ExplodeSystem.ModeActive && ExplodeSystem.SelectedCount > 0 )
{
ExplodeSystem.TryDetonate();
// Guarantee default mode even if detonate path was partial.
if ( ExplodeSystem.ModeActive )
ExplodeSystem.ModeActive = false;
StateHasChanged();
return;
}
if ( ExplodeSystem.ModeActive )
{
ExplodeSystem.TrySetMode( false );
GameAudio.PlayUi( GameAudio.Close );
}
else
{
if ( ExplodeSystem.TrySetMode( true ) )
GameAudio.PlayUi( GameAudio.Notice );
}
StateHasChanged();
}
void OnBuyM80()
{
DarkWeb.TryBegin( DarkListing.M80 );
StateHasChanged();
}
void OnToggleShop()
{
if ( ShopOpen )
{
OnCloseShop();
return;
}
OnOpenShop();
}
void OnOpenShop()
{
if ( MethRush.Open )
{
TankSim.ShowBanner( "Calm the meth rush first!" );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
if ( BattleSystem.IsOpen && BattleSystem.Phase != BattlePhase.Fighting )
BattleSystem.Close();
if ( ExplodeSystem.ModeActive )
ExplodeSystem.ModeActive = false;
if ( TrainingSystem.TrainOpen )
TrainingSystem.CancelTrain();
if ( TrainingSystem.InjectOpen )
TrainingSystem.CancelInject();
DarkWeb.Clear();
EconomyHelpOpen = false;
ShopOpen = true;
ShopTab = "fish";
Shop.SetLastMessage( "Click a row to buy." );
GameAudio.PlayUi( GameAudio.Notice );
StateHasChanged();
}
void OnCloseShop()
{
ShopOpen = false;
DarkWeb.Clear();
GameAudio.PlayUi( GameAudio.Close );
StateHasChanged();
}
void OnAchievements()
{
_newGameArmed = false;
GameFlow.OpenAchievements();
}
void OnBackAchievements()
{
_newGameArmed = false;
GameFlow.CloseAchievements();
}
void SetShopTab( string tab )
{
if ( DarkWeb.CheckoutOpen )
DarkWeb.Cancel();
// Shop is buy-only: fish / food / tank / decor / shady. No awards tab.
ShopTab = tab switch
{
"food" or "tank" or "decor" or "extra" => tab,
_ => "fish"
};
GameAudio.PlayUi( GameAudio.Pop );
StateHasChanged();
}
void OnDarkWebContinue()
{
DarkWeb.ContinueFromVendor();
StateHasChanged();
}
void OnDarkWebPay()
{
DarkWeb.ConfirmPay();
StateHasChanged();
}
void OnDarkWebCancel()
{
DarkWeb.Cancel();
StateHasChanged();
}
void OnBuyFish( FishSpecies species )
{
if ( species is null )
return;
if ( DarkWeb.CheckoutOpen )
return;
Shop.TryBuyFish( species );
StateHasChanged();
}
void OnBuyCapacity()
{
Shop.TryBuyCapacity();
StateHasChanged();
}
void OnBuyHabitat( HabitatDef def )
{
Shop.TryBuyHabitat( def );
StateHasChanged();
}
void OnBuySaltTank()
{
Shop.TryBuySaltTank();
StateHasChanged();
}
void OnBuyMeth()
{
DarkWeb.TryBegin( DarkListing.Meth );
StateHasChanged();
}
void OnBuyDecor( DecorDef def )
{
Shop.TryBuyDecor( def );
StateHasChanged();
}
void OnFeedMeth()
{
if ( ExplodeSystem.ModeActive )
return;
Shop.TryFeedMeth();
StateHasChanged();
}
void OnHitTrain()
{
// Hit uses current marker (+ grace). Force UI refresh so feedback is instant.
TrainingSystem.TryHitTrain();
StateHasChanged();
}
void OnCancelTrain()
{
TrainingSystem.CancelTrain();
StateHasChanged();
}
void OnScareShark()
{
if ( ExplodeSystem.ModeActive || BattleSystem.IsOpen )
return;
SharkSystem.TryScare();
StateHasChanged();
}
void OnFendShark()
{
if ( ExplodeSystem.ModeActive || BattleSystem.IsOpen )
return;
SharkSystem.TryFend();
StateHasChanged();
}
void OnBuyWmd()
{
DarkWeb.TryBegin( DarkListing.Wmd );
StateHasChanged();
}
void OnBuyWhale()
{
DarkWeb.TryBegin( DarkListing.Whale );
StateHasChanged();
}
void OnEndingGood()
{
ShopOpen = false;
EndingSystem.TryStartGood();
StateHasChanged();
}
void OnEndingBad()
{
ShopOpen = false;
EndingSystem.TryStartBad();
StateHasChanged();
}
void OnEndingMixed()
{
ShopOpen = false;
EndingSystem.TryStartMixed();
StateHasChanged();
}
void OnEndingClick()
{
if ( !EndingSystem.LinesFinished )
EndingSystem.AdvanceLine();
StateHasChanged();
}
void OnEndingDone()
{
EndingSystem.FinishToMenu();
StateHasChanged();
}
/// <summary>Tools Juice: inject if stocked, otherwise buy here (not listed in shop).</summary>
void OnJuiceTool()
{
if ( ExplodeSystem.ModeActive )
return;
if ( TrainingSystem.Syringes > 0 )
{
TrainingSystem.TryStartInject();
}
else
{
TrainingSystem.TryBuySyringe();
}
StateHasChanged();
}
void OnHitInject()
{
TrainingSystem.TryHitInject();
StateHasChanged();
}
void OnHitMethRush()
{
MethRush.TryHit();
StateHasChanged();
}
void OnBailMethRush()
{
MethRush.Bail();
StateHasChanged();
}
void OnCancelInject()
{
TrainingSystem.CancelInject();
StateHasChanged();
}
void OnViewFresh()
{
if ( _decorDragPiece is not null )
EndDecorDrag( 0f, 0f );
TankSim.TrySwitchTank( WaterType.Fresh );
}
void OnViewSalt()
{
if ( _decorDragPiece is not null )
EndDecorDrag( 0f, 0f );
TankSim.TrySwitchTank( WaterType.Salt );
}
bool DecorUiBlocked =>
ShopOpen || BattleSystem.IsOpen || TrainingSystem.TrainOpen
|| TrainingSystem.InjectOpen || MethRush.Open || ExplodeSystem.ModeActive
|| !string.IsNullOrEmpty( FishMenuId ) || FishRenameOpen;
/// <summary>Map screen mouse into tank-pixel space (matches DecorPiece X/Y).</summary>
bool TryGetTankLocalFromScreen( out float lx, out float ly )
{
lx = 0f;
ly = 0f;
var panel = _tankWaterPanel;
if ( panel is null || !panel.IsValid() )
return false;
var rect = panel.Box.Rect;
var w = rect.Width;
var h = rect.Height;
if ( w < 1f || h < 1f )
return false;
var mp = Mouse.Position;
// Scale screen coords into the fixed 1280×620 tank sim space.
lx = (mp.x - rect.Left) * (TankSim.Width / w);
ly = (mp.y - rect.Top) * (TankSim.Height / h);
return true;
}
/// <summary>Mouse down on a prop = grab and hold until mouse up.</summary>
void BeginDecorDrag( DecorPiece piece )
{
if ( piece is null )
return;
if ( GameFlow.Screen != GameScreen.Playing || !TankSim.IsActive )
return;
if ( DecorUiBlocked )
return;
// Already holding something — ignore (wait for release).
if ( _decorDragging )
return;
if ( !TankSim.TryBeginDecorDrag( piece ) )
return;
_decorDragPiece = piece;
_decorDragging = true;
_decorGrabAt = Time.Now;
_decorSawAttackDown = false;
_decorRecentVx = 0f;
_decorRecentVy = 0f;
var mp = Mouse.Position;
_decorLastScreenX = mp.x;
_decorLastScreenY = mp.y;
// Grab offset from current mouse → tank coords (no PanelEvent needed).
if ( TryGetTankLocalFromScreen( out var lx, out var ly ) )
{
_decorGrabOx = lx - piece.X;
_decorGrabOy = ly - piece.Y;
_decorLastLx = lx;
_decorLastLy = ly;
}
else
{
var def = piece.Def;
_decorGrabOx = def is not null ? def.Width * 0.5f : 40f;
_decorGrabOy = def is not null ? def.Height * 0.5f : 40f;
_decorLastLx = piece.X + _decorGrabOx;
_decorLastLy = piece.Y + _decorGrabOy;
}
// Stick immediately under cursor this frame.
ApplyDecorDragAtMouse();
CloseFishMenu();
GameAudio.PlayUi( GameAudio.Pop );
// No StateHasChanged — FrameId already bumped; rebuild mid-click was dropping hold.
}
/// <summary>While held, move events keep the prop under the cursor.</summary>
void OnDecorHoldMove()
{
if ( !_decorDragging || _decorDragPiece is null )
return;
ApplyDecorDragAtMouse();
}
/// <summary>Mouse button released anywhere on the play screen = throw / place.</summary>
void OnDecorHoldUp()
{
if ( !_decorDragging || _decorDragPiece is null )
return;
// Ignore synthetic / rebuild noise right after grab.
if ( Time.Now - _decorGrabAt < 0.06f )
return;
ApplyDecorDragAtMouse();
FinishDecorThrow();
}
void ApplyDecorDragAtMouse()
{
if ( _decorDragPiece is null || !_decorDragging )
return;
var mp = Mouse.Position;
float lx, ly, dx, dy;
if ( TryGetTankLocalFromScreen( out lx, out ly ) )
{
dx = lx - _decorLastLx;
dy = ly - _decorLastLy;
}
else
{
dx = mp.x - _decorLastScreenX;
dy = mp.y - _decorLastScreenY;
lx = _decorLastLx + dx;
ly = _decorLastLy + dy;
}
var def = _decorDragPiece.Def;
if ( def is not null )
{
var cx = lx - _decorGrabOx + def.Width * 0.5f;
var cy = ly - _decorGrabOy + def.Height * 0.5f;
TankSim.SetDecorHeldCenter( _decorDragPiece, cx, cy );
}
else
{
TankSim.NudgeDecorHeld( _decorDragPiece, dx, dy );
}
var dt = MathF.Max( Time.Delta, 1f / 120f );
var ivx = dx / dt;
var ivy = dy / dt;
_decorRecentVx = _decorRecentVx * 0.3f + ivx * 0.7f;
_decorRecentVy = _decorRecentVy * 0.3f + ivy * 0.7f;
_decorLastLx = lx;
_decorLastLy = ly;
_decorLastScreenX = mp.x;
_decorLastScreenY = mp.y;
}
void FinishDecorThrow()
{
if ( _decorDragPiece is null )
return;
var vx = _decorRecentVx * 1.9f;
var vy = _decorRecentVy * 1.9f;
var spd = MathF.Sqrt( vx * vx + vy * vy );
// Flick = throw; dead-still = soft place.
if ( spd < 120f && spd > 8f )
{
var s = 280f / MathF.Max( spd, 1f );
vx *= s;
vy *= s;
}
else if ( spd <= 8f )
{
vx = 0f;
vy = 0f;
}
EndDecorDrag( vx, vy );
}
void TickDecorDrag()
{
if ( !_decorDragging || _decorDragPiece is null )
return;
// Safety: sim dropped the hold.
if ( !_decorDragPiece.Held )
{
_decorDragPiece = null;
_decorDragging = false;
_decorSawAttackDown = false;
return;
}
// Every frame: keep prop stuck under cursor while button is held.
ApplyDecorDragAtMouse();
// Backup release: Attack1 (mouse1) if the Input system sees it.
// Only arm after we observed Down at least once this hold (UI can eat mouse input).
var attackDown = Input.Down( "Attack1" );
if ( attackDown )
_decorSawAttackDown = true;
else if ( _decorSawAttackDown && Time.Now - _decorGrabAt > 0.08f )
FinishDecorThrow();
}
void EndDecorDrag( float vx, float vy )
{
if ( _decorDragPiece is null )
return;
var piece = _decorDragPiece;
_decorDragPiece = null;
_decorDragging = false;
_decorSawAttackDown = false;
_decorRecentVx = 0f;
_decorRecentVy = 0f;
TankSim.EndDecorDrag( piece, vx, vy );
var outSpd = MathF.Sqrt( vx * vx + vy * vy );
GameAudio.PlayUi( outSpd > 60f ? GameAudio.Notice : GameAudio.Pop );
StateHasChanged();
}
void NudgeMusic( float delta )
{
GameSettings.MusicVolume = GameSettings.MusicVolume + delta;
GameAudio.PlayUi( GameAudio.Pop );
StateHasChanged();
}
void NudgeSfx( float delta )
{
GameSettings.SfxVolume = GameSettings.SfxVolume + delta;
GameAudio.PlayUi( GameAudio.Pop );
StateHasChanged();
}
void ToggleStrobe()
{
GameSettings.ReduceStrobe = !GameSettings.ReduceStrobe;
GameAudio.PlayUi( GameAudio.Notice );
StateHasChanged();
}
static string Pct( float v ) => $"{(int)MathF.Round( v * 100f )}%";
protected override int BuildHash()
{
var a = HashCode.Combine(
GameFlow.Screen,
GameSettings.MusicVolume,
GameSettings.SfxVolume,
GameSettings.ReduceStrobe,
TankSim.FrameId,
TankSim.FishCount,
TankSim.Capacity,
Economy.DisplayVersion
);
var b = HashCode.Combine(
GameFlow.CanContinue,
ShopOpen,
ShopTab,
Shop.Version,
FeedSystem.Version,
Chaos.Version,
ExplodeSystem.Version,
ExplodeSystem.ModeActive
);
var c = HashCode.Combine(
TankSim.SaltUnlocked,
TankSim.ViewingSalt,
TankSim.BannerVersion,
TankSim.HabitatRank,
TankSim.MaxDecor,
Shop.MethBags,
Shop.M80Bags,
HashCode.Combine( DeathFeel.Version, DeathFeel.PeakStage )
);
var d = HashCode.Combine(
BoomFeel.Version,
TankSim.AddictedFishCount,
Achievements.Version,
TrainingSystem.Version,
TrainingSystem.InjectOpen,
TrainingSystem.TrainOpen,
BattleSystem.Version,
SharkSystem.Version
);
// HashCode.Combine accepts at most 8 args — nest extras.
var e = HashCode.Combine(
EndingSystem.Version,
EndingSystem.LineIndex,
Karma.Version,
SpriteCatalog.Version,
GameAudio.WantsNoChillMusic,
ExplodeSystem.SelectedCount,
TrainingSystem.TrainHits,
TrainingSystem.Syringes
);
var f = HashCode.Combine(
TrainingSystem.TrainOpen,
(int)(TrainingSystem.TrainMarker * 1000f),
TrainingSystem.TrainRep,
TrainingSystem.InjectOpen,
(int)(TrainingSystem.Marker * 1000f),
// Zone must rebuild when it moves between reps
(int)(TrainingSystem.TrainZoneMin * 1000f),
(int)(TrainingSystem.TrainZoneMax * 1000f),
HashCode.Combine(
TankSim.BannerVersion,
DogeFloat.Version,
FishMenuId?.GetHashCode() ?? 0,
TrainingSystem.SessionSummary?.GetHashCode() ?? 0,
TrainingSystem.HasSessionSummary,
TrainingSystem.TrainHits,
TrainingSystem.TrainFeedback?.GetHashCode() ?? 0,
_decorDragging
)
);
var g = HashCode.Combine(
EconomyHelpOpen,
ShopOpen,
StatFloat.Version,
DarkWeb.Version,
DarkWeb.Step,
StatFloat.LastLine?.GetHashCode() ?? 0,
ContextHint?.GetHashCode() ?? 0,
// Don't hash draft text every keystroke (would rebuild TextEntry and steal focus)
FishRenameOpen ? 1 : 0
);
var mr = HashCode.Combine( MethRush.Version, MethRush.Open, (int)(MethRush.Marker * 1000f), MethRush.Cracks, MethRush.Round );
return HashCode.Combine( a, b, c, d, HashCode.Combine( e, f, g, mr ) );
}
}