🔍 s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (55 total matches found)
fpkreastudios.desertpump / .obj/__compiler_extra.cs
Game game
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Desert Pump" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "desertpump" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "fpkreastudios" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "fpkreastudios.desertpump" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "28" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-08-01T14:58:58.6069356Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.194.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.194.0")]
fpkreastudios.desertpump / Game/DesertPumpGame.cs
Game game
namespace DesertPump;

/// <summary>
/// The whole game. Holds the money, the water and the current pump, and is the only
/// thing allowed to change them - the HUD just calls into here and renders the result.
/// </summary>
[Title( "Desert Pump Game" )]
[Category( "Desert Pump" )]
[Icon( "water_drop" )]
public sealed partial class DesertPumpGame : Component
{
	/// <summary>The active game, so the HUD doesn't need a wired-up reference.</summary>
	public static DesertPumpGame Current { get; private set; }

	[Property, Group( "Save" )]
	public bool SaveEnabled { get; set; } = true;

	[Property, Group( "Save" )]
	public string SaveFile { get; set; } = "desertpump.json";

	/// <summary>Seconds between autosaves. Progress also saves on shutdown.</summary>
	[Property, Group( "Save" ), Range( 2f, 60f )]
	public float SaveInterval { get; set; } = 5f;

	/// <summary>Longest stretch of time away that still pays out idle water.</summary>
	[Property, Group( "Save" ), Range( 0f, 24f )]
	public float MaxIdleHours { get; set; } = 4f;

	[Property, Group( "Audio" )] public SoundEvent PumpSound { get; set; }
	[Property, Group( "Audio" )] public SoundEvent SellSound { get; set; }
	[Property, Group( "Audio" )] public SoundEvent UpgradeSound { get; set; }
	[Property, Group( "Audio" )] public SoundEvent DeniedSound { get; set; }

	public double Coins { get; private set; }
	public double Water { get; private set; }

	/// <summary>Index into <see cref="PumpModel.All"/>.</summary>
	public int PumpLevel { get; private set; }

	/// <summary>Shop upgrade levels, indexed by <see cref="UpgradeKind"/>.</summary>
	readonly int[] upgradeLevels = new int[3];

	public bool Muted => PumpSettings.Current.Muted;

	public long TotalPumps { get; private set; }
	public double TotalEarned { get; private set; }
	public double TotalLitresSold { get; private set; }

	/// <summary>Litres the pump produced while the game was closed, for the welcome-back note.</summary>
	public double IdleCatchUp { get; private set; }

	public PumpModel CurrentPump => PumpModel.Get( PumpLevel );
	public PumpTier Tier => CurrentPump.Tier;
	public PumpModel NextPump => PumpLevel < PumpModel.MaxIndex ? PumpModel.Get( PumpLevel + 1 ) : null;
	public bool IsMaxed => NextPump is null;

	// ---- effective stats: the tier's base numbers times whatever the shop has sold you

	/// <summary>Tank size including the Water Tank upgrade.</summary>
	public double TankCapacity => CurrentPump.Tank * MultiplierOf( UpgradeKind.Storage );

	/// <summary>Litres per manual pump, including Pump Power.</summary>
	public double LitresPerClick => CurrentPump.PerClick * MultiplierOf( UpgradeKind.Power );

	/// <summary>Coins per litre, including Market Contacts.</summary>
	public double PricePerLitre => CurrentPump.PricePerLitre * MultiplierOf( UpgradeKind.Value );

	public bool TankFull => Water >= TankCapacity - 0.0001;
	public float TankFraction => TankCapacity <= 0 ? 0f : (float)Math.Clamp( Water / TankCapacity, 0d, 1d );

	/// <summary>What the tank is worth right now.</summary>
	public double SaleValue => Water * PricePerLitre;

	public bool CanSell => Water > 0.0001;
	public bool CanUpgrade => NextPump is not null && Coins >= NextPump.Cost;

	public int LevelOf( UpgradeKind kind ) => upgradeLevels[(int)kind];

	public float MultiplierOf( UpgradeKind kind ) => UpgradeTrack.Get( kind ).MultiplierAt( LevelOf( kind ) );

	public bool IsMaxLevel( UpgradeKind kind ) => LevelOf( kind ) >= UpgradeTrack.Get( kind ).MaxLevel;

	/// <summary>Cost of the next level, or infinity once the track is maxed.</summary>
	public double CostOf( UpgradeKind kind )
	{
		return IsMaxLevel( kind )
			? double.PositiveInfinity
			: UpgradeTrack.Get( kind ).CostAt( LevelOf( kind ) );
	}

	public bool CanBuy( UpgradeKind kind ) => !IsMaxLevel( kind ) && Coins >= CostOf( kind );

	/// <summary>Fired on a successful manual pump, with the litres gained.</summary>
	public Action<double> Pumped { get; set; }

	/// <summary>Fired on a sale, with the coins gained.</summary>
	public Action<double> Sold { get; set; }

	/// <summary>Fired after buying a new pump, with the tier we moved to.</summary>
	public Action<PumpModel> Upgraded { get; set; }

	/// <summary>Fired when an action was refused - tank full, broke, nothing to sell.</summary>
	public Action<string> Refused { get; set; }

	/// <summary>Fired after buying a shop upgrade.</summary>
	public Action<UpgradeTrack> Purchased { get; set; }

	TimeSince timeSinceSave;

	/// <summary>
	/// True once <see cref="Load"/> has run, which only happens in play mode. The copy of
	/// this component living in the editor's scene never wakes, so it sits on default
	/// zeroes - and without this guard its shutdown save would write those zeroes straight
	/// over a real run. Never write state we haven't read.
	/// </summary>
	bool loaded;

	protected override void OnAwake()
	{
		Current = this;
		Load();
	}

	/// <summary>
	/// Quitting tears the scene down, and which of these fires first depends on how you
	/// left - closing the window, stopping play mode, or loading another scene. Saving
	/// from both, plus after every earn and spend, means there's no way out that loses
	/// a run.
	/// </summary>
	protected override void OnDisabled()
	{
		Save();
	}

	protected override void OnDestroy()
	{
		Save();

		if ( Current == this )
			Current = null;
	}

	protected override void OnUpdate()
	{
		// A code hotload wipes statics without re-running OnAwake, which would leave
		// Current null and the HUD showing "no game in the scene" for the rest of the
		// session. Cheaper to re-claim it than to debug that every time.
		if ( Current != this ) Current = this;

		// This is a mouse game - without this the cursor gets locked to the view in
		// play mode and none of the buttons can be clicked.
		Mouse.Visibility = MouseVisibility.Visible;

		// Space bar is a second pump button so you're not glued to the mouse.
		if ( Input.Pressed( "Jump" ) )
		{
			Pump();
		}

		var passive = CurrentPump.PerSecond * Time.Delta;
		if ( passive > 0f )
		{
			AddWater( passive );
		}

		TickGusher();
		TickTree();

		if ( PumpSettings.Current.AutoSell && TankFull )
		{
			SellAll();
		}

		if ( SaveEnabled && timeSinceSave > SaveInterval )
		{
			Save();
		}
	}

	/// <summary>
	/// One manual pump. Returns the litres actually gained - zero means the tank was already full.
	/// </summary>
	public double Pump()
	{
		if ( TankFull )
		{
			Refuse( "Tank is full - sell your water" );
			return 0;
		}

		var gained = AddWater( LitresPerClick );

		TotalPumps++;
		Play( PumpSound );
		Pumped?.Invoke( gained );

		return gained;
	}

	/// <summary>
	/// Empty the tank into coins. Returns what we earned.
	/// </summary>
	public double SellAll()
	{
		if ( !CanSell )
		{
			Refuse( "No water to sell" );
			return 0;
		}

		var litres = Water;
		var earned = SaleValue;

		Water = 0;
		Coins += earned;
		TotalEarned += earned;
		TotalLitresSold += litres;
		IdleCatchUp = 0;

		Play( SellSound );
		Sold?.Invoke( earned );
		Save();

		return earned;
	}

	/// <summary>
	/// Buy the next pump in the list. Water carries over, clamped to the new tank.
	/// </summary>
	public bool BuyNextPump()
	{
		var next = NextPump;

		if ( next is null )
		{
			Refuse( "You own every pump in the desert" );
			return false;
		}

		if ( Coins < next.Cost )
		{
			Refuse( $"Need {Numbers.Short( next.Cost - Coins )} more coins" );
			return false;
		}

		Coins -= next.Cost;
		PumpLevel = next.Index;
		Water = Math.Min( Water, TankCapacity );

		Play( UpgradeSound );
		Upgraded?.Invoke( next );
		Save();

		return true;
	}

	/// <summary>Hand over coins from outside the sell loop - rewards, console commands.</summary>
	public void AddCoins( double amount )
	{
		// A NaN or infinity in here poisons the save and every number on screen with
		// no way back, so refuse it at the door rather than storing it.
		if ( amount <= 0 || double.IsNaN( amount ) || double.IsInfinity( amount ) )
			return;

		Coins += amount;
		TotalEarned += amount;
	}

	/// <summary>
	/// Buy one level of a shop upgrade. Returns false and complains if you can't afford it.
	/// </summary>
	public bool BuyUpgrade( UpgradeKind kind )
	{
		var track = UpgradeTrack.Get( kind );

		if ( IsMaxLevel( kind ) )
		{
			Refuse( $"{track.Name} is fully upgraded" );
			return false;
		}

		var cost = CostOf( kind );
		if ( Coins < cost )
		{
			Refuse( $"Need {Numbers.Short( cost - Coins )} more coins" );
			return false;
		}

		Coins -= cost;
		upgradeLevels[(int)kind]++;

		// A smaller tank than the water in it can't happen, but a bigger one is free room.
		Water = Math.Min( Water, TankCapacity );

		Play( UpgradeSound );
		Purchased?.Invoke( track );
		Save();

		return true;
	}

	public void ToggleMute()
	{
		var settings = PumpSettings.Current;

		settings.Muted = !settings.Muted;
		settings.Save();
	}

	/// <summary>Wipe the save and start over from the hand pump.</summary>
	public void ResetProgress()
	{
		Coins = 0;
		Water = 0;
		PumpLevel = 0;
		Array.Clear( upgradeLevels );
		ownedBackgrounds.Clear();
		ownedBackgrounds.Add( BackgroundStyle.DefaultId );
		SelectedBackground = BackgroundStyle.DefaultId;
		lastDailyDay = 0;
		DailyStreak = 0;
		TreeHeight = 0;
		TreeWater = 0;
		treeGrowsAt = 0;
		TotalPumps = 0;
		TotalEarned = 0;
		TotalLitresSold = 0;
		IdleCatchUp = 0;

		Save();
	}

	/// <summary>Adds water up to the tank limit and returns how much actually fit.</summary>
	double AddWater( double amount )
	{
		var before = Water;
		Water = Math.Min( TankCapacity, Water + amount );

		return Water - before;
	}

	void Refuse( string reason )
	{
		Play( DeniedSound );
		Refused?.Invoke( reason );
	}

	void Play( SoundEvent sound )
	{
		var volume = PumpSettings.Current.EffectiveSfx;

		if ( sound is null || volume <= 0f )
			return;

		var handle = Sound.Play( sound, (Sandbox.Audio.Mixer)null );

		if ( handle.IsValid() )
		{
			handle.Volume = volume;
		}
	}

	void Load()
	{
		timeSinceSave = 0;

		if ( !SaveEnabled )
			return;

		// From here on we own the file - a missing save just means a fresh run.
		loaded = true;

		var save = FileSystem.Data.ReadJsonOrDefault<PumpSave>( SaveFile, null );
		if ( save is null )
			return;

		// Stash the run as we found it. One session of history is enough to rescue a
		// player whose save gets eaten by a bug, and it costs one small write per load.
		FileSystem.Data.WriteJson( SaveFile + ".bak", save );

		Coins = Math.Max( 0, save.Coins );
		PumpLevel = Math.Clamp( save.PumpLevel, 0, PumpModel.MaxIndex );

		ownedBackgrounds.Clear();
		ownedBackgrounds.Add( BackgroundStyle.DefaultId );
		foreach ( var id in save.OwnedBackgrounds ?? new List<string>() )
		{
			ownedBackgrounds.Add( id );
		}

		SelectedBackground = OwnsBackground( save.SelectedBackground )
			? save.SelectedBackground
			: BackgroundStyle.DefaultId;

		lastDailyDay = save.LastDailyDay;
		TreeHeight = Math.Max( 0, save.TreeHeight );
		TreeWater = Math.Max( 0, save.TreeWater );
		treeGrowsAt = save.TreeGrowsAt;
		DailyStreak = save.DailyStreak;

		upgradeLevels[(int)UpgradeKind.Storage] = ClampLevel( save.StorageLevel, UpgradeKind.Storage );
		upgradeLevels[(int)UpgradeKind.Power] = ClampLevel( save.PowerLevel, UpgradeKind.Power );
		upgradeLevels[(int)UpgradeKind.Value] = ClampLevel( save.ValueLevel, UpgradeKind.Value );

		// Levels have to be in before the tank is clamped, or a full tank gets trimmed
		// to the un-upgraded size on every load.
		Water = Math.Clamp( save.Water, 0, TankCapacity );
		TotalPumps = save.TotalPumps;
		TotalEarned = save.TotalEarned;
		TotalLitresSold = save.TotalLitresSold;

		PayIdleTime( save.SavedAt );
	}

	static int ClampLevel( int level, UpgradeKind kind ) =>
		Math.Clamp( level, 0, UpgradeTrack.Get( kind ).MaxLevel );

	/// <summary>Runs the passive pump for the time we were away, capped so it can't be farmed.</summary>
	void PayIdleTime( long savedAt )
	{
		if ( savedAt <= 0 || CurrentPump.PerSecond <= 0f )
			return;

		var away = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - savedAt;
		if ( away <= 0 )
			return;

		var seconds = Math.Min( away, (long)(MaxIdleHours * 3600f) );
		IdleCatchUp = AddWater( CurrentPump.PerSecond * seconds );
	}

	void Save()
	{
		timeSinceSave = 0;

		if ( !SaveEnabled )
			return;

		// Never overwrite a real run with state we never loaded. See the field comment.
		if ( !loaded )
			return;

		FileSystem.Data.WriteJson( SaveFile, new PumpSave
		{
			Coins = Coins,
			Water = Water,
			PumpLevel = PumpLevel,
			OwnedBackgrounds = ownedBackgrounds.ToList(),
			SelectedBackground = SelectedBackground,
			LastDailyDay = lastDailyDay,
			TreeHeight = TreeHeight,
			TreeWater = TreeWater,
			TreeGrowsAt = treeGrowsAt,
			DailyStreak = DailyStreak,
			StorageLevel = LevelOf( UpgradeKind.Storage ),
			PowerLevel = LevelOf( UpgradeKind.Power ),
			ValueLevel = LevelOf( UpgradeKind.Value ),
			Muted = Muted,
			TotalPumps = TotalPumps,
			TotalEarned = TotalEarned,
			TotalLitresSold = TotalLitresSold,
			SavedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
		} );
	}
}
fpkreastudios.desertpump / Game/PumpSettings.cs
Game game
namespace DesertPump;

/// <summary>
/// Player preferences. Kept in their own file so wiping a run never costs you your
/// volume levels, and so they survive a reset.
/// </summary>
public sealed class PumpSettings
{
	public const string FileName = "settings.json";

	public float MasterVolume { get; set; } = 0.8f;
	public float MusicVolume { get; set; } = 0.55f;
	public float SfxVolume { get; set; } = 1f;

	/// <summary>Silences everything, music included. The speaker button toggles this.</summary>
	public bool Muted { get; set; }

	/// <summary>The little "+12L" numbers that fly off the pump.</summary>
	public bool ShowFloatingNumbers { get; set; } = true;

	/// <summary>Sell the tank automatically the moment it fills. Off by default.</summary>
	public bool AutoSell { get; set; }

	/// <summary>Final multiplier for one-shot effects.</summary>
	[System.Text.Json.Serialization.JsonIgnore]
	public float EffectiveSfx => Muted ? 0f : MasterVolume * SfxVolume;

	/// <summary>Final multiplier for the background track.</summary>
	[System.Text.Json.Serialization.JsonIgnore]
	public float EffectiveMusic => Muted ? 0f : MasterVolume * MusicVolume;

	static PumpSettings loaded;

	/// <summary>
	/// The live settings. Lazily read so a code hotload - which clears statics - just
	/// picks them back up off disk instead of reverting to defaults.
	/// </summary>
	public static PumpSettings Current
	{
		get
		{
			loaded ??= FileSystem.Data.ReadJsonOrDefault<PumpSettings>( FileName, null ) ?? new PumpSettings();
			return loaded;
		}
	}

	public void Save()
	{
		MasterVolume = Math.Clamp( MasterVolume, 0f, 1f );
		MusicVolume = Math.Clamp( MusicVolume, 0f, 1f );
		SfxVolume = Math.Clamp( SfxVolume, 0f, 1f );

		FileSystem.Data.WriteJson( FileName, this );
	}
}
fpkreastudios.desertpump / Game/PumpTier.cs
Game game
namespace DesertPump;

/// <summary>
/// One of the twelve tiers. A tier is nine pumps that share a setting - you finish
/// the desert before you ever see a glacier.
/// </summary>
public sealed class PumpTier
{
	/// <summary>1 to 12, the way the player sees it.</summary>
	public int Number { get; init; }

	public string Name { get; init; }
	public string Tagline { get; init; }

	/// <summary>Drives the placeholder pump art for every pump in this tier.</summary>
	public Color Tint { get; init; }

	/// <summary>The nine pump names, cheapest first.</summary>
	public string[] PumpNames { get; init; }

	public int FirstPumpIndex => (Number - 1) * PumpsPerTier;
	public int LastPumpIndex => FirstPumpIndex + PumpsPerTier - 1;

	public const int PumpsPerTier = 9;

	/// <summary>Re-read on hotload so edits apply without restarting - see PumpModel.All.</summary>
	[SkipHotload]
	public static readonly PumpTier[] All = new PumpTier[]
	{
		new()
		{
			Number = 1,
			Name = "Desert Wells",
			Tagline = "Sand, rust, and whatever you can pull up by hand.",
			Tint = new Color( 0.72f, 0.50f, 0.24f ),
			PumpNames = new[]
			{
				"Rusty Hand Pump", "Iron Well Pump", "Steam Pressure Pump",
				"Diesel Derrick", "Solar Filtration Rig", "Oasis Temple Spring",
				"Hydro Tower", "Vortex Extractor", "Ancient Geyser Monolith"
			}
		},
		new()
		{
			Number = 2,
			Name = "The Steamworks",
			Tagline = "Brass, pressure gauges, and a great deal of hissing.",
			Tint = new Color( 0.66f, 0.42f, 0.20f ),
			PumpNames = new[]
			{
				"Brass Piston Well", "Boiler Draw", "Twin Cylinder Rig",
				"Pressure Cathedral", "Governor Engine", "Flywheel Colossus",
				"Condenser Array", "Turbine Hall", "The Great Manifold"
			}
		},
		new()
		{
			Number = 3,
			Name = "Oil Country",
			Tagline = "If it worked for crude, it'll work for water.",
			Tint = new Color( 0.34f, 0.34f, 0.30f ),
			PumpNames = new[]
			{
				"Nodding Donkey", "Wildcat Rig", "Slant Drill",
				"Offshore Jacket", "Deep Casing Pump", "Fracture Head",
				"Pipeline Junction", "Refinery Tap", "The Black Column"
			}
		},
		new()
		{
			Number = 4,
			Name = "Solar Frontier",
			Tagline = "Free light, endless panels, no moving parts.",
			Tint = new Color( 0.28f, 0.46f, 0.78f ),
			PumpNames = new[]
			{
				"Panel Trickle", "Mirror Trough", "Heliostat Field",
				"Molten Salt Loop", "Photovoltaic Mesa", "Tower of Light",
				"Orbital Reflector", "Fusion Preheater", "The Sun Engine"
			}
		},
		new()
		{
			Number = 5,
			Name = "The Lost Oasis",
			Tagline = "Someone was pumping here long before you.",
			Tint = new Color( 0.86f, 0.70f, 0.26f ),
			PumpNames = new[]
			{
				"Sunken Step Well", "Palm Court Spring", "Mosaic Cistern",
				"Colonnade Fountain", "Serpent Aqueduct", "Sun Disc Basin",
				"Hall of Rains", "Temple of the Flood", "The First Spring"
			}
		},
		new()
		{
			Number = 6,
			Name = "Deep Aquifer",
			Tagline = "Down where the rock has been holding its breath.",
			Tint = new Color( 0.30f, 0.52f, 0.55f ),
			PumpNames = new[]
			{
				"Bore Shaft", "Karst Siphon", "Cavern Lake Pump",
				"Sinkhole Intake", "Mantle Straw", "Pressure Fault",
				"Abyssal Column", "Tectonic Draw", "The Sleeping Sea"
			}
		},
		new()
		{
			Number = 7,
			Name = "Storm Harvest",
			Tagline = "Stop digging. Start taking it out of the sky.",
			Tint = new Color( 0.42f, 0.46f, 0.62f ),
			PumpNames = new[]
			{
				"Rain Catcher", "Cloud Seeder", "Squall Funnel",
				"Lightning Condenser", "Monsoon Gate", "Cyclone Tap",
				"Hurricane Yoke", "Jet Stream Weir", "The Storm Crown"
			}
		},
		new()
		{
			Number = 8,
			Name = "Glacier Melt",
			Tagline = "Ten thousand years of snow, cashed in.",
			Tint = new Color( 0.58f, 0.78f, 0.86f ),
			PumpNames = new[]
			{
				"Meltwater Channel", "Crevasse Pump", "Ice Core Drill",
				"Moraine Sluice", "Calving Front Rig", "Ice Shelf Siphon",
				"Polar Reservoir", "Continental Thaw", "The Long Winter Tap"
			}
		},
		new()
		{
			Number = 9,
			Name = "Ocean Reclaim",
			Tagline = "Salt is just an engineering problem.",
			Tint = new Color( 0.20f, 0.52f, 0.62f ),
			PumpNames = new[]
			{
				"Beach Well", "Reverse Osmosis Bank", "Desalination Pier",
				"Tidal Intake", "Trench Pump", "Current Harness",
				"Abyssal Plain Array", "Continental Shelf Works", "The Ocean Engine"
			}
		},
		new()
		{
			Number = 10,
			Name = "Sky Condensers",
			Tagline = "The whole atmosphere, wrung out like a cloth.",
			Tint = new Color( 0.66f, 0.72f, 0.86f ),
			PumpNames = new[]
			{
				"Fog Net", "Dew Spire", "Stratosphere Coil",
				"Cloud Anchor", "Mesosphere Rig", "Aurora Collector",
				"Orbital Ring Tap", "Atmospheric Siphon", "The Great Condenser"
			}
		},
		new()
		{
			Number = 11,
			Name = "Void Springs",
			Tagline = "The water is coming from somewhere. Don't ask where.",
			Tint = new Color( 0.52f, 0.36f, 0.78f ),
			PumpNames = new[]
			{
				"Rift Trickle", "Phase Well", "Folded Space Pump",
				"Null Aquifer", "Entropy Siphon", "Singularity Draw",
				"Event Horizon Tap", "Vacuum Spring", "The Hollow Between"
			}
		},
		new()
		{
			Number = 12,
			Name = "Cosmic Wellspring",
			Tagline = "Comets are mostly ice, and there are rather a lot of them.",
			Tint = new Color( 0.30f, 0.80f, 0.92f ),
			PumpNames = new[]
			{
				"Comet Harpoon", "Ring System Skimmer", "Ice Moon Bore",
				"Nebula Trawler", "Rogue World Tap", "Stellar Nursery Pump",
				"Galactic Aqueduct", "Dark Flow Intake", "The Wellspring"
			}
		},
	};

	public static int Count => All.Length;

	/// <summary>Total pumps across every tier.</summary>
	public static int TotalPumps => Count * PumpsPerTier;

	/// <summary>Tier for a 0-based pump index.</summary>
	public static PumpTier ForPump( int pumpIndex ) =>
		All[Math.Clamp( pumpIndex / PumpsPerTier, 0, Count - 1 )];

	/// <summary>Tier by its 1-12 number.</summary>
	public static PumpTier Get( int number ) => All[Math.Clamp( number - 1, 0, Count - 1 )];
}
fpkreastudios.desertpump / styles/base/_navigator.scss
Game game
.navigator-body
{
	&.hidden
	{
		display: none;
	}
}
fpkreastudios.desertpump / ui/components/packageflairbar.razor.scss
Game game
PackageFlairBar
{
	position: absolute;
	top: 6px;
	left: 6px;
	z-index: 2;
	flex-direction: row;
	gap: 5px;
	pointer-events: none;

	.flair
	{
		position: relative;
		align-items: center;
		justify-content: center;
		width: 22px;
		height: 22px;
		pointer-events: all;
		border-radius: 4px;
		color: white;
		border: 1px solid rgba( white, 0.1 );
		outline: 1px solid #0006;
		opacity: 0.7;

		&:hover
		{
			opacity: 1;
		}

		icon
		{
			font-size: 15px;
		}
	}
}
fpkreastudios.desertpump / styles/base.scss
Game game
@import "base/_splitcontainer.scss";
@import "base/_navigator.scss";

