MainShell.razor
@using System;
@using System.Collections.Generic;
@using System.Globalization;
@using System.Linq;
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace NoChillquarium
<root class="shell @(StrobeClass) @(GameSettings.ReduceStrobe ? "reduce-strobe" : "")">
@* Wallpaper on warning / menu / settings only — play stays a dark cabinet. *@
<div class="menu-bg @(ShowMenuArt ? "has-art" : "")">
@if ( ShowMenuArt && SpriteCatalog.Has( "menu_wallpaper" ) )
{
<spritebox class="menu-wallpaper" sprite="menu_wallpaper"></spritebox>
<div class="menu-bg-dim"></div>
}
</div>
@if ( GameFlow.Screen == GameScreen.MainMenu )
{
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="@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.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-tagline">@EndingSystem.Tagline</div>
<div class="ending-line">@EndingSystem.CurrentLine</div>
<div class="ending-meta">@Karma.MeterName @Karma.Label (@Karma.ValueText) · Chaos @Chaos.ValueText · @EndingSystem.ProgressLabel</div>
@if ( EndingSystem.LinesFinished )
{
<div class="cq-btn primary ending-menu-btn" @onclick="@OnEndingDone" @onmouseover="@OnHover">Back to menu</div>
<div class="ending-hint">Sunny Pines never forgets.</div>
}
else
{
<div class="ending-hint">Click · next · @EndingSystem.ProgressLabel</div>
}
</div>
</div>
}
else if ( GameFlow.Screen == GameScreen.Playing )
{
@{
var shakeX = MathF.Round( DeathFeel.ShakeX + BoomFeel.ShakeX + FusionFeel.ShakeX ).ToString( "0" );
var shakeY = MathF.Round( DeathFeel.ShakeY + BoomFeel.ShakeY + FusionFeel.ShakeY ).ToString( "0" );
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 fusionA = Math.Clamp( FusionFeel.Flash, 0f, 1f ).ToString( "0.###", CultureInfo.InvariantCulture );
var fusionClass = FusionFeel.IsActive ? ("fusing " + FusionFeel.FlashClass) : "";
var boomClass = BoomFeel.IsActive ? "booming" : "";
var combineClass = FishFusion.PickingSecond ? "combine-pick" : "";
// 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" : "") @(GrandmaGifts.IsLookingAround ? "looking-around" : "") @(!GrandmaGifts.HasTank ? "pre-tank" : "")"
@onmousemove="@(() => OnDecorHoldMove())"
@onmouseup="@(() => OnDecorHoldUp())">
@if ( GrandmaGifts.HasTank && RoomScene.HasBackground )
{
<div class="play-room">
<spritebox class="play-room-spr" sprite="@RoomScene.BackgroundId"></spritebox>
<div class="play-room-dim"></div>
</div>
}
<div class="play-bezel">
<div class="play-bezel-inner">
@* HUD: wallet | census — hidden while looking around the lot *@
@if ( !GrandmaGifts.IsLookingAround )
{
<div class="cq-topbar">
@* Spacer keeps wallet centered against the fish census *@
<div class="cq-top-left"></div>
<div class="cq-top-center">
@* Wallet HUD only after Grandma hands over the cold wallet gift. *@
@if ( GrandmaGifts.HasReceived( "cold_wallet" ) )
{
<div class="cold-chip @(Economy.WalletPulse ? "pulse" : "") @(EconomyHelpOpen ? "help-open" : "")"
@onclick="@OnToggleEconomyHelp" @onmouseover="@OnHover" title="Dogecoin cold wallet · Grandma's gift">
<div class="cold-chip-shell">
<div class="cold-chip-body">
<div class="cold-chip-brand">DOGECOIN COLD 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">from Grandma · tap for tips</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">
@if ( GrandmaGifts.HasTank )
{
<div class="ach-chip @(AchievementSystem.BoardOpen ? "on" : "")"
@onclick="@OnToggleAchievements" @onmouseover="@OnHover"
title="Porch trophies">
<div class="ach-chip-kicker">ACH</div>
<div class="ach-chip-n">@AchievementSystem.ProgressLabel</div>
</div>
<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>
}
@if ( !GrandmaGifts.IsLookingAround )
{
@* Quest + coach pills — always try to answer “what now?” *@
@* Fixed-height chrome — quest + optional context hint never reflow the tank. *@
<div class="cq-hint-row">
@{
var questKicker = "";
var questText = "";
var questIsStory = false;
var questClass = "coach";
// Story quest only after tank gift starts the tutorial (not on boot).
if ( GrandmaGifts.HasTank
&& !StorySystem.IsDone
&& !string.IsNullOrEmpty( StorySystem.ObjectiveText )
&& !GrandmaGifts.IsBusy )
{
questKicker = StorySystem.ProgressText;
questText = StorySystem.ObjectiveText;
questIsStory = true;
questClass = "story";
}
else if ( !GrandmaGifts.IsBusy && !StorySystem.CardOpen )
{
var coach = ProgressionCoach.Current;
if ( coach is not null && !string.IsNullOrEmpty( coach.Text ) )
{
questKicker = coach.Kicker;
questText = coach.Text;
questClass = coach.Kind switch
{
"gift" => "coach gift",
"expand" => "coach expand",
"story" => "story",
_ => "coach"
};
}
}
var hintText = ContextHint ?? "";
var hasQuest = !string.IsNullOrEmpty( questText );
var hasHint = !string.IsNullOrEmpty( hintText );
}
<div class="cq-quest-slot">
@if ( hasQuest )
{
<div class="cq-quest @questClass"
@onclick="@(() => OnQuestClick(questIsStory))"
@onmouseover="@OnHover"
title="Click for details / open the right panel">
<span class="cq-quest-next">NEXT</span>
<span class="cq-quest-kicker">@questKicker</span>
<span class="cq-quest-text">@questText</span>
</div>
}
</div>
<div class="cq-hint-slot @(hasHint ? "lit" : "")">
@if ( hasHint )
{
<div class="cq-hint @ContextHintClass">@hintText</div>
}
</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 && !GrandmaGifts.IsLookingAround )
{
<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>
}
@* Stage chrome hidden during lot monologue (full-screen look-around) *@
@if ( !GrandmaGifts.IsLookingAround )
{
<div class="cq-stage-row">
<div class="cq-stage">
@if ( !GrandmaGifts.HasTank )
{
@* Pre-tank stage — trailer lot only (monologue overlays full-screen when active) *@
<div class="lot-stage @(GrandmaGifts.IsLookingAround ? "dim-behind" : "")"
@onclick="@OnLotStageClick" @onmouseover="@OnHover">
@if ( SpriteCatalog.Has( "ui_trailer_lot" ) )
{
<spritebox class="lot-stage-art" sprite="ui_trailer_lot"></spritebox>
}
else
{
<div class="lot-stage-fallback"></div>
}
<div class="lot-stage-vignette"></div>
@if ( !GrandmaGifts.IsLookingAround )
{
<div class="lot-stage-caption">
<div class="lot-stage-title">SUNNY PINES</div>
<div class="lot-stage-sub">Mobile Estates · Trailer Lot</div>
@if ( !GrandmaGifts.HasTank )
{
<div class="lot-stage-go">Click · show Grandma the form</div>
}
</div>
}
</div>
}
else
{
<div class="tank-rig" style="left: @(shakeX)px; top: @(shakeY)px;">
<div class="tank-glass @habClass @(ExplodeSystem.ModeActive ? "m80-mode" : "") [email protected] @boomClass @fusionClass @combineClass [email protected] @(MethRush.Open ? "meth-rush" : "") @(_decorDragPiece is not null ? "decor-holding" : "") @(!string.IsNullOrEmpty( FishMenuId ) ? "menu-open" : "")">
@* Overlay tip — never in flex chrome, so grab/throw can't reflow the stage. *@
@if ( _decorDragPiece is not null )
{
<div class="decor-throw-tip">Release to throw</div>
}
<div class="tank-rim top"></div>
<div class="tank-water @(TankSim.HasBackdrop ? "has-backdrop" : "")" @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>
@if ( TankSim.HasBackdrop )
{
<div class="tank-backdrop">
<spritebox class="tank-backdrop-spr" sprite="@TankSim.ActiveBackdrop.SpriteId"></spritebox>
</div>
}
<div class="tank-depth d1"></div>
<div class="tank-depth d2"></div>
<div class="tank-caustic"></div>
<div class="tank-shine"></div>
<div class="tank-scan">
@foreach ( var scanTop in PixelFx.ScanTops )
{
<div class="gba-scan-line" style="top: @scanTop;"></div>
}
</div>
<div class="tank-bubbles">
<div class="bubble b1"></div>
<div class="bubble b2"></div>
<div class="bubble b3"></div>
<div class="bubble b4"></div>
<div class="bubble b5"></div>
<div class="bubble b6"></div>
<div class="bubble b7"></div>
<div class="bubble b8"></div>
<div class="bubble b9"></div>
</div>
<div class="hab-prop"></div>
<div class="tank-gravel has-sprite">
@if ( !string.IsNullOrEmpty( TankSim.GravelSpriteId ) )
{
<spritebox class="tank-gravel-spr" sprite="@TankSim.GravelSpriteId"></spritebox>
}
else
{
<div class="pebble p1"></div>
<div class="pebble p2"></div>
<div class="pebble p3"></div>
<div class="pebble p4"></div>
<div class="pebble p5"></div>
<div class="pebble p6"></div>
<div class="pebble p7"></div>
<div class="pebble p8"></div>
}
</div>
<div class="tank-glass-lip left"></div>
<div class="tank-glass-lip right"></div>
@* Glass stress from failed meth calms — clean crack sprites *@
@if ( MethRush.Cracks > 0 )
{
<div class="tank-cracks [email protected]">
<div class="crack-spr c1">
<spritebox class="crack-sprite" sprite="crack_a"></spritebox>
</div>
@if ( MethRush.Cracks >= 2 )
{
<div class="crack-spr c2">
<spritebox class="crack-sprite" sprite="crack_b"></spritebox>
</div>
}
@if ( MethRush.Cracks >= 3 )
{
<div class="crack-spr c3">
<spritebox class="crack-sprite" sprite="crack_c"></spritebox>
</div>
}
@if ( MethRush.Cracks >= 4 )
{
<div class="crack-spr c4">
<spritebox class="crack-sprite" sprite="crack_d"></spritebox>
</div>
<div class="crack-spr c5">
<spritebox class="crack-sprite" sprite="crack_a"></spritebox>
</div>
}
@if ( MethRush.Cracks >= 5 )
{
<div class="crack-spr spider">
<spritebox class="crack-sprite" sprite="crack_spider"></spritebox>
</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>
@* Birth ring/spark under fish (z 10–11 < fish 12). *@
@if ( FusionFeel.IsActive )
{
var fr = (40f + FusionFeel.RingNorm * 280f).ToString( "0.#", CultureInfo.InvariantCulture );
var fox = FusionFeel.OriginX.ToString( "0.#", CultureInfo.InvariantCulture );
var foy = FusionFeel.OriginY.ToString( "0.#", CultureInfo.InvariantCulture );
var fro = (1f - FusionFeel.RingNorm * 0.85f).ToString( "0.##", CultureInfo.InvariantCulture );
<div class="fusion-ring" style="left: @(fox)px; top: @(foy)px; width: @(fr)px; height: @(fr)px; opacity: @fro;"></div>
<div class="fusion-spark" style="left: @(fox)px; top: @(foy)px;"></div>
}
@* Full-screen strobe still paints over the tank. *@
@if ( FusionFeel.IsActive && FusionFeel.Flash > 0.02f )
{
<div class="fusion-flash @FusionFeel.FlashClass" style="opacity: @fusionA;"></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 );
var foodDef = FoodDef.FindExact( pellet.FoodId );
var wormSprite = foodDef is not null && foodDef.HasPelletSprite ? foodDef.PelletSpriteId : null;
@if ( !string.IsNullOrEmpty( wormSprite ) )
{
<div class="pellet pellet-sprite" style="left: @(pl)px; top: @(pt)px;">
<spritebox class="pellet-sprite-fill" sprite="@wormSprite"></spritebox>
</div>
}
else
{
<div class="pellet" style="left: @(pl)px; top: @(pt)px; background-color: @pellet.Color;"></div>
}
}
@* Rare breed: eggs bobbing until they hatch into guppies. *@
@foreach ( var egg in BreedingSystem.Eggs )
{
var el = egg.X.ToString( "0.##", CultureInfo.InvariantCulture );
var et = (egg.Y + MathF.Sin( egg.Life * 2.1f + egg.BobPhase ) * 2.5f).ToString( "0.##", CultureInfo.InvariantCulture );
var pulse = (0.85f + egg.Progress * 0.35f).ToString( "0.##", CultureInfo.InvariantCulture );
var stage = egg.Progress > 0.75f ? "hatching" : egg.Progress > 0.4f ? "ripe" : "new";
<div class="tank-egg @stage" style="left: @(el)px; top: @(et)px; transform: scale(@pulse);"></div>
}
@* Court hearts / hatch sparks. *@
@foreach ( var bp in BreedingSystem.Particles )
{
var bpl = bp.X.ToString( "0.##", CultureInfo.InvariantCulture );
var bpt = bp.Y.ToString( "0.##", CultureInfo.InvariantCulture );
var bop = Math.Clamp( bp.Norm, 0.05f, 1f ).ToString( "0.##", CultureInfo.InvariantCulture );
var sc = (0.55f + (1f - bp.Norm) * 0.55f).ToString( "0.##", CultureInfo.InvariantCulture );
<div class="breed-fx @bp.Kind" style="left: @(bpl)px; top: @(bpt)px; opacity: @bop; transform: scale(@sc);"></div>
}
@if ( BreedingSystem.IsCourting )
{
var cmx = BreedingSystem.CourtMidX.ToString( "0.#", CultureInfo.InvariantCulture );
var cmy = BreedingSystem.CourtMidY.ToString( "0.#", CultureInfo.InvariantCulture );
var cr = (28f + BreedingSystem.CourtNorm * 70f).ToString( "0.#", CultureInfo.InvariantCulture );
var cro = (0.75f - BreedingSystem.CourtNorm * 0.55f).ToString( "0.##", CultureInfo.InvariantCulture );
<div class="mate-ring" style="left: @(cmx)px; top: @(cmy)px; width: @(cr)px; height: @(cr)px; opacity: @cro;"></div>
}
@if ( BreedingSystem.LayFlash > 0.02f )
{
var lox = BreedingSystem.LayX.ToString( "0.#", CultureInfo.InvariantCulture );
var loy = BreedingSystem.LayY.ToString( "0.#", CultureInfo.InvariantCulture );
var lop = Math.Clamp( BreedingSystem.LayFlash, 0f, 1f ).ToString( "0.##", CultureInfo.InvariantCulture );
var lr = (36f + (1f - BreedingSystem.LayFlash) * 90f).ToString( "0.#", CultureInfo.InvariantCulture );
<div class="lay-flash" style="left: @(lox)px; top: @(loy)px; width: @(lr)px; height: @(lr)px; opacity: @lop;"></div>
}
@foreach ( var fish in TankSim.Fish )
{
if ( fish.OnAdventure )
continue;
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 || fish.IsPregnant ? $"fat fat-{(fish.IsPregnant ? Math.Max( 2, fish.FatStage ) : fish.FatStage)}" : "";
var pregnantClass = fish.IsPregnant ? "pregnant" : "";
var courtingClass = BreedingSystem.IsCourtingFish( fish.InstanceId ) ? "courting" : "";
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 rarityClass = fish.RarityCss;
var growthClass = "growth-" + fish.GrowthStage;
var monsterClass = fish.IsMonster ? "monster" : "";
var monsterMeth = fish.IsMonster && fish.OnMeth ? "monster-meth" : "";
var tentacleClass = fish.Species?.Id == "monster_tentacle" ? "tentacle-beast" : "";
var combineSel = FishFusion.PickingSecond && fish.InstanceId == FishFusion.PendingId ? "combine-a" : "";
var id = fish.InstanceId;
var spriteId = fish.DisplaySpriteId;
// Growth (guppy→adult) + jacked + overfeed chonk + trained shape.
// Monsters on meth: DrawScale already huge.
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 — skip when this fish's menu is open
// so the sprite doesn't thrash out from under the buttons.
var menuHeld = fish.InstanceId == FishMenuId;
if ( fish.Stoned && !menuHeld )
{
tx += MathF.Sin( Time.Now * 3.1f + fish.BobPhase ) * 2.4f;
ty += MathF.Cos( Time.Now * 2.4f + fish.BobPhase * 1.3f ) * 3.2f;
}
if ( fish.IsMonster && fish.OnMeth && !menuHeld )
{
tx += MathF.Sin( Time.Now * 11f + fish.BobPhase ) * 5f;
ty += MathF.Cos( Time.Now * 9f + fish.BobPhase ) * 4f;
}
// Court dance / pregnant jiggle (menu open = stay still for clicks).
if ( !menuHeld && BreedingSystem.IsCourtingFish( fish.InstanceId ) )
{
tx += MathF.Sin( Time.Now * 7.5f + fish.BobPhase ) * 3.2f;
ty += MathF.Cos( Time.Now * 6.2f + fish.BobPhase ) * 2.4f;
}
else if ( !menuHeld && fish.IsPregnant )
{
tx += MathF.Sin( Time.Now * 2.2f + fish.BobPhase ) * 1.4f;
ty += MathF.Cos( Time.Now * 1.7f + fish.BobPhase ) * 1.8f;
}
// 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 );
}
// Visual size (what you see) vs hit size (easier click while swimming).
var visW = fish.Species.Width * scaleX;
var visH = fish.Species.Height * scaleY;
const float hitPadX = 18f;
const float hitPadY = 16f;
var hitW = Math.Max( visW + hitPadX * 2f, 72f );
var hitH = Math.Max( visH + hitPadY * 2f, 52f );
var leftJ = (fish.X - hitW * 0.5f + tx).ToString( "0.##", CultureInfo.InvariantCulture );
var topJ = (fish.DrawY - hitH * 0.5f + ty).ToString( "0.##", CultureInfo.InvariantCulture );
var hitWs = hitW.ToString( "0.##", CultureInfo.InvariantCulture );
var hitHs = hitH.ToString( "0.##", CultureInfo.InvariantCulture );
var visWs = visW.ToString( "0.##", CultureInfo.InvariantCulture );
var visHs = visH.ToString( "0.##", CultureInfo.InvariantCulture );
var visLeft = ((hitW - visW) * 0.5f).ToString( "0.##", CultureInfo.InvariantCulture );
var visTop = ((hitH - visH) * 0.5f).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);" : "";
// Outer = fat click target; inner .fish-vis = drawn size.
var fishStyle = $"left: {leftJ}px; top: {topJ}px; width: {hitWs}px; height: {hitHs}px;";
var visStyle = $"left: {visLeft}px; top: {visTop}px; width: {visWs}px; height: {visHs}px; {xform}";
<div class="fish @flip @sel @fresh @sickClass @hurtClass @meth @stoned @wd @hooked @jacked @fatClass @pregnantClass @courtingClass @spdClass @agiClass @endClass @battlePick @menuOn @rarityClass @growthClass @monsterClass @monsterMeth @tentacleClass @combineSel @(SpriteCatalog.Has( spriteId ) ? "has-sprite" : "")"
style="@fishStyle"
@onclick="@(() => OnFishClick(id))">
<div class="fish-vis" style="@visStyle">
@if ( fish.IsPregnant )
{
<div class="pregnant-glow"></div>
}
@if ( fish.Rarity >= FishRarity.Uncommon )
{
<div class="rarity-halo">
<div class="h n"></div>
<div class="h e"></div>
<div class="h s"></div>
<div class="h w"></div>
</div>
}
@if ( fish.Rarity == FishRarity.PaintedRare )
{
<div class="rarity-paint a"></div>
<div class="rarity-paint b"></div>
}
@if ( fish.IsMonster && !SpriteCatalog.Has( spriteId ) )
{
@if ( fish.Species?.Id == "monster_tentacle" )
{
<div class="tentacle-aura"></div>
<div class="tentacle-arm t1"></div>
<div class="tentacle-arm t2"></div>
<div class="tentacle-arm t3"></div>
<div class="tentacle-arm t4"></div>
<div class="tentacle-arm t5"></div>
<div class="tentacle-eye a"></div>
<div class="tentacle-eye b"></div>
<div class="tentacle-eye c"></div>
}
else
{
<div class="monster-glitch"></div>
<div class="monster-eye-extra a"></div>
<div class="monster-eye-extra b"></div>
<div class="monster-aura"></div>
}
}
@if ( SpriteCatalog.Has( spriteId ) )
{
@* Mirror on the textured panel when swimming left *@
<spritebox class="fish-sprite" sprite="@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>
}
@* Stat tells only on placeholder fish — boxes look like debug gizmos on sprites. *@
@if ( !SpriteCatalog.Has( spriteId ) )
{
@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>@* fish-vis *@
</div>@* fish *@
}
@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 drop in WaterFx.TankDrops )
{
var dl = drop.X.ToString( "0.#", CultureInfo.InvariantCulture );
var dtop = drop.Y.ToString( "0.#", CultureInfo.InvariantCulture );
var dw = drop.W.ToString( "0.#", CultureInfo.InvariantCulture );
var dh = drop.H.ToString( "0.#", CultureInfo.InvariantCulture );
var fade = Math.Clamp( drop.Life / Math.Max( 0.05f, drop.MaxLife ), 0.15f, 1f ).ToString( "0.##", CultureInfo.InvariantCulture );
<div class="water-drop @(drop.Jet ? "jet" : "")" style="left: @(dl)px; top: @(dtop)px; width: @(dw)px; height: @(dh)px; opacity: @fade;">
@if ( SpriteCatalog.Has( drop.Sprite ) )
{
<spritebox class="water-drop-spr" sprite="@drop.Sprite"></spritebox>
}
</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 spark in PixelFx.TankSparks )
{
var sl = MathF.Round( spark.X ).ToString( "0" );
var st = MathF.Round( spark.Y ).ToString( "0" );
var ss = spark.Size.ToString();
var fade = Math.Clamp( spark.Life / Math.Max( 0.05f, spark.MaxLife ), 0.2f, 1f ).ToString( "0.##", CultureInfo.InvariantCulture );
<div class="px-spark" style="left: @(sl)px; top: @(st)px; width: @(ss)px; height: @(ss)px; background-color: @spark.Color; opacity: @fade;"></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;
// Center short labels (~+3 SPD) without burying lane offset.
var half = sf.Kind is "end" or "spd" or "agi" ? 28f : 48f;
var sfl = (sf.X - half).ToString( "0.#", CultureInfo.InvariantCulture );
var sft = (sf.Y - 10f).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 ) );
var rateLbl = "+" + Economy.FormatDoge( menuFish.EffectiveIncomePerSecond ) + "/s";
// Chillquarium card: stage · % · rarity · income
var growthLine = menuFish.GrowthStageName + " " + menuFish.GrowthPercent + "% · "
+ menuFish.RarityLabel + " · " + rateLbl;
string statusLine = "";
if ( menuFish.IsFightInjured ) statusLine = "Hurt · no fights " + menuFish.FightCooldownLeftText;
else if ( menuFish.IsPregnant ) statusLine = "Carrying eggs";
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 = menuFish.FatStatusLabel;
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;
// Always show power verbs (locked = grey + hint). Hiding them kills discovery.
var trainOn = Progression.TrainUnlocked;
var fightOn = Progression.FightUnlocked;
var combineOn = Progression.CombineUnlocked;
var flushOn = LotRunSystem.Unlocked && !menuFish.IsFightInjured;
var trainLbl = trainOn ? "Train · free" : ("Train · " + (string.IsNullOrEmpty( Progression.TrainHint ) ? "soon" : Progression.TrainHint));
var fightLbl = menuFish.IsFightInjured
? ("Hurt " + menuFish.FightCooldownLeftText)
: fightOn
? "Fight · ~2× purse"
: ("Fight · " + (string.IsNullOrEmpty( Progression.FightHint ) ? "soon" : Progression.FightHint));
var combineLbl = combineOn
? "Combine · birth Ð"
: ("Combine · " + (string.IsNullOrEmpty( FishFusion.UnlockHint ) ? "soon" : FishFusion.UnlockHint));
var flushLbl = flushOn
? "Flush · Adventure"
: menuFish.IsFightInjured
? "Flush · hurt"
: ("Flush · " + (string.IsNullOrEmpty( Progression.AdventureHint ) ? "soon" : Progression.AdventureHint));
var canMove = TankSim.HasMultipleTanks;
var moveLbl = canMove ? ("Move → " + TankSim.OtherTankLabel) : "";
// Suggested next action — one lit primary when it matters.
var suggest = "";
if ( trainOn && !StorySystem.HasTrainedOnce ) suggest = "train";
else if ( combineOn && !StorySystem.HasCombinedOnce && TankSim.TotalFishCount >= 2 ) suggest = "combine";
else if ( fightOn && BattleSystem.Wins + BattleSystem.Losses == 0 && !menuFish.IsFightInjured ) suggest = "fight";
else if ( flushOn && !LotRunSystem.HasFlushed ) suggest = "flush";
else if ( trainOn ) suggest = "train";
var tipLine = suggest switch
{
"train" => "Tip · Train free (stats pay in Fight + Adventure)",
"combine" => "Tip · Combine two fish · birth pays Ð",
"fight" => "Tip · Fight · entry ≈ half a win",
"flush" => "Tip · Flush starts Adventure Mode",
_ => ""
};
var menuW = FishRenameOpen ? 248f : 228f;
// Honest height — wrapping labels used to underestimate and clip Sell.
const float rowH = 40f;
const float headerH = 118f;
var statusH = string.IsNullOrEmpty( statusLine ) ? 0f : 20f;
var tipH = string.IsNullOrEmpty( tipLine ) ? 0f : 36f;
var btnRows = 5;
if ( canMove ) btnRows++;
var menuH = FishRenameOpen ? 230f : (headerH + statusH + tipH + btnRows * rowH + 16f);
LayoutFishMenu( menuW, menuH, out var ml, out var mt );
var mlS = ml.ToString( "0.#", CultureInfo.InvariantCulture );
var mtS = mt.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: @(mlS)px; top: @(mtS)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>
<div class="fish-menu-sub @menuFish.RarityCss">@growthLine</div>
<div class="fish-menu-growth" title="Growth toward adult">
<div class="fish-menu-growth-fill" style="width: @((menuFish.Growth01 * 100f).ToString( "0", CultureInfo.InvariantCulture ))%;"></div>
</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>
@if ( !string.IsNullOrEmpty( tipLine ) )
{
<div class="fish-menu-tip">@tipLine</div>
}
<div class="fish-menu-btn @(trainOn ? (suggest == "train" ? "primary" : "") : "locked")" @onclick="@OnFishMenuTrain" @onmouseover="@OnHover" title="@trainLbl">@trainLbl</div>
<div class="fish-menu-btn @(fightOn && !menuFish.IsFightInjured ? (suggest == "fight" ? "primary" : "") : "locked") @(menuFish.IsFightInjured ? "quiet" : "")" @onclick="@OnFishMenuFight" @onmouseover="@OnHover" title="@fightLbl">@fightLbl</div>
<div class="fish-menu-btn @(combineOn ? (suggest == "combine" ? "primary" : "warn") : "locked")" @onclick="@OnFishMenuCombine" @onmouseover="@OnHover" title="@combineLbl">@combineLbl</div>
@if ( canMove )
{
<div class="fish-menu-btn" @onclick="@OnFishMenuMove" @onmouseover="@OnHover">@moveLbl</div>
}
<div class="fish-menu-btn @(flushOn ? (suggest == "flush" ? "primary" : "warn") : "locked")" @onclick="@OnFishMenuFlush" @onmouseover="@OnHover" title="Adventure Mode — flush, crawl, faucet home if they die">@flushLbl</div>
<div class="fish-menu-btn" @onclick="@OnFishMenuSell" @onmouseover="@OnHover">@sellLbl</div>
}
</div>
</div>
}
}
</div>
<div class="tank-rim bottom"></div>
<div class="tank-base"></div>
</div>
<div class="tank-shadow"></div>
@if ( RoomScene.HasStand )
{
<div class="tank-stand">
<spritebox class="tank-stand-spr" sprite="@RoomScene.StandId"></spritebox>
</div>
}
</div>
}
@if ( JanitorPeep.IsVisible )
{
<div class="janitor-cameo @(JanitorPeep.Leaving ? "out" : "in") @(CrimeSystem.CanOfferJanitor ? "offer" : "")" @onclick="@OnPokeJanitor">
<div class="janitor-cameo-window">
<div class="janitor-cameo-pane">
@if ( SpriteCatalog.Has( "ui_janitor" ) )
{
<div class="janitor-cameo-face">
<spritebox class="janitor-cameo-spr" sprite="ui_janitor"></spritebox>
</div>
}
else
{
<div class="janitor-cameo-face fallback">?</div>
}
<div class="janitor-cameo-mullion-v"></div>
<div class="janitor-cameo-mullion-h"></div>
</div>
<div class="janitor-cameo-sill">@CrimeSystem.JanitorSill</div>
</div>
<div class="janitor-cameo-bubble">
<div class="janitor-cameo-name">JANITOR</div>
<div class="janitor-cameo-line">@JanitorPeep.Line</div>
</div>
</div>
}
</div>
@* Right rail — tools + shop only after Grandma gifts the tank *@
@if ( GrandmaGifts.HasTank )
{
<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. *@
@{
var feedSprite = FeedSystem.HasAnyFood && FeedSystem.Selected is not null
? FeedSystem.Selected.InvSpriteId
: "inv_food";
}
<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="@feedSprite"></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 class="inv-slot kick @(CrimeSystem.CanKick ? "" : "empty")"
@onclick="@OnKickTank" @onmouseover="@OnHover"
title="Kick the tank. Fish panic. Glass remembers.">
<div class="inv-icon-wrap">
<div class="inv-glyph">KIK</div>
</div>
<div class="inv-label">Kick</div>
<div class="inv-meta">glass</div>
</div>
<div class="inv-slot crime @(CrimeSystem.SheetOpen ? "on" : "") @(CrimeSystem.SheetCount > 0 ? "hot" : "")"
@onclick="@OnToggleCrimeSheet" @onmouseover="@OnHover"
title="Porch felonies. The good stuff.">
<div class="inv-icon-wrap">
<div class="inv-glyph">BAD</div>
@if ( CrimeSystem.SheetCount > 0 )
{
<div class="inv-qty">@CrimeSystem.SheetCount</div>
}
</div>
<div class="inv-label">Bad</div>
<div class="inv-meta">@CrimeSystem.SheetMeta</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>
@if ( LotRunSystem.Unlocked )
{
<div class="lot-run-tool @(LotRunSystem.IsOpen ? "on" : "")"
@onclick="@OnOpenLotRun" @onmouseover="@OnHover"
title="@(LotRunSystem.HasFlushed ? "Adventure Mode — maps off the lot" : "Adventure starts with Flush on a fish")">
<div class="lot-run-tool-icon">ADV</div>
<div class="lot-run-tool-label">Adventure</div>
</div>
}
</div>
}
else
{
@* Keep stage centered before tools exist *@
<div class="inv-panel inv-panel-ghost"></div>
}
@if ( CrimeSystem.SheetOpen && GrandmaGifts.HasTank )
{
<div class="crime-sheet">
<div class="crime-sheet-title">BAD IDEAS</div>
<div class="crime-sheet-kicker">Porch felonies. Grandma is taking notes.</div>
@if ( CrimeSystem.CanRobTin )
{
<div class="crime-row" @onclick="@OnRobCookieTin" @onmouseover="@OnHover">
<div class="crime-row-name">Rob funeral tin</div>
<div class="crime-row-meta">Grandpa's leftover Ð. She can still count.</div>
</div>
}
@if ( CrimeSystem.CanOfferJanitor )
{
<div class="crime-row" @onclick="@OnOfferJanitorMeth" @onmouseover="@OnHover">
<div class="crime-row-name">Offer janitor meth</div>
<div class="crime-row-meta">He will say no. He's high on Folgers.</div>
</div>
}
@if ( CrimeSystem.CanDosePipes )
{
<div class="crime-row" @onclick="@OnDosePipes" @onmouseover="@OnHover">
<div class="crime-row-name">Flush a bag</div>
<div class="crime-row-meta">Dose the pipes. He lives with them.</div>
</div>
}
@if ( CrimeSystem.CanGiftGrandma )
{
<div class="crime-row" @onclick="@OnGiftGrandmaMeth" @onmouseover="@OnHover">
<div class="crime-row-name">Gift Grandma "sugar"</div>
<div class="crime-row-meta">She will put it in the soup. The soup will refuse.</div>
</div>
}
@if ( CrimeSystem.SheetCount <= 0 )
{
<div class="crime-row quiet">
<div class="crime-row-name">Nothing juicy</div>
<div class="crime-row-meta">Kick the glass. Fight Brad. Buy a bag.</div>
</div>
}
<div class="crime-sheet-close cq-btn slim" @onclick="@OnToggleCrimeSheet">Close</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" title="Pay Ð to drive a shark off">@SharkSystem.ScareButtonLabel</div>
<div class="cq-btn danger @(SharkSystem.CanFend ? "" : "off")" @onclick="@OnFendShark" @onmouseover="@OnHover" title="Train power vs shark · loot on kill">@SharkSystem.FendButtonLabel</div>
}
@if ( EndingSystem.CanGoodBoy )
{
<div class="cq-btn primary" @onclick="@OnEndingGood" @onmouseover="@OnHover">@EndingSystem.GoodButtonLabel</div>
}
@if ( EndingSystem.CanBadBoy )
{
<div class="cq-btn danger" @onclick="@OnEndingBad" @onmouseover="@OnHover">@EndingSystem.BadButtonLabel</div>
}
@if ( EndingSystem.CanMixed )
{
<div class="cq-btn warn" @onclick="@OnEndingMixed" @onmouseover="@OnHover">@EndingSystem.MixedButtonLabel</div>
}
</div>
</div>
}
</div>@* play-bezel-inner *@
</div>@* play-bezel *@
@if ( AchievementSystem.ToastVisible && AchievementSystem.ToastDef is not null )
{
var ach = AchievementSystem.ToastDef;
<div class="ach-toast">
<div class="ach-toast-kicker">TROPHY</div>
<div class="ach-toast-name">@ach.Name</div>
<div class="ach-toast-punch">@ach.Punch</div>
</div>
}
@if ( AchievementSystem.BoardOpen && GrandmaGifts.HasTank )
{
<div class="shop-backdrop" @onclick="@OnCloseAchievements"></div>
<div class="ach-board">
<div class="ach-board-head">
<div class="ach-board-title">Porch trophies</div>
<div class="ach-board-meta">@AchievementSystem.ProgressLabel</div>
<div class="cq-btn slim shop-close" @onclick="@OnCloseAchievements" @onmouseover="@OnHover">Close</div>
</div>
<div class="ach-board-scroll">
@foreach ( var def in AchievementSystem.Catalog )
{
var on = AchievementSystem.IsUnlocked( def.Id );
<div class="ach-row @(on ? "on" : "off")">
<div class="ach-row-mark">@(on ? "●" : "○")</div>
<div class="ach-row-text">
<div class="ach-row-name">@def.Name</div>
<div class="ach-row-hint">@(on ? def.Punch : def.Hint)</div>
</div>
</div>
}
</div>
</div>
}
@* Wallet click → money help (only once Grandma gave the cold wallet) *@
@if ( EconomyHelpOpen && GrandmaGifts.HasReceived( "cold_wallet" ) )
{
<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">Dogecoin cold wallet</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>
}
@* Pre-game: looking around the lot — player inner monologue over the park *@
@if ( GrandmaGifts.IsDialogue && GrandmaGifts.ActiveProp == GrandmaGiftProp.Establishing )
{
<div class="mono-lot" @onclick="@OnGrandmaGiftAdvance">
<div class="mono-lot-pan">
@if ( SpriteCatalog.Has( "ui_trailer_lot" ) )
{
<spritebox class="mono-lot-art" sprite="ui_trailer_lot"></spritebox>
}
else
{
<div class="mono-lot-fallback"></div>
}
</div>
<div class="mono-lot-shade"></div>
<div class="mono-lot-place">
<div class="mono-lot-place-name">SUNNY PINES</div>
<div class="mono-lot-place-sub">Walk home · suspended · again</div>
</div>
<div class="mono-thought">
<div class="mono-thought-label">you · troublemaker</div>
<div class="mono-thought-text">@GrandmaGifts.VisibleText</div>
<div class="mono-thought-foot">
<span class="mono-thought-prog">@(GrandmaGifts.LineIndex + 1)/@GrandmaGifts.LineCount</span>
<span class="mono-thought-hint">@GrandmaGifts.AdvanceHint</span>
</div>
</div>
</div>
}
@* Lot character gifts: dialogue → item popup *@
else if ( GrandmaGifts.IsDialogue )
{
<div class="grandma-prologue @GrandmaGifts.SceneCssClass @GrandmaGifts.ThemeClass" @onclick="@OnGrandmaGiftAdvance">
<div class="grandma-prologue-stage">
@if ( GrandmaGifts.ActiveProp == GrandmaGiftProp.Tank )
{
<div class="grandma-gift-tank">
<div class="grandma-gift-glass"></div>
<div class="grandma-gift-water"></div>
<div class="grandma-gift-gravel"></div>
@if ( SpriteCatalog.Has( "plant_green" ) )
{
<div class="grandma-gift-plant">
<spritebox class="grandma-gift-plant-spr" sprite="plant_green"></spritebox>
</div>
}
@if ( SpriteCatalog.Has( "goldfish" ) )
{
<div class="grandma-gift-fish a">
<spritebox class="grandma-gift-fish-spr" sprite="goldfish"></spritebox>
</div>
<div class="grandma-gift-fish b">
<spritebox class="grandma-gift-fish-spr" sprite="goldfish"></spritebox>
</div>
}
<div class="grandma-gift-label">FOR YOU</div>
</div>
}
else if ( GrandmaGifts.ActiveProp == GrandmaGiftProp.Wallet )
{
<div class="grandma-gift-wallet">
<div class="grandma-gift-wallet-shell">
<div class="grandma-gift-wallet-brand">DOGECOIN COLD WALLET</div>
<div class="grandma-gift-wallet-lcd">
<div class="grandma-gift-wallet-bal">@GrandmaGifts.WalletBalanceLabel</div>
<div class="grandma-gift-wallet-sub">FROM GRANDMA</div>
</div>
<div class="grandma-gift-wallet-leds">
<div class="grandma-gift-wallet-btn"></div>
<div class="grandma-gift-wallet-btn"></div>
<div class="grandma-gift-wallet-led"></div>
</div>
</div>
<div class="grandma-gift-label">HER GIFT</div>
</div>
}
else if ( GrandmaGifts.ActiveProp == GrandmaGiftProp.Fusion )
{
@* Trailer window — janitor peeks in from outside *@
<div class="grandma-gift-window">
<div class="grandma-window-wall">
<div class="grandma-window-stud a"></div>
<div class="grandma-window-stud b"></div>
<div class="grandma-window-frame">
<div class="grandma-window-pane">
<div class="grandma-window-sky"></div>
<div class="grandma-window-lot-hint"></div>
@if ( SpriteCatalog.Has( GrandmaGifts.PortraitSprite ) )
{
<div class="grandma-window-peep">
<spritebox class="grandma-window-face" sprite="@GrandmaGifts.PortraitSprite"></spritebox>
</div>
}
else
{
<div class="grandma-window-peep fallback">
<div class="grandma-window-face-fallback">?</div>
</div>
}
<div class="grandma-window-mullion-v"></div>
<div class="grandma-window-mullion-h"></div>
<div class="grandma-window-glare"></div>
</div>
</div>
<div class="grandma-window-sill">
<div class="grandma-window-tap">* tap tap *</div>
</div>
</div>
<div class="grandma-gift-label">TRAILER WINDOW</div>
</div>
}
else if ( GrandmaGifts.ActiveProp == GrandmaGiftProp.Fight )
{
<div class="grandma-gift-fight">
<div class="grandma-fight-badge">VS</div>
<div class="grandma-fight-name">BRAD</div>
<div class="grandma-fight-sub">UNIT 4B · GARAGE</div>
<div class="grandma-gift-label">CIRCUIT</div>
</div>
}
else if ( GrandmaGifts.ActiveProp == GrandmaGiftProp.Adventure )
{
<div class="grandma-gift-adventure">
<div class="grandma-toilet">
<div class="grandma-toilet-tank"></div>
<div class="grandma-toilet-bowl">
<div class="grandma-toilet-water"></div>
@if ( SpriteCatalog.Has( "goldfish" ) )
{
<div class="grandma-toilet-fish">
<spritebox class="grandma-gift-fish-spr" sprite="goldfish"></spritebox>
</div>
}
</div>
<div class="grandma-toilet-base"></div>
</div>
<div class="grandma-gift-label">FLUSH?</div>
</div>
}
@* Janitor = window prop only; You monologue = no portrait. *@
@if ( GrandmaGifts.ActiveProp != GrandmaGiftProp.Fusion
&& !string.IsNullOrEmpty( GrandmaGifts.PortraitSprite ) )
{
<div class="grandma-portrait-wrap @GrandmaGifts.ThemeClass">
@if ( SpriteCatalog.Has( GrandmaGifts.PortraitSprite ) )
{
<spritebox class="grandma-portrait" sprite="@GrandmaGifts.PortraitSprite"></spritebox>
}
else
{
<div class="grandma-portrait-fallback">@GrandmaGifts.Speaker</div>
}
</div>
}
</div>
<div class="grandma-dialogue @GrandmaGifts.ThemeClass">
<div class="grandma-dialogue-name">@GrandmaGifts.Speaker</div>
<div class="grandma-dialogue-text">@GrandmaGifts.VisibleText</div>
<div class="grandma-dialogue-foot">
<span class="grandma-dialogue-prog">@(GrandmaGifts.LineIndex + 1)/@GrandmaGifts.LineCount</span>
<span class="grandma-dialogue-hint">@GrandmaGifts.AdvanceHint</span>
</div>
</div>
</div>
}
else if ( GrandmaGifts.IsItemPopup )
{
<div class="grandma-item-popup-layer">
<div class="grandma-item-popup @GrandmaGifts.ThemeClass">
<div class="grandma-item-kicker">@GrandmaGifts.PopupKicker</div>
<div class="grandma-item-title">@GrandmaGifts.ItemTitle</div>
@if ( GrandmaGifts.ActiveProp == GrandmaGiftProp.Establishing )
{
<div class="grandma-gift-lot popup-size">
@if ( SpriteCatalog.Has( "ui_trailer_lot" ) )
{
<spritebox class="grandma-gift-lot-spr" sprite="ui_trailer_lot"></spritebox>
}
<div class="grandma-gift-label">YOUR PROMISE</div>
</div>
}
else if ( GrandmaGifts.ActiveProp == GrandmaGiftProp.Tank )
{
<div class="grandma-gift-tank popup-size">
<div class="grandma-gift-glass"></div>
<div class="grandma-gift-water"></div>
<div class="grandma-gift-gravel"></div>
@if ( SpriteCatalog.Has( "goldfish" ) )
{
<div class="grandma-gift-fish a">
<spritebox class="grandma-gift-fish-spr" sprite="goldfish"></spritebox>
</div>
}
</div>
}
else if ( GrandmaGifts.ActiveProp == GrandmaGiftProp.Wallet )
{
<div class="grandma-gift-wallet popup-size">
<div class="grandma-gift-wallet-shell">
<div class="grandma-gift-wallet-brand">DOGECOIN COLD WALLET</div>
<div class="grandma-gift-wallet-lcd">
<div class="grandma-gift-wallet-bal">@GrandmaGifts.WalletBalanceLabel</div>
<div class="grandma-gift-wallet-sub">FROM GRANDMA</div>
</div>
</div>
</div>
}
else if ( GrandmaGifts.ActiveProp == GrandmaGiftProp.Fusion )
{
<div class="grandma-gift-fusion popup-size">
@if ( SpriteCatalog.Has( "goldfish" ) )
{
<div class="grandma-fusion-fish left">
<spritebox class="grandma-gift-fish-spr" sprite="goldfish"></spritebox>
</div>
}
<div class="grandma-fusion-plus">+</div>
@if ( SpriteCatalog.Has( "goldfish" ) )
{
<div class="grandma-fusion-fish right">
<spritebox class="grandma-gift-fish-spr" sprite="goldfish"></spritebox>
</div>
}
<div class="grandma-fusion-eq">=</div>
<div class="grandma-fusion-blob">?</div>
</div>
}
else if ( GrandmaGifts.ActiveProp == GrandmaGiftProp.Fight )
{
<div class="grandma-gift-fight popup-size">
<div class="grandma-fight-badge">VS</div>
<div class="grandma-fight-name">BRAD</div>
<div class="grandma-fight-sub">UNIT 4B · GARAGE</div>
</div>
}
else if ( GrandmaGifts.ActiveProp == GrandmaGiftProp.Adventure )
{
<div class="grandma-gift-adventure popup-size">
<div class="grandma-toilet mini">
<div class="grandma-toilet-tank"></div>
<div class="grandma-toilet-bowl">
<div class="grandma-toilet-water"></div>
</div>
<div class="grandma-toilet-base"></div>
</div>
<div class="grandma-gift-label">FLUSH</div>
</div>
}
<div class="grandma-item-body">@GrandmaGifts.ItemBody</div>
<div class="cq-btn primary" @onclick="@OnGrandmaGiftAdvance" @onmouseover="@OnHover">@GrandmaGifts.PopupButton</div>
</div>
</div>
}
@* Floating notices — absolute, never push tank / dock *@
@if ( !GrandmaGifts.IsBusy && ( !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>
}
@* Story beat card — not during Grandma gift sequences *@
@if ( !GrandmaGifts.IsBusy && StorySystem.CardOpen )
{
<div class="shop-backdrop" @onclick="@(() => OnDismissStory())"></div>
<div class="story-panel">
<div class="story-kicker">@StorySystem.ChapterLabel · @StorySystem.ProgressText</div>
<div class="story-title">@StorySystem.CardTitle</div>
<div class="story-body">
@foreach ( var para in StoryBodyParas )
{
<div class="story-p">@para</div>
}
</div>
@if ( !string.IsNullOrEmpty( StorySystem.ObjectiveText ) )
{
<div class="story-objective">
<span class="story-obj-label">Do next</span>
<span class="story-obj-text">@StorySystem.ObjectiveText</span>
</div>
}
<div class="story-actions">
<div class="cq-btn primary" @onclick="@(() => OnDismissStory())" @onmouseover="@OnHover">Continue</div>
</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;
if ( BattleSystem.ChampionshipTitles > 0 )
battleTitle = battleTitle + " · Titles x" + BattleSystem.ChampionshipTitles;
var fightBtnLbl = "Fight!";
if ( BattleSystem.Template is not null )
{
var feeLbl = Economy.FormatDoge( BattleSystem.Template.EntryFee );
fightBtnLbl = BattleSystem.Template.IsChampionship
? ("Enter Cup \u00D0" + feeLbl)
: ("Fight! \u00D0" + feeLbl);
}
<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 · ~28s lockout")
: "Pick a circuit · pick fish · Fight. Wins ~2×+ entry.";
if ( BattleSystem.Template is not null && BattleSystem.Template.IsChampionship )
lobbyMsg = "Championship: " + BattleSystem.Template.ChampionshipRounds
+ " rounds · one entry · team locked after start.";
else if ( BattleSystem.Template is not null && !string.IsNullOrEmpty( BattleSystem.OddsLabel ) )
lobbyMsg = BattleSystem.OddsLabel;
}
<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;
var champClass = pick.IsChampionship ? "champ" : "";
if ( unlocked )
{
if ( pick.IsChampionship )
{
lineName = BattleSystem.ChampionshipTitles > 0
? (pick.Name + " belts " + BattleSystem.ChampionshipTitles)
: pick.Name;
lineMeta = pick.ChampionshipRounds + " rounds · team "
+ pick.TeamSize + " · entry \u00D0" + fee + " · title belt";
}
else
{
lineName = beaten ? (pick.Name + " done") : pick.Name;
lineMeta = "team " + pick.TeamSize + " entry \u00D0" + fee + " win \u00D0" + 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") @champClass"
@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)";
if ( BattleSystem.Template.IsChampionship )
rosterLbl = "Cup roster " + have + " / " + need + " (locked after enter)";
<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>
@if ( CrimeSystem.CanSpikeBrad )
{
<div class="cq-btn warn" @onclick="@OnSpikeBrad" @onmouseover="@OnHover" title="Dose Brad's bucket">Spike bucket</div>
}
else if ( CrimeSystem.BradSpikedPending )
{
<div class="cq-btn quiet">Bucket spiked</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 \u00D0" + 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 = MathF.Round( BattleSystem.ArenaShake * MathF.Sin( Time.Now * 40f ) ).ToString( "0" );
var stageShakeY = MathF.Round( BattleSystem.ArenaShake * 0.6f * MathF.Cos( Time.Now * 33f ) ).ToString( "0" );
<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 drop in WaterFx.BattleDrops )
{
var wdl = drop.X.ToString( "0.#", CultureInfo.InvariantCulture );
var wdt = drop.Y.ToString( "0.#", CultureInfo.InvariantCulture );
var wdw = drop.W.ToString( "0.#", CultureInfo.InvariantCulture );
var wdh = drop.H.ToString( "0.#", CultureInfo.InvariantCulture );
var wdf = Math.Clamp( drop.Life / Math.Max( 0.05f, drop.MaxLife ), 0.15f, 1f ).ToString( "0.##", CultureInfo.InvariantCulture );
<div class="water-drop battle @(drop.Jet ? "jet" : "")" style="left: @(wdl)px; top: @(wdt)px; width: @(wdw)px; height: @(wdh)px; opacity: @wdf;">
@if ( SpriteCatalog.Has( drop.Sprite ) )
{
<spritebox class="water-drop-spr" sprite="@drop.Sprite"></spritebox>
}
</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 )
{
var ackLbl = BattleSystem.ResultAdvancesChamp ? "Next Round" : "Lobby";
var rematchOn = BattleSystem.CanRematch;
<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="battle-result-actions">
@if ( rematchOn )
{
<div class="cq-btn primary" @onclick="@OnRematchBattle" @onmouseover="@OnHover">Rematch</div>
}
@if ( CrimeSystem.CanRansack )
{
<div class="cq-btn warn" @onclick="@OnRansackGarage" @onmouseover="@OnHover" title="Empty Brad's sock drawer">Ransack</div>
}
<div class="cq-btn @(rematchOn ? "quiet" : "primary")" @onclick="@OnAckBattle" @onmouseover="@OnHover">@ackLbl</div>
</div>
</div>
}
else
{
string fightMsg;
if ( BattleSystem.ChampRunActive && BattleSystem.ChampRoundsTotal > 0 )
fightMsg = "Cup " + BattleSystem.ChampRound + "/" + BattleSystem.ChampRoundsTotal + " · fins flying…";
else
fightMsg = "Round " + BattleSystem.Round + " · fins flying…";
<div class="battle-msg">@fightMsg</div>
}
}
</div>
}
@* Adventure Mode — flush → crawl → die=faucet home · live=keep mapping *@
@if ( LotRunSystem.IsOpen )
{
<div class="shop-backdrop battle-dim" @onclick="@OnLotRunBackdrop"></div>
@if ( AdvVignette.HasLeft )
{
<div class="adv-vignette left @(LotRunSystem.Location?.ThemeClass ?? "")">
<div class="adv-vignette-frame loc">
<spritebox class="adv-vignette-spr" sprite="@AdvVignette.LeftId"></spritebox>
</div>
<div class="adv-vignette-cap">@AdvVignette.LeftCaption</div>
</div>
}
@if ( AdvVignette.HasRight )
{
<div class="adv-vignette right @(LotRunSystem.Location?.ThemeClass ?? "")">
<div class="adv-vignette-frame ev">
<spritebox class="adv-vignette-spr" sprite="@AdvVignette.RightId"></spritebox>
</div>
<div class="adv-vignette-cap">@AdvVignette.RightCaption</div>
</div>
}
<div class="lot-run-panel @(LotRunSystem.IsFlushing ? "run-toilet" : LotRunSystem.IsFaucetReturn ? "run-faucet" : (LotRunSystem.Location?.ThemeClass ?? ""))">
<div class="battle-head">
<div class="battle-head-title">
@if ( LotRunSystem.Phase == LotRunPhase.Flush )
{
<span>Adventure · Flush</span>
}
else if ( LotRunSystem.Phase == LotRunPhase.Faucet )
{
<span>Adventure · Return</span>
}
else if ( LotRunSystem.Phase == LotRunPhase.Map )
{
<span>Adventure</span>
}
else if ( LotRunSystem.Location is not null )
{
<span>@LotRunSystem.Location.Name</span>
@if ( !string.IsNullOrEmpty( LotRunSystem.ProgressLabel ) && LotRunSystem.Phase != LotRunPhase.Team )
{
<span class="lot-run-prog"> · @LotRunSystem.ProgressLabel</span>
}
}
else
{
<span>Adventure</span>
}
</div>
<div class="cq-btn slim shop-close" @onclick="@OnCloseLotRun" @onmouseover="@OnHover">Close</div>
</div>
@if ( LotRunSystem.Phase == LotRunPhase.Flush )
{
<div class="lot-run-room flush-scene" @onclick="@OnLotRunAdvanceFlush">
<div class="adv-stage flush-stage">
@if ( SpriteCatalog.Has( "adv_cg_flush" ) )
{
<spritebox class="adv-stage-spr" sprite="adv_cg_flush"></spritebox>
}
else
{
<div class="adv-toilet">
<div class="adv-toilet-tank"></div>
<div class="adv-toilet-bowl">
<div class="adv-swirl a"></div>
<div class="adv-swirl b"></div>
<div class="adv-fish-dot"></div>
</div>
<div class="adv-toilet-base"></div>
</div>
<div class="adv-pipe-row">
<div class="adv-pipe"></div>
<div class="adv-pipe mid"></div>
<div class="adv-pipe"></div>
</div>
}
</div>
<div class="lot-run-room-kind">FLUSH</div>
<div class="lot-run-room-title">@LotRunSystem.FlushFishName</div>
<div class="lot-run-room-body">@LotRunSystem.FlushVisibleLine</div>
<div class="lot-run-room-hint">
@(LotRunSystem.FlushLineIndex + 1)/@LotRunSystem.FlushLineCount · click
</div>
</div>
}
else if ( LotRunSystem.Phase == LotRunPhase.Faucet )
{
<div class="lot-run-room faucet-scene" @onclick="@OnLotRunAdvanceFaucet">
<div class="adv-stage faucet-stage">
@if ( SpriteCatalog.Has( "adv_cg_faucet" ) )
{
<spritebox class="adv-stage-spr" sprite="adv_cg_faucet"></spritebox>
}
else
{
<div class="adv-sink">
<div class="adv-faucet-neck"></div>
<div class="adv-faucet-spout"></div>
<div class="adv-water-stream"></div>
<div class="adv-basin">
<div class="adv-fish-dot home"></div>
</div>
</div>
}
</div>
<div class="lot-run-room-kind">RETURN</div>
<div class="lot-run-room-title">Kitchen faucet</div>
<div class="lot-run-room-body">@LotRunSystem.FaucetVisibleLine</div>
<div class="lot-run-room-hint">
@(LotRunSystem.FaucetLineIndex + 1)/@LotRunSystem.FaucetLineCount · click
</div>
</div>
}
else if ( LotRunSystem.Phase == LotRunPhase.Map )
{
<div class="battle-msg">Survive = more maps. Die = faucet home hurt (keep bagged Ð). Train before hard maps.</div>
<div class="lot-run-locations">
@foreach ( var loc in LotLocationDef.Catalog )
{
var unlocked = LotRunSystem.IsLocationUnlocked( loc );
var cleared = LotRunSystem.IsCleared( loc.Id );
var lockHint = LotRunSystem.LocationLockHint( loc );
var meta = unlocked
? (LotRunSystem.LocationRewardSummary( loc ) + (cleared ? " · cleared" : ""))
: lockHint;
var theme = loc.ThemeClass ?? "";
<div class="lot-run-loc @(unlocked ? "" : "locked") @(cleared ? "cleared" : "") @theme"
@onclick="@(() => OnLotRunPickLocation(loc))" @onmouseover="@OnHover">
<div class="lot-run-loc-art @theme">
<div class="lot-run-loc-glyph"></div>
</div>
<div class="lot-run-loc-text">
<div class="lot-run-loc-name">@loc.Name</div>
<div class="lot-run-loc-note">@loc.Note</div>
<div class="lot-run-loc-blurb">@loc.Blurb</div>
<div class="lot-run-loc-meta">@meta</div>
</div>
</div>
}
</div>
}
else if ( LotRunSystem.Phase == LotRunPhase.Team && LotRunSystem.Location is not null )
{
var need = LotRunSystem.Location.TeamSize;
var have = LotRunSystem.SelectedCount;
var startLbl = "Start Ð" + Economy.FormatDoge( LotRunSystem.Location.EntryFee );
var teamPow = LotRunSystem.SelectedTeamPower.ToString( "0.#", CultureInfo.InvariantCulture );
<div class="battle-msg">Select @need · @have/@need · power @teamPow · train = safer wins + more Ð</div>
<div class="battle-roster-row">
@foreach ( var fish in TankSim.Fish )
{
var on = LotRunSystem.SelectedIds.Contains( fish.InstanceId );
var hurt = fish.IsFightInjured;
var pow = BattleSystem.CombatPower( fish ).ToString( "0.#", CultureInfo.InvariantCulture );
var chipLbl = hurt
? (fish.DisplayName + " hurt")
: (fish.DisplayName + " " + pow);
<div class="battle-chip @(on ? "on" : "") @(hurt ? "hurt" : "")"
@onclick="@(() => OnLotRunToggleFish(fish.InstanceId))" @onmouseover="@OnHover">
<span>@chipLbl</span>
</div>
}
</div>
<div class="battle-start-row">
<div class="cq-btn quiet" @onclick="@OnLotRunBackMap" @onmouseover="@OnHover">Back</div>
<div class="cq-btn primary big @(LotRunSystem.CanStartRun() ? "" : "off")"
@onclick="@OnLotRunStart" @onmouseover="@OnHover">@startLbl</div>
</div>
}
else if ( LotRunSystem.Phase == LotRunPhase.Room && LotRunSystem.CurrentRoom is not null )
{
var room = LotRunSystem.CurrentRoom;
var bag = LotRunSystem.RunEarningsLabel;
if ( LotRunSystem.CrawlLive )
{
var biteL = PipeCrawl.BiteLoPx.ToString( "0.#", CultureInfo.InvariantCulture );
var biteW = PipeCrawl.BiteWidth.ToString( "0.#", CultureInfo.InvariantCulture );
var fishTop = (PipeCrawl.FishY - 18f + MathF.Sin( PipeCrawl.Bob ) * 5f).ToString( "0.#", CultureInfo.InvariantCulture );
var shout = PipeCrawl.Shout;
<div class="pipe-crawl @(PipeCrawl.HurtFlash > 0f ? "hurt" : "") @(PipeCrawl.InBiteWindow ? "hot" : "") @(PipeCrawl.BossWave ? "boss" : "")">
<div class="pipe-hud">
<div class="pipe-hp">
@foreach ( var pipOn in PipeCrawl.HpPips )
{
<div class="pipe-pip @(pipOn ? "on" : "")"></div>
}
</div>
<div class="pipe-hud-mid">
<span class="pipe-room">@PipeCrawl.RoomTitle</span>
<span class="pipe-prog">@LotRunSystem.ProgressLabel</span>
</div>
<div class="pipe-hud-right">
@if ( PipeCrawl.Combo >= 2 )
{
<span class="pipe-combo">x@(PipeCrawl.Combo)</span>
}
<span class="pipe-bag">@bag</span>
</div>
</div>
<div class="pipe-field @(PipeCrawl.ChompFlash > 0f ? "chomp" : "")" @onclick="@OnLotRunChomp">
<div class="pipe-ribs">
<div class="pipe-rib r1"></div>
<div class="pipe-rib r2"></div>
<div class="pipe-rib r3"></div>
<div class="pipe-rib r4"></div>
</div>
<div class="pipe-flow"></div>
<div class="pipe-zone" style="left: @(biteL)px; width: @(biteW)px;"></div>
<div class="pipe-zone-label">CHOMP</div>
<div class="pipe-fish @(PipeCrawl.HurtFlash > 0f ? "ouch" : "")" style="top: @(fishTop)px;">
@if ( SpriteCatalog.Has( PipeCrawl.FishSprite ) )
{
<spritebox class="pipe-fish-spr" sprite="@PipeCrawl.FishSprite"></spritebox>
}
</div>
@foreach ( var thing in PipeCrawl.Things )
{
var tl = thing.X.ToString( "0.#", CultureInfo.InvariantCulture );
var tt = thing.Y.ToString( "0.#", CultureInfo.InvariantCulture );
var tw = thing.W.ToString( "0.#", CultureInfo.InvariantCulture );
var th = thing.H.ToString( "0.#", CultureInfo.InvariantCulture );
var deadCls = thing.Dead ? "pop" : "";
<div class="pipe-thing @deadCls @thing.CssKind"
style="left: @(tl)px; top: @(tt)px; width: @(tw)px; height: @(th)px;">
@if ( SpriteCatalog.Has( thing.SpriteId ) )
{
<spritebox class="pipe-thing-spr" sprite="@thing.SpriteId"></spritebox>
}
</div>
}
@if ( !string.IsNullOrEmpty( shout ) )
{
<div class="pipe-shout">@shout</div>
}
</div>
<div class="pipe-hint">Click / Space · bite in the yellow · misses hurt</div>
</div>
}
else
{
var stakes = LotRunSystem.RoomStakesLabel;
<div class="lot-run-room">
<div class="lot-run-stakes-bar">
<span class="lot-run-bag">@bag</span>
@if ( !string.IsNullOrEmpty( LotRunSystem.ProgressLabel ) )
{
<span class="lot-run-stakes-sep">·</span>
<span>@LotRunSystem.ProgressLabel</span>
}
</div>
<div class="lot-run-room-kind">@LotRunSystem.RoomKindLabel</div>
<div class="lot-run-room-title">@room.Title</div>
<div class="lot-run-room-body">@room.Body</div>
@if ( !string.IsNullOrEmpty( stakes ) )
{
<div class="lot-run-room-stakes">@stakes</div>
}
@if ( room.Kind == LotRoomKind.Choice )
{
<div class="lot-run-choices">
<div class="cq-btn primary" @onclick="@OnLotRunChoiceA" @onmouseover="@OnHover">@room.ChoiceA</div>
<div class="cq-btn warn" @onclick="@OnLotRunChoiceB" @onmouseover="@OnHover">@room.ChoiceB</div>
</div>
}
else
{
var actLbl = string.IsNullOrEmpty( room.Button ) ? "Continue" : room.Button;
<div class="cq-btn primary big" @onclick="@OnLotRunAct" @onmouseover="@OnHover">@actLbl</div>
}
</div>
}
}
else if ( LotRunSystem.Phase == LotRunPhase.RoomResult )
{
<div class="battle-result">
<div class="battle-result-title">@LotRunSystem.RoomResultTitle</div>
<div class="battle-result-body">@LotRunSystem.RoomResultBody</div>
<div class="cq-btn primary" style="margin-top: 10px;" @onclick="@OnLotRunContinue" @onmouseover="@OnHover">Continue</div>
</div>
}
else if ( LotRunSystem.Phase == LotRunPhase.Done )
{
<div class="battle-result @(LotRunSystem.RunWon ? "win" : "lose")">
<div class="battle-result-title">@LotRunSystem.DoneTitle</div>
<div class="battle-result-body">@LotRunSystem.DoneBody</div>
<div class="cq-btn primary" style="margin-top: 10px;" @onclick="@OnLotRunDoneMap" @onmouseover="@OnHover">Map</div>
</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>
@if ( Progression.DecorShopUnlocked )
{
<div class="cq-tab @(ShopTab == "decor" ? "on" : "")" @onclick="@(() => SetShopTab("decor"))" @onmouseover="@OnHover">Decor</div>
}
@if ( Progression.ShadyUnlocked )
{
<div class="cq-tab @(ShopTab == "extra" ? "on" : "")" @onclick="@(() => SetShopTab("extra"))" @onmouseover="@OnHover">Shady</div>
}
</div>
@* Fixed-height status strip — never reflows the shop list *@
<div class="shop-msg-slot">
<div class="shop-msg dw-msg">@Shop.LastMessage</div>
</div>
<div class="shop-body">
@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>
}
<div class="shop-list @(DarkWeb.CheckoutOpen ? "checkout-dim" : "") @(ShopTab == "fish" ? "fish-packs-list" : "")">
@if ( ShopTab == "fish" )
{
var oddsLine = FishRarityInfo.ShopOddsLine( TankSim.RarityLuck );
var capLine = TankSim.FishCount >= TankSim.Capacity
? "FULL · buy expands +2 · Ð" + Economy.FormatDoge( Shop.CapacityCost )
: TankSim.FishCount + "/" + TankSim.Capacity + " · " + oddsLine;
var pityHot = FishRarityInfo.PityHot;
var pityWord = FishRarityInfo.PityWord;
var pityPips = FishRarityInfo.PityPips;
<div class="shop-chase-bar">
<div class="shop-chase-left">
<div class="shop-chase-title">Packs · mash for @FishRarityInfo.ChaseLine</div>
<div class="shop-chase-meta">@capLine</div>
</div>
<div class="pity-meter @(pityHot ? "hot" : "")">
<div class="pity-pip @(pityPips > 0 ? "on" : "")"></div>
<div class="pity-pip @(pityPips > 1 ? "on" : "")"></div>
<div class="pity-pip @(pityPips > 2 ? "on" : "")"></div>
<div class="pity-pip @(pityPips > 3 ? "on" : "")"></div>
<div class="pity-pip @(pityPips > 4 ? "on" : "")"></div>
@if ( !string.IsNullOrEmpty( pityWord ) )
{
<div class="pity-word">@pityWord</div>
}
</div>
</div>
<div class="shop-fish-layout">
<div class="shop-packs">
@foreach ( var species in FishSpecies.ShopCatalog )
{
var unlocked = FishSpecies.IsShopUnlocked( species );
// Hide far-future locks so the list stays scannable.
if ( !unlocked && species.UnlockPlaytime > Economy.PlaytimeSeconds + 12f * 60f )
continue;
var can = Shop.CanBuyFish( species );
var actLbl = Shop.FishActLabel( species );
var waterTag = species.Water == WaterType.Salt ? "SALT" : "FRESH";
var mismatch = species.Water != TankSim.Water && species.Water != WaterType.Any;
var costLbl = unlocked
? "Ð" + Economy.FormatDoge( species.BuyCost )
: FishSpecies.ShopUnlockHint( species );
var packClass = !unlocked ? "locked" : can ? "can-buy" : "cant-buy";
if ( mismatch )
packClass += " mismatch";
var buySpecies = species;
<div class="shop-pack @packClass" @onclick="@(() => OnBuyFish(buySpecies))" @onmouseover="@OnHover">
<div class="shop-pack-art">
@if ( species.HasSprite )
{
<spritebox class="shop-pack-sprite" sprite="@species.SpriteId"></spritebox>
}
else
{
<div class="shop-pack-blob" style="background-color: @species.Color;"></div>
}
</div>
<div class="shop-pack-name">@species.Name</div>
<div class="shop-pack-water">@waterTag</div>
<div class="shop-pack-buy @(can ? "go" : "")">
<div class="shop-pack-cost">@(unlocked ? costLbl : "—")</div>
<div class="shop-pack-act">@actLbl</div>
</div>
</div>
}
</div>
<div class="shop-reveal-stage">
@if ( BuyReveal.IsActive )
{
<div class="@BuyReveal.CardClass" @onclick="@OnBuyRevealClick" @onmouseover="@OnHover">
<div class="buy-card-flap left"></div>
<div class="buy-card-flap right"></div>
<div class="buy-card-back">?</div>
<div class="buy-card-face">
<div class="buy-card-art">
@if ( BuyReveal.HasSprite )
{
<spritebox class="buy-card-sprite" sprite="@BuyReveal.SpriteId"></spritebox>
}
else
{
<div class="buy-card-blob" style="background-color: @BuyReveal.Color;"></div>
}
</div>
<div class="buy-card-name">@BuyReveal.SpeciesName</div>
<div class="buy-card-stamp">@BuyReveal.RarityLabel</div>
</div>
<div class="buy-card-hint">@BuyReveal.Hint</div>
</div>
}
else
{
<div class="buy-card idle">
<div class="buy-card-back idle-q">?</div>
<div class="buy-card-hint">@BuyReveal.Hint</div>
</div>
}
@foreach ( var spark in PixelFx.ShopSparks )
{
var sl = MathF.Round( spark.X ).ToString( "0" );
var st = MathF.Round( spark.Y ).ToString( "0" );
var ss = spark.Size.ToString();
var fade = Math.Clamp( spark.Life / Math.Max( 0.05f, spark.MaxLife ), 0.2f, 1f ).ToString( "0.##", CultureInfo.InvariantCulture );
<div class="px-spark shop" style="left: @(sl)px; top: @(st)px; width: @(ss)px; height: @(ss)px; background-color: @spark.Color; opacity: @fade;"></div>
}
</div>
</div>
}
else if ( ShopTab == "food" )
{
@foreach ( var food in FoodDef.Catalog )
{
var owned = FeedSystem.IsUnlocked( food );
// Collapse far food locks.
if ( !owned && food.UnlockPlaytime > Economy.PlaytimeSeconds + 15f * 60f )
continue;
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 growNote = "grow ×" + food.BoostMultiplier.ToString( "0.##", CultureInfo.InvariantCulture )
+ " · " + ((int)food.BoostDuration) + "s boost";
var rateFood = owned
? growNote
: (string.IsNullOrEmpty( reason ) ? growNote : reason);
var rowFood = owned ? (selected ? "owned can-buy" : "can-buy") : (canUnlock ? "can-buy" : "cant-buy");
var buyFood = food;
<div class="shop-row buyable @rowFood @(selected ? "owned" : "")"
@onclick="@(() => OnFoodRowClick(buyFood))" @onmouseover="@OnHover">
<div class="shop-col name">
@if ( !string.IsNullOrWhiteSpace( food.SpriteId ) && SpriteCatalog.Has( food.SpriteId ) )
{
<div class="legend-swatch has-sprite">
<spritebox class="swatch-sprite" sprite="@food.SpriteId"></spritebox>
</div>
}
else
{
<div class="legend-swatch" style="background-color: @food.PelletColor;"></div>
}
@food.DisplayInv
</div>
<div class="shop-col rate">@rateFood</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">Backgrounds</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.Backdrops )
{
var can = Shop.CanBuyDecor( decor );
var hung = string.Equals( TankSim.ActiveBackdropId, decor.Id, StringComparison.Ordinal )
|| (decor.Id == DecorDef.BackdropNone.Id && string.IsNullOrEmpty( TankSim.ActiveBackdropId ));
var decorCost = decor.Cost <= 0 ? "free" : "Ð" + Economy.FormatDoge( decor.Cost );
var decorRate = hung ? "hung" : decor.Note;
var buyDecor = decor;
<div class="shop-row buyable @(hung ? "equipped" : "")" @onclick="@(() => OnBuyDecor(buyDecor))" @onmouseover="@OnHover">
<div class="shop-col name">
@if ( decor.HasSprite )
{
<div class="legend-swatch has-sprite swatch-wide">
<spritebox class="swatch-sprite" sprite="@decor.SpriteId"></spritebox>
</div>
}
else
{
<div class="legend-swatch shop-swatch-empty"></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 @(hung ? "off" : can ? "primary" : "off")">@(hung ? "On" : "Hang")</div>
</div>
</div>
}
<div class="shop-row header">
<div class="shop-col name">Gravel</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.Substrates )
{
var can = Shop.CanBuyDecor( decor );
var laid = string.Equals( TankSim.ActiveGravelId, decor.Id, StringComparison.Ordinal )
|| (decor.Id == DecorDef.GravelNone.Id && string.IsNullOrEmpty( TankSim.ActiveGravelId ));
var decorCost = decor.Cost <= 0 ? "free" : "Ð" + Economy.FormatDoge( decor.Cost );
var decorRate = laid ? "down" : decor.Note;
var buyDecor = decor;
<div class="shop-row buyable @(laid ? "equipped" : "")" @onclick="@(() => OnBuyDecor(buyDecor))" @onmouseover="@OnHover">
<div class="shop-col name">
@if ( decor.HasSprite )
{
<div class="legend-swatch has-sprite swatch-wide">
<spritebox class="swatch-sprite" sprite="@decor.SpriteId"></spritebox>
</div>
}
else
{
<div class="legend-swatch shop-swatch-empty"></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 @(laid ? "off" : can ? "primary" : "off")">@(laid ? "On" : "Dump")</div>
</div>
</div>
}
<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.Props )
{
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>
}
else
{
@* Spacer so names without art don't sit on the clip edge *@
<div class="legend-swatch shop-swatch-empty"></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">
<div class="legend-swatch has-sprite">
@if ( SpriteCatalog.Has( "inv_m80" ) )
{
<spritebox class="swatch-sprite" sprite="inv_m80"></spritebox>
}
</div>
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">
<div class="legend-swatch has-sprite">
@if ( SpriteCatalog.Has( "inv_meth" ) )
{
<spritebox class="swatch-sprite" sprite="inv_meth"></spritebox>
}
</div>
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">
<div class="legend-swatch shop-swatch-empty"></div>
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">
<div class="legend-swatch shop-swatch-empty"></div>
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">Grandma's arcs</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">Pride</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">Your fault</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">Wuss</div>
<div class="shop-col rate">@EndingSystem.MixedHint</div>
<div class="shop-col cost"></div>
<div class="shop-col act"></div>
</div>
}
</div>
</div>
<div class="shop-hint dw-hint">Mash packs · Esc closes</div>
</div>
}
</div>@* playing *@
}
</root>
@code
{
bool ShopOpen;
string ShopTab = "fish";
bool ShowMenuArt =>
GameFlow.Screen == GameScreen.MainMenu
|| GameFlow.Screen == GameScreen.Settings;
/// <summary>Fish context menu target (instance id), or null/empty when closed.</summary>
string _fishMenuId;
string FishMenuId
{
get => _fishMenuId;
set
{
_fishMenuId = value;
// Keep sim focus in sync so thrash calm / open checks never desync.
TankSim.MenuFocusId = value;
}
}
/// <summary>Tank-local position snapshotted when the menu opens (does not chase thrash).</summary>
float FishMenuAnchorX;
float FishMenuAnchorY;
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 => "";
int ChaosBarWidth => (int)MathF.Round( Math.Clamp( Chaos.Value, 0f, 100f ) );
/// <summary>
/// Mode-specific coach (panels, menus, minigames). Free-play “what next?” lives in the quest bar.
/// </summary>
string ContextHint
{
get
{
if ( GrandmaGifts.IsBusy )
return GrandmaGifts.AdvanceHint;
if ( StorySystem.CardOpen )
return "Continue · read the beat";
if ( FishFusion.PickingSecond )
return "Combine · click fish OR decor · Esc cancels";
if ( FusionFeel.IsActive )
return "FUSION";
if ( BuyReveal.IsActive )
return BuyReveal.Hint;
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 )
{
if ( BattleSystem.ChampRunActive && BattleSystem.ChampRoundsTotal > 0 )
return "Cup " + BattleSystem.ChampRound + "/" + BattleSystem.ChampRoundsTotal;
return "Fighting…";
}
if ( BattleSystem.Phase == BattlePhase.Lobby )
return "Pick circuit · pick fish · Fight";
if ( BattleSystem.Phase == BattlePhase.Result )
return BattleSystem.ResultAdvancesChamp ? "Next Round" : "Continue";
if ( LotRunSystem.IsOpen )
{
if ( LotRunSystem.Phase == LotRunPhase.Flush )
return "Flush · click to continue";
if ( LotRunSystem.Phase == LotRunPhase.Faucet )
return "Faucet · fish coming home";
if ( LotRunSystem.Phase == LotRunPhase.Map )
return "Adventure · pick a crawl";
if ( LotRunSystem.Phase == LotRunPhase.Team )
return "Pick fish · Start run";
if ( LotRunSystem.Phase == LotRunPhase.Room )
return "Crawl · act or choose";
if ( LotRunSystem.Phase == LotRunPhase.RoomResult )
return "Continue";
if ( LotRunSystem.Phase == LotRunPhase.Done )
return LotRunSystem.RunWon ? "Continue Adventure" : "Home · continue Adventure";
}
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;
// Build the action strip to match visible fish-menu buttons.
var parts = new List<string>();
if ( Progression.TrainUnlocked ) parts.Add( "Train" );
if ( Progression.FightUnlocked ) parts.Add( "Fight" );
if ( Progression.CombineUnlocked ) parts.Add( "Combine" );
if ( TankSim.HasMultipleTanks ) parts.Add( "Move" );
parts.Add( "Sell" );
return string.Join( " · ", parts );
}
// Decor throw tip is a tank overlay — never inject into the hint row (reflows stage).
// Free-play progression is on the purple quest pill — keep hint quiet when idle.
return "";
}
}
string ContextHintClass
{
get
{
if ( GrandmaGifts.IsBusy )
return "focus";
if ( StorySystem.CardOpen )
return "focus";
if ( FishFusion.PickingSecond || FusionFeel.IsActive )
return "warn";
if ( MethRush.Open || ExplodeSystem.ModeActive )
return "warn";
if ( TrainingSystem.TrainOpen || TrainingSystem.InjectOpen || TrainingSystem.HasSessionSummary )
return "focus";
if ( BattleSystem.IsOpen )
return "warn";
if ( !string.IsNullOrEmpty( StorySystem.QuestHint ) )
return "nudge";
if ( TankSim.FishCount == 0 || !FeedSystem.HasAnyFood || TankSim.FishCount >= TankSim.Capacity )
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.StartMenuMusic();
}
protected override void OnDestroy()
{
GameFlow.Changed -= OnFlowChanged;
if ( TankSim.IsActive )
SaveGame.TrySave();
GameAudio.StopMusic();
FeedSystem.Clear();
ExplodeSystem.Clear();
SharkSystem.Clear();
DeathFeel.Clear();
BoomFeel.Clear();
FusionFeel.Clear();
BuyReveal.Clear();
PixelFx.Clear();
FishFusion.Clear();
BreedingSystem.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 )
{
Shop.UiOpen = ShopOpen;
try
{
TankSim.Tick( Time.Delta );
// Gift triggers after a little play; dialogue typewriter while open.
GrandmaGifts.Tick( Time.Delta );
if ( !GrandmaGifts.IsBusy )
{
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 );
BuyReveal.Tick( Time.Delta );
PixelFx.Tick( Time.Delta );
if ( !GrandmaGifts.IsBusy )
{
MethRush.Tick( Time.Delta );
if ( ShopOpen )
DarkWeb.Tick( Time.Delta );
Chaos.TickCalm( Time.Delta );
Karma.TickCalmCare( Time.Delta );
Karma.TickWuss( Time.Delta );
DeathFeel.Tick( Time.Delta );
BoomFeel.Tick( Time.Delta );
FusionFeel.Tick( Time.Delta );
TrainingSystem.Tick( Time.Delta );
StorySystem.Tick( Time.Delta );
}
// After gifts/story — flush queued porch quips once per frame.
GrandmaReact.Tick( Time.Delta );
JanitorPeep.Tick( Time.Delta );
CrimeSystem.Tick( Time.Delta );
AchievementSystem.Tick( Time.Delta );
WaterFx.Tick( Time.Delta );
if ( LotRunSystem.IsOpen )
LotRunSystem.Tick( Time.Delta );
// Fish died / left while menu open — drop focus so thrash calm clears.
if ( !string.IsNullOrEmpty( FishMenuId ) && TankSim.FindFish( FishMenuId ) is null )
CloseFishMenu();
}
catch ( Exception e )
{
Log.Warning( $"[NO-CHILLquarium] MainShell Update failed: {e}" );
}
}
else if ( GameFlow.Screen == GameScreen.Ending )
{
EndingSystem.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 ( LotRunSystem.CrawlLive && (Input.Pressed( "Jump" ) || Input.Pressed( "Attack1" )) )
{
LotRunSystem.Chomp();
return;
}
if ( !TrainingSystem.TrainOpen )
return;
// Bound to space in ProjectSettings/Input.config
if ( !Input.Pressed( "Jump" ) )
return;
OnHitTrain();
}
void OnLotRunChomp()
{
LotRunSystem.Chomp();
}
/// <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;
Shop.UiOpen = false;
BuyReveal.Clear();
PixelFx.Clear();
FishMenuId = null;
EconomyHelpOpen = false;
ExplodeSystem.Clear();
}
StateHasChanged();
}
List<string> StoryBodyParas
{
get
{
var body = StorySystem.CardBody;
var list = new List<string>();
if ( string.IsNullOrWhiteSpace( body ) )
return list;
foreach ( var raw in body.Split( new[] { '\n' }, StringSplitOptions.None ) )
{
var p = raw.Trim();
if ( p.Length > 0 )
list.Add( p );
}
return list;
}
}
public void OnDismissStory()
{
if ( GrandmaGifts.IsBusy )
{
OnGrandmaGiftAdvance();
return;
}
StorySystem.DismissCard();
StateHasChanged();
}
void OnGrandmaGiftAdvance()
{
GrandmaGifts.Advance();
StateHasChanged();
}
public void OnOpenStory()
{
if ( StorySystem.IsDone )
return;
// Don't steal focus from minigames / shops.
if ( MethRush.Open || TrainingSystem.TrainOpen || TrainingSystem.InjectOpen || BattleSystem.IsOpen || DarkWeb.CheckoutOpen )
return;
StorySystem.OpenCard();
StateHasChanged();
}
/// <summary>
/// Quest pill click: re-read story card, or jump to the coach’s next action (shop / fight / hint).
/// </summary>
void OnQuestClick( bool isStory )
{
if ( GrandmaGifts.IsBusy || StorySystem.CardOpen )
return;
if ( MethRush.Open || TrainingSystem.TrainOpen || TrainingSystem.InjectOpen
|| BattleSystem.Phase == BattlePhase.Fighting || DarkWeb.CheckoutOpen )
return;
if ( isStory )
{
OnOpenStory();
return;
}
var step = ProgressionCoach.Current;
if ( step is null || string.IsNullOrEmpty( step.Text ) )
return;
var kicker = step.Kicker ?? "";
var text = step.Text ?? "";
// Shop-shaped kicks open the right tab.
if ( kicker is "Shop" or "Grow" )
{
OpenShopTo( text.Contains( "Food", StringComparison.OrdinalIgnoreCase ) ? "food" : "fish" );
return;
}
if ( kicker == "Decor" )
{
OpenShopTo( "decor" );
return;
}
if ( kicker is "Tank" or "Habitat" or "Salt" )
{
OpenShopTo( "tank" );
return;
}
if ( kicker == "Shady" )
{
OpenShopTo( "extra" );
return;
}
if ( kicker == "Crime" )
{
if ( !CrimeSystem.SheetOpen )
CrimeSystem.ToggleSheet();
StateHasChanged();
return;
}
if ( kicker is "Fight" or "Cup" )
{
TryOpenFightFromQuest();
return;
}
if ( kicker == "Train" )
{
FishMenuId = null;
TankSim.ShowBanner( "Click a fish · Train" );
GameAudio.PlayUi( GameAudio.Notice );
StateHasChanged();
return;
}
if ( kicker == "Combine" )
{
FishMenuId = null;
TankSim.ShowBanner( "Click a fish · Combine" );
GameAudio.PlayUi( GameAudio.Notice );
StateHasChanged();
return;
}
// Gift / Lot — knock on Grandma's door (don't just toast).
if ( kicker is "Lot" or "Gift" or "Porch" || step.Kind == "gift" )
{
OnLotStageClick();
return;
}
// Free-play — echo the coach line as a soft banner.
TankSim.ShowBanner( text );
GameAudio.PlayUi( GameAudio.Pop );
StateHasChanged();
}
void OnLotStageClick()
{
if ( GrandmaGifts.IsBusy )
{
OnGrandmaGiftAdvance();
return;
}
if ( GrandmaGifts.HasTank )
return;
if ( GrandmaGifts.TryTrigger( "fish_tank" ) )
{
StateHasChanged();
return;
}
TankSim.ShowBanner( "Porch is quiet. Give it a second." );
GameAudio.PlayUi( GameAudio.Notice );
StateHasChanged();
}
void OpenShopTo( string tab )
{
if ( !GrandmaGifts.HasTank )
{
TankSim.ShowBanner( "No tank yet — talk to Grandma." );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
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;
FishMenuId = null;
ShopOpen = true;
// Soft locks still honor Progression.
if ( tab == "decor" && !Progression.DecorShopUnlocked )
{
ShopTab = "fish";
TankSim.ShowBanner( Progression.DecorHint );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
if ( tab == "extra" && !Progression.ShadyUnlocked )
{
ShopTab = "fish";
TankSim.ShowBanner( Progression.ShadyHint );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
ShopTab = tab switch
{
"food" or "tank" => tab,
"decor" when Progression.DecorShopUnlocked => "decor",
"extra" when Progression.ShadyUnlocked => "extra",
_ => "fish"
};
Shop.SetLastMessage( "Pick a pack." );
GameAudio.PlayUi( GameAudio.Notice );
StateHasChanged();
}
void TryOpenFightFromQuest()
{
if ( !Progression.FightUnlocked )
{
var hint = Progression.FightHint;
TankSim.ShowBanner( string.IsNullOrEmpty( hint ) ? "Not ready to fight yet" : hint );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
if ( BattleSystem.IsOpen )
{
GameAudio.PlayUi( GameAudio.Pop );
StateHasChanged();
return;
}
FishMenuId = null;
ShopOpen = false;
EconomyHelpOpen = false;
if ( ExplodeSystem.ModeActive )
ExplodeSystem.ModeActive = false;
BattleSystem.TryOpen();
StateHasChanged();
}
void OnToggleEconomyHelp()
{
// No cold wallet yet — nothing to explain.
if ( !GrandmaGifts.HasReceived( "cold_wallet" ) )
return;
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 OnToggleAchievements()
{
if ( !GrandmaGifts.HasTank )
return;
AchievementSystem.ToggleBoard();
if ( AchievementSystem.BoardOpen )
{
ShopOpen = false;
EconomyHelpOpen = false;
CrimeSystem.CloseSheet();
}
StateHasChanged();
}
void OnCloseAchievements()
{
AchievementSystem.CloseBoard();
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.MainMenu && _newGameArmed )
{
_newGameArmed = false;
StateHasChanged();
return;
}
if ( GameFlow.Screen != GameScreen.Playing )
return;
// Layered dismiss — one level per press
if ( GrandmaGifts.IsBusy )
{
OnGrandmaGiftAdvance();
return;
}
if ( StorySystem.CardOpen )
{
StorySystem.DismissCard();
StateHasChanged();
return;
}
if ( BuyReveal.IsActive && !ShopOpen )
{
BuyReveal.Click();
StateHasChanged();
return;
}
if ( FishFusion.PickingSecond )
{
FishFusion.Cancel();
StateHasChanged();
return;
}
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 ( AchievementSystem.BoardOpen )
{
OnCloseAchievements();
return;
}
if ( BattleSystem.IsOpen && BattleSystem.Phase != BattlePhase.Fighting )
{
BattleSystem.Close();
StateHasChanged();
return;
}
if ( LotRunSystem.IsOpen )
{
LotRunSystem.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;
}
// Combine mode: second fish click completes the fusion.
if ( FishFusion.PickingSecond )
{
FishMenuId = null;
FishFusion.TryComplete( instanceId );
StateHasChanged();
return;
}
// Default: open context menu on this fish (no scurry).
if ( FishMenuId == instanceId )
{
CloseFishMenu();
return;
}
OpenFishMenu( instanceId );
}
void OpenFishMenu( string instanceId )
{
var fish = TankSim.FindFish( instanceId );
if ( fish is null )
return;
// Freeze panel at open so meth thrash can't drag buttons around the tank.
FishMenuAnchorX = fish.X;
FishMenuAnchorY = fish.DrawY;
FishMenuId = instanceId; // also sets TankSim.MenuFocusId
GameAudio.PlayUi( GameAudio.Pop );
StateHasChanged();
}
/// <summary>
/// Keep the whole card inside the glass. Park it beside the fish so a
/// fat abom doesn't sit on the title; flip sides / clamp if we're on a rim.
/// </summary>
void LayoutFishMenu( float menuW, float menuH, out float left, out float top )
{
var tankW = TankSim.Width;
var tankH = TankSim.Height;
const float pad = 8f;
var ax = FishMenuAnchorX;
var ay = FishMenuAnchorY;
var rightOf = ax + 18f;
var leftOf = ax - menuW - 18f;
if ( ax > tankW * 0.55f && leftOf >= pad )
left = leftOf;
else if ( rightOf + menuW <= tankW - pad )
left = rightOf;
else
left = Math.Clamp( ax - menuW * 0.5f, pad, Math.Max( pad, tankW - menuW - pad ) );
top = ay - menuH * 0.45f;
var maxTop = Math.Max( pad, tankH - menuH - pad );
top = Math.Clamp( top, pad, maxTop );
left = Math.Clamp( left, pad, Math.Max( pad, tankW - menuW - pad ) );
}
void OnFishMenuCombine()
{
var id = FishMenuId;
FishMenuId = null;
FishRenameOpen = false;
FishRenameDraft = "";
_renameFocusPending = false;
if ( string.IsNullOrEmpty( id ) )
return;
if ( !FishFusion.IsUnlocked )
{
TankSim.ShowBanner( FishFusion.UnlockHint );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
if ( ExplodeSystem.ModeActive || BattleSystem.IsOpen || TrainingSystem.TrainOpen || TrainingSystem.InjectOpen || MethRush.Open )
{
TankSim.ShowBanner( "Finish the other mess first." );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
FishFusion.Begin( id );
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; // clears TankSim.MenuFocusId
StateHasChanged();
}
void OnFishMenuTrain()
{
var id = FishMenuId;
FishMenuId = null;
FishRenameOpen = false;
FishRenameDraft = "";
_renameFocusPending = false;
if ( string.IsNullOrEmpty( id ) )
return;
if ( !Progression.TrainUnlocked )
{
TankSim.ShowBanner( Progression.TrainHint );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
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;
if ( !Progression.FightUnlocked )
{
TankSim.ShowBanner( Progression.FightHint );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
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;
CrimeSystem.CloseSheet();
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();
}
void OnRematchBattle()
{
if ( !BattleSystem.TryRematch() )
{
TankSim.ShowBanner( "Can't rematch yet — need healthy fish + entry Ð." );
GameAudio.PlayUi( GameAudio.Error );
}
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 ( !GrandmaGifts.HasTank )
{
TankSim.ShowBanner( "No tank yet — talk to Grandma." );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
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();
CrimeSystem.CloseSheet();
AchievementSystem.CloseBoard();
DarkWeb.Clear();
EconomyHelpOpen = false;
ShopOpen = true;
ShopTab = "fish";
Shop.SetLastMessage( "Pick a pack." );
GameAudio.PlayUi( GameAudio.Notice );
StateHasChanged();
}
void OnCloseShop()
{
ShopOpen = false;
Shop.UiOpen = false;
BuyReveal.OnShopClosed();
DarkWeb.Clear();
GameAudio.PlayUi( GameAudio.Close );
StateHasChanged();
}
void SetShopTab( string tab )
{
if ( DarkWeb.CheckoutOpen )
DarkWeb.Cancel();
// Soft progression: decor / shady appear after earlier menu skills.
if ( tab == "decor" && !Progression.DecorShopUnlocked )
{
TankSim.ShowBanner( Progression.DecorHint );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
if ( tab == "extra" && !Progression.ShadyUnlocked )
{
TankSim.ShowBanner( Progression.ShadyHint );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
// Shop is buy-only: fish / food / tank / decor / shady. No awards tab.
ShopTab = tab switch
{
"food" or "tank" => tab,
"decor" when Progression.DecorShopUnlocked => "decor",
"extra" when Progression.ShadyUnlocked => "extra",
_ => "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 OnBuyRevealClick()
{
BuyReveal.Click();
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 OnKickTank()
{
if ( ExplodeSystem.ModeActive || BattleSystem.IsOpen || LotRunSystem.IsOpen )
return;
CrimeSystem.TryKickTank();
StateHasChanged();
}
void OnToggleCrimeSheet()
{
if ( ExplodeSystem.ModeActive || BattleSystem.IsOpen || LotRunSystem.IsOpen )
return;
CrimeSystem.ToggleSheet();
StateHasChanged();
}
void OnPokeJanitor()
{
if ( ExplodeSystem.ModeActive || JanitorPeep.Leaving )
return;
CrimeSystem.TryPokeJanitor();
StateHasChanged();
}
void OnOfferJanitorMeth()
{
CrimeSystem.TryOfferJanitorMeth();
StateHasChanged();
}
void OnRobCookieTin()
{
CrimeSystem.TryRobCookieTin();
StateHasChanged();
}
void OnDosePipes()
{
CrimeSystem.TryDosePipes();
StateHasChanged();
}
void OnGiftGrandmaMeth()
{
CrimeSystem.TryGiftGrandmaMeth();
StateHasChanged();
}
void OnSpikeBrad()
{
CrimeSystem.TrySpikeBrad();
StateHasChanged();
}
void OnRansackGarage()
{
CrimeSystem.TryRansackGarage();
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 OnOpenLotRun()
{
if ( MethRush.Open || TrainingSystem.TrainOpen || TrainingSystem.InjectOpen )
{
TankSim.ShowBanner( "Finish what you're doing first." );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
ShopOpen = false;
EconomyHelpOpen = false;
FishMenuId = null;
CrimeSystem.CloseSheet();
if ( BattleSystem.IsOpen )
BattleSystem.Close();
LotRunSystem.TryOpen();
StateHasChanged();
}
void OnCloseLotRun()
{
LotRunSystem.Close();
StateHasChanged();
}
void OnLotRunBackdrop()
{
// Don't abandon mid-room / flush / faucet with a misclick.
if ( LotRunSystem.Phase is LotRunPhase.Map or LotRunPhase.Team or LotRunPhase.Done )
OnCloseLotRun();
}
void OnFishMenuFlush()
{
var id = FishMenuId;
CloseFishMenu();
if ( string.IsNullOrEmpty( id ) )
return;
if ( MethRush.Open || TrainingSystem.TrainOpen || TrainingSystem.InjectOpen )
{
TankSim.ShowBanner( "Finish what you're doing first." );
GameAudio.PlayUi( GameAudio.Error );
StateHasChanged();
return;
}
ShopOpen = false;
EconomyHelpOpen = false;
if ( BattleSystem.IsOpen )
BattleSystem.Close();
LotRunSystem.TryFlushFish( id );
StateHasChanged();
}
void OnLotRunAdvanceFlush()
{
LotRunSystem.AdvanceFlush();
StateHasChanged();
}
void OnLotRunAdvanceFaucet()
{
LotRunSystem.AdvanceFaucet();
StateHasChanged();
}
void OnLotRunPickLocation( LotLocationDef loc )
{
LotRunSystem.SelectLocation( loc );
StateHasChanged();
}
void OnLotRunToggleFish( string id )
{
LotRunSystem.ToggleFish( id );
StateHasChanged();
}
void OnLotRunBackMap()
{
LotRunSystem.BackToMap();
StateHasChanged();
}
void OnLotRunStart()
{
LotRunSystem.StartRun();
StateHasChanged();
}
void OnLotRunAct()
{
LotRunSystem.ActPrimary();
StateHasChanged();
}
void OnLotRunChoiceA()
{
LotRunSystem.ActChoiceA();
StateHasChanged();
}
void OnLotRunChoiceB()
{
LotRunSystem.ActChoiceB();
StateHasChanged();
}
void OnLotRunContinue()
{
LotRunSystem.ContinueAfterResult();
StateHasChanged();
}
void OnLotRunDoneMap()
{
LotRunSystem.FinishToMap();
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;
// Combine mode: decor is a valid second parent (fish + prop = hybrid).
if ( FishFusion.PickingSecond )
{
CloseFishMenu();
FishFusion.TryCompleteWithDecor( piece );
StateHasChanged();
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()
{
// Keep Combine arg counts ≤ 8 (API limit). Nest extras.
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, TankSim.HabitatId, RoomScene.BackgroundId )
);
var d = HashCode.Combine(
BoomFeel.Version,
FusionFeel.Version,
FishFusion.Version,
TankSim.AddictedFishCount,
TrainingSystem.Version,
TrainingSystem.InjectOpen,
TrainingSystem.TrainOpen,
HashCode.Combine( BuyReveal.Version, (int)BuyReveal.Phase, PixelFx.Version, BattleSystem.Version, SharkSystem.Version, FishFusion.PickingSecond, LotRunSystem.Version, HashCode.Combine( BreedingSystem.Version, PipeCrawl.Version, WaterFx.Version, AdvVignette.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
)
);
// Quest coach changes with economy/fish without always bumping StorySystem.Version.
var coach = ProgressionCoach.Current;
var g = HashCode.Combine(
EconomyHelpOpen,
ShopOpen,
StatFloat.Version,
DarkWeb.Version,
DarkWeb.Step,
JanitorPeep.Version,
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, CrimeSystem.Version, CrimeSystem.SheetOpen ? 1 : 0, AchievementSystem.Version );
var st = HashCode.Combine(
StorySystem.Version,
StorySystem.CatalogRevision,
StorySystem.CardOpen,
StorySystem.IsDone,
StorySystem.Index,
StorySystem.CardTitle?.GetHashCode() ?? 0,
StorySystem.CardBody?.GetHashCode() ?? 0,
HashCode.Combine(
StorySystem.ObjectiveText?.GetHashCode() ?? 0,
coach?.Kicker?.GetHashCode() ?? 0,
coach?.Text?.GetHashCode() ?? 0,
coach?.Kind?.GetHashCode() ?? 0
)
);
var gg = HashCode.Combine(
GrandmaGifts.Version,
(int)GrandmaGifts.Phase,
GrandmaGifts.ActiveId?.GetHashCode() ?? 0,
GrandmaGifts.LineIndex,
GrandmaGifts.VisibleText?.GetHashCode() ?? 0,
GrandmaGifts.WalletBalanceLabel?.GetHashCode() ?? 0,
GrandmaGifts.SceneCssClass?.GetHashCode() ?? 0,
GrandmaGifts.PortraitSprite?.GetHashCode() ?? 0
);
return HashCode.Combine( a, b, c, d, HashCode.Combine( e, f, g, HashCode.Combine( mr, st, gg ) ) );
}
}