button
{
	cursor: pointer;
}

IconPanel
{
	font-family: Material Icons;
}

.is-half
{
	width: 50%;
}

.is-third
{
	width: 33%;
}

.is-quarter
{
	width: 25%;
}

button.has-subtitle
{
	position: relative;
	flex-direction: column;
	justify-content: flex-start;
	align-items: flex-start;
	padding-left: 40px; // icon space

	.iconpanel
	{
		position: absolute;
		left: 5px;
		top: 0;
		bottom: 0;
		align-items: center;
	}

	.button-label
	{
		font-weight: bold;
	}

	.button-subtitle
	{
		font-size: 12px;
		opacity: 0.5;
		mix-blend-mode: lighten;
	}
}
fpkreastudios.desertpump / ui/controls/color/coloralphacontrol.cs.scss
Game game
ColorAlphaControl
{
	gap: 0.5rem;
	flex-grow: 1;
	pointer-events: all;
	background: linear-gradient( to right, black, white );
	border-radius: 4px;
	padding: 2px;
	height: 12px;
	position: relative;
	cursor: pointer;
	border: 1px solid #333;

	&:hover
	{
		border: 1px solid #08f;
	}

	&:active
	{
		border: 1px solid #fff;
	}

	.handle
	{
		top: -5px;
		bottom: -5px;
		aspect-ratio: 1;
		border-radius: 100px;
		border: 2px solid #444;
		position: absolute;
		background-color: white;
		box-shadow: 2px 2px 16px #000a;
		transform: translateX( -50% );
		pointer-events: none;
	}
}
fpkreastudios.desertpump / ui/controls/color/colorpickercontrol.cs.scss
Game game
ColorPickerControl
{
	flex-direction: column;
	flex-shrink: 0;
	gap: 0.5rem;
	margin: 1rem;
}
fpkreastudios.desertpump / ui/components/packagelist.razor.scss
Game game
.package-list
{
    flex-shrink: 1;
    flex-wrap: wrap;
    flex-grow: 1;

    h1
    {
        width: 100%;
        margin-top: 50px;
        font-size: 40px;
    }

    PackageCard
    {
        &:hover
        {
            sound-in: "ui.button.over";
        }
    }

    VirtualGrid
    {
        width: 100%;
        height: 100%;

        .cell
        {
            
        }
    }
}
fpkreastudios.desertpump / ui/treepanel.razor.scss
Game game
$gold: #f5c542;
$panel: #1b2130;

.treepanel
{
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	align-items: center;
	justify-content: center;
	pointer-events: all;

	// Same reason as the shop - without this the HUD paints through the sheet.
	z-index: 100;
	font-family: Poppins;
	color: white;

	.scrim
	{
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		background-color: rgba( 8, 10, 16, 0.65 );
		pointer-events: all;
	}

	.sheet
	{
		width: 560px;
		max-width: 92%;
		max-height: 90%;
		flex-direction: column;
		background-color: $panel;
		border-radius: 24px;
		padding: 22px;
		border-bottom: 6px solid rgba( 0, 0, 0, 0.4 );
		pointer-events: all;
	}

	.head
	{
		align-items: center;
		margin-bottom: 14px;

		.title
		{
			flex-grow: 1;
			font-size: 24px;
			font-weight: 800;
			letter-spacing: 2px;
			align-items: center;

			i
			{
				font-family: Material Icons;
				font-size: 28px;
				margin-right: 10px;
				color: $gold;
			}
		}

		.closebtn
		{
			width: 44px;
			height: 44px;
			border-radius: 12px;
			background-color: rgba( 255, 255, 255, 0.08 );
			align-items: center;
			justify-content: center;
			cursor: pointer;
			pointer-events: all;

			i { font-family: Material Icons; font-size: 24px; }

			&:hover { background-color: rgba( 255, 255, 255, 0.18 ); }
		}
	}

	.note
	{
		font-size: 13px;
		opacity: 0.7;
		background-color: rgba( 255, 255, 255, 0.05 );
		border-radius: 12px;
		padding: 10px 14px;
		margin-bottom: 12px;
	}

	.rows
	{
		flex-direction: column;
		overflow-y: scroll;
	}

	.lbrow
	{
		align-items: center;
		background-color: rgba( 255, 255, 255, 0.05 );
		border-radius: 14px;
		padding: 12px 16px;
		margin-bottom: 8px;
		border: 2px solid rgba( 0, 0, 0, 0 );

		.rank
		{
			width: 44px;
			font-size: 20px;
			font-weight: 800;
			opacity: 0.7;
		}

		.who
		{
			flex-grow: 1;
			font-size: 17px;
		}

		.score
		{
			font-size: 20px;
			font-weight: 700;
			align-items: center;

			.num { font-size: 20px; font-weight: 700; }

			.unit
			{
				font-size: 12px;
				opacity: 0.6;
				margin-left: 6px;
			}
		}

		&.gold .rank { color: $gold; opacity: 1; }
		&.silver .rank { color: #cfd6e4; opacity: 1; }
		&.bronze .rank { color: #d09a5c; opacity: 1; }

		&.me
		{
			border-color: #7fd06f;
			background-color: rgba( 127, 208, 111, 0.12 );
		}
	}

	.refresh
	{
		align-self: center;
		margin-top: 8px;
		padding: 10px 28px;
		border-radius: 12px;
		background-color: rgba( 255, 255, 255, 0.08 );
		font-size: 14px;
		font-weight: 700;
		letter-spacing: 1px;
		align-items: center;
		cursor: pointer;
		pointer-events: all;

		i
		{
			font-family: Material Icons;
			font-size: 18px;
			margin-right: 8px;
		}

		&:hover { background-color: rgba( 255, 255, 255, 0.16 ); }
	}
}
fpkreastudios.desertpump / UI/TreePanel.razor
Game game
@using System
@using Sandbox
@using Sandbox.UI
@namespace DesertPump
@inherits Panel

<root class="treepanel">

	<div class="scrim" onclick="@Close"></div>

	<div class="sheet">

		<div class="head">
			<div class="title"><i>emoji_events</i> TALLEST TREES</div>
			<div class="closebtn" onclick="@Close"><i>close</i></div>
		</div>

		@if ( Leaderboard.Fetching )
		{
			<div class="note">Loading the board...</div>
		}
		else if ( !string.IsNullOrEmpty( Leaderboard.Status ) )
		{
			<div class="note">@Leaderboard.Status</div>
		}

		<div class="rows">
			@foreach ( var row in Leaderboard.Entries )
			{
				<div class="lbrow @( row.IsMe ? "me" : "" ) @RankClass( row.Rank )">
					<div class="rank">@row.Rank</div>
					<div class="who">@row.Name</div>
					<div class="score"><span class="num">@row.Height</span><span class="unit">stages</span></div>
				</div>
			}
		</div>

		<div class="refresh" onclick="@Reload">
			<i>refresh</i> REFRESH
		</div>

	</div>

</root>

@code
{
	public Action OnClose { get; set; }

	static DesertPumpGame Game => DesertPumpGame.Current;

	protected override void OnAfterTreeRender( bool firstTime )
	{
		base.OnAfterTreeRender( firstTime );

		if ( firstTime ) Reload();
	}

	void Reload()
	{
		var name = Connection.Local?.DisplayName;
		if ( string.IsNullOrWhiteSpace( name ) ) name = "You";

		_ = Leaderboard.Refresh( Game?.TreeHeight ?? 0, name );
		StateHasChanged();
	}

	static string RankClass( int rank ) => rank switch
	{
		1 => "gold",
		2 => "silver",
		3 => "bronze",
		_ => string.Empty
	};

	void Close() => OnClose?.Invoke();

	protected override int BuildHash() => HashCode.Combine(
		Leaderboard.Entries.Count, Leaderboard.Fetching, Leaderboard.Status, Game?.TreeHeight ?? 0 );
}
fpkreastudios.desertpump / styles/form/_dropdown.scss
Game game
$primary: red !default;
$primary-alt: white !default;

$switch-padding: 6px !default;

.button.popupbutton.dropdown
{
	cursor: pointer;
	transition: all .1s ease-out;
	position: relative;

	> .dropdown_indicator
	{
		position: absolute;
		right: 8px;
	}

	&.open
	{
		border-bottom-left-radius: 1px;
		border-bottom-right-radius: 1px;
		transition: border-radius 0.2s ease-out;
	}
}

select
{
	min-height: 40px;

	> option
	{
		display: none;
	}
}
fpkreastudios.desertpump / ui/controlsheet/controlsheetgroupheader.cs.scss
Game game
ControlSheetGroupHeader
{
	font-size: 1.33rem;
	color: red;
	gap: 2px;
	align-items: center;

	&.hidden
	{
		display: none;
	}

	> .title
	{
		font-weight: 600;
	}

	&.has-toggle
	{
		cursor: pointer;
		opacity: 0.8;

		&:before
		{
			content: ' ';
			width: 22px;
			height: 22px;
			background-color: #000a;
			align-items: center;
			justify-content: center;
			text-align: center;
			border-radius: 5px;
			border: 1px solid #555;
		}

		&:hover
		{
			opacity: 1;

			&:before
			{
				border-color: #888;
			}
		}

		&.checked
		{
			> .title
			{
				color: white;
			}

			&:before
			{
				content: '✓';
				font-weight: bold;
				color: #08f;
				border-color: #08f;
			}
		}
	}
}
fpkreastudios.desertpump / ui/dropdown.cs.scss
Game game
.dropdown
{
	gap: 2px;
	flex-grow: 1;
	cursor: pointer;
	justify-content: flex-end;
	align-items: center;
	padding: 0px 12px;

	.button-right-column
	{
		flex-grow: 1;
	}
}
fpkreastudios.desertpump / ui/gamehud.razor.scss
Game game
$sand: #e8bd7a;
$sand-dark: #cf9a4c;
$panel: rgba( 18, 22, 32, 0.72 );
$gold: #f5c542;
$water: #4fb0e8;

.hud
{
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	flex-direction: column;
	font-family: Poppins;
	color: white;
	overflow: hidden;

	// s&box does not inherit pointer-events down the tree and the screen root starts
	// as "none", so every clickable element sets "all" for itself. Setting it here
	// only would leave the whole UI dead to the mouse.
	pointer-events: all;
}

// ---------------------------------------------------------------- background

// Backdrop layers never take the mouse - they sit under the whole screen and would
// otherwise swallow clicks meant for the pump.
.sky,
.sun,
.dune,
.floor
{
	pointer-events: none;
}

// The rows that hold clickable things, in case the engine wants the whole ancestor
// chain live rather than inheriting from .hud.
.topbar,
.stage,
.actions,
.collection
{
	pointer-events: all;
}

.sky
{
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	background-image: linear-gradient( to bottom, #4aa3d8 0%, #8fd0ea 38%, #f4d69c 70%, #e9b264 100% );
}

.sun
{
	position: absolute;
	top: 70px;
	right: 160px;
	width: 170px;
	height: 170px;
	border-radius: 100%;
	background-color: #ffd84d;
	box-shadow: 0px 0px 90px 30px rgba( 255, 216, 77, 0.35 );
}

// Wide flat ellipses - only the top curve shows, which reads as a dune ridge.
.dune
{
	position: absolute;
	border-radius: 50%;

	&.far
	{
		left: -34%;
		width: 105%;
		bottom: 14%;
		height: 26%;
		background-color: $sand;
	}

	&.near
	{
		left: 28%;
		width: 105%;
		bottom: 12%;
		height: 24%;
		background-color: $sand;
	}
}

// A very wide ellipse, so the horizon is a gentle curve. As a plain rectangle its
// top edge cut a hard straight line across the sky wherever no dune covered it.
.floor
{
	position: absolute;
	bottom: -34%;
	left: -25%;
	width: 150%;
	height: 70%;
	border-radius: 50%;
	background-color: $sand-dark;
}

// ---------------------------------------------------------------- top bar

.topbar
{
	height: 96px;
	align-items: center;
	padding-left: 28px;
	padding-right: 28px;
	flex-shrink: 0;
}

.stat
{
	background-color: $panel;
	border-radius: 18px;
	padding: 10px 18px;
	align-items: center;
	margin-right: 16px;
}

.coins
{
	.coin
	{
		width: 32px;
		height: 32px;
		border-radius: 100%;
		background-color: $gold;
		border: 3px solid #a8791a;
		margin-right: 10px;
	}

	.value
	{
		font-size: 30px;
		font-weight: 700;
	}
}

.tankstat
{
	.tank
	{
		width: 26px;
		height: 54px;
		border-radius: 8px;
		background-color: rgba( 255, 255, 255, 0.16 );
		border: 2px solid rgba( 255, 255, 255, 0.45 );
		margin-right: 12px;
		flex-direction: column;
		justify-content: flex-end;
		overflow: hidden;
	}

	.tank-fill
	{
		width: 100%;
		background-color: $water;
		transition: height 0.12s linear;
	}

	.tank-text
	{
		flex-direction: column;
	}

	.value
	{
		font-size: 22px;
		font-weight: 600;
	}

	.unit
	{
		font-size: 15px;
		opacity: 0.7;
	}

	.sub
	{
		font-size: 13px;
		opacity: 0.65;
	}

	&.full .tank-fill
	{
		background-color: #ff8b4a;
	}

	&.full .sub
	{
		color: #ffb37a;
		opacity: 1;
	}
}

.spacer
{
	flex-grow: 1;
}

.iconbutton
{
	width: 56px;
	height: 56px;
	border-radius: 14px;
	background-color: $panel;
	align-items: center;
	justify-content: center;
	cursor: pointer;
	pointer-events: all;

	i
	{
		font-family: Material Icons;
		font-size: 30px;
	}

	&:hover
	{
		background-color: rgba( 40, 48, 64, 0.9 );
	}

	&.muted
	{
		background-color: rgba( 200, 110, 40, 0.85 );
	}
}

// ---------------------------------------------------------------- stage

.stage
{
	flex-grow: 1;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	position: relative;
}

.pumparea
{
	width: 330px;
	height: 330px;
	align-items: center;
	justify-content: center;
	position: relative;
	cursor: pointer;
	pointer-events: all;
	transition: transform 0.07s ease-out;

	.pumpart
	{
		width: 100%;
		height: 100%;
	}

	.glow
	{
		position: absolute;
		width: 78%;
		height: 78%;
		border-radius: 100%;
		background-color: rgba( 255, 255, 255, 0.14 );
	}

	.ground
	{
		position: absolute;
		bottom: 2%;
		width: 70%;
		height: 8%;
		border-radius: 50%;
		background-color: rgba( 90, 60, 20, 0.22 );
	}

	&:hover
	{
		transform: scale( 1.03 );
	}

	&.pumping
	{
		transform: scale( 0.93 );
	}

	&.full .glow
	{
		background-color: rgba( 255, 140, 60, 0.25 );
	}
}

.nameplate
{
	flex-direction: column;
	align-items: center;
	margin-top: 4px;
	padding: 10px 30px 14px 30px;
	border-radius: 22px;
	background-color: rgba( 16, 20, 30, 0.38 );

	.tierno
	{
		font-size: 14px;
		letter-spacing: 3px;
		opacity: 1;
		text-shadow: 0px 2px 0px rgba( 0, 0, 0, 0.6 );
	}

	.name
	{
		font-size: 40px;
		font-weight: 800;
		text-shadow: 0px 3px 0px rgba( 0, 0, 0, 0.35 );
	}

	.tagline
	{
		font-size: 16px;
		opacity: 1;
		text-shadow: 0px 2px 0px rgba( 0, 0, 0, 0.55 );
	}

	.perclick
	{
		margin-top: 8px;
		font-size: 15px;
		background-color: rgba( 18, 22, 32, 0.55 );
		padding: 4px 16px;
		border-radius: 999px;
	}
}

.floater
{
	position: absolute;
	left: 50%;
	font-size: 30px;
	font-weight: 800;
	color: #d3f0ff;
	text-shadow: 0px 2px 0px rgba( 0, 0, 0, 0.45 );
	pointer-events: none;

	&.coin
	{
		color: $gold;
	}
}

.toast
{
	position: absolute;
	bottom: 13%;
	background-color: rgba( 18, 22, 32, 0.85 );
	padding: 10px 22px;
	border-radius: 999px;
	font-size: 18px;
	pointer-events: none;
}

.idlenote
{
	position: absolute;
	top: 5%;
	background-color: rgba( 30, 90, 130, 0.85 );
	padding: 8px 20px;
	border-radius: 999px;
	font-size: 16px;
	pointer-events: none;
}

// Scoped to the stage on purpose - as a bare ".hint" this rule reached into the
// settings panel and threw its sub-labels to the bottom of the screen.
.stage .hint
{
	position: absolute;
	bottom: 4%;
	font-size: 14px;
	letter-spacing: 2px;
	opacity: 1;
	padding: 5px 16px;
	border-radius: 999px;
	background-color: rgba( 16, 20, 30, 0.38 );
	pointer-events: none;
}

// ---------------------------------------------------------------- actions

.actions
{
	height: 130px;
	align-items: center;
	justify-content: center;
	flex-shrink: 0;
}

.bigbutton
{
	width: 420px;
	height: 92px;
	border-radius: 22px;
	align-items: center;
	padding: 0px 24px;
	margin: 0px 12px;
	cursor: pointer;
	pointer-events: all;
	transition: transform 0.07s ease-out;
	border-bottom: 6px solid rgba( 0, 0, 0, 0.35 );

	.btn-icon i
	{
		font-family: Material Icons;
		font-size: 40px;
		margin-right: 16px;
	}

	.btn-text
	{
		flex-direction: column;
	}

	.btn-title
	{
		font-size: 23px;
		font-weight: 800;
	}

	.btn-sub
	{
		font-size: 16px;
		opacity: 0.85;
	}

	&:hover
	{
		transform: translateY( -3px );
	}

	&:active
	{
		transform: translateY( 2px );
	}

	&.sell
	{
		background-color: #3aa7e0;
	}

	&.buy
	{
		background-color: #f0a92c;
	}

	&.maxed
	{
		background-color: #7a5cc4;
		cursor: default;
	}

	&.disabled
	{
		background-color: #5f6675;
		opacity: 0.65;
		cursor: default;

		&:hover
		{
			transform: translateY( 0px );
		}
	}
}

// ---------------------------------------------------------------- collection

.collection
{
	height: 170px;
	align-items: center;
	justify-content: center;
	padding-bottom: 14px;
	flex-shrink: 0;
}

.slot
{
	width: 118px;
	height: 142px;
	margin: 0px 6px;
	padding: 8px;
	flex-direction: column;
	align-items: center;
	justify-content: flex-end;
	border-radius: 16px;
	background-color: rgba( 18, 22, 32, 0.4 );
	border: 3px solid rgba( 0, 0, 0, 0 );

	.slotart
	{
		width: 100%;
		height: 92px;
		position: relative;
		align-items: center;
		justify-content: center;
	}

	.question
	{
		position: absolute;
		font-size: 38px;
		font-weight: 800;
		color: rgba( 255, 255, 255, 0.7 );
	}

	.slotlabel
	{
		margin-top: 4px;
		font-size: 12px;
		text-align: center;
		opacity: 0.9;
	}

	&.owned
	{
		background-color: rgba( 42, 92, 60, 0.5 );
	}

	&.next
	{
		border-color: $gold;
	}
}

// ---------------------------------------------------------------- fallback

.missing
{
	position: absolute;
	top: 40%;
	left: 50%;
	margin-left: -260px;
	width: 520px;
	flex-direction: column;
	align-items: center;
	background-color: rgba( 18, 22, 32, 0.85 );
	border-radius: 18px;
	padding: 24px;

	.missing-title
	{
		font-size: 22px;
		font-weight: 700;
	}

	.missing-body
	{
		font-size: 15px;
		opacity: 0.8;
		margin-top: 6px;
	}
}

// ---------------------------------------------------------------- scenery

// Extra layers the default scene doesn't use - each background switches them on.
.stars,
.scenery
{
	position: absolute;
	pointer-events: none;
}

.stars
{
	top: 0;
	left: 0;
	width: 100%;
	height: 60%;
	opacity: 0;
}

.scenery
{
	bottom: 0;
	left: 0;
	width: 100%;
	height: 40%;
	opacity: 0;
}

// ---- Blood Moon: night dunes under something large and red

.scene-dusk
{
	.sky { background-image: linear-gradient( to bottom, #150a24 0%, #241436 40%, #5c2536 78%, #8c3a35 100% ); }

	.sun
	{
		background-color: #d94f43;
		box-shadow: 0px 0px 120px 40px rgba( 217, 79, 67, 0.4 );
		width: 210px;
		height: 210px;
	}

	.dune.far { background-color: #45283f; }
	.dune.near { background-color: #45283f; }
	.floor { background-color: #2c1a2c; }

	// a scatter of stars, faked with a few hard-stop gradients
	.stars
	{
		opacity: 0.9;
		background-image: linear-gradient( to bottom, rgba( 255, 255, 255, 0.16 ) 0%, rgba( 255, 255, 255, 0 ) 70% );
	}

	.nameplate .name { text-shadow: 0px 3px 0px rgba( 0, 0, 0, 0.55 ); }
}

// ---- Green Oasis: you pumped enough that things started growing

.scene-oasis
{
	.sky { background-image: linear-gradient( to bottom, #2f8fbf 0%, #6fc6c2 45%, #a9d98d 80%, #6ea84e 100% ); }

	.sun
	{
		background-color: #fff2a8;
		box-shadow: 0px 0px 100px 34px rgba( 255, 242, 168, 0.45 );
	}

	.dune.far { background-color: #67ab55; }
	.dune.near { background-color: #4f8f3a; }
	.floor { background-color: #3d7330; }

	.scenery
	{
		opacity: 1;
		background-image: linear-gradient( to top, rgba( 40, 90, 40, 0.55 ) 0%, rgba( 40, 90, 40, 0 ) 55% );
	}
}

// ---- Neon Wastes: the desert, some time after everyone left

.scene-neon
{
	.sky { background-image: linear-gradient( to bottom, #12042a 0%, #2a0b45 42%, #7b2062 76%, #ff5fa2 100% ); }

	.sun
	{
		background-color: #ff7ac6;
		box-shadow: 0px 0px 140px 46px rgba( 255, 95, 162, 0.45 );
		width: 220px;
		height: 220px;
	}

	.dune.far { background-color: #35114f; }
	.dune.near { background-color: #24093a; }
	.floor { background-color: #160526; }

	.stars
	{
		opacity: 0.8;
		background-image: linear-gradient( to bottom, rgba( 255, 255, 255, 0.12 ) 0%, rgba( 255, 255, 255, 0 ) 65% );
	}

	// horizon glow standing in for a synth grid
	.scenery
	{
		opacity: 1;
		background-image: linear-gradient( to top, rgba( 255, 95, 162, 0.35 ) 0%, rgba( 255, 95, 162, 0 ) 40% );
	}

	.nameplate .name { color: #ffd9ec; }
}

// ---------------------------------------------------------------- tier bar

.tierbar
{
	width: 260px;
	height: 8px;
	border-radius: 999px;
	background-color: rgba( 0, 0, 0, 0.45 );
	border: 1px solid rgba( 255, 255, 255, 0.25 );
	margin-top: 10px;
	overflow: hidden;
}

.tierfill
{
	height: 100%;
	background-color: $gold;
	transition: width 0.2s ease-out;
}

// ---------------------------------------------------------------- gusher

.gusher
{
	position: absolute;
	width: 96px;
	height: 96px;
	margin-left: -48px;
	margin-top: -48px;
	align-items: center;
	justify-content: center;
	cursor: pointer;
	pointer-events: all;
	animation: gusher-bob 1.6s ease-in-out infinite;

	// Teardrop rather than a disc, and blue-white rather than yellow - as a glowing
	// yellow circle it was indistinguishable from the sun behind it.
	.gusher-drop
	{
		width: 46px;
		height: 58px;
		border-radius: 50% 50% 50% 50%;
		background-image: linear-gradient( to bottom, #eaf9ff, #4fb0e8 );
		border: 3px solid #ffffff;
		box-shadow: 0px 0px 34px 10px rgba( 79, 176, 232, 0.6 );
	}

	.gusher-ring
	{
		position: absolute;
		width: 92px;
		height: 92px;
		border-radius: 100%;
		border: 3px solid rgba( 160, 225, 255, 0.7 );
		animation: gusher-pulse 1.6s ease-out infinite;
	}

	&:hover .gusher-drop { transform: scale( 1.12 ); }
}

@keyframes gusher-bob
{
	0% { transform: translateY( 0px ); }
	50% { transform: translateY( -12px ); }
	100% { transform: translateY( 0px ); }
}

@keyframes gusher-pulse
{
	0% { transform: scale( 0.7 ); opacity: 0.9; }
	100% { transform: scale( 1.25 ); opacity: 0; }
}

// ---------------------------------------------------------------- daily

.daily
{
	position: absolute;
	top: 110px;
	left: 28px;
	align-items: center;
	background-color: rgba( 24, 30, 44, 0.9 );
	border: 3px solid $gold;
	border-radius: 18px;
	padding: 12px 20px 12px 14px;
	cursor: pointer;
	pointer-events: all;
	animation: daily-glow 2.2s ease-in-out infinite;

	.daily-icon
	{
		margin-right: 12px;

		i
		{
			font-family: Material Icons;
			font-size: 34px;
			color: $gold;
		}
	}

	.daily-text { flex-direction: column; }
	.daily-title { font-size: 16px; font-weight: 800; letter-spacing: 1px; }
	.daily-sub { font-size: 13px; opacity: 0.8; }

	&:hover { transform: scale( 1.03 ); }
}

@keyframes daily-glow
{
	0% { box-shadow: 0px 0px 0px 0px rgba( 245, 197, 66, 0.5 ); }
	50% { box-shadow: 0px 0px 26px 4px rgba( 245, 197, 66, 0.45 ); }
	100% { box-shadow: 0px 0px 0px 0px rgba( 245, 197, 66, 0.5 ); }
}

// ---------------------------------------------------------------- the tree

.treearea
{
	position: absolute;
	left: 34px;
	bottom: 14%;
	align-items: flex-end;
	pointer-events: all;
}

.tree
{
	width: 96px;
	height: 150px;
	position: relative;
	cursor: pointer;
	pointer-events: all;
	transition: all 0.25s ease-out;

	.mound
	{
		position: absolute;
		bottom: 0;
		left: 12%;
		width: 76%;
		height: 12%;
		border-radius: 50%;
		background-color: rgba( 90, 60, 20, 0.35 );
	}

	.trunk
	{
		position: absolute;
		bottom: 8%;
		left: 44%;
		width: 12%;
		height: 26%;
		border-radius: 4px;
		background-color: #7a5230;
		transition: all 0.25s ease-out;
	}

	.canopy
	{
		position: absolute;
		border-radius: 100%;
		background-color: #4f9a45;
		opacity: 0;
		transition: all 0.25s ease-out;

		&.c1 { left: 26%; bottom: 30%; width: 48%; height: 26%; }
		&.c2 { left: 14%; bottom: 42%; width: 40%; height: 24%; background-color: #5cae50; }
		&.c3 { left: 44%; bottom: 44%; width: 42%; height: 26%; background-color: #438a3b; }
	}

	// A sapling at zero, a full crown by stage 6. Stage 0 still needs to read as a
	// plant - as a bare stub it was invisible against the sand.
	&.stage-0
	{
		.trunk { height: 24%; }
		.canopy.c1 { opacity: 1; transform: scale( 0.34 ); }
	}
	&.stage-1 { .trunk { height: 20%; } .canopy.c1 { opacity: 1; transform: scale( 0.6 ); } }
	&.stage-2 { .trunk { height: 26%; } .canopy.c1 { opacity: 1; transform: scale( 0.8 ); } }
	&.stage-3 { .trunk { height: 32%; } .canopy { opacity: 1; transform: scale( 0.85 ); } }
	&.stage-4 { .trunk { height: 38%; } .canopy { opacity: 1; transform: scale( 1 ); } }
	&.stage-5 { .trunk { height: 44%; } .canopy { opacity: 1; transform: scale( 1.12 ); } }
	&.stage-6 { .trunk { height: 50%; } .canopy { opacity: 1; transform: scale( 1.25 ); } }

	&:hover { transform: scale( 1.05 ); }

	&.growing .canopy { animation: tree-sway 3s ease-in-out infinite; }
}

@keyframes tree-sway
{
	0% { transform: translateX( -3px ); }
	50% { transform: translateX( 3px ); }
	100% { transform: translateX( -3px ); }
}

.treeinfo
{
	flex-direction: column;
	margin-left: 10px;
	margin-bottom: 8px;
	background-color: rgba( 18, 22, 32, 0.7 );
	border-radius: 14px;
	padding: 10px 12px;

	.treeheight
	{
		font-size: 15px;
		font-weight: 700;
		align-items: center;

		i
		{
			font-family: Material Icons;
			font-size: 18px;
			margin-right: 6px;
			color: #7fd06f;
		}
	}

	.treebar
	{
		width: 130px;
		height: 7px;
		border-radius: 999px;
		background-color: rgba( 0, 0, 0, 0.35 );
		margin-top: 6px;
		overflow: hidden;
	}

	.treefill
	{
		height: 100%;
		background-color: #7fd06f;
		transition: width 0.2s ease-out;
	}

	.treestatus
	{
		font-size: 12px;
		opacity: 0.75;
		margin-top: 4px;

		&.growing { color: #7fd06f; opacity: 1; }
	}

	.treebtn
	{
		margin-top: 8px;
		padding: 6px 12px;
		border-radius: 10px;
		background-color: #4f9a45;
		font-size: 13px;
		font-weight: 700;
		align-items: center;
		justify-content: center;
		cursor: pointer;
		pointer-events: all;

		i
		{
			font-family: Material Icons;
			font-size: 16px;
			margin-right: 6px;
		}

		&:hover { background-color: #5cae50; }

		&.board { background-color: #3aa7e0; }
		&.board:hover { background-color: #4bb8f0; }

		&.disabled
		{
			background-color: #5f6675;
			opacity: 0.6;
			cursor: default;
		}
	}
}
fpkreastudios.desertpump / UI/GameHud.razor
Game game
@using System
@using System.Globalization
@using Sandbox
@using Sandbox.UI
@namespace DesertPump
@inherits PanelComponent

<div class="hud @SceneClass">

	<div class="sky"></div>
	<div class="sun"></div>
	<div class="stars"></div>
	<div class="floor"></div>
	<div class="dune far"></div>
	<div class="dune near"></div>
	<div class="scenery"></div>

	@if ( State is null )
	{
		<div class="missing">
			<div class="missing-title">No Desert Pump Game in the scene</div>
			<div class="missing-body">Add a "Desert Pump Game" component to any GameObject.</div>
		</div>
	}
	else
	{
		<div class="topbar">

			<div class="stat coins">
				<div class="coin"></div>
				<div class="value">@Numbers.Short( State.Coins )</div>
			</div>

			<div class="stat tankstat @( State.TankFull ? "full" : "" )">
				<div class="tank">
					<div class="tank-fill" style="@FillStyle()"></div>
				</div>
				<div class="tank-text">
					<div class="value">@Numbers.Short( State.Water )<span class="unit"> / @Numbers.Short( State.TankCapacity )L</span></div>
					<div class="sub">@RateText()</div>
				</div>
			</div>

			<div class="spacer"></div>

			<div class="iconbutton shopbtn @( AnyUpgradeAffordable ? "highlight" : "" )" onclick="@OpenShop">
				<i>storefront</i>
			</div>

			<div class="iconbutton @( State.Muted ? "muted" : "" )" onclick="@ToggleMute">
				<i>@( State.Muted ? "volume_off" : "volume_up" )</i>
			</div>

			<div class="iconbutton" onclick="@OpenSettings">
				<i>settings</i>
			</div>

		</div>

		<div class="stage">

			<div class="pumparea @PumpAreaClass()" onclick="@DoPump">
				<div class="glow"></div>
				<PumpArt TierIndex="@State.PumpLevel" Artwork="@ArtworkFor( State.PumpLevel )"></PumpArt>
				<div class="ground"></div>
			</div>

			<div class="nameplate">
				<div class="tierno">@State.CurrentPump.Position &middot; @State.Tier.Name.ToUpper()</div>
				<div class="name">@State.CurrentPump.Name</div>
				<div class="tagline">@State.Tier.Tagline</div>
				<div class="tierbar">
					<div class="tierfill" style="@TierFillStyle()"></div>
				</div>
				<div class="perclick">+@Numbers.Short( State.LitresPerClick )L per pump - @Numbers.Short( State.PricePerLitre ) @Numbers.Plural( State.PricePerLitre, "coin", "coins" ) per litre</div>
			</div>

			@foreach ( var f in floaters )
			{
				<div class="floater @( f.Coin ? "coin" : "" )" style="@FloaterStyle( f )">@f.Text</div>
			}

			@if ( ShowToast )
			{
				<div class="toast">@toast</div>
			}

			@if ( ShowIdleNote )
			{
				<div class="idlenote">The pump made @Numbers.Short( idleNotice )L while you were away</div>
			}

			@if ( State.GusherActive )
			{
				<div class="gusher" style="@GusherStyle()" onclick="@ClaimGusher">
					<div class="gusher-drop"></div>
					<div class="gusher-ring"></div>
				</div>
			}

			<div class="treearea">

				<div class="tree stage-@State.TreeArtStage @( State.TreeIsGrowing ? "growing" : "" )"
					 onclick="@DoWaterTree">
					<div class="trunk"></div>
					<div class="canopy c1"></div>
					<div class="canopy c2"></div>
					<div class="canopy c3"></div>
					<div class="mound"></div>
				</div>

				<div class="treeinfo">
					<div class="treeheight"><i>park</i> @State.TreeHeight @( State.TreeHeight == 1 ? "stage" : "stages" )</div>

					@if ( State.TreeIsGrowing )
					{
						<div class="treestatus growing">Growing - @State.TreeTimeLeft</div>
					}
					else
					{
						<div class="treebar">
							<div class="treefill" style="@TreeFillStyle()"></div>
						</div>
						<div class="treestatus">@Numbers.Short( State.TreeWater )/@Numbers.Short( State.TreeWaterNeeded )L</div>
					}

					<div class="treebtn @( State.CanWaterTree ? "" : "disabled" )" onclick="@DoWaterTree">
						<i>water_drop</i> WATER
					</div>

					<div class="treebtn board" onclick="@OpenTree">
						<i>leaderboard</i> BOARD
					</div>
				</div>

			</div>

			<div class="hint">Click the pump or press SPACE</div>

		</div>

		@if ( State.CanClaimDaily )
		{
			<div class="daily" onclick="@ClaimDaily">
				<div class="daily-icon"><i>redeem</i></div>
				<div class="daily-text">
					<div class="daily-title">DAILY BONUS READY</div>
					<div class="daily-sub">@Numbers.Short( State.DailyReward ) coins &middot; day @State.ProjectedDailyStreak streak</div>
				</div>
			</div>
		}

		<div class="actions">

			<div class="bigbutton sell @( State.CanSell ? "" : "disabled" )" onclick="@DoSell">
				<div class="btn-icon"><i>local_drink</i></div>
				<div class="btn-text">
					<div class="btn-title">SELL WATER</div>
					<div class="btn-sub">+@Numbers.Short( State.SaleValue ) coins</div>
				</div>
			</div>

			@if ( State.IsMaxed )
			{
				<div class="bigbutton buy maxed">
					<div class="btn-icon"><i>emoji_events</i></div>
					<div class="btn-text">
						<div class="btn-title">ALL PUMPS OWNED</div>
						<div class="btn-sub">the desert is yours</div>
					</div>
				</div>
			}
			else
			{
				<div class="bigbutton buy @( State.CanUpgrade ? "" : "disabled" )" onclick="@DoBuy">
					<div class="btn-icon"><i>upgrade</i></div>
					<div class="btn-text">
						<div class="btn-title">BUY @State.NextPump.Name.ToUpper()</div>
						<div class="btn-sub">@Numbers.Short( State.NextPump.Cost ) coins</div>
					</div>
				</div>
			}

		</div>

		<div class="collection">
			@foreach ( var pump in VisiblePumps )
			{
				<div class="slot @SlotClass( pump )">
					<div class="slotart">
						<PumpArt TierIndex="@pump.Index"
								 Silhouette="@( pump.Index > State.PumpLevel )"
								 Artwork="@ArtworkFor( pump.Index )"></PumpArt>
						@if ( pump.Index > State.PumpLevel )
						{
							<div class="question">?</div>
						}
					</div>
					<div class="slotlabel">@SlotLabel( pump )</div>
				</div>
			}
		</div>

		@if ( showShop )
		{
			<ShopPanel OnClose="@CloseOverlays"></ShopPanel>
		}

		@if ( showSettings )
		{
			<SettingsPanel OnClose="@CloseOverlays"></SettingsPanel>
		}

		@if ( showTree )
		{
			<TreePanel OnClose="@CloseOverlays"></TreePanel>
		}
	}

</div>

@code
{
	/// <summary>Optional - left empty, the HUD finds the game itself.</summary>
	[Property] public DesertPumpGame Target { get; set; }

	/// <summary>
	/// The real pump artwork, one texture per tier in the same order as
	/// <see cref="PumpTier.All"/>. Any slot left empty falls back to placeholder art.
	/// </summary>
	[Property] public List<Texture> PumpArtwork { get; set; } = new();

	DesertPumpGame State => Target.IsValid() ? Target : DesertPumpGame.Current;

	const float FloaterLife = 1.1f;
	const float ToastLife = 2.2f;
	const float IdleNoticeLife = 8f;
	const float PumpAnimLife = 0.16f;

	class Floater
	{
		public string Text;
		public float X;
		public float Drift;
		public bool Coin;
		public RealTimeSince Age;
	}

	readonly List<Floater> floaters = new();

	string toast;
	RealTimeSince toastAge;

	double idleNotice;
	RealTimeSince idleAge;

	RealTimeSince timeSincePump;

	/// <summary>Bumped every frame so floaters get rebuilt while they're moving.</summary>
	int animTick;

	bool hooked;

	bool showShop;
	bool showSettings;
	bool showTree;

	/// <summary>Puts a nudge on the shop button when something in there is affordable.</summary>
	bool AnyUpgradeAffordable =>
		State is not null && UpgradeTrack.All.Any( x => State.CanBuy( x.Kind ) );

	/// <summary>
	/// The strip along the bottom shows the tier you're in, not all 108 pumps - nine
	/// slots you can actually finish is a far better carrot than a hundred you can't.
	/// </summary>
	IEnumerable<PumpModel> VisiblePumps
	{
		get
		{
			if ( State is null ) return Enumerable.Empty<PumpModel>();

			var first = State.Tier.FirstPumpIndex;
			return PumpModel.All.Skip( first ).Take( PumpTier.PumpsPerTier );
		}
	}

	string SceneClass => $"scene-{State?.SelectedBackground ?? BackgroundStyle.DefaultId}";

	void OpenShop()
	{
		showShop = !showShop;
		showSettings = false;
	}

	void OpenSettings()
	{
		showSettings = !showSettings;
		showShop = false;
	}

	void OpenTree()
	{
		showTree = !showTree;
		showShop = false;
		showSettings = false;
	}

	/// <summary>Open a panel from the console, so UI can be inspected without clicking.</summary>
	public void ShowPanel( string which )
	{
		showShop = which is "shop" or "bg";
		showSettings = which == "settings";
		showTree = which == "tree";

		StateHasChanged();
	}

	void CloseOverlays()
	{
		showShop = false;
		showSettings = false;
		showTree = false;
	}

	string TreeFillStyle() => $"width: {N( State.TreeFraction * 100f )}%;";

	void DoWaterTree()
	{
		var poured = State?.WaterTree() ?? 0;
		if ( poured <= 0 ) return;

		Spawn( $"-{Numbers.Short( poured )}L", false );

		toast = State.TreeIsGrowing
			? $"Tree is growing - back in {State.TreeTimeLeft}"
			: $"{Numbers.Short( State.TreeWaterRemaining )}L to go";
		toastAge = 0;
	}

	void OnTreeGrew( int height )
	{
		toast = $"Your tree grew to {height} {( height == 1 ? "stage" : "stages" )}";
		toastAge = 0;
	}

	bool ShowToast => !string.IsNullOrEmpty( toast ) && toastAge < ToastLife;
	bool ShowIdleNote => idleNotice > 0 && idleAge < IdleNoticeLife;

	/// <summary>
	/// The panel a PanelComponent builds for itself defaults to pointer-events: none,
	/// and no stylesheet rule can reach it - it has no class and a generated type name.
	/// Left alone it makes every button underneath it dead to the mouse.
	/// </summary>
	protected override void OnTreeFirstBuilt()
	{
		base.OnTreeFirstBuilt();

		Panel.Style.PointerEvents = PointerEvents.All;
	}

	protected override void OnStart()
	{
		Hook();

		if ( State is not null && State.IdleCatchUp > 0 )
		{
			idleNotice = State.IdleCatchUp;
			idleAge = 0;
		}
	}

	protected override void OnDestroy()
	{
		Unhook();
	}

	protected override void OnUpdate()
	{
		// The game component can come alive after us, so keep trying until it's there.
		if ( !hooked ) Hook();

		floaters.RemoveAll( x => x.Age > FloaterLife );

		if ( floaters.Count > 0 || timeSincePump < PumpAnimLife )
		{
			animTick = (int)(RealTime.Now * 30f);
		}
	}

	protected override int BuildHash()
	{
		var state = State;
		if ( state is null ) return 0;

		return HashCode.Combine(
			(long)state.Coins,
			(long)state.Water,
			state.PumpLevel,
			state.Muted,
			floaters.Count,
			animTick,
			ShowToast,
			HashCode.Combine( ShowIdleNote, showShop, showSettings, AnyUpgradeAffordable,
				state.GusherActive, state.CanClaimDaily, state.SelectedBackground,
				HashCode.Combine( showTree, state.TreeHeight, (int)state.TreeWater, (int)state.TreeSecondsLeft ) ) );
	}

	void Hook()
	{
		var state = State;
		if ( state is null || hooked ) return;

		state.Pumped += OnPumped;
		state.Sold += OnSold;
		state.Upgraded += OnUpgraded;
		state.Refused += OnRefused;
		state.Purchased += OnPurchased;
		state.TreeGrew += OnTreeGrew;

		hooked = true;
	}

	void Unhook()
	{
		var state = State;
		if ( state is null || !hooked ) return;

		state.Pumped -= OnPumped;
		state.Sold -= OnSold;
		state.Upgraded -= OnUpgraded;
		state.Refused -= OnRefused;
		state.Purchased -= OnPurchased;
		state.TreeGrew -= OnTreeGrew;

		hooked = false;
	}

	void DoPump() => State?.Pump();
	void DoSell() => State?.SellAll();
	void DoBuy() => State?.BuyNextPump();
	void ToggleMute() => State?.ToggleMute();

	void OnPumped( double litres )
	{
		timeSincePump = 0;
		Spawn( $"+{Numbers.Short( litres )}L", false );
	}

	void OnSold( double coins )
	{
		Spawn( $"+{Numbers.Short( coins )}", true );
		toast = null;
	}

	void OnUpgraded( PumpModel pump )
	{
		toast = $"{pump.Name} installed";
		toastAge = 0;
		idleNotice = 0;
	}

	void OnRefused( string reason )
	{
		toast = reason;
		toastAge = 0;
	}

	void OnPurchased( UpgradeTrack track )
	{
		toast = $"{track.Name} upgraded";
		toastAge = 0;
	}

	void Spawn( string text, bool coin )
	{
		if ( !PumpSettings.Current.ShowFloatingNumbers )
			return;

		// Keep the list short - a fast clicker can outrun the fade otherwise.
		if ( floaters.Count > 12 ) floaters.RemoveAt( 0 );

		floaters.Add( new Floater
		{
			Text = text,
			X = Rand( -70f, 70f ),
			Drift = Rand( -18f, 18f ),
			Coin = coin,
			Age = 0
		} );
	}

	Texture ArtworkFor( int index )
	{
		if ( PumpArtwork is null ) return null;
		if ( index < 0 || index >= PumpArtwork.Count ) return null;

		return PumpArtwork[index];
	}

	string RateText()
	{
		var perSecond = State.CurrentPump.PerSecond;

		return perSecond > 0
			? $"+{Numbers.Rate( perSecond )}L/s while idle"
			: "hand powered";
	}

	string PumpAreaClass()
	{
		var classes = timeSincePump < PumpAnimLife ? "pumping" : "";
		if ( State.TankFull ) classes += " full";

		return classes;
	}

	string SlotClass( PumpModel tier )
	{
		if ( tier.Index <= State.PumpLevel ) return "owned";

		return tier.Index == State.PumpLevel + 1 ? "locked next" : "locked";
	}

	string SlotLabel( PumpModel tier )
	{
		return tier.Index <= State.PumpLevel
			? tier.Name
			: $"{Numbers.Short( tier.Cost )} {Numbers.Plural( tier.Cost, "coin", "coins" )}";
	}

	string FillStyle() => $"height: {N( State.TankFraction * 100f )}%;";

	string TierFillStyle() => $"width: {N( State.TierProgress * 100f )}%;";

	string GusherStyle() =>
		$"left: {N( State.GusherX * 100f )}%; top: {N( State.GusherY * 100f )}%;";

	void ClaimGusher()
	{
		var won = State?.ClaimGusher() ?? 0;
		if ( won <= 0 ) return;

		Spawn( $"+{Numbers.Short( won )}", true );
		toast = "Gusher! Free coins";
		toastAge = 0;
	}

	void ClaimDaily()
	{
		var won = State?.ClaimDaily() ?? 0;
		if ( won <= 0 ) return;

		Spawn( $"+{Numbers.Short( won )}", true );
		toast = $"Day {State.DailyStreak} streak - come back tomorrow";
		toastAge = 0;
	}

	string FloaterStyle( Floater f )
	{
		var t = Math.Clamp( (float)f.Age / FloaterLife, 0f, 1f );
		var rise = 42f + t * 26f;
		var fade = 1f - t * t;

		return $"margin-left: {N( f.X + f.Drift * t )}px; bottom: {N( rise )}%; opacity: {N( fade )};";
	}

	/// <summary>Style strings need dots for decimals, whatever the machine's locale says.</summary>
	static string N( float value ) => value.ToString( "0.##", CultureInfo.InvariantCulture );

	static float Rand( float min, float max ) => min + (float)System.Random.Shared.NextDouble() * (max - min);
}
fpkreastudios.desertpump / UI/PumpArt.razor
Game game
@using System
@using Sandbox
@using Sandbox.UI
@namespace DesertPump
@inherits Panel

<root class="pumpart family-@Family @( Silhouette ? "silhouette" : "" )">

	@if ( Artwork is not null )
	{
		<Image Texture="@Artwork" class="photo"></Image>
	}
	else
	{
		<div class="mound" style="background-color: @Shade( 0.55f );"></div>
		<div class="body" style="background-color: @Shade( 1f );"></div>
		<div class="head" style="background-color: @Shade( 1.25f );"></div>
		<div class="arm" style="background-color: @Shade( 1.25f );"></div>
		<div class="spout" style="background-color: @Shade( 0.8f );"></div>
		<div class="stream" style="background-color: @Water( 1.15f );"></div>
		<div class="pool" style="background-color: @Water( 0.75f );"></div>
	}

</root>

@code
{
	/// <summary>Index into <see cref="PumpTier.All"/>.</summary>
	public int TierIndex { get; set; }

	/// <summary>Draw it as a black cut-out, the way locked pumps show in the collection.</summary>
	public bool Silhouette { get; set; }

	/// <summary>Real artwork. When set it replaces the whole placeholder build-up.</summary>
	public Texture Artwork { get; set; }

	PumpModel Pump => PumpModel.Get( TierIndex );

	/// <summary>Hand pump, rig, or tower - keeps the nine tiers visually distinct.</summary>
	int Family => Pump.Rank <= 3 ? 0 : Pump.Rank <= 6 ? 1 : 2;

	/// <summary>Tier colour, brightened or darkened, as a css hex string.</summary>
	string Shade( float multiplier )
	{
		if ( Silhouette ) return "#0b0d12";

		var c = Pump.Tint;

		return new Color(
			Math.Clamp( c.r * multiplier, 0f, 1f ),
			Math.Clamp( c.g * multiplier, 0f, 1f ),
			Math.Clamp( c.b * multiplier, 0f, 1f ) ).Hex;
	}

	string Water( float multiplier )
	{
		if ( Silhouette ) return "#0b0d12";

		return new Color(
			Math.Clamp( 0.35f * multiplier, 0f, 1f ),
			Math.Clamp( 0.72f * multiplier, 0f, 1f ),
			Math.Clamp( 0.95f * multiplier, 0f, 1f ) ).Hex;
	}

	protected override int BuildHash() => HashCode.Combine( TierIndex, Silhouette, Artwork );
}
fpkreastudios.desertpump / Game/UpgradeTrack.cs
Game game
namespace DesertPump;

public enum UpgradeKind
{
	/// <summary>Bigger tank, so you sell every few minutes instead of every few seconds.</summary>
	Storage,

	/// <summary>More litres per pump.</summary>
	Power,

	/// <summary>More coins per litre.</summary>
	Value,
}

/// <summary>
/// A shop upgrade you can buy over and over, each level costing more than the last.
/// </summary>
/// <remarks>
/// Cost growth deliberately outruns effect growth by a wide margin. A track where the
/// two are close never stops paying for itself, and the economy runs away - the first
/// pass at this balance finished the whole game in two minutes for exactly that reason.
/// </remarks>
public sealed class UpgradeTrack
{
	public UpgradeKind Kind { get; init; }
	public string Name { get; init; }
	public string Description { get; init; }

	/// <summary>Material icon name.</summary>
	public string Icon { get; init; }

	public double BaseCost { get; init; }
	public double CostGrowth { get; init; }

	/// <summary>What one level multiplies its stat by.</summary>
	public float EffectGrowth { get; init; }

	public int MaxLevel { get; init; }

	public double CostAt( int level ) => BaseCost * Math.Pow( CostGrowth, level );

	public float MultiplierAt( int level ) => MathF.Pow( EffectGrowth, level );

	/// <summary>"+12%" - what the next level buys you.</summary>
	public string PerLevelText => $"+{(EffectGrowth - 1f) * 100f:0}%";

	/// <summary>Re-read on hotload so balance edits apply without restarting - see PumpTier.All.</summary>
	[SkipHotload]
	public static readonly UpgradeTrack[] All = new UpgradeTrack[]
	{
		new()
		{
			Kind = UpgradeKind.Storage,
			Name = "Water Tank",
			Description = "Holds more water, so you can walk away between sales.",
			Icon = "water_drop",
			BaseCost = 150,
			CostGrowth = 1.55,
			EffectGrowth = 1.12f,
			MaxLevel = 200
		},
		new()
		{
			Kind = UpgradeKind.Power,
			Name = "Pump Power",
			Description = "Every pump pulls up more water.",
			Icon = "fitness_center",
			BaseCost = 250,
			CostGrowth = 1.60,
			EffectGrowth = 1.06f,
			MaxLevel = 200
		},
		new()
		{
			Kind = UpgradeKind.Value,
			Name = "Market Contacts",
			Description = "People pay better for the same water.",
			Icon = "handshake",
			BaseCost = 400,
			CostGrowth = 1.68,
			EffectGrowth = 1.05f,
			MaxLevel = 200
		},
	};

	public static UpgradeTrack Get( UpgradeKind kind ) => All[(int)kind];
}
fpkreastudios.desertpump / UI/SettingsPanel.razor
Game game
@using System
@using Sandbox
@using Sandbox.UI
@namespace DesertPump
@inherits Panel

<root class="settingspanel">

	<div class="scrim" onclick="@Close"></div>

	<div class="sheet">

		<div class="head">
			<div class="title"><i>settings</i> SETTINGS</div>
			<div class="closebtn" onclick="@Close"><i>close</i></div>
		</div>

		<div class="group">
			<div class="grouptitle">AUDIO</div>

			<div class="row">
				<div class="label">Master volume</div>
				<SliderControl class="simple" Value:bind=@Master Min="@(0f)" Max="@(1f)" Step="@(0.05f)"></SliderControl>
				<div class="value">@Percent( Master )</div>
			</div>

			<div class="row">
				<div class="label">Music</div>
				<SliderControl class="simple" Value:bind=@Music Min="@(0f)" Max="@(1f)" Step="@(0.05f)"></SliderControl>
				<div class="value">@Percent( Music )</div>
			</div>

			<div class="row">
				<div class="label">Sound effects</div>
				<SliderControl class="simple" Value:bind=@Sfx Min="@(0f)" Max="@(1f)" Step="@(0.05f)"></SliderControl>
				<div class="value">@Percent( Sfx )</div>
			</div>

			<div class="row toggle @( Settings.Muted ? "on" : "" )" onclick="@ToggleMute">
				<div class="label">Mute everything</div>
				<div class="switch"><div class="knob"></div></div>
			</div>
		</div>

		<div class="group">
			<div class="grouptitle">GAME</div>

			<div class="row toggle @( Settings.AutoSell ? "on" : "" )" onclick="@ToggleAutoSell">
				<div class="label">
					Auto-sell when the tank fills
					<span class="sub">Hands off, but you'll miss the coin sound</span>
				</div>
				<div class="switch"><div class="knob"></div></div>
			</div>

			<div class="row toggle @( Settings.ShowFloatingNumbers ? "on" : "" )" onclick="@ToggleFloaters">
				<div class="label">
					Floating numbers
					<span class="sub">The little +12L that fly off the pump</span>
				</div>
				<div class="switch"><div class="knob"></div></div>
			</div>
		</div>

		<div class="group danger">
			<div class="grouptitle">DANGER</div>

			<div class="row">
				<div class="label">
					Reset progress
					<span class="sub">Every pump, upgrade and coin. Settings are kept.</span>
				</div>

				@if ( confirmingReset )
				{
					<div class="resetrow">
						<div class="resetbtn confirm" onclick="@DoReset">Yes, wipe it</div>
						<div class="resetbtn" onclick="@( () => confirmingReset = false )">Cancel</div>
					</div>
				}
				else
				{
					<div class="resetbtn" onclick="@( () => confirmingReset = true )">Reset</div>
				}
			</div>
		</div>

		<div class="done" onclick="@Close">DONE</div>

	</div>

</root>

@code
{
	/// <summary>Raised when the player dismisses the settings.</summary>
	public Action OnClose { get; set; }

	static PumpSettings Settings => PumpSettings.Current;

	bool confirmingReset;

	// Slider bindings. Each setter persists, so a change survives even if the game
	// is killed rather than closed.
	float Master
	{
		get => Settings.MasterVolume;
		set => Apply( () => Settings.MasterVolume = value, Settings.MasterVolume == value );
	}

	float Music
	{
		get => Settings.MusicVolume;
		set => Apply( () => Settings.MusicVolume = value, Settings.MusicVolume == value );
	}

	float Sfx
	{
		get => Settings.SfxVolume;
		set => Apply( () => Settings.SfxVolume = value, Settings.SfxVolume == value );
	}

	void Apply( Action change, bool unchanged )
	{
		if ( unchanged ) return;

		change();
		Settings.Save();
		StateHasChanged();
	}

	void ToggleMute()
	{
		Settings.Muted = !Settings.Muted;
		Settings.Save();
		StateHasChanged();
	}

	void ToggleAutoSell()
	{
		Settings.AutoSell = !Settings.AutoSell;
		Settings.Save();
		StateHasChanged();
	}

	void ToggleFloaters()
	{
		Settings.ShowFloatingNumbers = !Settings.ShowFloatingNumbers;
		Settings.Save();
		StateHasChanged();
	}

	void DoReset()
	{
		DesertPumpGame.Current?.ResetProgress();
		confirmingReset = false;
		StateHasChanged();
	}

	void Close()
	{
		confirmingReset = false;
		OnClose?.Invoke();
	}

	static string Percent( float value ) => $"{value * 100f:0}%";

	protected override int BuildHash() => HashCode.Combine(
		Settings.MasterVolume, Settings.MusicVolume, Settings.SfxVolume,
		Settings.Muted, Settings.AutoSell, Settings.ShowFloatingNumbers, confirmingReset );
}
Debug: View Raw JSON Response
{
    "TotalCount": 55,
    "Files": [
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": ".obj/__compiler_extra.cs",
            "FileName": "__compiler_extra.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"Desert Pump\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"desertpump\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"fpkreastudios\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"fpkreastudios.desertpump\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"28\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-08-01T14:58:58.6069356Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.194.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.194.0\")]"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "Game/DesertPumpGame.cs",
            "FileName": "DesertPumpGame.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "namespace DesertPump;\r\n\r\n/// <summary>\r\n/// The whole game. Holds the money, the water and the current pump, and is the only\r\n/// thing allowed to change them - the HUD just calls into here and renders the result.\r\n/// </summary>\r\n[Title( \"Desert Pump Game\" )]\r\n[Category( \"Desert Pump\" )]\r\n[Icon( \"water_drop\" )]\r\npublic sealed partial class DesertPumpGame : Component\r\n{\r\n\t/// <summary>The active game, so the HUD doesn't need a wired-up reference.</summary>\r\n\tpublic static DesertPumpGame Current { get; private set; }\r\n\r\n\t[Property, Group( \"Save\" )]\r\n\tpublic bool SaveEnabled { get; set; } = true;\r\n\r\n\t[Property, Group( \"Save\" )]\r\n\tpublic string SaveFile { get; set; } = \"desertpump.json\";\r\n\r\n\t/// <summary>Seconds between autosaves. Progress also saves on shutdown.</summary>\r\n\t[Property, Group( \"Save\" ), Range( 2f, 60f )]\r\n\tpublic float SaveInterval { get; set; } = 5f;\r\n\r\n\t/// <summary>Longest stretch of time away that still pays out idle water.</summary>\r\n\t[Property, Group( \"Save\" ), Range( 0f, 24f )]\r\n\tpublic float MaxIdleHours { get; set; } = 4f;\r\n\r\n\t[Property, Group( \"Audio\" )] public SoundEvent PumpSound { get; set; }\r\n\t[Property, Group( \"Audio\" )] public SoundEvent SellSound { get; set; }\r\n\t[Property, Group( \"Audio\" )] public SoundEvent UpgradeSound { get; set; }\r\n\t[Property, Group( \"Audio\" )] public SoundEvent DeniedSound { get; set; }\r\n\r\n\tpublic double Coins { get; private set; }\r\n\tpublic double Water { get; private set; }\r\n\r\n\t/// <summary>Index into <see cref=\"PumpModel.All\"/>.</summary>\r\n\tpublic int PumpLevel { get; private set; }\r\n\r\n\t/// <summary>Shop upgrade levels, indexed by <see cref=\"UpgradeKind\"/>.</summary>\r\n\treadonly int[] upgradeLevels = new int[3];\r\n\r\n\tpublic bool Muted => PumpSettings.Current.Muted;\r\n\r\n\tpublic long TotalPumps { get; private set; }\r\n\tpublic double TotalEarned { get; private set; }\r\n\tpublic double TotalLitresSold { get; private set; }\r\n\r\n\t/// <summary>Litres the pump produced while the game was closed, for the welcome-back note.</summary>\r\n\tpublic double IdleCatchUp { get; private set; }\r\n\r\n\tpublic PumpModel CurrentPump => PumpModel.Get( PumpLevel );\r\n\tpublic PumpTier Tier => CurrentPump.Tier;\r\n\tpublic PumpModel NextPump => PumpLevel < PumpModel.MaxIndex ? PumpModel.Get( PumpLevel + 1 ) : null;\r\n\tpublic bool IsMaxed => NextPump is null;\r\n\r\n\t// ---- effective stats: the tier's base numbers times whatever the shop has sold you\r\n\r\n\t/// <summary>Tank size including the Water Tank upgrade.</summary>\r\n\tpublic double TankCapacity => CurrentPump.Tank * MultiplierOf( UpgradeKind.Storage );\r\n\r\n\t/// <summary>Litres per manual pump, including Pump Power.</summary>\r\n\tpublic double LitresPerClick => CurrentPump.PerClick * MultiplierOf( UpgradeKind.Power );\r\n\r\n\t/// <summary>Coins per litre, including Market Contacts.</summary>\r\n\tpublic double PricePerLitre => CurrentPump.PricePerLitre * MultiplierOf( UpgradeKind.Value );\r\n\r\n\tpublic bool TankFull => Water >= TankCapacity - 0.0001;\r\n\tpublic float TankFraction => TankCapacity <= 0 ? 0f : (float)Math.Clamp( Water / TankCapacity, 0d, 1d );\r\n\r\n\t/// <summary>What the tank is worth right now.</summary>\r\n\tpublic double SaleValue => Water * PricePerLitre;\r\n\r\n\tpublic bool CanSell => Water > 0.0001;\r\n\tpublic bool CanUpgrade => NextPump is not null && Coins >= NextPump.Cost;\r\n\r\n\tpublic int LevelOf( UpgradeKind kind ) => upgradeLevels[(int)kind];\r\n\r\n\tpublic float MultiplierOf( UpgradeKind kind ) => UpgradeTrack.Get( kind ).MultiplierAt( LevelOf( kind ) );\r\n\r\n\tpublic bool IsMaxLevel( UpgradeKind kind ) => LevelOf( kind ) >= UpgradeTrack.Get( kind ).MaxLevel;\r\n\r\n\t/// <summary>Cost of the next level, or infinity once the track is maxed.</summary>\r\n\tpublic double CostOf( UpgradeKind kind )\r\n\t{\r\n\t\treturn IsMaxLevel( kind )\r\n\t\t\t? double.PositiveInfinity\r\n\t\t\t: UpgradeTrack.Get( kind ).CostAt( LevelOf( kind ) );\r\n\t}\r\n\r\n\tpublic bool CanBuy( UpgradeKind kind ) => !IsMaxLevel( kind ) && Coins >= CostOf( kind );\r\n\r\n\t/// <summary>Fired on a successful manual pump, with the litres gained.</summary>\r\n\tpublic Action<double> Pumped { get; set; }\r\n\r\n\t/// <summary>Fired on a sale, with the coins gained.</summary>\r\n\tpublic Action<double> Sold { get; set; }\r\n\r\n\t/// <summary>Fired after buying a new pump, with the tier we moved to.</summary>\r\n\tpublic Action<PumpModel> Upgraded { get; set; }\r\n\r\n\t/// <summary>Fired when an action was refused - tank full, broke, nothing to sell.</summary>\r\n\tpublic Action<string> Refused { get; set; }\r\n\r\n\t/// <summary>Fired after buying a shop upgrade.</summary>\r\n\tpublic Action<UpgradeTrack> Purchased { get; set; }\r\n\r\n\tTimeSince timeSinceSave;\r\n\r\n\t/// <summary>\r\n\t/// True once <see cref=\"Load\"/> has run, which only happens in play mode. The copy of\r\n\t/// this component living in the editor's scene never wakes, so it sits on default\r\n\t/// zeroes - and without this guard its shutdown save would write those zeroes straight\r\n\t/// over a real run. Never write state we haven't read.\r\n\t/// </summary>\r\n\tbool loaded;\r\n\r\n\tprotected override void OnAwake()\r\n\t{\r\n\t\tCurrent = this;\r\n\t\tLoad();\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Quitting tears the scene down, and which of these fires first depends on how you\r\n\t/// left - closing the window, stopping play mode, or loading another scene. Saving\r\n\t/// from both, plus after every earn and spend, means there's no way out that loses\r\n\t/// a run.\r\n\t/// </summary>\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tSave();\r\n\t}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{\r\n\t\tSave();\r\n\r\n\t\tif ( Current == this )\r\n\t\t\tCurrent = null;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// A code hotload wipes statics without re-running OnAwake, which would leave\r\n\t\t// Current null and the HUD showing \"no game in the scene\" for the rest of the\r\n\t\t// session. Cheaper to re-claim it than to debug that every time.\r\n\t\tif ( Current != this ) Current = this;\r\n\r\n\t\t// This is a mouse game - without this the cursor gets locked to the view in\r\n\t\t// play mode and none of the buttons can be clicked.\r\n\t\tMouse.Visibility = MouseVisibility.Visible;\r\n\r\n\t\t// Space bar is a second pump button so you're not glued to the mouse.\r\n\t\tif ( Input.Pressed( \"Jump\" ) )\r\n\t\t{\r\n\t\t\tPump();\r\n\t\t}\r\n\r\n\t\tvar passive = CurrentPump.PerSecond * Time.Delta;\r\n\t\tif ( passive > 0f )\r\n\t\t{\r\n\t\t\tAddWater( passive );\r\n\t\t}\r\n\r\n\t\tTickGusher();\r\n\t\tTickTree();\r\n\r\n\t\tif ( PumpSettings.Current.AutoSell && TankFull )\r\n\t\t{\r\n\t\t\tSellAll();\r\n\t\t}\r\n\r\n\t\tif ( SaveEnabled && timeSinceSave > SaveInterval )\r\n\t\t{\r\n\t\t\tSave();\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// One manual pump. Returns the litres actually gained - zero means the tank was already full.\r\n\t/// </summary>\r\n\tpublic double Pump()\r\n\t{\r\n\t\tif ( TankFull )\r\n\t\t{\r\n\t\t\tRefuse( \"Tank is full - sell your water\" );\r\n\t\t\treturn 0;\r\n\t\t}\r\n\r\n\t\tvar gained = AddWater( LitresPerClick );\r\n\r\n\t\tTotalPumps++;\r\n\t\tPlay( PumpSound );\r\n\t\tPumped?.Invoke( gained );\r\n\r\n\t\treturn gained;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Empty the tank into coins. Returns what we earned.\r\n\t/// </summary>\r\n\tpublic double SellAll()\r\n\t{\r\n\t\tif ( !CanSell )\r\n\t\t{\r\n\t\t\tRefuse( \"No water to sell\" );\r\n\t\t\treturn 0;\r\n\t\t}\r\n\r\n\t\tvar litres = Water;\r\n\t\tvar earned = SaleValue;\r\n\r\n\t\tWater = 0;\r\n\t\tCoins += earned;\r\n\t\tTotalEarned += earned;\r\n\t\tTotalLitresSold += litres;\r\n\t\tIdleCatchUp = 0;\r\n\r\n\t\tPlay( SellSound );\r\n\t\tSold?.Invoke( earned );\r\n\t\tSave();\r\n\r\n\t\treturn earned;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Buy the next pump in the list. Water carries over, clamped to the new tank.\r\n\t/// </summary>\r\n\tpublic bool BuyNextPump()\r\n\t{\r\n\t\tvar next = NextPump;\r\n\r\n\t\tif ( next is null )\r\n\t\t{\r\n\t\t\tRefuse( \"You own every pump in the desert\" );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tif ( Coins < next.Cost )\r\n\t\t{\r\n\t\t\tRefuse( $\"Need {Numbers.Short( next.Cost - Coins )} more coins\" );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tCoins -= next.Cost;\r\n\t\tPumpLevel = next.Index;\r\n\t\tWater = Math.Min( Water, TankCapacity );\r\n\r\n\t\tPlay( UpgradeSound );\r\n\t\tUpgraded?.Invoke( next );\r\n\t\tSave();\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/// <summary>Hand over coins from outside the sell loop - rewards, console commands.</summary>\r\n\tpublic void AddCoins( double amount )\r\n\t{\r\n\t\t// A NaN or infinity in here poisons the save and every number on screen with\r\n\t\t// no way back, so refuse it at the door rather than storing it.\r\n\t\tif ( amount <= 0 || double.IsNaN( amount ) || double.IsInfinity( amount ) )\r\n\t\t\treturn;\r\n\r\n\t\tCoins += amount;\r\n\t\tTotalEarned += amount;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Buy one level of a shop upgrade. Returns false and complains if you can't afford it.\r\n\t/// </summary>\r\n\tpublic bool BuyUpgrade( UpgradeKind kind )\r\n\t{\r\n\t\tvar track = UpgradeTrack.Get( kind );\r\n\r\n\t\tif ( IsMaxLevel( kind ) )\r\n\t\t{\r\n\t\t\tRefuse( $\"{track.Name} is fully upgraded\" );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tvar cost = CostOf( kind );\r\n\t\tif ( Coins < cost )\r\n\t\t{\r\n\t\t\tRefuse( $\"Need {Numbers.Short( cost - Coins )} more coins\" );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tCoins -= cost;\r\n\t\tupgradeLevels[(int)kind]++;\r\n\r\n\t\t// A smaller tank than the water in it can't happen, but a bigger one is free room.\r\n\t\tWater = Math.Min( Water, TankCapacity );\r\n\r\n\t\tPlay( UpgradeSound );\r\n\t\tPurchased?.Invoke( track );\r\n\t\tSave();\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\tpublic void ToggleMute()\r\n\t{\r\n\t\tvar settings = PumpSettings.Current;\r\n\r\n\t\tsettings.Muted = !settings.Muted;\r\n\t\tsettings.Save();\r\n\t}\r\n\r\n\t/// <summary>Wipe the save and start over from the hand pump.</summary>\r\n\tpublic void ResetProgress()\r\n\t{\r\n\t\tCoins = 0;\r\n\t\tWater = 0;\r\n\t\tPumpLevel = 0;\r\n\t\tArray.Clear( upgradeLevels );\r\n\t\townedBackgrounds.Clear();\r\n\t\townedBackgrounds.Add( BackgroundStyle.DefaultId );\r\n\t\tSelectedBackground = BackgroundStyle.DefaultId;\r\n\t\tlastDailyDay = 0;\r\n\t\tDailyStreak = 0;\r\n\t\tTreeHeight = 0;\r\n\t\tTreeWater = 0;\r\n\t\ttreeGrowsAt = 0;\r\n\t\tTotalPumps = 0;\r\n\t\tTotalEarned = 0;\r\n\t\tTotalLitresSold = 0;\r\n\t\tIdleCatchUp = 0;\r\n\r\n\t\tSave();\r\n\t}\r\n\r\n\t/// <summary>Adds water up to the tank limit and returns how much actually fit.</summary>\r\n\tdouble AddWater( double amount )\r\n\t{\r\n\t\tvar before = Water;\r\n\t\tWater = Math.Min( TankCapacity, Water + amount );\r\n\r\n\t\treturn Water - before;\r\n\t}\r\n\r\n\tvoid Refuse( string reason )\r\n\t{\r\n\t\tPlay( DeniedSound );\r\n\t\tRefused?.Invoke( reason );\r\n\t}\r\n\r\n\tvoid Play( SoundEvent sound )\r\n\t{\r\n\t\tvar volume = PumpSettings.Current.EffectiveSfx;\r\n\r\n\t\tif ( sound is null || volume <= 0f )\r\n\t\t\treturn;\r\n\r\n\t\tvar handle = Sound.Play( sound, (Sandbox.Audio.Mixer)null );\r\n\r\n\t\tif ( handle.IsValid() )\r\n\t\t{\r\n\t\t\thandle.Volume = volume;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid Load()\r\n\t{\r\n\t\ttimeSinceSave = 0;\r\n\r\n\t\tif ( !SaveEnabled )\r\n\t\t\treturn;\r\n\r\n\t\t// From here on we own the file - a missing save just means a fresh run.\r\n\t\tloaded = true;\r\n\r\n\t\tvar save = FileSystem.Data.ReadJsonOrDefault<PumpSave>( SaveFile, null );\r\n\t\tif ( save is null )\r\n\t\t\treturn;\r\n\r\n\t\t// Stash the run as we found it. One session of history is enough to rescue a\r\n\t\t// player whose save gets eaten by a bug, and it costs one small write per load.\r\n\t\tFileSystem.Data.WriteJson( SaveFile + \".bak\", save );\r\n\r\n\t\tCoins = Math.Max( 0, save.Coins );\r\n\t\tPumpLevel = Math.Clamp( save.PumpLevel, 0, PumpModel.MaxIndex );\r\n\r\n\t\townedBackgrounds.Clear();\r\n\t\townedBackgrounds.Add( BackgroundStyle.DefaultId );\r\n\t\tforeach ( var id in save.OwnedBackgrounds ?? new List<string>() )\r\n\t\t{\r\n\t\t\townedBackgrounds.Add( id );\r\n\t\t}\r\n\r\n\t\tSelectedBackground = OwnsBackground( save.SelectedBackground )\r\n\t\t\t? save.SelectedBackground\r\n\t\t\t: BackgroundStyle.DefaultId;\r\n\r\n\t\tlastDailyDay = save.LastDailyDay;\r\n\t\tTreeHeight = Math.Max( 0, save.TreeHeight );\r\n\t\tTreeWater = Math.Max( 0, save.TreeWater );\r\n\t\ttreeGrowsAt = save.TreeGrowsAt;\r\n\t\tDailyStreak = save.DailyStreak;\r\n\r\n\t\tupgradeLevels[(int)UpgradeKind.Storage] = ClampLevel( save.StorageLevel, UpgradeKind.Storage );\r\n\t\tupgradeLevels[(int)UpgradeKind.Power] = ClampLevel( save.PowerLevel, UpgradeKind.Power );\r\n\t\tupgradeLevels[(int)UpgradeKind.Value] = ClampLevel( save.ValueLevel, UpgradeKind.Value );\r\n\r\n\t\t// Levels have to be in before the tank is clamped, or a full tank gets trimmed\r\n\t\t// to the un-upgraded size on every load.\r\n\t\tWater = Math.Clamp( save.Water, 0, TankCapacity );\r\n\t\tTotalPumps = save.TotalPumps;\r\n\t\tTotalEarned = save.TotalEarned;\r\n\t\tTotalLitresSold = save.TotalLitresSold;\r\n\r\n\t\tPayIdleTime( save.SavedAt );\r\n\t}\r\n\r\n\tstatic int ClampLevel( int level, UpgradeKind kind ) =>\r\n\t\tMath.Clamp( level, 0, UpgradeTrack.Get( kind ).MaxLevel );\r\n\r\n\t/// <summary>Runs the passive pump for the time we were away, capped so it can't be farmed.</summary>\r\n\tvoid PayIdleTime( long savedAt )\r\n\t{\r\n\t\tif ( savedAt <= 0 || CurrentPump.PerSecond <= 0f )\r\n\t\t\treturn;\r\n\r\n\t\tvar away = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - savedAt;\r\n\t\tif ( away <= 0 )\r\n\t\t\treturn;\r\n\r\n\t\tvar seconds = Math.Min( away, (long)(MaxIdleHours * 3600f) );\r\n\t\tIdleCatchUp = AddWater( CurrentPump.PerSecond * seconds );\r\n\t}\r\n\r\n\tvoid Save()\r\n\t{\r\n\t\ttimeSinceSave = 0;\r\n\r\n\t\tif ( !SaveEnabled )\r\n\t\t\treturn;\r\n\r\n\t\t// Never overwrite a real run with state we never loaded. See the field comment.\r\n\t\tif ( !loaded )\r\n\t\t\treturn;\r\n\r\n\t\tFileSystem.Data.WriteJson( SaveFile, new PumpSave\r\n\t\t{\r\n\t\t\tCoins = Coins,\r\n\t\t\tWater = Water,\r\n\t\t\tPumpLevel = PumpLevel,\r\n\t\t\tOwnedBackgrounds = ownedBackgrounds.ToList(),\r\n\t\t\tSelectedBackground = SelectedBackground,\r\n\t\t\tLastDailyDay = lastDailyDay,\r\n\t\t\tTreeHeight = TreeHeight,\r\n\t\t\tTreeWater = TreeWater,\r\n\t\t\tTreeGrowsAt = treeGrowsAt,\r\n\t\t\tDailyStreak = DailyStreak,\r\n\t\t\tStorageLevel = LevelOf( UpgradeKind.Storage ),\r\n\t\t\tPowerLevel = LevelOf( UpgradeKind.Power ),\r\n\t\t\tValueLevel = LevelOf( UpgradeKind.Value ),\r\n\t\t\tMuted = Muted,\r\n\t\t\tTotalPumps = TotalPumps,\r\n\t\t\tTotalEarned = TotalEarned,\r\n\t\t\tTotalLitresSold = TotalLitresSold,\r\n\t\t\tSavedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()\r\n\t\t} );\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "Game/PumpSettings.cs",
            "FileName": "PumpSettings.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "namespace DesertPump;\n\n/// <summary>\n/// Player preferences. Kept in their own file so wiping a run never costs you your\n/// volume levels, and so they survive a reset.\n/// </summary>\npublic sealed class PumpSettings\n{\n\tpublic const string FileName = \"settings.json\";\n\n\tpublic float MasterVolume { get; set; } = 0.8f;\n\tpublic float MusicVolume { get; set; } = 0.55f;\n\tpublic float SfxVolume { get; set; } = 1f;\n\n\t/// <summary>Silences everything, music included. The speaker button toggles this.</summary>\n\tpublic bool Muted { get; set; }\n\n\t/// <summary>The little \"+12L\" numbers that fly off the pump.</summary>\n\tpublic bool ShowFloatingNumbers { get; set; } = true;\n\n\t/// <summary>Sell the tank automatically the moment it fills. Off by default.</summary>\n\tpublic bool AutoSell { get; set; }\n\n\t/// <summary>Final multiplier for one-shot effects.</summary>\n\t[System.Text.Json.Serialization.JsonIgnore]\n\tpublic float EffectiveSfx => Muted ? 0f : MasterVolume * SfxVolume;\n\n\t/// <summary>Final multiplier for the background track.</summary>\n\t[System.Text.Json.Serialization.JsonIgnore]\n\tpublic float EffectiveMusic => Muted ? 0f : MasterVolume * MusicVolume;\n\n\tstatic PumpSettings loaded;\n\n\t/// <summary>\n\t/// The live settings. Lazily read so a code hotload - which clears statics - just\n\t/// picks them back up off disk instead of reverting to defaults.\n\t/// </summary>\n\tpublic static PumpSettings Current\n\t{\n\t\tget\n\t\t{\n\t\t\tloaded ??= FileSystem.Data.ReadJsonOrDefault<PumpSettings>( FileName, null ) ?? new PumpSettings();\n\t\t\treturn loaded;\n\t\t}\n\t}\n\n\tpublic void Save()\n\t{\n\t\tMasterVolume = Math.Clamp( MasterVolume, 0f, 1f );\n\t\tMusicVolume = Math.Clamp( MusicVolume, 0f, 1f );\n\t\tSfxVolume = Math.Clamp( SfxVolume, 0f, 1f );\n\n\t\tFileSystem.Data.WriteJson( FileName, this );\n\t}\n}\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "Game/PumpTier.cs",
            "FileName": "PumpTier.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "namespace DesertPump;\n\n/// <summary>\n/// One of the twelve tiers. A tier is nine pumps that share a setting - you finish\n/// the desert before you ever see a glacier.\n/// </summary>\npublic sealed class PumpTier\n{\n\t/// <summary>1 to 12, the way the player sees it.</summary>\n\tpublic int Number { get; init; }\n\n\tpublic string Name { get; init; }\n\tpublic string Tagline { get; init; }\n\n\t/// <summary>Drives the placeholder pump art for every pump in this tier.</summary>\n\tpublic Color Tint { get; init; }\n\n\t/// <summary>The nine pump names, cheapest first.</summary>\n\tpublic string[] PumpNames { get; init; }\n\n\tpublic int FirstPumpIndex => (Number - 1) * PumpsPerTier;\n\tpublic int LastPumpIndex => FirstPumpIndex + PumpsPerTier - 1;\n\n\tpublic const int PumpsPerTier = 9;\n\n\t/// <summary>Re-read on hotload so edits apply without restarting - see PumpModel.All.</summary>\n\t[SkipHotload]\n\tpublic static readonly PumpTier[] All = new PumpTier[]\n\t{\n\t\tnew()\n\t\t{\n\t\t\tNumber = 1,\n\t\t\tName = \"Desert Wells\",\n\t\t\tTagline = \"Sand, rust, and whatever you can pull up by hand.\",\n\t\t\tTint = new Color( 0.72f, 0.50f, 0.24f ),\n\t\t\tPumpNames = new[]\n\t\t\t{\n\t\t\t\t\"Rusty Hand Pump\", \"Iron Well Pump\", \"Steam Pressure Pump\",\n\t\t\t\t\"Diesel Derrick\", \"Solar Filtration Rig\", \"Oasis Temple Spring\",\n\t\t\t\t\"Hydro Tower\", \"Vortex Extractor\", \"Ancient Geyser Monolith\"\n\t\t\t}\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tNumber = 2,\n\t\t\tName = \"The Steamworks\",\n\t\t\tTagline = \"Brass, pressure gauges, and a great deal of hissing.\",\n\t\t\tTint = new Color( 0.66f, 0.42f, 0.20f ),\n\t\t\tPumpNames = new[]\n\t\t\t{\n\t\t\t\t\"Brass Piston Well\", \"Boiler Draw\", \"Twin Cylinder Rig\",\n\t\t\t\t\"Pressure Cathedral\", \"Governor Engine\", \"Flywheel Colossus\",\n\t\t\t\t\"Condenser Array\", \"Turbine Hall\", \"The Great Manifold\"\n\t\t\t}\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tNumber = 3,\n\t\t\tName = \"Oil Country\",\n\t\t\tTagline = \"If it worked for crude, it'll work for water.\",\n\t\t\tTint = new Color( 0.34f, 0.34f, 0.30f ),\n\t\t\tPumpNames = new[]\n\t\t\t{\n\t\t\t\t\"Nodding Donkey\", \"Wildcat Rig\", \"Slant Drill\",\n\t\t\t\t\"Offshore Jacket\", \"Deep Casing Pump\", \"Fracture Head\",\n\t\t\t\t\"Pipeline Junction\", \"Refinery Tap\", \"The Black Column\"\n\t\t\t}\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tNumber = 4,\n\t\t\tName = \"Solar Frontier\",\n\t\t\tTagline = \"Free light, endless panels, no moving parts.\",\n\t\t\tTint = new Color( 0.28f, 0.46f, 0.78f ),\n\t\t\tPumpNames = new[]\n\t\t\t{\n\t\t\t\t\"Panel Trickle\", \"Mirror Trough\", \"Heliostat Field\",\n\t\t\t\t\"Molten Salt Loop\", \"Photovoltaic Mesa\", \"Tower of Light\",\n\t\t\t\t\"Orbital Reflector\", \"Fusion Preheater\", \"The Sun Engine\"\n\t\t\t}\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tNumber = 5,\n\t\t\tName = \"The Lost Oasis\",\n\t\t\tTagline = \"Someone was pumping here long before you.\",\n\t\t\tTint = new Color( 0.86f, 0.70f, 0.26f ),\n\t\t\tPumpNames = new[]\n\t\t\t{\n\t\t\t\t\"Sunken Step Well\", \"Palm Court Spring\", \"Mosaic Cistern\",\n\t\t\t\t\"Colonnade Fountain\", \"Serpent Aqueduct\", \"Sun Disc Basin\",\n\t\t\t\t\"Hall of Rains\", \"Temple of the Flood\", \"The First Spring\"\n\t\t\t}\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tNumber = 6,\n\t\t\tName = \"Deep Aquifer\",\n\t\t\tTagline = \"Down where the rock has been holding its breath.\",\n\t\t\tTint = new Color( 0.30f, 0.52f, 0.55f ),\n\t\t\tPumpNames = new[]\n\t\t\t{\n\t\t\t\t\"Bore Shaft\", \"Karst Siphon\", \"Cavern Lake Pump\",\n\t\t\t\t\"Sinkhole Intake\", \"Mantle Straw\", \"Pressure Fault\",\n\t\t\t\t\"Abyssal Column\", \"Tectonic Draw\", \"The Sleeping Sea\"\n\t\t\t}\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tNumber = 7,\n\t\t\tName = \"Storm Harvest\",\n\t\t\tTagline = \"Stop digging. Start taking it out of the sky.\",\n\t\t\tTint = new Color( 0.42f, 0.46f, 0.62f ),\n\t\t\tPumpNames = new[]\n\t\t\t{\n\t\t\t\t\"Rain Catcher\", \"Cloud Seeder\", \"Squall Funnel\",\n\t\t\t\t\"Lightning Condenser\", \"Monsoon Gate\", \"Cyclone Tap\",\n\t\t\t\t\"Hurricane Yoke\", \"Jet Stream Weir\", \"The Storm Crown\"\n\t\t\t}\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tNumber = 8,\n\t\t\tName = \"Glacier Melt\",\n\t\t\tTagline = \"Ten thousand years of snow, cashed in.\",\n\t\t\tTint = new Color( 0.58f, 0.78f, 0.86f ),\n\t\t\tPumpNames = new[]\n\t\t\t{\n\t\t\t\t\"Meltwater Channel\", \"Crevasse Pump\", \"Ice Core Drill\",\n\t\t\t\t\"Moraine Sluice\", \"Calving Front Rig\", \"Ice Shelf Siphon\",\n\t\t\t\t\"Polar Reservoir\", \"Continental Thaw\", \"The Long Winter Tap\"\n\t\t\t}\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tNumber = 9,\n\t\t\tName = \"Ocean Reclaim\",\n\t\t\tTagline = \"Salt is just an engineering problem.\",\n\t\t\tTint = new Color( 0.20f, 0.52f, 0.62f ),\n\t\t\tPumpNames = new[]\n\t\t\t{\n\t\t\t\t\"Beach Well\", \"Reverse Osmosis Bank\", \"Desalination Pier\",\n\t\t\t\t\"Tidal Intake\", \"Trench Pump\", \"Current Harness\",\n\t\t\t\t\"Abyssal Plain Array\", \"Continental Shelf Works\", \"The Ocean Engine\"\n\t\t\t}\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tNumber = 10,\n\t\t\tName = \"Sky Condensers\",\n\t\t\tTagline = \"The whole atmosphere, wrung out like a cloth.\",\n\t\t\tTint = new Color( 0.66f, 0.72f, 0.86f ),\n\t\t\tPumpNames = new[]\n\t\t\t{\n\t\t\t\t\"Fog Net\", \"Dew Spire\", \"Stratosphere Coil\",\n\t\t\t\t\"Cloud Anchor\", \"Mesosphere Rig\", \"Aurora Collector\",\n\t\t\t\t\"Orbital Ring Tap\", \"Atmospheric Siphon\", \"The Great Condenser\"\n\t\t\t}\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tNumber = 11,\n\t\t\tName = \"Void Springs\",\n\t\t\tTagline = \"The water is coming from somewhere. Don't ask where.\",\n\t\t\tTint = new Color( 0.52f, 0.36f, 0.78f ),\n\t\t\tPumpNames = new[]\n\t\t\t{\n\t\t\t\t\"Rift Trickle\", \"Phase Well\", \"Folded Space Pump\",\n\t\t\t\t\"Null Aquifer\", \"Entropy Siphon\", \"Singularity Draw\",\n\t\t\t\t\"Event Horizon Tap\", \"Vacuum Spring\", \"The Hollow Between\"\n\t\t\t}\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tNumber = 12,\n\t\t\tName = \"Cosmic Wellspring\",\n\t\t\tTagline = \"Comets are mostly ice, and there are rather a lot of them.\",\n\t\t\tTint = new Color( 0.30f, 0.80f, 0.92f ),\n\t\t\tPumpNames = new[]\n\t\t\t{\n\t\t\t\t\"Comet Harpoon\", \"Ring System Skimmer\", \"Ice Moon Bore\",\n\t\t\t\t\"Nebula Trawler\", \"Rogue World Tap\", \"Stellar Nursery Pump\",\n\t\t\t\t\"Galactic Aqueduct\", \"Dark Flow Intake\", \"The Wellspring\"\n\t\t\t}\n\t\t},\n\t};\n\n\tpublic static int Count => All.Length;\n\n\t/// <summary>Total pumps across every tier.</summary>\n\tpublic static int TotalPumps => Count * PumpsPerTier;\n\n\t/// <summary>Tier for a 0-based pump index.</summary>\n\tpublic static PumpTier ForPump( int pumpIndex ) =>\n\t\tAll[Math.Clamp( pumpIndex / PumpsPerTier, 0, Count - 1 )];\n\n\t/// <summary>Tier by its 1-12 number.</summary>\n\tpublic static PumpTier Get( int number ) => All[Math.Clamp( number - 1, 0, Count - 1 )];\n}\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "styles/base/_navigator.scss",
            "FileName": "_navigator.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": ".navigator-body\r\n{\r\n\t&.hidden\r\n\t{\r\n\t\tdisplay: none;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "ui/components/packageflairbar.razor.scss",
            "FileName": "packageflairbar.razor.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "PackageFlairBar\r\n{\r\n\tposition: absolute;\r\n\ttop: 6px;\r\n\tleft: 6px;\r\n\tz-index: 2;\r\n\tflex-direction: row;\r\n\tgap: 5px;\r\n\tpointer-events: none;\r\n\r\n\t.flair\r\n\t{\r\n\t\tposition: relative;\r\n\t\talign-items: center;\r\n\t\tjustify-content: center;\r\n\t\twidth: 22px;\r\n\t\theight: 22px;\r\n\t\tpointer-events: all;\r\n\t\tborder-radius: 4px;\r\n\t\tcolor: white;\r\n\t\tborder: 1px solid rgba( white, 0.1 );\r\n\t\toutline: 1px solid #0006;\r\n\t\topacity: 0.7;\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t\topacity: 1;\r\n\t\t}\r\n\r\n\t\ticon\r\n\t\t{\r\n\t\t\tfont-size: 15px;\r\n\t\t}\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "styles/base.scss",
            "FileName": "base.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "@import \"base/_splitcontainer.scss\";\r\n@import \"base/_navigator.scss\";\r\n\r\nbutton\r\n{\r\n\tcursor: pointer;\r\n}\r\n\r\nIconPanel\r\n{\r\n\tfont-family: Material Icons;\r\n}\r\n\r\n.is-half\r\n{\r\n\twidth: 50%;\r\n}\r\n\r\n.is-third\r\n{\r\n\twidth: 33%;\r\n}\r\n\r\n.is-quarter\r\n{\r\n\twidth: 25%;\r\n}\r\n\r\nbutton.has-subtitle\r\n{\r\n\tposition: relative;\r\n\tflex-direction: column;\r\n\tjustify-content: flex-start;\r\n\talign-items: flex-start;\r\n\tpadding-left: 40px; // icon space\r\n\r\n\t.iconpanel\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tleft: 5px;\r\n\t\ttop: 0;\r\n\t\tbottom: 0;\r\n\t\talign-items: center;\r\n\t}\r\n\r\n\t.button-label\r\n\t{\r\n\t\tfont-weight: bold;\r\n\t}\r\n\r\n\t.button-subtitle\r\n\t{\r\n\t\tfont-size: 12px;\r\n\t\topacity: 0.5;\r\n\t\tmix-blend-mode: lighten;\r\n\t}\r\n}"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "ui/controls/color/coloralphacontrol.cs.scss",
            "FileName": "coloralphacontrol.cs.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "ColorAlphaControl\r\n{\r\n\tgap: 0.5rem;\r\n\tflex-grow: 1;\r\n\tpointer-events: all;\r\n\tbackground: linear-gradient( to right, black, white );\r\n\tborder-radius: 4px;\r\n\tpadding: 2px;\r\n\theight: 12px;\r\n\tposition: relative;\r\n\tcursor: pointer;\r\n\tborder: 1px solid #333;\r\n\r\n\t&:hover\r\n\t{\r\n\t\tborder: 1px solid #08f;\r\n\t}\r\n\r\n\t&:active\r\n\t{\r\n\t\tborder: 1px solid #fff;\r\n\t}\r\n\r\n\t.handle\r\n\t{\r\n\t\ttop: -5px;\r\n\t\tbottom: -5px;\r\n\t\taspect-ratio: 1;\r\n\t\tborder-radius: 100px;\r\n\t\tborder: 2px solid #444;\r\n\t\tposition: absolute;\r\n\t\tbackground-color: white;\r\n\t\tbox-shadow: 2px 2px 16px #000a;\r\n\t\ttransform: translateX( -50% );\r\n\t\tpointer-events: none;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "ui/controls/color/colorpickercontrol.cs.scss",
            "FileName": "colorpickercontrol.cs.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "ColorPickerControl\r\n{\r\n\tflex-direction: column;\r\n\tflex-shrink: 0;\r\n\tgap: 0.5rem;\r\n\tmargin: 1rem;\r\n}"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "ui/components/packagelist.razor.scss",
            "FileName": "packagelist.razor.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": ".package-list\r\n{\r\n    flex-shrink: 1;\r\n    flex-wrap: wrap;\r\n    flex-grow: 1;\r\n\r\n    h1\r\n    {\r\n        width: 100%;\r\n        margin-top: 50px;\r\n        font-size: 40px;\r\n    }\r\n\r\n    PackageCard\r\n    {\r\n        &:hover\r\n        {\r\n            sound-in: \"ui.button.over\";\r\n        }\r\n    }\r\n\r\n    VirtualGrid\r\n    {\r\n        width: 100%;\r\n        height: 100%;\r\n\r\n        .cell\r\n        {\r\n            \r\n        }\r\n    }\r\n}"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "ui/treepanel.razor.scss",
            "FileName": "treepanel.razor.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "$gold: #f5c542;\r\n$panel: #1b2130;\r\n\r\n.treepanel\r\n{\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tleft: 0;\r\n\twidth: 100%;\r\n\theight: 100%;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n\tpointer-events: all;\r\n\r\n\t// Same reason as the shop - without this the HUD paints through the sheet.\r\n\tz-index: 100;\r\n\tfont-family: Poppins;\r\n\tcolor: white;\r\n\r\n\t.scrim\r\n\t{\r\n\t\tposition: absolute;\r\n\t\ttop: 0;\r\n\t\tleft: 0;\r\n\t\twidth: 100%;\r\n\t\theight: 100%;\r\n\t\tbackground-color: rgba( 8, 10, 16, 0.65 );\r\n\t\tpointer-events: all;\r\n\t}\r\n\r\n\t.sheet\r\n\t{\r\n\t\twidth: 560px;\r\n\t\tmax-width: 92%;\r\n\t\tmax-height: 90%;\r\n\t\tflex-direction: column;\r\n\t\tbackground-color: $panel;\r\n\t\tborder-radius: 24px;\r\n\t\tpadding: 22px;\r\n\t\tborder-bottom: 6px solid rgba( 0, 0, 0, 0.4 );\r\n\t\tpointer-events: all;\r\n\t}\r\n\r\n\t.head\r\n\t{\r\n\t\talign-items: center;\r\n\t\tmargin-bottom: 14px;\r\n\r\n\t\t.title\r\n\t\t{\r\n\t\t\tflex-grow: 1;\r\n\t\t\tfont-size: 24px;\r\n\t\t\tfont-weight: 800;\r\n\t\t\tletter-spacing: 2px;\r\n\t\t\talign-items: center;\r\n\r\n\t\t\ti\r\n\t\t\t{\r\n\t\t\t\tfont-family: Material Icons;\r\n\t\t\t\tfont-size: 28px;\r\n\t\t\t\tmargin-right: 10px;\r\n\t\t\t\tcolor: $gold;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t.closebtn\r\n\t\t{\r\n\t\t\twidth: 44px;\r\n\t\t\theight: 44px;\r\n\t\t\tborder-radius: 12px;\r\n\t\t\tbackground-color: rgba( 255, 255, 255, 0.08 );\r\n\t\t\talign-items: center;\r\n\t\t\tjustify-content: center;\r\n\t\t\tcursor: pointer;\r\n\t\t\tpointer-events: all;\r\n\r\n\t\t\ti { font-family: Material Icons; font-size: 24px; }\r\n\r\n\t\t\t&:hover { background-color: rgba( 255, 255, 255, 0.18 ); }\r\n\t\t}\r\n\t}\r\n\r\n\t.note\r\n\t{\r\n\t\tfont-size: 13px;\r\n\t\topacity: 0.7;\r\n\t\tbackground-color: rgba( 255, 255, 255, 0.05 );\r\n\t\tborder-radius: 12px;\r\n\t\tpadding: 10px 14px;\r\n\t\tmargin-bottom: 12px;\r\n\t}\r\n\r\n\t.rows\r\n\t{\r\n\t\tflex-direction: column;\r\n\t\toverflow-y: scroll;\r\n\t}\r\n\r\n\t.lbrow\r\n\t{\r\n\t\talign-items: center;\r\n\t\tbackground-color: rgba( 255, 255, 255, 0.05 );\r\n\t\tborder-radius: 14px;\r\n\t\tpadding: 12px 16px;\r\n\t\tmargin-bottom: 8px;\r\n\t\tborder: 2px solid rgba( 0, 0, 0, 0 );\r\n\r\n\t\t.rank\r\n\t\t{\r\n\t\t\twidth: 44px;\r\n\t\t\tfont-size: 20px;\r\n\t\t\tfont-weight: 800;\r\n\t\t\topacity: 0.7;\r\n\t\t}\r\n\r\n\t\t.who\r\n\t\t{\r\n\t\t\tflex-grow: 1;\r\n\t\t\tfont-size: 17px;\r\n\t\t}\r\n\r\n\t\t.score\r\n\t\t{\r\n\t\t\tfont-size: 20px;\r\n\t\t\tfont-weight: 700;\r\n\t\t\talign-items: center;\r\n\r\n\t\t\t.num { font-size: 20px; font-weight: 700; }\r\n\r\n\t\t\t.unit\r\n\t\t\t{\r\n\t\t\t\tfont-size: 12px;\r\n\t\t\t\topacity: 0.6;\r\n\t\t\t\tmargin-left: 6px;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t&.gold .rank { color: $gold; opacity: 1; }\r\n\t\t&.silver .rank { color: #cfd6e4; opacity: 1; }\r\n\t\t&.bronze .rank { color: #d09a5c; opacity: 1; }\r\n\r\n\t\t&.me\r\n\t\t{\r\n\t\t\tborder-color: #7fd06f;\r\n\t\t\tbackground-color: rgba( 127, 208, 111, 0.12 );\r\n\t\t}\r\n\t}\r\n\r\n\t.refresh\r\n\t{\r\n\t\talign-self: center;\r\n\t\tmargin-top: 8px;\r\n\t\tpadding: 10px 28px;\r\n\t\tborder-radius: 12px;\r\n\t\tbackground-color: rgba( 255, 255, 255, 0.08 );\r\n\t\tfont-size: 14px;\r\n\t\tfont-weight: 700;\r\n\t\tletter-spacing: 1px;\r\n\t\talign-items: center;\r\n\t\tcursor: pointer;\r\n\t\tpointer-events: all;\r\n\r\n\t\ti\r\n\t\t{\r\n\t\t\tfont-family: Material Icons;\r\n\t\t\tfont-size: 18px;\r\n\t\t\tmargin-right: 8px;\r\n\t\t}\r\n\r\n\t\t&:hover { background-color: rgba( 255, 255, 255, 0.16 ); }\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "UI/TreePanel.razor",
            "FileName": "TreePanel.razor",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "@using System\r\n@using Sandbox\r\n@using Sandbox.UI\r\n@namespace DesertPump\r\n@inherits Panel\r\n\r\n<root class=\"treepanel\">\r\n\r\n\t<div class=\"scrim\" onclick=\"@Close\"></div>\r\n\r\n\t<div class=\"sheet\">\r\n\r\n\t\t<div class=\"head\">\r\n\t\t\t<div class=\"title\"><i>emoji_events</i> TALLEST TREES</div>\r\n\t\t\t<div class=\"closebtn\" onclick=\"@Close\"><i>close</i></div>\r\n\t\t</div>\r\n\r\n\t\t@if ( Leaderboard.Fetching )\r\n\t\t{\r\n\t\t\t<div class=\"note\">Loading the board...</div>\r\n\t\t}\r\n\t\telse if ( !string.IsNullOrEmpty( Leaderboard.Status ) )\r\n\t\t{\r\n\t\t\t<div class=\"note\">@Leaderboard.Status</div>\r\n\t\t}\r\n\r\n\t\t<div class=\"rows\">\r\n\t\t\t@foreach ( var row in Leaderboard.Entries )\r\n\t\t\t{\r\n\t\t\t\t<div class=\"lbrow @( row.IsMe ? \"me\" : \"\" ) @RankClass( row.Rank )\">\r\n\t\t\t\t\t<div class=\"rank\">@row.Rank</div>\r\n\t\t\t\t\t<div class=\"who\">@row.Name</div>\r\n\t\t\t\t\t<div class=\"score\"><span class=\"num\">@row.Height</span><span class=\"unit\">stages</span></div>\r\n\t\t\t\t</div>\r\n\t\t\t}\r\n\t\t</div>\r\n\r\n\t\t<div class=\"refresh\" onclick=\"@Reload\">\r\n\t\t\t<i>refresh</i> REFRESH\r\n\t\t</div>\r\n\r\n\t</div>\r\n\r\n</root>\r\n\r\n@code\r\n{\r\n\tpublic Action OnClose { get; set; }\r\n\r\n\tstatic DesertPumpGame Game => DesertPumpGame.Current;\r\n\r\n\tprotected override void OnAfterTreeRender( bool firstTime )\r\n\t{\r\n\t\tbase.OnAfterTreeRender( firstTime );\r\n\r\n\t\tif ( firstTime ) Reload();\r\n\t}\r\n\r\n\tvoid Reload()\r\n\t{\r\n\t\tvar name = Connection.Local?.DisplayName;\r\n\t\tif ( string.IsNullOrWhiteSpace( name ) ) name = \"You\";\r\n\r\n\t\t_ = Leaderboard.Refresh( Game?.TreeHeight ?? 0, name );\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tstatic string RankClass( int rank ) => rank switch\r\n\t{\r\n\t\t1 => \"gold\",\r\n\t\t2 => \"silver\",\r\n\t\t3 => \"bronze\",\r\n\t\t_ => string.Empty\r\n\t};\r\n\r\n\tvoid Close() => OnClose?.Invoke();\r\n\r\n\tprotected override int BuildHash() => HashCode.Combine(\r\n\t\tLeaderboard.Entries.Count, Leaderboard.Fetching, Leaderboard.Status, Game?.TreeHeight ?? 0 );\r\n}\r\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "styles/form/_dropdown.scss",
            "FileName": "_dropdown.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "\r\n$primary: red !default;\r\n$primary-alt: white !default;\r\n\r\n$switch-padding: 6px !default;\r\n\r\n.button.popupbutton.dropdown\r\n{\r\n\tcursor: pointer;\r\n\ttransition: all .1s ease-out;\r\n\tposition: relative;\r\n\r\n\t> .dropdown_indicator\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tright: 8px;\r\n\t}\r\n\r\n\t&.open\r\n\t{\r\n\t\tborder-bottom-left-radius: 1px;\r\n\t\tborder-bottom-right-radius: 1px;\r\n\t\ttransition: border-radius 0.2s ease-out;\r\n\t}\r\n}\r\n\r\nselect\r\n{\r\n\tmin-height: 40px;\r\n\r\n\t> option\r\n\t{\r\n\t\tdisplay: none;\r\n\t}\r\n}"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "ui/controlsheet/controlsheetgroupheader.cs.scss",
            "FileName": "controlsheetgroupheader.cs.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "ControlSheetGroupHeader\r\n{\r\n\tfont-size: 1.33rem;\r\n\tcolor: red;\r\n\tgap: 2px;\r\n\talign-items: center;\r\n\r\n\t&.hidden\r\n\t{\r\n\t\tdisplay: none;\r\n\t}\r\n\r\n\t> .title\r\n\t{\r\n\t\tfont-weight: 600;\r\n\t}\r\n\r\n\t&.has-toggle\r\n\t{\r\n\t\tcursor: pointer;\r\n\t\topacity: 0.8;\r\n\r\n\t\t&:before\r\n\t\t{\r\n\t\t\tcontent: ' ';\r\n\t\t\twidth: 22px;\r\n\t\t\theight: 22px;\r\n\t\t\tbackground-color: #000a;\r\n\t\t\talign-items: center;\r\n\t\t\tjustify-content: center;\r\n\t\t\ttext-align: center;\r\n\t\t\tborder-radius: 5px;\r\n\t\t\tborder: 1px solid #555;\r\n\t\t}\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t\topacity: 1;\r\n\r\n\t\t\t&:before\r\n\t\t\t{\r\n\t\t\t\tborder-color: #888;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t&.checked\r\n\t\t{\r\n\t\t\t> .title\r\n\t\t\t{\r\n\t\t\t\tcolor: white;\r\n\t\t\t}\r\n\r\n\t\t\t&:before\r\n\t\t\t{\r\n\t\t\t\tcontent: '\u2713';\r\n\t\t\t\tfont-weight: bold;\r\n\t\t\t\tcolor: #08f;\r\n\t\t\t\tborder-color: #08f;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "ui/dropdown.cs.scss",
            "FileName": "dropdown.cs.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": ".dropdown\r\n{\r\n\tgap: 2px;\r\n\tflex-grow: 1;\r\n\tcursor: pointer;\r\n\tjustify-content: flex-end;\r\n\talign-items: center;\r\n\tpadding: 0px 12px;\r\n\r\n\t.button-right-column\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "ui/gamehud.razor.scss",
            "FileName": "gamehud.razor.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "$sand: #e8bd7a;\r\n$sand-dark: #cf9a4c;\r\n$panel: rgba( 18, 22, 32, 0.72 );\r\n$gold: #f5c542;\r\n$water: #4fb0e8;\r\n\r\n.hud\r\n{\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tleft: 0;\r\n\twidth: 100%;\r\n\theight: 100%;\r\n\tflex-direction: column;\r\n\tfont-family: Poppins;\r\n\tcolor: white;\r\n\toverflow: hidden;\r\n\r\n\t// s&box does not inherit pointer-events down the tree and the screen root starts\r\n\t// as \"none\", so every clickable element sets \"all\" for itself. Setting it here\r\n\t// only would leave the whole UI dead to the mouse.\r\n\tpointer-events: all;\r\n}\r\n\r\n// ---------------------------------------------------------------- background\r\n\r\n// Backdrop layers never take the mouse - they sit under the whole screen and would\r\n// otherwise swallow clicks meant for the pump.\r\n.sky,\r\n.sun,\r\n.dune,\r\n.floor\r\n{\r\n\tpointer-events: none;\r\n}\r\n\r\n// The rows that hold clickable things, in case the engine wants the whole ancestor\r\n// chain live rather than inheriting from .hud.\r\n.topbar,\r\n.stage,\r\n.actions,\r\n.collection\r\n{\r\n\tpointer-events: all;\r\n}\r\n\r\n.sky\r\n{\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tleft: 0;\r\n\twidth: 100%;\r\n\theight: 100%;\r\n\tbackground-image: linear-gradient( to bottom, #4aa3d8 0%, #8fd0ea 38%, #f4d69c 70%, #e9b264 100% );\r\n}\r\n\r\n.sun\r\n{\r\n\tposition: absolute;\r\n\ttop: 70px;\r\n\tright: 160px;\r\n\twidth: 170px;\r\n\theight: 170px;\r\n\tborder-radius: 100%;\r\n\tbackground-color: #ffd84d;\r\n\tbox-shadow: 0px 0px 90px 30px rgba( 255, 216, 77, 0.35 );\r\n}\r\n\r\n// Wide flat ellipses - only the top curve shows, which reads as a dune ridge.\r\n.dune\r\n{\r\n\tposition: absolute;\r\n\tborder-radius: 50%;\r\n\r\n\t&.far\r\n\t{\r\n\t\tleft: -34%;\r\n\t\twidth: 105%;\r\n\t\tbottom: 14%;\r\n\t\theight: 26%;\r\n\t\tbackground-color: $sand;\r\n\t}\r\n\r\n\t&.near\r\n\t{\r\n\t\tleft: 28%;\r\n\t\twidth: 105%;\r\n\t\tbottom: 12%;\r\n\t\theight: 24%;\r\n\t\tbackground-color: $sand;\r\n\t}\r\n}\r\n\r\n// A very wide ellipse, so the horizon is a gentle curve. As a plain rectangle its\r\n// top edge cut a hard straight line across the sky wherever no dune covered it.\r\n.floor\r\n{\r\n\tposition: absolute;\r\n\tbottom: -34%;\r\n\tleft: -25%;\r\n\twidth: 150%;\r\n\theight: 70%;\r\n\tborder-radius: 50%;\r\n\tbackground-color: $sand-dark;\r\n}\r\n\r\n// ---------------------------------------------------------------- top bar\r\n\r\n.topbar\r\n{\r\n\theight: 96px;\r\n\talign-items: center;\r\n\tpadding-left: 28px;\r\n\tpadding-right: 28px;\r\n\tflex-shrink: 0;\r\n}\r\n\r\n.stat\r\n{\r\n\tbackground-color: $panel;\r\n\tborder-radius: 18px;\r\n\tpadding: 10px 18px;\r\n\talign-items: center;\r\n\tmargin-right: 16px;\r\n}\r\n\r\n.coins\r\n{\r\n\t.coin\r\n\t{\r\n\t\twidth: 32px;\r\n\t\theight: 32px;\r\n\t\tborder-radius: 100%;\r\n\t\tbackground-color: $gold;\r\n\t\tborder: 3px solid #a8791a;\r\n\t\tmargin-right: 10px;\r\n\t}\r\n\r\n\t.value\r\n\t{\r\n\t\tfont-size: 30px;\r\n\t\tfont-weight: 700;\r\n\t}\r\n}\r\n\r\n.tankstat\r\n{\r\n\t.tank\r\n\t{\r\n\t\twidth: 26px;\r\n\t\theight: 54px;\r\n\t\tborder-radius: 8px;\r\n\t\tbackground-color: rgba( 255, 255, 255, 0.16 );\r\n\t\tborder: 2px solid rgba( 255, 255, 255, 0.45 );\r\n\t\tmargin-right: 12px;\r\n\t\tflex-direction: column;\r\n\t\tjustify-content: flex-end;\r\n\t\toverflow: hidden;\r\n\t}\r\n\r\n\t.tank-fill\r\n\t{\r\n\t\twidth: 100%;\r\n\t\tbackground-color: $water;\r\n\t\ttransition: height 0.12s linear;\r\n\t}\r\n\r\n\t.tank-text\r\n\t{\r\n\t\tflex-direction: column;\r\n\t}\r\n\r\n\t.value\r\n\t{\r\n\t\tfont-size: 22px;\r\n\t\tfont-weight: 600;\r\n\t}\r\n\r\n\t.unit\r\n\t{\r\n\t\tfont-size: 15px;\r\n\t\topacity: 0.7;\r\n\t}\r\n\r\n\t.sub\r\n\t{\r\n\t\tfont-size: 13px;\r\n\t\topacity: 0.65;\r\n\t}\r\n\r\n\t&.full .tank-fill\r\n\t{\r\n\t\tbackground-color: #ff8b4a;\r\n\t}\r\n\r\n\t&.full .sub\r\n\t{\r\n\t\tcolor: #ffb37a;\r\n\t\topacity: 1;\r\n\t}\r\n}\r\n\r\n.spacer\r\n{\r\n\tflex-grow: 1;\r\n}\r\n\r\n.iconbutton\r\n{\r\n\twidth: 56px;\r\n\theight: 56px;\r\n\tborder-radius: 14px;\r\n\tbackground-color: $panel;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n\tcursor: pointer;\r\n\tpointer-events: all;\r\n\r\n\ti\r\n\t{\r\n\t\tfont-family: Material Icons;\r\n\t\tfont-size: 30px;\r\n\t}\r\n\r\n\t&:hover\r\n\t{\r\n\t\tbackground-color: rgba( 40, 48, 64, 0.9 );\r\n\t}\r\n\r\n\t&.muted\r\n\t{\r\n\t\tbackground-color: rgba( 200, 110, 40, 0.85 );\r\n\t}\r\n}\r\n\r\n// ---------------------------------------------------------------- stage\r\n\r\n.stage\r\n{\r\n\tflex-grow: 1;\r\n\tflex-direction: column;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n\tposition: relative;\r\n}\r\n\r\n.pumparea\r\n{\r\n\twidth: 330px;\r\n\theight: 330px;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n\tposition: relative;\r\n\tcursor: pointer;\r\n\tpointer-events: all;\r\n\ttransition: transform 0.07s ease-out;\r\n\r\n\t.pumpart\r\n\t{\r\n\t\twidth: 100%;\r\n\t\theight: 100%;\r\n\t}\r\n\r\n\t.glow\r\n\t{\r\n\t\tposition: absolute;\r\n\t\twidth: 78%;\r\n\t\theight: 78%;\r\n\t\tborder-radius: 100%;\r\n\t\tbackground-color: rgba( 255, 255, 255, 0.14 );\r\n\t}\r\n\r\n\t.ground\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tbottom: 2%;\r\n\t\twidth: 70%;\r\n\t\theight: 8%;\r\n\t\tborder-radius: 50%;\r\n\t\tbackground-color: rgba( 90, 60, 20, 0.22 );\r\n\t}\r\n\r\n\t&:hover\r\n\t{\r\n\t\ttransform: scale( 1.03 );\r\n\t}\r\n\r\n\t&.pumping\r\n\t{\r\n\t\ttransform: scale( 0.93 );\r\n\t}\r\n\r\n\t&.full .glow\r\n\t{\r\n\t\tbackground-color: rgba( 255, 140, 60, 0.25 );\r\n\t}\r\n}\r\n\r\n.nameplate\r\n{\r\n\tflex-direction: column;\r\n\talign-items: center;\r\n\tmargin-top: 4px;\r\n\tpadding: 10px 30px 14px 30px;\r\n\tborder-radius: 22px;\r\n\tbackground-color: rgba( 16, 20, 30, 0.38 );\r\n\r\n\t.tierno\r\n\t{\r\n\t\tfont-size: 14px;\r\n\t\tletter-spacing: 3px;\r\n\t\topacity: 1;\r\n\t\ttext-shadow: 0px 2px 0px rgba( 0, 0, 0, 0.6 );\r\n\t}\r\n\r\n\t.name\r\n\t{\r\n\t\tfont-size: 40px;\r\n\t\tfont-weight: 800;\r\n\t\ttext-shadow: 0px 3px 0px rgba( 0, 0, 0, 0.35 );\r\n\t}\r\n\r\n\t.tagline\r\n\t{\r\n\t\tfont-size: 16px;\r\n\t\topacity: 1;\r\n\t\ttext-shadow: 0px 2px 0px rgba( 0, 0, 0, 0.55 );\r\n\t}\r\n\r\n\t.perclick\r\n\t{\r\n\t\tmargin-top: 8px;\r\n\t\tfont-size: 15px;\r\n\t\tbackground-color: rgba( 18, 22, 32, 0.55 );\r\n\t\tpadding: 4px 16px;\r\n\t\tborder-radius: 999px;\r\n\t}\r\n}\r\n\r\n.floater\r\n{\r\n\tposition: absolute;\r\n\tleft: 50%;\r\n\tfont-size: 30px;\r\n\tfont-weight: 800;\r\n\tcolor: #d3f0ff;\r\n\ttext-shadow: 0px 2px 0px rgba( 0, 0, 0, 0.45 );\r\n\tpointer-events: none;\r\n\r\n\t&.coin\r\n\t{\r\n\t\tcolor: $gold;\r\n\t}\r\n}\r\n\r\n.toast\r\n{\r\n\tposition: absolute;\r\n\tbottom: 13%;\r\n\tbackground-color: rgba( 18, 22, 32, 0.85 );\r\n\tpadding: 10px 22px;\r\n\tborder-radius: 999px;\r\n\tfont-size: 18px;\r\n\tpointer-events: none;\r\n}\r\n\r\n.idlenote\r\n{\r\n\tposition: absolute;\r\n\ttop: 5%;\r\n\tbackground-color: rgba( 30, 90, 130, 0.85 );\r\n\tpadding: 8px 20px;\r\n\tborder-radius: 999px;\r\n\tfont-size: 16px;\r\n\tpointer-events: none;\r\n}\r\n\r\n// Scoped to the stage on purpose - as a bare \".hint\" this rule reached into the\r\n// settings panel and threw its sub-labels to the bottom of the screen.\r\n.stage .hint\r\n{\r\n\tposition: absolute;\r\n\tbottom: 4%;\r\n\tfont-size: 14px;\r\n\tletter-spacing: 2px;\r\n\topacity: 1;\r\n\tpadding: 5px 16px;\r\n\tborder-radius: 999px;\r\n\tbackground-color: rgba( 16, 20, 30, 0.38 );\r\n\tpointer-events: none;\r\n}\r\n\r\n// ---------------------------------------------------------------- actions\r\n\r\n.actions\r\n{\r\n\theight: 130px;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n\tflex-shrink: 0;\r\n}\r\n\r\n.bigbutton\r\n{\r\n\twidth: 420px;\r\n\theight: 92px;\r\n\tborder-radius: 22px;\r\n\talign-items: center;\r\n\tpadding: 0px 24px;\r\n\tmargin: 0px 12px;\r\n\tcursor: pointer;\r\n\tpointer-events: all;\r\n\ttransition: transform 0.07s ease-out;\r\n\tborder-bottom: 6px solid rgba( 0, 0, 0, 0.35 );\r\n\r\n\t.btn-icon i\r\n\t{\r\n\t\tfont-family: Material Icons;\r\n\t\tfont-size: 40px;\r\n\t\tmargin-right: 16px;\r\n\t}\r\n\r\n\t.btn-text\r\n\t{\r\n\t\tflex-direction: column;\r\n\t}\r\n\r\n\t.btn-title\r\n\t{\r\n\t\tfont-size: 23px;\r\n\t\tfont-weight: 800;\r\n\t}\r\n\r\n\t.btn-sub\r\n\t{\r\n\t\tfont-size: 16px;\r\n\t\topacity: 0.85;\r\n\t}\r\n\r\n\t&:hover\r\n\t{\r\n\t\ttransform: translateY( -3px );\r\n\t}\r\n\r\n\t&:active\r\n\t{\r\n\t\ttransform: translateY( 2px );\r\n\t}\r\n\r\n\t&.sell\r\n\t{\r\n\t\tbackground-color: #3aa7e0;\r\n\t}\r\n\r\n\t&.buy\r\n\t{\r\n\t\tbackground-color: #f0a92c;\r\n\t}\r\n\r\n\t&.maxed\r\n\t{\r\n\t\tbackground-color: #7a5cc4;\r\n\t\tcursor: default;\r\n\t}\r\n\r\n\t&.disabled\r\n\t{\r\n\t\tbackground-color: #5f6675;\r\n\t\topacity: 0.65;\r\n\t\tcursor: default;\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t\ttransform: translateY( 0px );\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// ---------------------------------------------------------------- collection\r\n\r\n.collection\r\n{\r\n\theight: 170px;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n\tpadding-bottom: 14px;\r\n\tflex-shrink: 0;\r\n}\r\n\r\n.slot\r\n{\r\n\twidth: 118px;\r\n\theight: 142px;\r\n\tmargin: 0px 6px;\r\n\tpadding: 8px;\r\n\tflex-direction: column;\r\n\talign-items: center;\r\n\tjustify-content: flex-end;\r\n\tborder-radius: 16px;\r\n\tbackground-color: rgba( 18, 22, 32, 0.4 );\r\n\tborder: 3px solid rgba( 0, 0, 0, 0 );\r\n\r\n\t.slotart\r\n\t{\r\n\t\twidth: 100%;\r\n\t\theight: 92px;\r\n\t\tposition: relative;\r\n\t\talign-items: center;\r\n\t\tjustify-content: center;\r\n\t}\r\n\r\n\t.question\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tfont-size: 38px;\r\n\t\tfont-weight: 800;\r\n\t\tcolor: rgba( 255, 255, 255, 0.7 );\r\n\t}\r\n\r\n\t.slotlabel\r\n\t{\r\n\t\tmargin-top: 4px;\r\n\t\tfont-size: 12px;\r\n\t\ttext-align: center;\r\n\t\topacity: 0.9;\r\n\t}\r\n\r\n\t&.owned\r\n\t{\r\n\t\tbackground-color: rgba( 42, 92, 60, 0.5 );\r\n\t}\r\n\r\n\t&.next\r\n\t{\r\n\t\tborder-color: $gold;\r\n\t}\r\n}\r\n\r\n// ---------------------------------------------------------------- fallback\r\n\r\n.missing\r\n{\r\n\tposition: absolute;\r\n\ttop: 40%;\r\n\tleft: 50%;\r\n\tmargin-left: -260px;\r\n\twidth: 520px;\r\n\tflex-direction: column;\r\n\talign-items: center;\r\n\tbackground-color: rgba( 18, 22, 32, 0.85 );\r\n\tborder-radius: 18px;\r\n\tpadding: 24px;\r\n\r\n\t.missing-title\r\n\t{\r\n\t\tfont-size: 22px;\r\n\t\tfont-weight: 700;\r\n\t}\r\n\r\n\t.missing-body\r\n\t{\r\n\t\tfont-size: 15px;\r\n\t\topacity: 0.8;\r\n\t\tmargin-top: 6px;\r\n\t}\r\n}\r\n\r\n// ---------------------------------------------------------------- scenery\r\n\r\n// Extra layers the default scene doesn't use - each background switches them on.\r\n.stars,\r\n.scenery\r\n{\r\n\tposition: absolute;\r\n\tpointer-events: none;\r\n}\r\n\r\n.stars\r\n{\r\n\ttop: 0;\r\n\tleft: 0;\r\n\twidth: 100%;\r\n\theight: 60%;\r\n\topacity: 0;\r\n}\r\n\r\n.scenery\r\n{\r\n\tbottom: 0;\r\n\tleft: 0;\r\n\twidth: 100%;\r\n\theight: 40%;\r\n\topacity: 0;\r\n}\r\n\r\n// ---- Blood Moon: night dunes under something large and red\r\n\r\n.scene-dusk\r\n{\r\n\t.sky { background-image: linear-gradient( to bottom, #150a24 0%, #241436 40%, #5c2536 78%, #8c3a35 100% ); }\r\n\r\n\t.sun\r\n\t{\r\n\t\tbackground-color: #d94f43;\r\n\t\tbox-shadow: 0px 0px 120px 40px rgba( 217, 79, 67, 0.4 );\r\n\t\twidth: 210px;\r\n\t\theight: 210px;\r\n\t}\r\n\r\n\t.dune.far { background-color: #45283f; }\r\n\t.dune.near { background-color: #45283f; }\r\n\t.floor { background-color: #2c1a2c; }\r\n\r\n\t// a scatter of stars, faked with a few hard-stop gradients\r\n\t.stars\r\n\t{\r\n\t\topacity: 0.9;\r\n\t\tbackground-image: linear-gradient( to bottom, rgba( 255, 255, 255, 0.16 ) 0%, rgba( 255, 255, 255, 0 ) 70% );\r\n\t}\r\n\r\n\t.nameplate .name { text-shadow: 0px 3px 0px rgba( 0, 0, 0, 0.55 ); }\r\n}\r\n\r\n// ---- Green Oasis: you pumped enough that things started growing\r\n\r\n.scene-oasis\r\n{\r\n\t.sky { background-image: linear-gradient( to bottom, #2f8fbf 0%, #6fc6c2 45%, #a9d98d 80%, #6ea84e 100% ); }\r\n\r\n\t.sun\r\n\t{\r\n\t\tbackground-color: #fff2a8;\r\n\t\tbox-shadow: 0px 0px 100px 34px rgba( 255, 242, 168, 0.45 );\r\n\t}\r\n\r\n\t.dune.far { background-color: #67ab55; }\r\n\t.dune.near { background-color: #4f8f3a; }\r\n\t.floor { background-color: #3d7330; }\r\n\r\n\t.scenery\r\n\t{\r\n\t\topacity: 1;\r\n\t\tbackground-image: linear-gradient( to top, rgba( 40, 90, 40, 0.55 ) 0%, rgba( 40, 90, 40, 0 ) 55% );\r\n\t}\r\n}\r\n\r\n// ---- Neon Wastes: the desert, some time after everyone left\r\n\r\n.scene-neon\r\n{\r\n\t.sky { background-image: linear-gradient( to bottom, #12042a 0%, #2a0b45 42%, #7b2062 76%, #ff5fa2 100% ); }\r\n\r\n\t.sun\r\n\t{\r\n\t\tbackground-color: #ff7ac6;\r\n\t\tbox-shadow: 0px 0px 140px 46px rgba( 255, 95, 162, 0.45 );\r\n\t\twidth: 220px;\r\n\t\theight: 220px;\r\n\t}\r\n\r\n\t.dune.far { background-color: #35114f; }\r\n\t.dune.near { background-color: #24093a; }\r\n\t.floor { background-color: #160526; }\r\n\r\n\t.stars\r\n\t{\r\n\t\topacity: 0.8;\r\n\t\tbackground-image: linear-gradient( to bottom, rgba( 255, 255, 255, 0.12 ) 0%, rgba( 255, 255, 255, 0 ) 65% );\r\n\t}\r\n\r\n\t// horizon glow standing in for a synth grid\r\n\t.scenery\r\n\t{\r\n\t\topacity: 1;\r\n\t\tbackground-image: linear-gradient( to top, rgba( 255, 95, 162, 0.35 ) 0%, rgba( 255, 95, 162, 0 ) 40% );\r\n\t}\r\n\r\n\t.nameplate .name { color: #ffd9ec; }\r\n}\r\n\r\n// ---------------------------------------------------------------- tier bar\r\n\r\n.tierbar\r\n{\r\n\twidth: 260px;\r\n\theight: 8px;\r\n\tborder-radius: 999px;\r\n\tbackground-color: rgba( 0, 0, 0, 0.45 );\r\n\tborder: 1px solid rgba( 255, 255, 255, 0.25 );\r\n\tmargin-top: 10px;\r\n\toverflow: hidden;\r\n}\r\n\r\n.tierfill\r\n{\r\n\theight: 100%;\r\n\tbackground-color: $gold;\r\n\ttransition: width 0.2s ease-out;\r\n}\r\n\r\n// ---------------------------------------------------------------- gusher\r\n\r\n.gusher\r\n{\r\n\tposition: absolute;\r\n\twidth: 96px;\r\n\theight: 96px;\r\n\tmargin-left: -48px;\r\n\tmargin-top: -48px;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n\tcursor: pointer;\r\n\tpointer-events: all;\r\n\tanimation: gusher-bob 1.6s ease-in-out infinite;\r\n\r\n\t// Teardrop rather than a disc, and blue-white rather than yellow - as a glowing\r\n\t// yellow circle it was indistinguishable from the sun behind it.\r\n\t.gusher-drop\r\n\t{\r\n\t\twidth: 46px;\r\n\t\theight: 58px;\r\n\t\tborder-radius: 50% 50% 50% 50%;\r\n\t\tbackground-image: linear-gradient( to bottom, #eaf9ff, #4fb0e8 );\r\n\t\tborder: 3px solid #ffffff;\r\n\t\tbox-shadow: 0px 0px 34px 10px rgba( 79, 176, 232, 0.6 );\r\n\t}\r\n\r\n\t.gusher-ring\r\n\t{\r\n\t\tposition: absolute;\r\n\t\twidth: 92px;\r\n\t\theight: 92px;\r\n\t\tborder-radius: 100%;\r\n\t\tborder: 3px solid rgba( 160, 225, 255, 0.7 );\r\n\t\tanimation: gusher-pulse 1.6s ease-out infinite;\r\n\t}\r\n\r\n\t&:hover .gusher-drop { transform: scale( 1.12 ); }\r\n}\r\n\r\n@keyframes gusher-bob\r\n{\r\n\t0% { transform: translateY( 0px ); }\r\n\t50% { transform: translateY( -12px ); }\r\n\t100% { transform: translateY( 0px ); }\r\n}\r\n\r\n@keyframes gusher-pulse\r\n{\r\n\t0% { transform: scale( 0.7 ); opacity: 0.9; }\r\n\t100% { transform: scale( 1.25 ); opacity: 0; }\r\n}\r\n\r\n// ---------------------------------------------------------------- daily\r\n\r\n.daily\r\n{\r\n\tposition: absolute;\r\n\ttop: 110px;\r\n\tleft: 28px;\r\n\talign-items: center;\r\n\tbackground-color: rgba( 24, 30, 44, 0.9 );\r\n\tborder: 3px solid $gold;\r\n\tborder-radius: 18px;\r\n\tpadding: 12px 20px 12px 14px;\r\n\tcursor: pointer;\r\n\tpointer-events: all;\r\n\tanimation: daily-glow 2.2s ease-in-out infinite;\r\n\r\n\t.daily-icon\r\n\t{\r\n\t\tmargin-right: 12px;\r\n\r\n\t\ti\r\n\t\t{\r\n\t\t\tfont-family: Material Icons;\r\n\t\t\tfont-size: 34px;\r\n\t\t\tcolor: $gold;\r\n\t\t}\r\n\t}\r\n\r\n\t.daily-text { flex-direction: column; }\r\n\t.daily-title { font-size: 16px; font-weight: 800; letter-spacing: 1px; }\r\n\t.daily-sub { font-size: 13px; opacity: 0.8; }\r\n\r\n\t&:hover { transform: scale( 1.03 ); }\r\n}\r\n\r\n@keyframes daily-glow\r\n{\r\n\t0% { box-shadow: 0px 0px 0px 0px rgba( 245, 197, 66, 0.5 ); }\r\n\t50% { box-shadow: 0px 0px 26px 4px rgba( 245, 197, 66, 0.45 ); }\r\n\t100% { box-shadow: 0px 0px 0px 0px rgba( 245, 197, 66, 0.5 ); }\r\n}\r\n\r\n// ---------------------------------------------------------------- the tree\r\n\r\n.treearea\r\n{\r\n\tposition: absolute;\r\n\tleft: 34px;\r\n\tbottom: 14%;\r\n\talign-items: flex-end;\r\n\tpointer-events: all;\r\n}\r\n\r\n.tree\r\n{\r\n\twidth: 96px;\r\n\theight: 150px;\r\n\tposition: relative;\r\n\tcursor: pointer;\r\n\tpointer-events: all;\r\n\ttransition: all 0.25s ease-out;\r\n\r\n\t.mound\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tbottom: 0;\r\n\t\tleft: 12%;\r\n\t\twidth: 76%;\r\n\t\theight: 12%;\r\n\t\tborder-radius: 50%;\r\n\t\tbackground-color: rgba( 90, 60, 20, 0.35 );\r\n\t}\r\n\r\n\t.trunk\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tbottom: 8%;\r\n\t\tleft: 44%;\r\n\t\twidth: 12%;\r\n\t\theight: 26%;\r\n\t\tborder-radius: 4px;\r\n\t\tbackground-color: #7a5230;\r\n\t\ttransition: all 0.25s ease-out;\r\n\t}\r\n\r\n\t.canopy\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tborder-radius: 100%;\r\n\t\tbackground-color: #4f9a45;\r\n\t\topacity: 0;\r\n\t\ttransition: all 0.25s ease-out;\r\n\r\n\t\t&.c1 { left: 26%; bottom: 30%; width: 48%; height: 26%; }\r\n\t\t&.c2 { left: 14%; bottom: 42%; width: 40%; height: 24%; background-color: #5cae50; }\r\n\t\t&.c3 { left: 44%; bottom: 44%; width: 42%; height: 26%; background-color: #438a3b; }\r\n\t}\r\n\r\n\t// A sapling at zero, a full crown by stage 6. Stage 0 still needs to read as a\r\n\t// plant - as a bare stub it was invisible against the sand.\r\n\t&.stage-0\r\n\t{\r\n\t\t.trunk { height: 24%; }\r\n\t\t.canopy.c1 { opacity: 1; transform: scale( 0.34 ); }\r\n\t}\r\n\t&.stage-1 { .trunk { height: 20%; } .canopy.c1 { opacity: 1; transform: scale( 0.6 ); } }\r\n\t&.stage-2 { .trunk { height: 26%; } .canopy.c1 { opacity: 1; transform: scale( 0.8 ); } }\r\n\t&.stage-3 { .trunk { height: 32%; } .canopy { opacity: 1; transform: scale( 0.85 ); } }\r\n\t&.stage-4 { .trunk { height: 38%; } .canopy { opacity: 1; transform: scale( 1 ); } }\r\n\t&.stage-5 { .trunk { height: 44%; } .canopy { opacity: 1; transform: scale( 1.12 ); } }\r\n\t&.stage-6 { .trunk { height: 50%; } .canopy { opacity: 1; transform: scale( 1.25 ); } }\r\n\r\n\t&:hover { transform: scale( 1.05 ); }\r\n\r\n\t&.growing .canopy { animation: tree-sway 3s ease-in-out infinite; }\r\n}\r\n\r\n@keyframes tree-sway\r\n{\r\n\t0% { transform: translateX( -3px ); }\r\n\t50% { transform: translateX( 3px ); }\r\n\t100% { transform: translateX( -3px ); }\r\n}\r\n\r\n.treeinfo\r\n{\r\n\tflex-direction: column;\r\n\tmargin-left: 10px;\r\n\tmargin-bottom: 8px;\r\n\tbackground-color: rgba( 18, 22, 32, 0.7 );\r\n\tborder-radius: 14px;\r\n\tpadding: 10px 12px;\r\n\r\n\t.treeheight\r\n\t{\r\n\t\tfont-size: 15px;\r\n\t\tfont-weight: 700;\r\n\t\talign-items: center;\r\n\r\n\t\ti\r\n\t\t{\r\n\t\t\tfont-family: Material Icons;\r\n\t\t\tfont-size: 18px;\r\n\t\t\tmargin-right: 6px;\r\n\t\t\tcolor: #7fd06f;\r\n\t\t}\r\n\t}\r\n\r\n\t.treebar\r\n\t{\r\n\t\twidth: 130px;\r\n\t\theight: 7px;\r\n\t\tborder-radius: 999px;\r\n\t\tbackground-color: rgba( 0, 0, 0, 0.35 );\r\n\t\tmargin-top: 6px;\r\n\t\toverflow: hidden;\r\n\t}\r\n\r\n\t.treefill\r\n\t{\r\n\t\theight: 100%;\r\n\t\tbackground-color: #7fd06f;\r\n\t\ttransition: width 0.2s ease-out;\r\n\t}\r\n\r\n\t.treestatus\r\n\t{\r\n\t\tfont-size: 12px;\r\n\t\topacity: 0.75;\r\n\t\tmargin-top: 4px;\r\n\r\n\t\t&.growing { color: #7fd06f; opacity: 1; }\r\n\t}\r\n\r\n\t.treebtn\r\n\t{\r\n\t\tmargin-top: 8px;\r\n\t\tpadding: 6px 12px;\r\n\t\tborder-radius: 10px;\r\n\t\tbackground-color: #4f9a45;\r\n\t\tfont-size: 13px;\r\n\t\tfont-weight: 700;\r\n\t\talign-items: center;\r\n\t\tjustify-content: center;\r\n\t\tcursor: pointer;\r\n\t\tpointer-events: all;\r\n\r\n\t\ti\r\n\t\t{\r\n\t\t\tfont-family: Material Icons;\r\n\t\t\tfont-size: 16px;\r\n\t\t\tmargin-right: 6px;\r\n\t\t}\r\n\r\n\t\t&:hover { background-color: #5cae50; }\r\n\r\n\t\t&.board { background-color: #3aa7e0; }\r\n\t\t&.board:hover { background-color: #4bb8f0; }\r\n\r\n\t\t&.disabled\r\n\t\t{\r\n\t\t\tbackground-color: #5f6675;\r\n\t\t\topacity: 0.6;\r\n\t\t\tcursor: default;\r\n\t\t}\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "UI/GameHud.razor",
            "FileName": "GameHud.razor",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "@using System\r\n@using System.Globalization\r\n@using Sandbox\r\n@using Sandbox.UI\r\n@namespace DesertPump\r\n@inherits PanelComponent\r\n\r\n<div class=\"hud @SceneClass\">\r\n\r\n\t<div class=\"sky\"></div>\r\n\t<div class=\"sun\"></div>\r\n\t<div class=\"stars\"></div>\r\n\t<div class=\"floor\"></div>\r\n\t<div class=\"dune far\"></div>\r\n\t<div class=\"dune near\"></div>\r\n\t<div class=\"scenery\"></div>\r\n\r\n\t@if ( State is null )\r\n\t{\r\n\t\t<div class=\"missing\">\r\n\t\t\t<div class=\"missing-title\">No Desert Pump Game in the scene</div>\r\n\t\t\t<div class=\"missing-body\">Add a \"Desert Pump Game\" component to any GameObject.</div>\r\n\t\t</div>\r\n\t}\r\n\telse\r\n\t{\r\n\t\t<div class=\"topbar\">\r\n\r\n\t\t\t<div class=\"stat coins\">\r\n\t\t\t\t<div class=\"coin\"></div>\r\n\t\t\t\t<div class=\"value\">@Numbers.Short( State.Coins )</div>\r\n\t\t\t</div>\r\n\r\n\t\t\t<div class=\"stat tankstat @( State.TankFull ? \"full\" : \"\" )\">\r\n\t\t\t\t<div class=\"tank\">\r\n\t\t\t\t\t<div class=\"tank-fill\" style=\"@FillStyle()\"></div>\r\n\t\t\t\t</div>\r\n\t\t\t\t<div class=\"tank-text\">\r\n\t\t\t\t\t<div class=\"value\">@Numbers.Short( State.Water )<span class=\"unit\"> / @Numbers.Short( State.TankCapacity )L</span></div>\r\n\t\t\t\t\t<div class=\"sub\">@RateText()</div>\r\n\t\t\t\t</div>\r\n\t\t\t</div>\r\n\r\n\t\t\t<div class=\"spacer\"></div>\r\n\r\n\t\t\t<div class=\"iconbutton shopbtn @( AnyUpgradeAffordable ? \"highlight\" : \"\" )\" onclick=\"@OpenShop\">\r\n\t\t\t\t<i>storefront</i>\r\n\t\t\t</div>\r\n\r\n\t\t\t<div class=\"iconbutton @( State.Muted ? \"muted\" : \"\" )\" onclick=\"@ToggleMute\">\r\n\t\t\t\t<i>@( State.Muted ? \"volume_off\" : \"volume_up\" )</i>\r\n\t\t\t</div>\r\n\r\n\t\t\t<div class=\"iconbutton\" onclick=\"@OpenSettings\">\r\n\t\t\t\t<i>settings</i>\r\n\t\t\t</div>\r\n\r\n\t\t</div>\r\n\r\n\t\t<div class=\"stage\">\r\n\r\n\t\t\t<div class=\"pumparea @PumpAreaClass()\" onclick=\"@DoPump\">\r\n\t\t\t\t<div class=\"glow\"></div>\r\n\t\t\t\t<PumpArt TierIndex=\"@State.PumpLevel\" Artwork=\"@ArtworkFor( State.PumpLevel )\"></PumpArt>\r\n\t\t\t\t<div class=\"ground\"></div>\r\n\t\t\t</div>\r\n\r\n\t\t\t<div class=\"nameplate\">\r\n\t\t\t\t<div class=\"tierno\">@State.CurrentPump.Position &middot; @State.Tier.Name.ToUpper()</div>\r\n\t\t\t\t<div class=\"name\">@State.CurrentPump.Name</div>\r\n\t\t\t\t<div class=\"tagline\">@State.Tier.Tagline</div>\r\n\t\t\t\t<div class=\"tierbar\">\r\n\t\t\t\t\t<div class=\"tierfill\" style=\"@TierFillStyle()\"></div>\r\n\t\t\t\t</div>\r\n\t\t\t\t<div class=\"perclick\">+@Numbers.Short( State.LitresPerClick )L per pump - @Numbers.Short( State.PricePerLitre ) @Numbers.Plural( State.PricePerLitre, \"coin\", \"coins\" ) per litre</div>\r\n\t\t\t</div>\r\n\r\n\t\t\t@foreach ( var f in floaters )\r\n\t\t\t{\r\n\t\t\t\t<div class=\"floater @( f.Coin ? \"coin\" : \"\" )\" style=\"@FloaterStyle( f )\">@f.Text</div>\r\n\t\t\t}\r\n\r\n\t\t\t@if ( ShowToast )\r\n\t\t\t{\r\n\t\t\t\t<div class=\"toast\">@toast</div>\r\n\t\t\t}\r\n\r\n\t\t\t@if ( ShowIdleNote )\r\n\t\t\t{\r\n\t\t\t\t<div class=\"idlenote\">The pump made @Numbers.Short( idleNotice )L while you were away</div>\r\n\t\t\t}\r\n\r\n\t\t\t@if ( State.GusherActive )\r\n\t\t\t{\r\n\t\t\t\t<div class=\"gusher\" style=\"@GusherStyle()\" onclick=\"@ClaimGusher\">\r\n\t\t\t\t\t<div class=\"gusher-drop\"></div>\r\n\t\t\t\t\t<div class=\"gusher-ring\"></div>\r\n\t\t\t\t</div>\r\n\t\t\t}\r\n\r\n\t\t\t<div class=\"treearea\">\r\n\r\n\t\t\t\t<div class=\"tree stage-@State.TreeArtStage @( State.TreeIsGrowing ? \"growing\" : \"\" )\"\r\n\t\t\t\t\t onclick=\"@DoWaterTree\">\r\n\t\t\t\t\t<div class=\"trunk\"></div>\r\n\t\t\t\t\t<div class=\"canopy c1\"></div>\r\n\t\t\t\t\t<div class=\"canopy c2\"></div>\r\n\t\t\t\t\t<div class=\"canopy c3\"></div>\r\n\t\t\t\t\t<div class=\"mound\"></div>\r\n\t\t\t\t</div>\r\n\r\n\t\t\t\t<div class=\"treeinfo\">\r\n\t\t\t\t\t<div class=\"treeheight\"><i>park</i> @State.TreeHeight @( State.TreeHeight == 1 ? \"stage\" : \"stages\" )</div>\r\n\r\n\t\t\t\t\t@if ( State.TreeIsGrowing )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\t<div class=\"treestatus growing\">Growing - @State.TreeTimeLeft</div>\r\n\t\t\t\t\t}\r\n\t\t\t\t\telse\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\t<div class=\"treebar\">\r\n\t\t\t\t\t\t\t<div class=\"treefill\" style=\"@TreeFillStyle()\"></div>\r\n\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t<div class=\"treestatus\">@Numbers.Short( State.TreeWater )/@Numbers.Short( State.TreeWaterNeeded )L</div>\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\t<div class=\"treebtn @( State.CanWaterTree ? \"\" : \"disabled\" )\" onclick=\"@DoWaterTree\">\r\n\t\t\t\t\t\t<i>water_drop</i> WATER\r\n\t\t\t\t\t</div>\r\n\r\n\t\t\t\t\t<div class=\"treebtn board\" onclick=\"@OpenTree\">\r\n\t\t\t\t\t\t<i>leaderboard</i> BOARD\r\n\t\t\t\t\t</div>\r\n\t\t\t\t</div>\r\n\r\n\t\t\t</div>\r\n\r\n\t\t\t<div class=\"hint\">Click the pump or press SPACE</div>\r\n\r\n\t\t</div>\r\n\r\n\t\t@if ( State.CanClaimDaily )\r\n\t\t{\r\n\t\t\t<div class=\"daily\" onclick=\"@ClaimDaily\">\r\n\t\t\t\t<div class=\"daily-icon\"><i>redeem</i></div>\r\n\t\t\t\t<div class=\"daily-text\">\r\n\t\t\t\t\t<div class=\"daily-title\">DAILY BONUS READY</div>\r\n\t\t\t\t\t<div class=\"daily-sub\">@Numbers.Short( State.DailyReward ) coins &middot; day @State.ProjectedDailyStreak streak</div>\r\n\t\t\t\t</div>\r\n\t\t\t</div>\r\n\t\t}\r\n\r\n\t\t<div class=\"actions\">\r\n\r\n\t\t\t<div class=\"bigbutton sell @( State.CanSell ? \"\" : \"disabled\" )\" onclick=\"@DoSell\">\r\n\t\t\t\t<div class=\"btn-icon\"><i>local_drink</i></div>\r\n\t\t\t\t<div class=\"btn-text\">\r\n\t\t\t\t\t<div class=\"btn-title\">SELL WATER</div>\r\n\t\t\t\t\t<div class=\"btn-sub\">+@Numbers.Short( State.SaleValue ) coins</div>\r\n\t\t\t\t</div>\r\n\t\t\t</div>\r\n\r\n\t\t\t@if ( State.IsMaxed )\r\n\t\t\t{\r\n\t\t\t\t<div class=\"bigbutton buy maxed\">\r\n\t\t\t\t\t<div class=\"btn-icon\"><i>emoji_events</i></div>\r\n\t\t\t\t\t<div class=\"btn-text\">\r\n\t\t\t\t\t\t<div class=\"btn-title\">ALL PUMPS OWNED</div>\r\n\t\t\t\t\t\t<div class=\"btn-sub\">the desert is yours</div>\r\n\t\t\t\t\t</div>\r\n\t\t\t\t</div>\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\t<div class=\"bigbutton buy @( State.CanUpgrade ? \"\" : \"disabled\" )\" onclick=\"@DoBuy\">\r\n\t\t\t\t\t<div class=\"btn-icon\"><i>upgrade</i></div>\r\n\t\t\t\t\t<div class=\"btn-text\">\r\n\t\t\t\t\t\t<div class=\"btn-title\">BUY @State.NextPump.Name.ToUpper()</div>\r\n\t\t\t\t\t\t<div class=\"btn-sub\">@Numbers.Short( State.NextPump.Cost ) coins</div>\r\n\t\t\t\t\t</div>\r\n\t\t\t\t</div>\r\n\t\t\t}\r\n\r\n\t\t</div>\r\n\r\n\t\t<div class=\"collection\">\r\n\t\t\t@foreach ( var pump in VisiblePumps )\r\n\t\t\t{\r\n\t\t\t\t<div class=\"slot @SlotClass( pump )\">\r\n\t\t\t\t\t<div class=\"slotart\">\r\n\t\t\t\t\t\t<PumpArt TierIndex=\"@pump.Index\"\r\n\t\t\t\t\t\t\t\t Silhouette=\"@( pump.Index > State.PumpLevel )\"\r\n\t\t\t\t\t\t\t\t Artwork=\"@ArtworkFor( pump.Index )\"></PumpArt>\r\n\t\t\t\t\t\t@if ( pump.Index > State.PumpLevel )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t<div class=\"question\">?</div>\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t</div>\r\n\t\t\t\t\t<div class=\"slotlabel\">@SlotLabel( pump )</div>\r\n\t\t\t\t</div>\r\n\t\t\t}\r\n\t\t</div>\r\n\r\n\t\t@if ( showShop )\r\n\t\t{\r\n\t\t\t<ShopPanel OnClose=\"@CloseOverlays\"></ShopPanel>\r\n\t\t}\r\n\r\n\t\t@if ( showSettings )\r\n\t\t{\r\n\t\t\t<SettingsPanel OnClose=\"@CloseOverlays\"></SettingsPanel>\r\n\t\t}\r\n\r\n\t\t@if ( showTree )\r\n\t\t{\r\n\t\t\t<TreePanel OnClose=\"@CloseOverlays\"></TreePanel>\r\n\t\t}\r\n\t}\r\n\r\n</div>\r\n\r\n@code\r\n{\r\n\t/// <summary>Optional - left empty, the HUD finds the game itself.</summary>\r\n\t[Property] public DesertPumpGame Target { get; set; }\r\n\r\n\t/// <summary>\r\n\t/// The real pump artwork, one texture per tier in the same order as\r\n\t/// <see cref=\"PumpTier.All\"/>. Any slot left empty falls back to placeholder art.\r\n\t/// </summary>\r\n\t[Property] public List<Texture> PumpArtwork { get; set; } = new();\r\n\r\n\tDesertPumpGame State => Target.IsValid() ? Target : DesertPumpGame.Current;\r\n\r\n\tconst float FloaterLife = 1.1f;\r\n\tconst float ToastLife = 2.2f;\r\n\tconst float IdleNoticeLife = 8f;\r\n\tconst float PumpAnimLife = 0.16f;\r\n\r\n\tclass Floater\r\n\t{\r\n\t\tpublic string Text;\r\n\t\tpublic float X;\r\n\t\tpublic float Drift;\r\n\t\tpublic bool Coin;\r\n\t\tpublic RealTimeSince Age;\r\n\t}\r\n\r\n\treadonly List<Floater> floaters = new();\r\n\r\n\tstring toast;\r\n\tRealTimeSince toastAge;\r\n\r\n\tdouble idleNotice;\r\n\tRealTimeSince idleAge;\r\n\r\n\tRealTimeSince timeSincePump;\r\n\r\n\t/// <summary>Bumped every frame so floaters get rebuilt while they're moving.</summary>\r\n\tint animTick;\r\n\r\n\tbool hooked;\r\n\r\n\tbool showShop;\r\n\tbool showSettings;\r\n\tbool showTree;\r\n\r\n\t/// <summary>Puts a nudge on the shop button when something in there is affordable.</summary>\r\n\tbool AnyUpgradeAffordable =>\r\n\t\tState is not null && UpgradeTrack.All.Any( x => State.CanBuy( x.Kind ) );\r\n\r\n\t/// <summary>\r\n\t/// The strip along the bottom shows the tier you're in, not all 108 pumps - nine\r\n\t/// slots you can actually finish is a far better carrot than a hundred you can't.\r\n\t/// </summary>\r\n\tIEnumerable<PumpModel> VisiblePumps\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tif ( State is null ) return Enumerable.Empty<PumpModel>();\r\n\r\n\t\t\tvar first = State.Tier.FirstPumpIndex;\r\n\t\t\treturn PumpModel.All.Skip( first ).Take( PumpTier.PumpsPerTier );\r\n\t\t}\r\n\t}\r\n\r\n\tstring SceneClass => $\"scene-{State?.SelectedBackground ?? BackgroundStyle.DefaultId}\";\r\n\r\n\tvoid OpenShop()\r\n\t{\r\n\t\tshowShop = !showShop;\r\n\t\tshowSettings = false;\r\n\t}\r\n\r\n\tvoid OpenSettings()\r\n\t{\r\n\t\tshowSettings = !showSettings;\r\n\t\tshowShop = false;\r\n\t}\r\n\r\n\tvoid OpenTree()\r\n\t{\r\n\t\tshowTree = !showTree;\r\n\t\tshowShop = false;\r\n\t\tshowSettings = false;\r\n\t}\r\n\r\n\t/// <summary>Open a panel from the console, so UI can be inspected without clicking.</summary>\r\n\tpublic void ShowPanel( string which )\r\n\t{\r\n\t\tshowShop = which is \"shop\" or \"bg\";\r\n\t\tshowSettings = which == \"settings\";\r\n\t\tshowTree = which == \"tree\";\r\n\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tvoid CloseOverlays()\r\n\t{\r\n\t\tshowShop = false;\r\n\t\tshowSettings = false;\r\n\t\tshowTree = false;\r\n\t}\r\n\r\n\tstring TreeFillStyle() => $\"width: {N( State.TreeFraction * 100f )}%;\";\r\n\r\n\tvoid DoWaterTree()\r\n\t{\r\n\t\tvar poured = State?.WaterTree() ?? 0;\r\n\t\tif ( poured <= 0 ) return;\r\n\r\n\t\tSpawn( $\"-{Numbers.Short( poured )}L\", false );\r\n\r\n\t\ttoast = State.TreeIsGrowing\r\n\t\t\t? $\"Tree is growing - back in {State.TreeTimeLeft}\"\r\n\t\t\t: $\"{Numbers.Short( State.TreeWaterRemaining )}L to go\";\r\n\t\ttoastAge = 0;\r\n\t}\r\n\r\n\tvoid OnTreeGrew( int height )\r\n\t{\r\n\t\ttoast = $\"Your tree grew to {height} {( height == 1 ? \"stage\" : \"stages\" )}\";\r\n\t\ttoastAge = 0;\r\n\t}\r\n\r\n\tbool ShowToast => !string.IsNullOrEmpty( toast ) && toastAge < ToastLife;\r\n\tbool ShowIdleNote => idleNotice > 0 && idleAge < IdleNoticeLife;\r\n\r\n\t/// <summary>\r\n\t/// The panel a PanelComponent builds for itself defaults to pointer-events: none,\r\n\t/// and no stylesheet rule can reach it - it has no class and a generated type name.\r\n\t/// Left alone it makes every button underneath it dead to the mouse.\r\n\t/// </summary>\r\n\tprotected override void OnTreeFirstBuilt()\r\n\t{\r\n\t\tbase.OnTreeFirstBuilt();\r\n\r\n\t\tPanel.Style.PointerEvents = PointerEvents.All;\r\n\t}\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tHook();\r\n\r\n\t\tif ( State is not null && State.IdleCatchUp > 0 )\r\n\t\t{\r\n\t\t\tidleNotice = State.IdleCatchUp;\r\n\t\t\tidleAge = 0;\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{\r\n\t\tUnhook();\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// The game component can come alive after us, so keep trying until it's there.\r\n\t\tif ( !hooked ) Hook();\r\n\r\n\t\tfloaters.RemoveAll( x => x.Age > FloaterLife );\r\n\r\n\t\tif ( floaters.Count > 0 || timeSincePump < PumpAnimLife )\r\n\t\t{\r\n\t\t\tanimTick = (int)(RealTime.Now * 30f);\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override int BuildHash()\r\n\t{\r\n\t\tvar state = State;\r\n\t\tif ( state is null ) return 0;\r\n\r\n\t\treturn HashCode.Combine(\r\n\t\t\t(long)state.Coins,\r\n\t\t\t(long)state.Water,\r\n\t\t\tstate.PumpLevel,\r\n\t\t\tstate.Muted,\r\n\t\t\tfloaters.Count,\r\n\t\t\tanimTick,\r\n\t\t\tShowToast,\r\n\t\t\tHashCode.Combine( ShowIdleNote, showShop, showSettings, AnyUpgradeAffordable,\r\n\t\t\t\tstate.GusherActive, state.CanClaimDaily, state.SelectedBackground,\r\n\t\t\t\tHashCode.Combine( showTree, state.TreeHeight, (int)state.TreeWater, (int)state.TreeSecondsLeft ) ) );\r\n\t}\r\n\r\n\tvoid Hook()\r\n\t{\r\n\t\tvar state = State;\r\n\t\tif ( state is null || hooked ) return;\r\n\r\n\t\tstate.Pumped += OnPumped;\r\n\t\tstate.Sold += OnSold;\r\n\t\tstate.Upgraded += OnUpgraded;\r\n\t\tstate.Refused += OnRefused;\r\n\t\tstate.Purchased += OnPurchased;\r\n\t\tstate.TreeGrew += OnTreeGrew;\r\n\r\n\t\thooked = true;\r\n\t}\r\n\r\n\tvoid Unhook()\r\n\t{\r\n\t\tvar state = State;\r\n\t\tif ( state is null || !hooked ) return;\r\n\r\n\t\tstate.Pumped -= OnPumped;\r\n\t\tstate.Sold -= OnSold;\r\n\t\tstate.Upgraded -= OnUpgraded;\r\n\t\tstate.Refused -= OnRefused;\r\n\t\tstate.Purchased -= OnPurchased;\r\n\t\tstate.TreeGrew -= OnTreeGrew;\r\n\r\n\t\thooked = false;\r\n\t}\r\n\r\n\tvoid DoPump() => State?.Pump();\r\n\tvoid DoSell() => State?.SellAll();\r\n\tvoid DoBuy() => State?.BuyNextPump();\r\n\tvoid ToggleMute() => State?.ToggleMute();\r\n\r\n\tvoid OnPumped( double litres )\r\n\t{\r\n\t\ttimeSincePump = 0;\r\n\t\tSpawn( $\"+{Numbers.Short( litres )}L\", false );\r\n\t}\r\n\r\n\tvoid OnSold( double coins )\r\n\t{\r\n\t\tSpawn( $\"+{Numbers.Short( coins )}\", true );\r\n\t\ttoast = null;\r\n\t}\r\n\r\n\tvoid OnUpgraded( PumpModel pump )\r\n\t{\r\n\t\ttoast = $\"{pump.Name} installed\";\r\n\t\ttoastAge = 0;\r\n\t\tidleNotice = 0;\r\n\t}\r\n\r\n\tvoid OnRefused( string reason )\r\n\t{\r\n\t\ttoast = reason;\r\n\t\ttoastAge = 0;\r\n\t}\r\n\r\n\tvoid OnPurchased( UpgradeTrack track )\r\n\t{\r\n\t\ttoast = $\"{track.Name} upgraded\";\r\n\t\ttoastAge = 0;\r\n\t}\r\n\r\n\tvoid Spawn( string text, bool coin )\r\n\t{\r\n\t\tif ( !PumpSettings.Current.ShowFloatingNumbers )\r\n\t\t\treturn;\r\n\r\n\t\t// Keep the list short - a fast clicker can outrun the fade otherwise.\r\n\t\tif ( floaters.Count > 12 ) floaters.RemoveAt( 0 );\r\n\r\n\t\tfloaters.Add( new Floater\r\n\t\t{\r\n\t\t\tText = text,\r\n\t\t\tX = Rand( -70f, 70f ),\r\n\t\t\tDrift = Rand( -18f, 18f ),\r\n\t\t\tCoin = coin,\r\n\t\t\tAge = 0\r\n\t\t} );\r\n\t}\r\n\r\n\tTexture ArtworkFor( int index )\r\n\t{\r\n\t\tif ( PumpArtwork is null ) return null;\r\n\t\tif ( index < 0 || index >= PumpArtwork.Count ) return null;\r\n\r\n\t\treturn PumpArtwork[index];\r\n\t}\r\n\r\n\tstring RateText()\r\n\t{\r\n\t\tvar perSecond = State.CurrentPump.PerSecond;\r\n\r\n\t\treturn perSecond > 0\r\n\t\t\t? $\"+{Numbers.Rate( perSecond )}L/s while idle\"\r\n\t\t\t: \"hand powered\";\r\n\t}\r\n\r\n\tstring PumpAreaClass()\r\n\t{\r\n\t\tvar classes = timeSincePump < PumpAnimLife ? \"pumping\" : \"\";\r\n\t\tif ( State.TankFull ) classes += \" full\";\r\n\r\n\t\treturn classes;\r\n\t}\r\n\r\n\tstring SlotClass( PumpModel tier )\r\n\t{\r\n\t\tif ( tier.Index <= State.PumpLevel ) return \"owned\";\r\n\r\n\t\treturn tier.Index == State.PumpLevel + 1 ? \"locked next\" : \"locked\";\r\n\t}\r\n\r\n\tstring SlotLabel( PumpModel tier )\r\n\t{\r\n\t\treturn tier.Index <= State.PumpLevel\r\n\t\t\t? tier.Name\r\n\t\t\t: $\"{Numbers.Short( tier.Cost )} {Numbers.Plural( tier.Cost, \"coin\", \"coins\" )}\";\r\n\t}\r\n\r\n\tstring FillStyle() => $\"height: {N( State.TankFraction * 100f )}%;\";\r\n\r\n\tstring TierFillStyle() => $\"width: {N( State.TierProgress * 100f )}%;\";\r\n\r\n\tstring GusherStyle() =>\r\n\t\t$\"left: {N( State.GusherX * 100f )}%; top: {N( State.GusherY * 100f )}%;\";\r\n\r\n\tvoid ClaimGusher()\r\n\t{\r\n\t\tvar won = State?.ClaimGusher() ?? 0;\r\n\t\tif ( won <= 0 ) return;\r\n\r\n\t\tSpawn( $\"+{Numbers.Short( won )}\", true );\r\n\t\ttoast = \"Gusher! Free coins\";\r\n\t\ttoastAge = 0;\r\n\t}\r\n\r\n\tvoid ClaimDaily()\r\n\t{\r\n\t\tvar won = State?.ClaimDaily() ?? 0;\r\n\t\tif ( won <= 0 ) return;\r\n\r\n\t\tSpawn( $\"+{Numbers.Short( won )}\", true );\r\n\t\ttoast = $\"Day {State.DailyStreak} streak - come back tomorrow\";\r\n\t\ttoastAge = 0;\r\n\t}\r\n\r\n\tstring FloaterStyle( Floater f )\r\n\t{\r\n\t\tvar t = Math.Clamp( (float)f.Age / FloaterLife, 0f, 1f );\r\n\t\tvar rise = 42f + t * 26f;\r\n\t\tvar fade = 1f - t * t;\r\n\r\n\t\treturn $\"margin-left: {N( f.X + f.Drift * t )}px; bottom: {N( rise )}%; opacity: {N( fade )};\";\r\n\t}\r\n\r\n\t/// <summary>Style strings need dots for decimals, whatever the machine's locale says.</summary>\r\n\tstatic string N( float value ) => value.ToString( \"0.##\", CultureInfo.InvariantCulture );\r\n\r\n\tstatic float Rand( float min, float max ) => min + (float)System.Random.Shared.NextDouble() * (max - min);\r\n}\r\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "UI/PumpArt.razor",
            "FileName": "PumpArt.razor",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "@using System\r\n@using Sandbox\r\n@using Sandbox.UI\r\n@namespace DesertPump\r\n@inherits Panel\r\n\r\n<root class=\"pumpart family-@Family @( Silhouette ? \"silhouette\" : \"\" )\">\r\n\r\n\t@if ( Artwork is not null )\r\n\t{\r\n\t\t<Image Texture=\"@Artwork\" class=\"photo\"></Image>\r\n\t}\r\n\telse\r\n\t{\r\n\t\t<div class=\"mound\" style=\"background-color: @Shade( 0.55f );\"></div>\r\n\t\t<div class=\"body\" style=\"background-color: @Shade( 1f );\"></div>\r\n\t\t<div class=\"head\" style=\"background-color: @Shade( 1.25f );\"></div>\r\n\t\t<div class=\"arm\" style=\"background-color: @Shade( 1.25f );\"></div>\r\n\t\t<div class=\"spout\" style=\"background-color: @Shade( 0.8f );\"></div>\r\n\t\t<div class=\"stream\" style=\"background-color: @Water( 1.15f );\"></div>\r\n\t\t<div class=\"pool\" style=\"background-color: @Water( 0.75f );\"></div>\r\n\t}\r\n\r\n</root>\r\n\r\n@code\r\n{\r\n\t/// <summary>Index into <see cref=\"PumpTier.All\"/>.</summary>\r\n\tpublic int TierIndex { get; set; }\r\n\r\n\t/// <summary>Draw it as a black cut-out, the way locked pumps show in the collection.</summary>\r\n\tpublic bool Silhouette { get; set; }\r\n\r\n\t/// <summary>Real artwork. When set it replaces the whole placeholder build-up.</summary>\r\n\tpublic Texture Artwork { get; set; }\r\n\r\n\tPumpModel Pump => PumpModel.Get( TierIndex );\r\n\r\n\t/// <summary>Hand pump, rig, or tower - keeps the nine tiers visually distinct.</summary>\r\n\tint Family => Pump.Rank <= 3 ? 0 : Pump.Rank <= 6 ? 1 : 2;\r\n\r\n\t/// <summary>Tier colour, brightened or darkened, as a css hex string.</summary>\r\n\tstring Shade( float multiplier )\r\n\t{\r\n\t\tif ( Silhouette ) return \"#0b0d12\";\r\n\r\n\t\tvar c = Pump.Tint;\r\n\r\n\t\treturn new Color(\r\n\t\t\tMath.Clamp( c.r * multiplier, 0f, 1f ),\r\n\t\t\tMath.Clamp( c.g * multiplier, 0f, 1f ),\r\n\t\t\tMath.Clamp( c.b * multiplier, 0f, 1f ) ).Hex;\r\n\t}\r\n\r\n\tstring Water( float multiplier )\r\n\t{\r\n\t\tif ( Silhouette ) return \"#0b0d12\";\r\n\r\n\t\treturn new Color(\r\n\t\t\tMath.Clamp( 0.35f * multiplier, 0f, 1f ),\r\n\t\t\tMath.Clamp( 0.72f * multiplier, 0f, 1f ),\r\n\t\t\tMath.Clamp( 0.95f * multiplier, 0f, 1f ) ).Hex;\r\n\t}\r\n\r\n\tprotected override int BuildHash() => HashCode.Combine( TierIndex, Silhouette, Artwork );\r\n}\r\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "Game/UpgradeTrack.cs",
            "FileName": "UpgradeTrack.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "namespace DesertPump;\r\n\r\npublic enum UpgradeKind\r\n{\r\n\t/// <summary>Bigger tank, so you sell every few minutes instead of every few seconds.</summary>\r\n\tStorage,\r\n\r\n\t/// <summary>More litres per pump.</summary>\r\n\tPower,\r\n\r\n\t/// <summary>More coins per litre.</summary>\r\n\tValue,\r\n}\r\n\r\n/// <summary>\r\n/// A shop upgrade you can buy over and over, each level costing more than the last.\r\n/// </summary>\r\n/// <remarks>\r\n/// Cost growth deliberately outruns effect growth by a wide margin. A track where the\r\n/// two are close never stops paying for itself, and the economy runs away - the first\r\n/// pass at this balance finished the whole game in two minutes for exactly that reason.\r\n/// </remarks>\r\npublic sealed class UpgradeTrack\r\n{\r\n\tpublic UpgradeKind Kind { get; init; }\r\n\tpublic string Name { get; init; }\r\n\tpublic string Description { get; init; }\r\n\r\n\t/// <summary>Material icon name.</summary>\r\n\tpublic string Icon { get; init; }\r\n\r\n\tpublic double BaseCost { get; init; }\r\n\tpublic double CostGrowth { get; init; }\r\n\r\n\t/// <summary>What one level multiplies its stat by.</summary>\r\n\tpublic float EffectGrowth { get; init; }\r\n\r\n\tpublic int MaxLevel { get; init; }\r\n\r\n\tpublic double CostAt( int level ) => BaseCost * Math.Pow( CostGrowth, level );\r\n\r\n\tpublic float MultiplierAt( int level ) => MathF.Pow( EffectGrowth, level );\r\n\r\n\t/// <summary>\"+12%\" - what the next level buys you.</summary>\r\n\tpublic string PerLevelText => $\"+{(EffectGrowth - 1f) * 100f:0}%\";\r\n\r\n\t/// <summary>Re-read on hotload so balance edits apply without restarting - see PumpTier.All.</summary>\r\n\t[SkipHotload]\r\n\tpublic static readonly UpgradeTrack[] All = new UpgradeTrack[]\r\n\t{\r\n\t\tnew()\r\n\t\t{\r\n\t\t\tKind = UpgradeKind.Storage,\r\n\t\t\tName = \"Water Tank\",\r\n\t\t\tDescription = \"Holds more water, so you can walk away between sales.\",\r\n\t\t\tIcon = \"water_drop\",\r\n\t\t\tBaseCost = 150,\r\n\t\t\tCostGrowth = 1.55,\r\n\t\t\tEffectGrowth = 1.12f,\r\n\t\t\tMaxLevel = 200\r\n\t\t},\r\n\t\tnew()\r\n\t\t{\r\n\t\t\tKind = UpgradeKind.Power,\r\n\t\t\tName = \"Pump Power\",\r\n\t\t\tDescription = \"Every pump pulls up more water.\",\r\n\t\t\tIcon = \"fitness_center\",\r\n\t\t\tBaseCost = 250,\r\n\t\t\tCostGrowth = 1.60,\r\n\t\t\tEffectGrowth = 1.06f,\r\n\t\t\tMaxLevel = 200\r\n\t\t},\r\n\t\tnew()\r\n\t\t{\r\n\t\t\tKind = UpgradeKind.Value,\r\n\t\t\tName = \"Market Contacts\",\r\n\t\t\tDescription = \"People pay better for the same water.\",\r\n\t\t\tIcon = \"handshake\",\r\n\t\t\tBaseCost = 400,\r\n\t\t\tCostGrowth = 1.68,\r\n\t\t\tEffectGrowth = 1.05f,\r\n\t\t\tMaxLevel = 200\r\n\t\t},\r\n\t};\r\n\r\n\tpublic static UpgradeTrack Get( UpgradeKind kind ) => All[(int)kind];\r\n}\r\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "UI/SettingsPanel.razor",
            "FileName": "SettingsPanel.razor",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 338006,
            "Code": "@using System\r\n@using Sandbox\r\n@using Sandbox.UI\r\n@namespace DesertPump\r\n@inherits Panel\r\n\r\n<root class=\"settingspanel\">\r\n\r\n\t<div class=\"scrim\" onclick=\"@Close\"></div>\r\n\r\n\t<div class=\"sheet\">\r\n\r\n\t\t<div class=\"head\">\r\n\t\t\t<div class=\"title\"><i>settings</i> SETTINGS</div>\r\n\t\t\t<div class=\"closebtn\" onclick=\"@Close\"><i>close</i></div>\r\n\t\t</div>\r\n\r\n\t\t<div class=\"group\">\r\n\t\t\t<div class=\"grouptitle\">AUDIO</div>\r\n\r\n\t\t\t<div class=\"row\">\r\n\t\t\t\t<div class=\"label\">Master volume</div>\r\n\t\t\t\t<SliderControl class=\"simple\" Value:bind=@Master Min=\"@(0f)\" Max=\"@(1f)\" Step=\"@(0.05f)\"></SliderControl>\r\n\t\t\t\t<div class=\"value\">@Percent( Master )</div>\r\n\t\t\t</div>\r\n\r\n\t\t\t<div class=\"row\">\r\n\t\t\t\t<div class=\"label\">Music</div>\r\n\t\t\t\t<SliderControl class=\"simple\" Value:bind=@Music Min=\"@(0f)\" Max=\"@(1f)\" Step=\"@(0.05f)\"></SliderControl>\r\n\t\t\t\t<div class=\"value\">@Percent( Music )</div>\r\n\t\t\t</div>\r\n\r\n\t\t\t<div class=\"row\">\r\n\t\t\t\t<div class=\"label\">Sound effects</div>\r\n\t\t\t\t<SliderControl class=\"simple\" Value:bind=@Sfx Min=\"@(0f)\" Max=\"@(1f)\" Step=\"@(0.05f)\"></SliderControl>\r\n\t\t\t\t<div class=\"value\">@Percent( Sfx )</div>\r\n\t\t\t</div>\r\n\r\n\t\t\t<div class=\"row toggle @( Settings.Muted ? \"on\" : \"\" )\" onclick=\"@ToggleMute\">\r\n\t\t\t\t<div class=\"label\">Mute everything</div>\r\n\t\t\t\t<div class=\"switch\"><div class=\"knob\"></div></div>\r\n\t\t\t</div>\r\n\t\t</div>\r\n\r\n\t\t<div class=\"group\">\r\n\t\t\t<div class=\"grouptitle\">GAME</div>\r\n\r\n\t\t\t<div class=\"row toggle @( Settings.AutoSell ? \"on\" : \"\" )\" onclick=\"@ToggleAutoSell\">\r\n\t\t\t\t<div class=\"label\">\r\n\t\t\t\t\tAuto-sell when the tank fills\r\n\t\t\t\t\t<span class=\"sub\">Hands off, but you'll miss the coin sound</span>\r\n\t\t\t\t</div>\r\n\t\t\t\t<div class=\"switch\"><div class=\"knob\"></div></div>\r\n\t\t\t</div>\r\n\r\n\t\t\t<div class=\"row toggle @( Settings.ShowFloatingNumbers ? \"on\" : \"\" )\" onclick=\"@ToggleFloaters\">\r\n\t\t\t\t<div class=\"label\">\r\n\t\t\t\t\tFloating numbers\r\n\t\t\t\t\t<span class=\"sub\">The little +12L that fly off the pump</span>\r\n\t\t\t\t</div>\r\n\t\t\t\t<div class=\"switch\"><div class=\"knob\"></div></div>\r\n\t\t\t</div>\r\n\t\t</div>\r\n\r\n\t\t<div class=\"group danger\">\r\n\t\t\t<div class=\"grouptitle\">DANGER</div>\r\n\r\n\t\t\t<div class=\"row\">\r\n\t\t\t\t<div class=\"label\">\r\n\t\t\t\t\tReset progress\r\n\t\t\t\t\t<span class=\"sub\">Every pump, upgrade and coin. Settings are kept.</span>\r\n\t\t\t\t</div>\r\n\r\n\t\t\t\t@if ( confirmingReset )\r\n\t\t\t\t{\r\n\t\t\t\t\t<div class=\"resetrow\">\r\n\t\t\t\t\t\t<div class=\"resetbtn confirm\" onclick=\"@DoReset\">Yes, wipe it</div>\r\n\t\t\t\t\t\t<div class=\"resetbtn\" onclick=\"@( () => confirmingReset = false )\">Cancel</div>\r\n\t\t\t\t\t</div>\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\t<div class=\"resetbtn\" onclick=\"@( () => confirmingReset = true )\">Reset</div>\r\n\t\t\t\t}\r\n\t\t\t</div>\r\n\t\t</div>\r\n\r\n\t\t<div class=\"done\" onclick=\"@Close\">DONE</div>\r\n\r\n\t</div>\r\n\r\n</root>\r\n\r\n@code\r\n{\r\n\t/// <summary>Raised when the player dismisses the settings.</summary>\r\n\tpublic Action OnClose { get; set; }\r\n\r\n\tstatic PumpSettings Settings => PumpSettings.Current;\r\n\r\n\tbool confirmingReset;\r\n\r\n\t// Slider bindings. Each setter persists, so a change survives even if the game\r\n\t// is killed rather than closed.\r\n\tfloat Master\r\n\t{\r\n\t\tget => Settings.MasterVolume;\r\n\t\tset => Apply( () => Settings.MasterVolume = value, Settings.MasterVolume == value );\r\n\t}\r\n\r\n\tfloat Music\r\n\t{\r\n\t\tget => Settings.MusicVolume;\r\n\t\tset => Apply( () => Settings.MusicVolume = value, Settings.MusicVolume == value );\r\n\t}\r\n\r\n\tfloat Sfx\r\n\t{\r\n\t\tget => Settings.SfxVolume;\r\n\t\tset => Apply( () => Settings.SfxVolume = value, Settings.SfxVolume == value );\r\n\t}\r\n\r\n\tvoid Apply( Action change, bool unchanged )\r\n\t{\r\n\t\tif ( unchanged ) return;\r\n\r\n\t\tchange();\r\n\t\tSettings.Save();\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tvoid ToggleMute()\r\n\t{\r\n\t\tSettings.Muted = !Settings.Muted;\r\n\t\tSettings.Save();\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tvoid ToggleAutoSell()\r\n\t{\r\n\t\tSettings.AutoSell = !Settings.AutoSell;\r\n\t\tSettings.Save();\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tvoid ToggleFloaters()\r\n\t{\r\n\t\tSettings.ShowFloatingNumbers = !Settings.ShowFloatingNumbers;\r\n\t\tSettings.Save();\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tvoid DoReset()\r\n\t{\r\n\t\tDesertPumpGame.Current?.ResetProgress();\r\n\t\tconfirmingReset = false;\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tvoid Close()\r\n\t{\r\n\t\tconfirmingReset = false;\r\n\t\tOnClose?.Invoke();\r\n\t}\r\n\r\n\tstatic string Percent( float value ) => $\"{value * 100f:0}%\";\r\n\r\n\tprotected override int BuildHash() => HashCode.Combine(\r\n\t\tSettings.MasterVolume, Settings.MusicVolume, Settings.SfxVolume,\r\n\t\tSettings.Muted, Settings.AutoSell, Settings.ShowFloatingNumbers, confirmingReset );\r\n}\r\n"
        }
    ]
}