s&box Package Code Search

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

Showing code results for query: * (58 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-20T18:13:47.6067714Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.110.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.110.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; }

	/// <summary>Played on a surge. Falls back to the upgrade sting when it's left empty.</summary>
	[Property, Group( "Audio" )] public SoundEvent SurgeSound { 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 an ordinary manual pump, with the litres gained. A pump that came up a
	/// surge fires <see cref="Surged"/> instead - exactly one of the two per click.
	/// </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();
		}

		// The pump and everyone you've hired, together.
		var passive = TotalWaterPerSecond * Time.Delta;
		if ( passive > 0f )
		{
			AddWater( passive );
		}

		TickPressure();
		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>
	/// <remarks>
	/// Pressure and surges only exist on this path. Idle water, workers and the tree all
	/// go through <see cref="AddWater"/> untouched, so hands-off income is exactly what
	/// it always was.
	/// </remarks>
	public double Pump()
	{
		if ( TankFull )
		{
			Refuse( "Tank is full - sell your water" );
			return 0;
		}

		var surge = RollSurge();
		var litres = LitresPerClick * PressureBonus * (surge ? SurgeMultiplier : 1f);

		var gained = AddWater( litres );

		// Pressure builds even on the click that overflowed the tank - the rhythm is the
		// player's, and dropping it because the tank happened to be full feels like a bug.
		BumpPressure();
		TotalPumps++;

		if ( surge )
		{
			SurgeCount++;
			Play( SurgeSound ?? UpgradeSound );
			Surged?.Invoke( gained );
		}
		else
		{
			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;
		Array.Clear( workerLevels );
		TreeHeight = 0;
		TreeWater = 0;
		treeGrowsAt = 0;
		TotalPumps = 0;
		TotalEarned = 0;
		TotalLitresSold = 0;
		IdleCatchUp = 0;
		Pressure = 0;
		SurgeCount = 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 = ReadSave( SaveFile );

		// Nothing came back but there is a file on disk, so it is unreadable rather
		// than absent - a write cut short by a crash, most likely. The backup below is
		// one session behind, which beats handing the player a fresh run and no
		// explanation. This is the half of that feature that was missing: the backup
		// was being written every load and never once read.
		if ( save is null && FileSystem.Data.FileExists( SaveFile ) )
		{
			save = ReadSave( SaveFile + ".bak" );

			Log.Warning( save is null
				? $"{SaveFile} is unreadable and so is its backup - starting a fresh run."
				: $"{SaveFile} is unreadable - restored the backup from the last session." );
		}

		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;
		Array.Clear( workerLevels );
		var levels = save.WorkerLevels ?? new List<int>();
		for ( var i = 0; i < Math.Min( levels.Count, workerLevels.Length ); i++ )
		{
			workerLevels[i] = Math.Clamp( levels[i], 0, WorkerType.MaxLevel );
		}

		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 );
	}

	/// <summary>
	/// Read one save file, or null if it isn't there and null if it's rubbish.
	/// </summary>
	/// <remarks>
	/// ReadJsonOrDefault already returns the default on a missing or malformed file, so
	/// the catch is only for what it doesn't cover - a locked or unreadable file. Either
	/// way a broken save must never take the game down with it: that turns "you lost
	/// your progress" into "the game no longer starts".
	/// </remarks>
	PumpSave ReadSave( string path )
	{
		try
		{
			return FileSystem.Data.ReadJsonOrDefault<PumpSave>( path, null );
		}
		catch ( Exception e )
		{
			Log.Warning( $"Couldn't read {path}: {e.Message}" );
			return null;
		}
	}

	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 || TotalWaterPerSecond <= 0 )
			return;

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

		var seconds = Math.Min( away, (long)(MaxIdleHours * 3600f) );
		IdleCatchUp = AddWater( TotalWaterPerSecond * 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,
			WorkerLevels = workerLevels.ToList(),
			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/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 ),
			// These nine are all ways of getting water out of a hole in the ground.
			// The old set were leftovers from when there were nine tiers rather than
			// twelve, so tier 1 - the one whose whole premise is hand-pulled water -
			// contained a Steam Pressure Pump, a Diesel Derrick and a Solar Filtration
			// Rig, each of which is the name of a tier the player has not reached yet.
			PumpNames = new[]
			{
				"Rusty Hand Pump", "Iron Well Pump", "Bucket Windlass",
				"Windmill Well", "Cattle Trough Bore", "Stone Well House",
				"Desert Water Tower", "Twin Bore Rig", "The Great Standpipe"
			}
		},
		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 / 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 / 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 / Game/DesertPumpGame.Workers.cs
Game game
namespace DesertPump;

/// <summary>
/// The hired help. Workers add litres per second on top of the pump, and unlike the
/// pump they keep working while you're doing something else entirely.
/// </summary>
public sealed partial class DesertPumpGame
{
	readonly int[] workerLevels = new int[WorkerType.Count];

	public int WorkerLevel( WorkerType worker ) => workerLevels[worker.Index];

	public bool IsHired( WorkerType worker ) => WorkerLevel( worker ) > 0;

	public bool IsWorkerMaxed( WorkerType worker ) => WorkerLevel( worker ) >= WorkerType.MaxLevel;

	/// <summary>Cost of the next level, or infinity when they've nothing left to learn.</summary>
	public double WorkerCost( WorkerType worker ) => IsWorkerMaxed( worker )
		? double.PositiveInfinity
		: worker.CostAt( WorkerLevel( worker ) );

	public bool CanHire( WorkerType worker ) => !IsWorkerMaxed( worker ) && Coins >= WorkerCost( worker );

	/// <summary>What one worker is currently producing.</summary>
	public double WorkerOutput( WorkerType worker ) => worker.OutputAt( WorkerLevel( worker ) );

	/// <summary>Everything the crew makes between them, litres per second.</summary>
	public double WorkerWaterPerSecond
	{
		get
		{
			double total = 0;

			foreach ( var worker in WorkerType.All )
			{
				total += WorkerOutput( worker );
			}

			return total;
		}
	}

	/// <summary>How many of the fifteen are on the payroll.</summary>
	public int HiredCount
	{
		get
		{
			var count = 0;

			foreach ( var worker in WorkerType.All )
			{
				if ( IsHired( worker ) ) count++;
			}

			return count;
		}
	}

	/// <summary>The pump and the crew together - what the tank actually fills at.</summary>
	public double TotalWaterPerSecond => CurrentPump.PerSecond + WorkerWaterPerSecond;

	/// <summary>Fired after hiring or promoting someone.</summary>
	public Action<WorkerType> Hired { get; set; }

	/// <summary>Hire a worker, or promote one you already have.</summary>
	public bool BuyWorker( WorkerType worker )
	{
		if ( IsWorkerMaxed( worker ) )
		{
			Refuse( $"{worker.Name} can't be promoted any further" );
			return false;
		}

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

		Coins -= cost;
		workerLevels[worker.Index]++;

		Play( UpgradeSound );
		Hired?.Invoke( worker );
		Save();

		return true;
	}
}
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 / UI/GameHud.razor
Game game
@using System
@using System.Globalization
@using Sandbox
@using Sandbox.UI
@namespace DesertPump
@inherits PanelComponent

<div class="hud @SceneClass @( ShakeClass )">

	<div class="sky"></div>
	<div class="sun">
		<div class="sun-rays"></div>
	</div>
	<div class="stars"></div>

	<div class="clouds">
		<div class="cloud c1"></div>
		<div class="cloud c2"></div>
		<div class="cloud c3"></div>
	</div>

	<div class="floor"></div>
	<div class="dune far"></div>
	<div class="dune near"></div>
	<div class="shimmer"></div>
	<div class="scenery"></div>

	<div class="motes">
		<div class="mote m1"></div>
		<div class="mote m2"></div>
		<div class="mote m3"></div>
		<div class="mote m4"></div>
		<div class="mote m5"></div>
		<div class="mote m6"></div>
		<div class="mote m7"></div>
		<div class="mote m8"></div>
	</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 @( CoinPopping ? "pop" : "" )">
				<div class="coin"></div>
				<div class="value">@Numbers.Short( State.Coins )</div>
				@if ( CoinPopping )
				{
					<div class="coinflash"></div>
				}
			</div>

			<div class="stat tankstat @( State.TankFull ? "full" : "" )">
				<div class="tank">
					<div class="tank-fill" style="@FillStyle()">
						<div class="tank-wave"></div>
					</div>
					<div class="tank-shine"></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>
				@if ( AnyUpgradeAffordable )
				{
					<div class="dot"></div>
				}
			</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="pumpwrap">

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

				@if ( ShowClickRing )
				{
					<div class="clickring"></div>
				}

				@if ( ShowSurgeBurst )
				{
					<div class="surgering"></div>
				}

				<div class="fx">
					@foreach ( var p in particles )
					{
						<div class="particle @p.Kind" style="@ParticleStyle( p )"></div>
					}

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

			</div>

			<div class="pressure s@( State.PressureStage ) @( State.Pressure > 0.02f ? "" : "cold" )">
				<div class="pressure-bar">
					<div class="pressure-fill" style="@PressureFillStyle()"></div>
				</div>
				<div class="pressure-label">
					<span class="pressure-name">PRESSURE</span>
					<span class="pressure-value">@State.PressureBonusText</span>
				</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">[email protected]( State.LitresPerClick )L per pump - @Numbers.Short( State.PricePerLitre ) @Numbers.Plural( State.PricePerLitre, "coin", "coins" ) per litre</div>
			</div>

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

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

			<div class="treearea">

				<div class="tree @( State.TreeIsGrowing ? "growing" : "" )" onclick="@DoWaterTree"
					 style="background-image: url( '/ui/tree/tree_@(State.TreeArtStage).svg' );">
				</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>

			@* Input.Pressed is edge triggered, so SPACE is one pump per press - holding it
			   does nothing and telling the player otherwise reads as the game being broken. *@
			<div class="hint">Click the pump or tap SPACE - keep a rhythm going to build pressure</div>

			@* Last in the stage on purpose. It spawns at a random spot whose range
			   overlaps the tree, and drawn any earlier the tree paints over it and eats
			   the click - so the gusher silently waters the tree and then expires. It is
			   a rare, timed reward; nothing in here should ever be allowed on top of it. *@
			@if ( State.GusherActive )
			{
				<div class="gusher" style="@GusherStyle()" onclick="@ClaimGusher">
					<div class="gusher-drop"></div>
					<div class="gusher-ring"></div>
					<div class="gusher-ring two"></div>
				</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">[email protected]( State.SaleValue ) coins</div>
				</div>
				<div class="shine"></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 ? "ready" : "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 class="shine"></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>
					@if ( pump.Index <= State.PumpLevel )
					{
						<div class="tick"><i>check</i></div>
					}
				</div>
			}
		</div>

		@if ( ShowCelebration )
		{
			<div class="celebrate">
				<div class="celebrate-ring"></div>
				<div class="celebrate-ring two"></div>
				<div class="celebrate-card">
					<div class="celebrate-kicker">NEW PUMP INSTALLED</div>
					<div class="celebrate-name">@celebrateName</div>
					<div class="celebrate-sub">@celebrateSub</div>
				</div>
			</div>
		}

		@if ( ShowSurgeFlash )
		{
			<div class="surgeflash"></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 SurgeFloaterLife = 1.5f;
	const float ToastLife = 2.2f;
	const float IdleNoticeLife = 8f;
	const float PumpAnimLife = 0.16f;
	const float ClickRingLife = 0.42f;
	const float SurgeFlashLife = 0.4f;
	const float SurgeBurstLife = 0.6f;
	const float CoinPopLife = 0.36f;
	const float CelebrationLife = 2.6f;
	const float ShakeLife = 0.32f;

	/// <summary>Pixels per second squared pulling the burst particles back down.</summary>
	const float Gravity = 1150f;

	/// <summary>Must match the width of .floater in the stylesheet - see FloaterStyle.</summary>
	const float FloaterWidth = 300f;

	/// <summary>Above this the whole effects layer stops spawning - a fast clicker can outrun it.</summary>
	const int MaxParticles = 40;

	class Floater
	{
		public string Text;
		public string Kind;
		public float X;
		public float Drift;
		public float Life;
		public RealTimeSince Age;
	}

	/// <summary>
	/// One piece of the burst. Positions are integrated in C# rather than handed to a CSS
	/// keyframe because each one needs its own arc - see <see cref="ParticleStyle"/>.
	/// </summary>
	class Particle
	{
		public string Kind;
		public float X;
		public float Y;
		public float Vx;
		public float Vy;
		public float Size;
		public float Spin;
		public float Life;
		public RealTimeSince Age;
	}

	readonly List<Floater> floaters = new();
	readonly List<Particle> particles = new();

	string toast;
	RealTimeSince toastAge;

	double idleNotice;
	RealTimeSince idleAge;

	// Every one-shot effect below is gated on "has it been less than N seconds since
	// this fired". Left at the default timestamp of zero that reads as "it fired at
	// engine time zero" - fine in the editor, which has been up for hours, but in a
	// fresh build RealTime.Now starts near zero too and the first frame would come up
	// mid surge-flash. Start them all firmly in the past instead.
	const float LongAgo = 9999f;

	RealTimeSince timeSincePump = LongAgo;
	RealTimeSince clickRingAge = LongAgo;
	RealTimeSince surgeAge = LongAgo;
	RealTimeSince coinPopAge = LongAgo;
	RealTimeSince celebrateAge = LongAgo;
	RealTimeSince shakeAge = LongAgo;

	string celebrateName;
	string celebrateSub;

	/// <summary>Set when a shake is wanted, and which of the two flavours.</summary>
	string shakeKind;

	/// <summary>Bumped every frame so the particles 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}";

	// ---- one-shot effect gates ---------------------------------------------

	/// <summary>
	/// The settings toggle that governs all the per-action juice - numbers, droplets,
	/// coins, rings, flashes and the shake. The new-pump card is deliberately left out
	/// of it: that one is a milestone rather than noise, and a player who turned the
	/// effects down still wants to be told they bought something.
	/// </summary>
	static bool EffectsOn => PumpSettings.Current.ShowFloatingNumbers;

	bool ShowClickRing => EffectsOn && clickRingAge < ClickRingLife;
	bool ShowSurgeFlash => EffectsOn && surgeAge < SurgeFlashLife;
	bool ShowSurgeBurst => EffectsOn && surgeAge < SurgeBurstLife;
	bool CoinPopping => EffectsOn && coinPopAge < CoinPopLife;
	bool ShowCelebration => celebrateName is not null && celebrateAge < CelebrationLife;

	/// <summary>
	/// The shake is a class on the root rather than an inline transform, so the whole
	/// keyframe runs on the GPU and the C# side only has to say when it starts.
	/// </summary>
	string ShakeClass => shakeAge < ShakeLife ? $"shake-{shakeKind}" : "";

	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" or "workers";
		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", "water" );

		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 > x.Life );
		particles.RemoveAll( x => x.Age > x.Life );

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

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

		// The counts have to be in here, not just animTick. animTick only advances while
		// something is animating, so on the frame the last floater is removed it freezes
		// - and if nothing else in the hash moved, the tree never rebuilds and that
		// floater stays on screen forever, stuck at whatever opacity it last drew at.
		// At pump 1 there is no idle income to accidentally cover for it.
		// ShowSurgeFlash belongs here for the same reason the counts do - it is a panel
		// that has to come off the screen, and nothing else in the hash is guaranteed to
		// have moved on the frame it expires. Combine takes eight, so the last two share
		// a nested one rather than being OR'd together, which would hide a swap.
		var effects = HashCode.Combine(
			floaters.Count, particles.Count, animTick, ShakeClass,
			ShowClickRing, ShowSurgeBurst, ShowSurgeFlash,
			HashCode.Combine( CoinPopping, ShowCelebration ) );

		var overlays = HashCode.Combine(
			showShop, showSettings, showTree, ShowToast, ShowIdleNote,
			AnyUpgradeAffordable, state.SelectedBackground, state.CanClaimDaily );

		var world = HashCode.Combine(
			state.GusherActive, state.TreeHeight, Numbers.Bucket( state.TreeWater ),
			(int)state.TreeSecondsLeft );

		// Numbers go through Numbers.Bucket rather than a cast. Coins reach 2.5e51 by
		// the last pump, and a double that big cast to long lands on the same value
		// every time - so the hash would stop moving and the wallet would freeze on
		// screen from tier 9 onward. See the remarks on Bucket.
		return HashCode.Combine(
			Numbers.Bucket( state.Coins ),
			Numbers.Bucket( state.Water ),
			state.PumpLevel,
			state.Muted,
			// Twenty bands rather than the raw float - the gauge still reads as smooth
			// and the tree isn't rebuilt on every frame that pressure bleeds off.
			(int)(state.Pressure * 20f),
			effects,
			overlays,
			world );
	}

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

		state.Pumped += OnPumped;
		state.Surged += OnSurged;
		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.Surged -= OnSurged;
		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;
		clickRingAge = 0;

		Spawn( $"+{Numbers.Short( litres )}L", "water" );

		// More water on screen the harder you're going, so the gauge isn't the only
		// thing telling you pressure is up.
		var count = 4 + (int)(State.Pressure * 5f);
		SprayDroplets( count );
	}

	void OnSurged( double litres )
	{
		timeSincePump = 0;
		clickRingAge = 0;
		surgeAge = 0;

		Shake( "hard" );
		Spawn( $"SURGE  +{Numbers.Short( litres )}L", "surge" );
		SprayDroplets( 14 );
		SpawnSparks( 12 );
	}

	void OnSold( double coins )
	{
		Spawn( $"+{Numbers.Short( coins )}", "coin" );
		SpawnCoins( 10 );

		coinPopAge = 0;
		toast = null;
	}

	void OnUpgraded( PumpModel pump )
	{
		celebrateName = pump.Name;
		celebrateSub = pump.Rank == 1
			? $"{pump.Tier.Name} - {pump.Tier.Tagline}"
			: $"{Numbers.Short( pump.PerClick )}L per pump  -  {Numbers.Short( pump.Tank )}L tank";
		celebrateAge = 0;

		Shake( "soft" );
		SpawnSparks( 18 );

		toast = null;
		idleNotice = 0;
	}

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

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

	void Shake( string kind )
	{
		if ( !PumpSettings.Current.ShowFloatingNumbers )
			return;

		shakeKind = kind;
		shakeAge = 0;
	}

	void Spawn( string text, string kind )
	{
		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,
			Kind = kind,
			X = kind == "surge" ? Rand( -30f, 30f ) : Rand( -70f, 70f ),
			Drift = Rand( -18f, 18f ),
			Life = kind == "surge" ? SurgeFloaterLife : FloaterLife,
			Age = 0
		} );
	}

	/// <summary>Water thrown up out of the pump head.</summary>
	void SprayDroplets( int count )
	{
		for ( var i = 0; i < count; i++ )
		{
			Add( new Particle
			{
				Kind = "drop",
				X = Rand( -26f, 26f ),
				Y = Rand( -96f, -60f ),
				Vx = Rand( -280f, 280f ),
				Vy = Rand( -440f, -230f ),
				Size = Rand( 7f, 16f ),
				Spin = 0f,
				Life = Rand( 0.55f, 0.85f ),
				Age = 0
			} );
		}
	}

	/// <summary>Coins tossed out on a sale.</summary>
	void SpawnCoins( int count )
	{
		for ( var i = 0; i < count; i++ )
		{
			Add( new Particle
			{
				Kind = "coinbit",
				X = Rand( -40f, 40f ),
				Y = Rand( -30f, 20f ),
				Vx = Rand( -240f, 240f ),
				Vy = Rand( -520f, -320f ),
				Size = Rand( 14f, 24f ),
				Spin = Rand( -420f, 420f ),
				Life = Rand( 0.75f, 1.05f ),
				Age = 0
			} );
		}
	}

	/// <summary>The flat radial burst a surge or a new pump throws off.</summary>
	void SpawnSparks( int count )
	{
		for ( var i = 0; i < count; i++ )
		{
			var angle = (i / (float)count) * MathF.PI * 2f + Rand( -0.2f, 0.2f );
			var speed = Rand( 380f, 620f );

			Add( new Particle
			{
				Kind = "spark",
				X = 0f,
				Y = -40f,
				Vx = MathF.Cos( angle ) * speed,
				Vy = MathF.Sin( angle ) * speed,
				Size = Rand( 8f, 15f ),
				Spin = Rand( -240f, 240f ),
				Life = Rand( 0.45f, 0.7f ),
				Age = 0
			} );
		}
	}

	void Add( Particle particle )
	{
		if ( !PumpSettings.Current.ShowFloatingNumbers )
			return;

		// Drop the oldest rather than refusing the new one, so a burst always reads as
		// a burst even when the screen is already busy.
		if ( particles.Count >= MaxParticles )
			particles.RemoveAt( 0 );

		particles.Add( particle );
	}

	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.TotalWaterPerSecond;

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

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

		return classes;
	}

	/// <summary>The pump's halo brightens and widens with pressure.</summary>
	string GlowStyle()
	{
		var p = State.Pressure;
		var size = 78f + p * 14f;
		var alpha = 0.14f + p * 0.22f;

		return $"width: {N( size )}%; height: {N( size )}%; " +
			$"background-color: rgba( 255, 255, 255, {N( alpha )} );";
	}

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

	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 )}", "coin" );
		SpawnCoins( 14 );
		coinPopAge = 0;

		toast = "Gusher! Free coins";
		toastAge = 0;
	}

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

		Spawn( $"+{Numbers.Short( won )}", "coin" );
		SpawnCoins( 16 );
		coinPopAge = 0;

		toast = $"Day {State.DailyStreak} streak - come back tomorrow";
		toastAge = 0;
	}

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

		// Surge numbers punch out to full size and settle, so they read as an event
		// rather than as one more of the numbers already streaming past.
		var scale = f.Kind == "surge"
			? 1f + (1f - MathF.Min( t * 5f, 1f )) * 0.5f
			: 1f;

		// Centred on the anchor by hand: the panel is a fixed 300px wide with centred
		// text, so half its width comes back off the left. Text has no width to measure.
		return $"left: {N( f.X + f.Drift * t - FloaterWidth * 0.5f )}px; top: {N( -rise )}px; " +
			$"opacity: {N( fade )}; transform: scale( {N( scale )} );";
	}

	string ParticleStyle( Particle p )
	{
		var t = (float)p.Age;
		var k = Math.Clamp( t / p.Life, 0f, 1f );

		// Sparks fly flat - they're a burst of light, not something thrown in the air.
		var gravity = p.Kind == "spark" ? 0f : Gravity;

		var x = p.X + p.Vx * t;
		var y = p.Y + p.Vy * t + 0.5f * gravity * t * t;

		var fade = 1f - k * k;
		var scale = p.Kind == "spark" ? 1f - k * 0.7f : 1f;

		return $"left: {N( x - p.Size * 0.5f )}px; top: {N( y - p.Size * 0.5f )}px; " +
			$"width: {N( p.Size )}px; height: {N( p.Size )}px; opacity: {N( fade )}; " +
			$"transform: rotate( {N( p.Spin * t )}deg ) scale( {N( scale )} );";
	}

	/// <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 @( Silhouette ? "silhouette" : "" )">

	@if ( Artwork is not null )
	{
		<Image Texture="@Artwork" class="photo"></Image>
	}
	else
	{
		<div class="drawing" style="background-image: url( '@ArtPath' );"></div>
	}

</root>

@code
{
	/// <summary>Index into <see cref="PumpModel.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>Overrides the drawn artwork entirely, if you'd rather use your own.</summary>
	public Texture Artwork { get; set; }

	PumpModel Pump => PumpModel.Get( TierIndex );

	/// <summary>
	/// One illustration per pump - all 108 of them, and no two the same machine. The
	/// tier owns the palette and what the thing stands on; everything else about a pump
	/// is drawn for the name it carries. See tools/make_pumps.py.
	/// </summary>
	string ArtPath => $"/ui/pumps/pump_{Pump.Index + 1:000}.svg";

	// There used to be a 0.92-1.0 scale ramp by rank here, from back when the nine
	// pumps in a tier were one drawing and the ramp was doing the work of selling a
	// purchase. Every drawing includes the ground it stands on, so scaling the image
	// scaled the ground with it and the desert changed size when you bought a pump.
	// The drawings differ on their own now, so the ramp is gone and the horizon holds.

	protected override int BuildHash() => HashCode.Combine( TierIndex, Silhouette, Artwork );
}
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,
.clouds,
.motes,
.shimmer,
.fx
{
	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 );
	align-items: center;
	justify-content: center;
	animation: sun-breathe 9s ease-in-out infinite;
}

// A slowly turning square behind the disc. One panel, and it reads as light bending
// off something very bright - eight individual ray divs looked like a cartwheel.
.sun-rays
{
	position: absolute;
	width: 230%;
	height: 230%;
	border-radius: 22%;
	background-color: rgba( 255, 240, 180, 0.10 );
	animation: sun-turn 70s linear infinite;
}

@keyframes sun-turn
{
	0% { transform: rotate( 0deg ); }
	100% { transform: rotate( 360deg ); }
}

// Scale, not blur radius. The disc and its shadow are drawn once and the transform
// moves the result, where animating the shadow re-blurs a 170px quad every frame for
// as long as the game is running - and the sun is on screen the whole time.
@keyframes sun-breathe
{
	0% { transform: scale( 1 ); }
	50% { transform: scale( 1.07 ); }
	100% { transform: scale( 1 ); }
}

// ---------------------------------------------------------------- clouds

.clouds
{
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 55%;
	overflow: hidden;
}

// Soft ellipses with a wide shadow rather than drawn puffs - at this scale the shadow
// is what sells it, and it costs one panel each.
.cloud
{
	position: absolute;
	border-radius: 50%;
	background-color: rgba( 255, 255, 255, 0.5 );
	box-shadow: 0px 0px 50px 24px rgba( 255, 255, 255, 0.3 );

	&.c1
	{
		top: 12%;
		width: 260px;
		height: 54px;
		animation: drift-a 96s linear infinite;
	}

	&.c2
	{
		top: 30%;
		width: 180px;
		height: 38px;
		opacity: 0.7;
		animation: drift-b 138s linear infinite;
	}

	&.c3
	{
		top: 5%;
		width: 340px;
		height: 62px;
		opacity: 0.45;
		animation: drift-c 176s linear infinite;
	}
}

@keyframes drift-a
{
	0% { left: -22%; }
	100% { left: 108%; }
}

@keyframes drift-b
{
	0% { left: -16%; }
	100% { left: 104%; }
}

@keyframes drift-c
{
	0% { left: -30%; }
	100% { left: 112%; }
}

// ---------------------------------------------------------------- dust

.motes
{
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	overflow: hidden;
}

// Specks of sand on the wind. Purely atmosphere, and the one thing on screen that
// keeps moving when the player has stopped.
.mote
{
	position: absolute;
	width: 5px;
	height: 5px;
	border-radius: 100%;
	background-color: rgba( 255, 244, 214, 0.55 );

	&.m1 { top: 42%; width: 4px; height: 4px; animation: mote-a 17s linear infinite; }
	&.m2 { top: 58%; animation: mote-b 23s linear infinite; }
	&.m3 { top: 71%; width: 3px; height: 3px; animation: mote-c 14s linear infinite; }
	&.m4 { top: 36%; animation: mote-b 29s linear infinite; }
	&.m5 { top: 64%; width: 6px; height: 6px; animation: mote-a 21s linear infinite; }
	&.m6 { top: 79%; width: 4px; height: 4px; animation: mote-c 26s linear infinite; }
	&.m7 { top: 50%; width: 3px; height: 3px; animation: mote-b 19s linear infinite; }
	&.m8 { top: 86%; animation: mote-a 31s linear infinite; }
}

@keyframes mote-a
{
	0% { left: -3%; opacity: 0; transform: translateY( 0px ); }
	14% { opacity: 0.8; }
	86% { opacity: 0.8; }
	100% { left: 103%; opacity: 0; transform: translateY( -46px ); }
}

@keyframes mote-b
{
	0% { left: -3%; opacity: 0; transform: translateY( 0px ); }
	18% { opacity: 0.6; }
	82% { opacity: 0.6; }
	100% { left: 103%; opacity: 0; transform: translateY( 30px ); }
}

@keyframes mote-c
{
	0% { left: -3%; opacity: 0; transform: translateY( 0px ); }
	20% { opacity: 0.9; }
	80% { opacity: 0.9; }
	100% { left: 103%; opacity: 0; transform: translateY( -18px ); }
}

// ---------------------------------------------------------------- heat haze

// A band of light sitting on the horizon, breathing. Off by default and turned up by
// the scenes where the air would actually be shaking.
.shimmer
{
	position: absolute;
	bottom: 8%;
	left: 0;
	width: 100%;
	height: 22%;
	opacity: 0;
	background-image: linear-gradient( to top, rgba( 255, 255, 255, 0.22 ) 0%, rgba( 255, 255, 255, 0 ) 100% );
	animation: haze 7s ease-in-out infinite;
}

// Deliberately does not touch opacity - that is the switch each scene uses to turn the
// haze on, and a keyframe writing it would override the switch and show it everywhere.
@keyframes haze
{
	0% { transform: scaleY( 1 ); }
	50% { transform: scaleY( 1.3 ); }
	100% { transform: scaleY( 1 ); }
}

// 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
{
	position: relative;
	transition: transform 0.12s ease-out;

	.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;
	}

	// Mounted only for the moment after a sale, so it starts its animation fresh every
	// time. Toggling a class on a panel that is already there does not reliably restart
	// one, and a payout that doesn't visibly land is the whole point of this.
	.coinflash
	{
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		border-radius: 18px;
		border: 2px solid $gold;
		animation: coin-flash 0.36s ease-out;
	}

	&.pop
	{
		transform: scale( 1.06 );
	}
}

@keyframes coin-flash
{
	0% { opacity: 0.95; transform: scale( 1 ); }
	100% { opacity: 0; transform: scale( 1.18 ); }
}

.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;
		position: relative;
	}

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

	// A pale disc riding the surface, wider than the tank so only its curve shows. It
	// rocks side to side, which turns a rectangle sliding up into something with liquid in it.
	.tank-wave
	{
		position: absolute;
		top: -5px;
		left: -30%;
		width: 160%;
		height: 10px;
		border-radius: 50%;
		background-color: #8fd8ff;
		animation: tank-rock 1.9s ease-in-out infinite;
	}

	// Fixed glass highlight down the left, so the tank reads as a cylinder.
	.tank-shine
	{
		position: absolute;
		top: 8%;
		left: 14%;
		width: 5px;
		height: 60%;
		border-radius: 999px;
		background-color: rgba( 255, 255, 255, 0.35 );
	}

	.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 .tank-wave
	{
		background-color: #ffb98a;
	}

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

	// A full tank is wasted pumping, so the whole readout nags rather than sitting there.
	&.full
	{
		animation: tank-nag 1.1s ease-in-out infinite;
	}
}

@keyframes tank-rock
{
	0% { transform: translateX( -4px ) rotate( -2deg ); }
	50% { transform: translateX( 4px ) rotate( 2deg ); }
	100% { transform: translateX( -4px ) rotate( -2deg ); }
}

@keyframes tank-nag
{
	0% { box-shadow: 0px 0px 0px 0px rgba( 255, 139, 74, 0.5 ); }
	50% { box-shadow: 0px 0px 18px 3px rgba( 255, 139, 74, 0.55 ); }
	100% { box-shadow: 0px 0px 0px 0px rgba( 255, 139, 74, 0.5 ); }
}

.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;
	position: relative;
	margin-left: 10px;
	transition: transform 0.09s ease-out, background-color 0.12s linear;

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

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

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

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

	// Something in the shop is affordable. A dot in the corner rather than a colour
	// change on the button, so it reads the way an unread badge does everywhere else.
	.dot
	{
		position: absolute;
		top: -4px;
		right: -4px;
		width: 16px;
		height: 16px;
		border-radius: 100%;
		background-color: $gold;
		border: 2px solid rgba( 18, 22, 32, 0.9 );
	}

	&.highlight
	{
		animation: btn-nudge 2.4s ease-in-out infinite;
	}
}

@keyframes btn-nudge
{
	0% { transform: translateY( 0px ); }
	8% { transform: translateY( -5px ); }
	16% { transform: translateY( 0px ); }
	24% { transform: translateY( -3px ); }
	32% { transform: translateY( 0px ); }
	100% { transform: translateY( 0px ); }
}

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

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

// Wraps the pump so the effects layer has something the exact size and place of the
// pump to anchor to. Anchoring to .stage instead put every burst too low, because the
// nameplate under the pump pushes the pump above the stage's centre.
.pumpwrap
{
	position: relative;
	align-items: center;
	justify-content: center;
}

.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%;
	}

	// Size and brightness come in as an inline style - they track pressure.
	.glow
	{
		position: absolute;
		border-radius: 100%;
		background-color: rgba( 255, 255, 255, 0.14 );
		transition: width 0.2s ease-out, height 0.2s ease-out;
	}

	// Only shows once pressure is pinned. The ring, not the glow, is what tells you
	// you're at the top - a glow that just keeps brightening has no ceiling to read.
	.heatring
	{
		position: absolute;
		width: 86%;
		height: 86%;
		border-radius: 100%;
		border: 3px solid rgba( 255, 190, 90, 0.85 );
		opacity: 0;
	}

	.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 );
	}

	&.hot
	{
		.heatring
		{
			opacity: 1;
			animation: heat-ring 1.05s ease-out infinite;
		}

		.pumpart
		{
			animation: hot-throb 0.62s ease-in-out infinite;
		}
	}
}

@keyframes heat-ring
{
	0% { transform: scale( 0.94 ); opacity: 0.9; }
	100% { transform: scale( 1.18 ); opacity: 0; }
}

@keyframes hot-throb
{
	0% { transform: scale( 1 ); }
	50% { transform: scale( 1.035 ); }
	100% { transform: scale( 1 ); }
}

// ---------------------------------------------------------------- effects layer

// A zero-size anchor dead centre on the pump. Everything in here positions itself off
// it with margins, which is why the particle code can work in plain pixel offsets.
.fx
{
	position: absolute;
	left: 50%;
	top: 50%;
	width: 0px;
	height: 0px;
	align-items: center;
	justify-content: center;
}

.particle
{
	position: absolute;
	pointer-events: none;

	// Water thrown up out of the pump head.
	&.drop
	{
		border-radius: 50%;
		background-color: #9fe0ff;
		border: 2px solid rgba( 255, 255, 255, 0.75 );
	}

	// Coins off a sale. Round, gold, and spinning - the spin comes in inline.
	&.coinbit
	{
		border-radius: 100%;
		background-color: $gold;
		border: 3px solid #a8791a;
	}

	// The flat radial burst on a surge or a new pump.
	&.spark
	{
		border-radius: 100%;
		background-color: #fff6d8;
		box-shadow: 0px 0px 14px 4px rgba( 255, 214, 120, 0.75 );
	}
}

// A ring thrown off the pump on every click. Mounted fresh each time so it always
// plays from the start.
// Centred by hand with half its size off the top and left. The layout engine will
// centre an absolutely positioned child off the parent's alignment, but only while no
// inset is set - too subtle a rule to hang the whole effects layer on.
.clickring
{
	position: absolute;
	left: 50%;
	top: 50%;
	margin-left: -105px;
	margin-top: -105px;
	width: 210px;
	height: 210px;
	border-radius: 100%;
	border: 4px solid rgba( 190, 235, 255, 0.75 );
	pointer-events: none;
	animation: click-ring 0.42s ease-out;
}

@keyframes click-ring
{
	0% { transform: scale( 0.42 ); opacity: 0.85; }
	100% { transform: scale( 1.3 ); opacity: 0; }
}

.surgering
{
	position: absolute;
	left: 50%;
	top: 50%;
	margin-left: -150px;
	margin-top: -150px;
	width: 300px;
	height: 300px;
	border-radius: 100%;
	border: 6px solid rgba( 255, 226, 150, 0.9 );
	box-shadow: 0px 0px 40px 10px rgba( 255, 200, 90, 0.5 );
	pointer-events: none;
	animation: surge-ring 0.6s ease-out;
}

@keyframes surge-ring
{
	0% { transform: scale( 0.3 ); opacity: 1; }
	100% { transform: scale( 1.7 ); opacity: 0; }
}

// A wash of warm light over the whole screen for a fifth of a second.
.surgeflash
{
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	background-color: rgba( 255, 220, 150, 0.4 );
	pointer-events: none;
	animation: surge-flash 0.4s ease-out;
}

@keyframes surge-flash
{
	0% { opacity: 0.75; }
	100% { opacity: 0; }
}

// ---------------------------------------------------------------- shake

// The stage shakes, not the whole HUD. Moving the root would drag the sky and the
// dunes with it and show bare edges at the screen border.
.hud.shake-soft .stage
{
	animation: shake-soft 0.32s ease-out;
}

.hud.shake-hard .stage
{
	animation: shake-hard 0.32s ease-out;
}

@keyframes shake-soft
{
	0% { transform: translateX( 0px ) translateY( 0px ); }
	25% { transform: translateX( -4px ) translateY( 2px ); }
	50% { transform: translateX( 3px ) translateY( -2px ); }
	75% { transform: translateX( -2px ) translateY( 1px ); }
	100% { transform: translateX( 0px ) translateY( 0px ); }
}

@keyframes shake-hard
{
	0% { transform: translateX( 0px ) translateY( 0px ); }
	15% { transform: translateX( -9px ) translateY( 4px ); }
	32% { transform: translateX( 8px ) translateY( -5px ); }
	50% { transform: translateX( -6px ) translateY( 3px ); }
	70% { transform: translateX( 4px ) translateY( -2px ); }
	85% { transform: translateX( -2px ) translateY( 1px ); }
	100% { transform: translateX( 0px ) translateY( 0px ); }
}

// ---------------------------------------------------------------- pressure

// Sits between the pump and the nameplate. Always laid out, faded out when cold, so
// showing and hiding it never shunts the nameplate up and down the screen.
.pressure
{
	flex-direction: column;
	align-items: center;
	margin-top: 6px;
	transition: opacity 0.25s linear;

	&.cold { opacity: 0.25; }

	.pressure-bar
	{
		width: 240px;
		height: 10px;
		border-radius: 999px;
		background-color: rgba( 0, 0, 0, 0.45 );
		border: 1px solid rgba( 255, 255, 255, 0.22 );
		overflow: hidden;
	}

	.pressure-fill
	{
		height: 100%;
		background-color: $water;
		transition: width 0.1s linear, background-color 0.3s linear;
	}

	.pressure-label
	{
		margin-top: 4px;
		align-items: center;
	}

	.pressure-name
	{
		font-size: 11px;
		letter-spacing: 3px;
		opacity: 0.7;
		margin-right: 8px;
	}

	.pressure-value
	{
		font-size: 14px;
		font-weight: 800;
		color: #bfe9ff;
	}

	// Four bands rather than a continuous ramp - a colour you can name is a colour you
	// can chase, and the game tells you which band you're in without a number.
	&.s1 .pressure-fill { background-color: #6fd3a0; }
	&.s1 .pressure-value { color: #9ff0c6; }

	&.s2 .pressure-fill { background-color: #ffb54a; }
	&.s2 .pressure-value { color: #ffd08a; }

	&.s3
	{
		.pressure-fill
		{
			background-color: #fff0c0;
			box-shadow: 0px 0px 14px 3px rgba( 255, 200, 90, 0.8 );
		}

		.pressure-value
		{
			color: #fff2cc;
		}

		.pressure-bar
		{
			border-color: rgba( 255, 214, 120, 0.9 );
			animation: pressure-max 0.8s ease-in-out infinite;
		}
	}
}

@keyframes pressure-max
{
	0% { box-shadow: 0px 0px 0px 0px rgba( 255, 200, 90, 0.6 ); }
	50% { box-shadow: 0px 0px 16px 3px rgba( 255, 200, 90, 0.6 ); }
	100% { box-shadow: 0px 0px 0px 0px rgba( 255, 200, 90, 0.6 ); }
}

.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;
	}
}

// Lives in .fx now, so left/top come in inline off the pump's centre. The fixed width
// with centred text is what lets the code place it without measuring the string - keep
// it in step with FloaterWidth in GameHud.razor.
.floater
{
	position: absolute;
	width: 300px;
	text-align: center;
	justify-content: center;
	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;
	}

	&.surge
	{
		font-size: 40px;
		color: #fff4d2;
		letter-spacing: 2px;
		text-shadow: 0px 3px 0px rgba( 120, 60, 0, 0.55 );
	}
}

.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;
	align-items: center;
	border: 2px solid rgba( 140, 210, 255, 0.5 );

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

// 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 );
	position: relative;
	overflow: hidden;

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

	// A band of light that sweeps across the face. Parked off the left edge and only
	// sent across when the button is actually worth pressing.
	.shine
	{
		position: absolute;
		top: 0;
		left: -40%;
		width: 30%;
		height: 100%;
		background-image: linear-gradient( to right, rgba( 255, 255, 255, 0 ) 0%, rgba( 255, 255, 255, 0.35 ) 50%, rgba( 255, 255, 255, 0 ) 100% );
		opacity: 0;
	}

	.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;

		&:not(.disabled) .shine
		{
			opacity: 1;
			animation: shine-sweep 3.4s linear infinite;
		}
	}

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

	// Affordable. This is the moment the whole loop is pointed at, so it gets the
	// brightest thing on screen after the pump itself.
	&.buy.ready
	{
		animation: buy-ready 1.6s ease-in-out infinite;

		.shine
		{
			opacity: 1;
			animation: shine-sweep 2.2s linear infinite;
		}
	}

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

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

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

@keyframes shine-sweep
{
	0% { left: -40%; }
	40% { left: 110%; }
	100% { left: 110%; }
}

@keyframes buy-ready
{
	0% { box-shadow: 0px 0px 0px 0px rgba( 245, 197, 66, 0.55 ); }
	50% { box-shadow: 0px 0px 30px 6px rgba( 245, 197, 66, 0.5 ); }
	100% { box-shadow: 0px 0px 0px 0px rgba( 245, 197, 66, 0.55 ); }
}

// ---------------------------------------------------------------- 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 );
	position: relative;
	transition: transform 0.12s ease-out, background-color 0.2s linear;

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

	.tick
	{
		position: absolute;
		top: 6px;
		right: 6px;
		width: 22px;
		height: 22px;
		border-radius: 100%;
		background-color: #4f9a45;
		align-items: center;
		justify-content: center;

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

	.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;
		box-shadow: 0px 0px 16px 2px rgba( 245, 197, 66, 0.4 );
		animation: slot-next 2s ease-in-out infinite;
	}
}

// The glow is a fixed shadow and the pulse is a colour. Animating the blur radius
// instead makes the renderer rebuild a blurred quad on every frame of every second the
// game is open, and this one is always on because there is always a next pump.
//
// The border, specifically. .slot animates neither - it transitions transform for its
// hover lift and background-color for its owned state, and a keyframe on either of
// those would fight something: an animation outranks the hover for as long as it runs,
// which would have quietly killed the lift on the one slot the player is most likely
// to point at.
@keyframes slot-next
{
	0% { border-color: rgba( 245, 197, 66, 0.45 ); }
	50% { border-color: rgba( 255, 226, 140, 1 ); }
	100% { border-color: rgba( 245, 197, 66, 0.45 ); }
}

// ---------------------------------------------------------------- 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: 118px;
	height: 150px;
	position: relative;
	cursor: pointer;
	pointer-events: all;
	background-repeat: no-repeat;
	background-position: bottom center;
	background-size: contain;
	transition: transform 0.25s ease-out;

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

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

@keyframes tree-sway
{
	0% { transform: rotate( -1.5deg ); }
	50% { transform: rotate( 1.5deg ); }
	100% { transform: rotate( -1.5deg ); }
}

.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;
		}
	}
}

// ---- Sandstorm: ochre haze, the horizon eaten by dust

.scene-sandstorm
{
	.sky { background-image: linear-gradient( to bottom, #d9ae6b 0%, #c99a52 45%, #a97c3c 78%, #7a5326 100% ); }
	.sun { background-color: #f0d9a6; box-shadow: 0px 0px 130px 50px rgba( 240, 217, 166, 0.3 ); }
	.dune.far { background-color: #a9803f; }
	.dune.near { background-color: #96702f; }
	.floor { background-color: #7a5326; }
	.scenery
	{
		opacity: 1;
		background-image: linear-gradient( to top, rgba( 200, 160, 90, 0.4 ) 0%, rgba( 200, 160, 90, 0 ) 60% );
	}
}

// ---- Golden Hour: the twenty minutes a day the desert looks kind

.scene-golden
{
	.sky { background-image: linear-gradient( to bottom, #5b3a86 0%, #c1476a 38%, #f2884b 74%, #f7b167 100% ); }
	.sun { background-color: #fff0c4; box-shadow: 0px 0px 120px 44px rgba( 255, 200, 120, 0.5 ); }
	.dune.far { background-color: #a4553f; }
	.dune.near { background-color: #8c4436; }
	.floor { background-color: #6d3630; }
}

// ---- Starfield: no moon, no cloud, rather a lot of sky

.scene-starfield
{
	.sky { background-image: linear-gradient( to bottom, #04060f 0%, #070b1c 40%, #18213f 75%, #2b3560 100% ); }
	.sun
	{
		background-color: #dfe6ff;
		width: 90px;
		height: 90px;
		box-shadow: 0px 0px 70px 22px rgba( 223, 230, 255, 0.35 );
	}
	.dune.far { background-color: #1b2340; }
	.dune.near { background-color: #141a30; }
	.floor { background-color: #0d1122; }
	.stars
	{
		opacity: 1;
		background-image: linear-gradient( to bottom, rgba( 255, 255, 255, 0.22 ) 0%, rgba( 255, 255, 255, 0 ) 75% );
	}
}

// ---- Aurora: green light over the ice

.scene-aurora
{
	.sky { background-image: linear-gradient( to bottom, #04121f 0%, #0b2c3a 35%, #1c6b63 70%, #3fd6a0 100% ); }
	.sun { background-color: #b9f5e2; box-shadow: 0px 0px 110px 40px rgba( 100, 230, 190, 0.4 ); }
	.dune.far { background-color: #1d5a5a; }
	.dune.near { background-color: #16414a; }
	.floor { background-color: #0f2c35; }
	.stars
	{
		opacity: 1;
		background-image: linear-gradient( to bottom, rgba( 120, 255, 210, 0.18 ) 0%, rgba( 120, 255, 210, 0 ) 70% );
	}
	.scenery
	{
		opacity: 1;
		background-image: linear-gradient( to top, rgba( 63, 214, 160, 0.3 ) 0%, rgba( 63, 214, 160, 0 ) 45% );
	}
}

// ---- Volcanic: warm rock, water comes up boiling

.scene-volcanic
{
	.sky { background-image: linear-gradient( to bottom, #120404 0%, #1a0806 40%, #7d2412 78%, #e2542a 100% ); }
	.sun { background-color: #ff8a4a; box-shadow: 0px 0px 130px 46px rgba( 226, 84, 42, 0.5 ); }
	.dune.far { background-color: #3a1410; }
	.dune.near { background-color: #2a0d0a; }
	.floor { background-color: #1c0806; }
	.scenery
	{
		opacity: 1;
		background-image: linear-gradient( to top, rgba( 226, 84, 42, 0.45 ) 0%, rgba( 226, 84, 42, 0 ) 40% );
	}
}

// ---- Crystal Cavern: deep enough that the walls glow

.scene-crystal
{
	.sky { background-image: linear-gradient( to bottom, #0a0620 0%, #160d33 40%, #3f2a70 76%, #7f6ae0 100% ); }
	.sun { background-color: #cfc2ff; box-shadow: 0px 0px 120px 42px rgba( 160, 140, 255, 0.45 ); }
	.dune.far { background-color: #2e2059; }
	.dune.near { background-color: #241947; }
	.floor { background-color: #170f2e; }
	.stars
	{
		opacity: 1;
		background-image: linear-gradient( to bottom, rgba( 190, 170, 255, 0.2 ) 0%, rgba( 190, 170, 255, 0 ) 70% );
	}
}

// ---------------------------------------------------------------- new pump

// Buying a pump is the reward the whole loop points at, and it used to be a line of
// toast text. Two and a half seconds of the screen stopping to say so instead.
.celebrate
{
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	align-items: center;
	justify-content: center;
	pointer-events: none;
	animation: celebrate-fade 2.6s ease-out;

	.celebrate-ring
	{
		position: absolute;
		left: 50%;
		top: 50%;
		margin-left: -210px;
		margin-top: -210px;
		width: 420px;
		height: 420px;
		border-radius: 100%;
		border: 8px solid rgba( 255, 226, 150, 0.85 );
		animation: celebrate-ring 1.1s ease-out;

		&.two
		{
			margin-left: -130px;
			margin-top: -130px;
			width: 260px;
			height: 260px;
			border-width: 5px;
			border-color: rgba( 255, 255, 255, 0.8 );
			animation: celebrate-ring 1.5s ease-out;
		}
	}

	.celebrate-card
	{
		flex-direction: column;
		align-items: center;
		background-color: rgba( 16, 20, 30, 0.86 );
		border: 3px solid $gold;
		border-radius: 24px;
		padding: 20px 46px;
		animation: celebrate-card 2.6s ease-out;
	}

	.celebrate-kicker
	{
		font-size: 14px;
		letter-spacing: 5px;
		color: $gold;
		font-weight: 700;
	}

	.celebrate-name
	{
		font-size: 46px;
		font-weight: 800;
		margin-top: 2px;
		text-shadow: 0px 3px 0px rgba( 0, 0, 0, 0.45 );
	}

	.celebrate-sub
	{
		font-size: 16px;
		opacity: 0.8;
		margin-top: 2px;
	}
}

@keyframes celebrate-fade
{
	0% { opacity: 1; }
	80% { opacity: 1; }
	100% { opacity: 0; }
}

@keyframes celebrate-ring
{
	0% { transform: scale( 0.2 ); opacity: 0.95; }
	100% { transform: scale( 1.6 ); opacity: 0; }
}

@keyframes celebrate-card
{
	0% { transform: scale( 0.6 ); opacity: 0; }
	9% { transform: scale( 1.08 ); opacity: 1; }
	16% { transform: scale( 1 ); opacity: 1; }
	86% { transform: scale( 1 ); opacity: 1; }
	100% { transform: scale( 1 ); opacity: 0; }
}

// ---------------------------------------------------------------- scene weather

// The ambient layers are shared markup, so each scene only has to say what its own
// air is doing. Dawn is the baseline: clouds, a little haze off the sand.

.scene-dawn
{
	.shimmer { opacity: 0.7; }
}

.scene-dusk
{
	.cloud { background-color: rgba( 120, 70, 90, 0.5 ); box-shadow: 0px 0px 50px 24px rgba( 120, 70, 90, 0.3 ); }
	.mote { background-color: rgba( 255, 190, 190, 0.4 ); }
	.shimmer { opacity: 0.25; }
}

.scene-oasis
{
	.cloud { background-color: rgba( 255, 255, 255, 0.7 ); }
	// Seeds and pollen rather than sand, drifting rather than blowing through.
	.mote { background-color: rgba( 220, 255, 190, 0.7 ); }
	.shimmer { opacity: 0; }
}

.scene-neon
{
	.cloud { background-color: rgba( 255, 120, 190, 0.28 ); box-shadow: 0px 0px 60px 30px rgba( 255, 95, 162, 0.22 ); }
	.mote { background-color: rgba( 255, 150, 220, 0.75 ); }
	.shimmer { opacity: 0.4; }
}

// Enough dust in the air that the clouds have gone and the haze has taken over.
.scene-sandstorm
{
	.cloud { background-color: rgba( 220, 190, 140, 0.4 ); box-shadow: 0px 0px 70px 40px rgba( 200, 160, 90, 0.3 ); }
	.mote { background-color: rgba( 255, 230, 180, 0.85 ); width: 7px; height: 7px; }
	.shimmer { opacity: 1; }
}

.scene-golden
{
	.cloud { background-color: rgba( 255, 210, 180, 0.55 ); box-shadow: 0px 0px 60px 26px rgba( 255, 170, 120, 0.35 ); }
	.mote { background-color: rgba( 255, 220, 170, 0.7 ); }
	.shimmer { opacity: 0.6; }
}

// No cloud and no wind - the motes hold still enough to read as stars.
.scene-starfield
{
	.clouds { opacity: 0; }
	.mote { background-color: rgba( 235, 240, 255, 0.9 ); }
	.shimmer { opacity: 0; }
}

.scene-aurora
{
	.cloud { background-color: rgba( 120, 255, 210, 0.2 ); box-shadow: 0px 0px 70px 34px rgba( 100, 230, 190, 0.22 ); }
	.mote { background-color: rgba( 235, 255, 250, 0.85 ); width: 6px; height: 6px; }
	.shimmer { opacity: 0.3; }
}

// Embers, and air you can see moving.
.scene-volcanic
{
	.cloud { background-color: rgba( 90, 40, 30, 0.5 ); box-shadow: 0px 0px 70px 34px rgba( 226, 84, 42, 0.25 ); }
	.mote { background-color: rgba( 255, 160, 90, 0.9 ); box-shadow: 0px 0px 8px 2px rgba( 255, 120, 50, 0.6 ); }
	.shimmer { opacity: 1; }
}

.scene-crystal
{
	.clouds { opacity: 0; }
	.mote { background-color: rgba( 210, 190, 255, 0.85 ); box-shadow: 0px 0px 8px 2px rgba( 170, 140, 255, 0.5 ); }
	.shimmer { opacity: 0.35; }
}
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];
}
Debug: View Raw JSON Response
{
    "TotalCount": 58,
    "Files": [
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": ".obj/__compiler_extra.cs",
            "FileName": "__compiler_extra.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 343538,
            "IsPrivate": false,
            "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-20T18:13:47.6067714Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.110.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.110.0\")]"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "Game/DesertPumpGame.cs",
            "FileName": "DesertPumpGame.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 343538,
            "IsPrivate": false,
            "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\t/// <summary>Played on a surge. Falls back to the upgrade sting when it's left empty.</summary>\r\n\t[Property, Group( \"Audio\" )] public SoundEvent SurgeSound { 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>\r\n\t/// Fired on an ordinary manual pump, with the litres gained. A pump that came up a\r\n\t/// surge fires <see cref=\"Surged\"/> instead - exactly one of the two per click.\r\n\t/// </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\t// The pump and everyone you've hired, together.\r\n\t\tvar passive = TotalWaterPerSecond * 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\tTickPressure();\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\t/// <remarks>\r\n\t/// Pressure and surges only exist on this path. Idle water, workers and the tree all\r\n\t/// go through <see cref=\"AddWater\"/> untouched, so hands-off income is exactly what\r\n\t/// it always was.\r\n\t/// </remarks>\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 surge = RollSurge();\r\n\t\tvar litres = LitresPerClick * PressureBonus * (surge ? SurgeMultiplier : 1f);\r\n\r\n\t\tvar gained = AddWater( litres );\r\n\r\n\t\t// Pressure builds even on the click that overflowed the tank - the rhythm is the\r\n\t\t// player's, and dropping it because the tank happened to be full feels like a bug.\r\n\t\tBumpPressure();\r\n\t\tTotalPumps++;\r\n\r\n\t\tif ( surge )\r\n\t\t{\r\n\t\t\tSurgeCount++;\r\n\t\t\tPlay( SurgeSound ?? UpgradeSound );\r\n\t\t\tSurged?.Invoke( gained );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tPlay( PumpSound );\r\n\t\t\tPumped?.Invoke( gained );\r\n\t\t}\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\tArray.Clear( workerLevels );\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\t\tPressure = 0;\r\n\t\tSurgeCount = 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 = ReadSave( SaveFile );\r\n\r\n\t\t// Nothing came back but there is a file on disk, so it is unreadable rather\r\n\t\t// than absent - a write cut short by a crash, most likely. The backup below is\r\n\t\t// one session behind, which beats handing the player a fresh run and no\r\n\t\t// explanation. This is the half of that feature that was missing: the backup\r\n\t\t// was being written every load and never once read.\r\n\t\tif ( save is null && FileSystem.Data.FileExists( SaveFile ) )\r\n\t\t{\r\n\t\t\tsave = ReadSave( SaveFile + \".bak\" );\r\n\r\n\t\t\tLog.Warning( save is null\r\n\t\t\t\t? $\"{SaveFile} is unreadable and so is its backup - starting a fresh run.\"\r\n\t\t\t\t: $\"{SaveFile} is unreadable - restored the backup from the last session.\" );\r\n\t\t}\r\n\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\tArray.Clear( workerLevels );\r\n\t\tvar levels = save.WorkerLevels ?? new List<int>();\r\n\t\tfor ( var i = 0; i < Math.Min( levels.Count, workerLevels.Length ); i++ )\r\n\t\t{\r\n\t\t\tworkerLevels[i] = Math.Clamp( levels[i], 0, WorkerType.MaxLevel );\r\n\t\t}\r\n\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\t/// <summary>\r\n\t/// Read one save file, or null if it isn't there and null if it's rubbish.\r\n\t/// </summary>\r\n\t/// <remarks>\r\n\t/// ReadJsonOrDefault already returns the default on a missing or malformed file, so\r\n\t/// the catch is only for what it doesn't cover - a locked or unreadable file. Either\r\n\t/// way a broken save must never take the game down with it: that turns \"you lost\r\n\t/// your progress\" into \"the game no longer starts\".\r\n\t/// </remarks>\r\n\tPumpSave ReadSave( string path )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\treturn FileSystem.Data.ReadJsonOrDefault<PumpSave>( path, null );\r\n\t\t}\r\n\t\tcatch ( Exception e )\r\n\t\t{\r\n\t\t\tLog.Warning( $\"Couldn't read {path}: {e.Message}\" );\r\n\t\t\treturn null;\r\n\t\t}\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 || TotalWaterPerSecond <= 0 )\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( TotalWaterPerSecond * 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\tWorkerLevels = workerLevels.ToList(),\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/PumpTier.cs",
            "FileName": "PumpTier.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 343538,
            "IsPrivate": false,
            "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\t// These nine are all ways of getting water out of a hole in the ground.\n\t\t\t// The old set were leftovers from when there were nine tiers rather than\n\t\t\t// twelve, so tier 1 - the one whose whole premise is hand-pulled water -\n\t\t\t// contained a Steam Pressure Pump, a Diesel Derrick and a Solar Filtration\n\t\t\t// Rig, each of which is the name of a tier the player has not reached yet.\n\t\t\tPumpNames = new[]\n\t\t\t{\n\t\t\t\t\"Rusty Hand Pump\", \"Iron Well Pump\", \"Bucket Windlass\",\n\t\t\t\t\"Windmill Well\", \"Cattle Trough Bore\", \"Stone Well House\",\n\t\t\t\t\"Desert Water Tower\", \"Twin Bore Rig\", \"The Great Standpipe\"\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": 343538,
            "IsPrivate": false,
            "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": 343538,
            "IsPrivate": false,
            "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": "Game/PumpSettings.cs",
            "FileName": "PumpSettings.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 343538,
            "IsPrivate": false,
            "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": "styles/base.scss",
            "FileName": "base.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 343538,
            "IsPrivate": false,
            "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": 343538,
            "IsPrivate": false,
            "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": 343538,
            "IsPrivate": false,
            "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": 343538,
            "IsPrivate": false,
            "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": 343538,
            "IsPrivate": false,
            "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": "Game/DesertPumpGame.Workers.cs",
            "FileName": "DesertPumpGame.Workers.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 343538,
            "IsPrivate": false,
            "Code": "namespace DesertPump;\n\n/// <summary>\n/// The hired help. Workers add litres per second on top of the pump, and unlike the\n/// pump they keep working while you're doing something else entirely.\n/// </summary>\npublic sealed partial class DesertPumpGame\n{\n\treadonly int[] workerLevels = new int[WorkerType.Count];\n\n\tpublic int WorkerLevel( WorkerType worker ) => workerLevels[worker.Index];\n\n\tpublic bool IsHired( WorkerType worker ) => WorkerLevel( worker ) > 0;\n\n\tpublic bool IsWorkerMaxed( WorkerType worker ) => WorkerLevel( worker ) >= WorkerType.MaxLevel;\n\n\t/// <summary>Cost of the next level, or infinity when they've nothing left to learn.</summary>\n\tpublic double WorkerCost( WorkerType worker ) => IsWorkerMaxed( worker )\n\t\t? double.PositiveInfinity\n\t\t: worker.CostAt( WorkerLevel( worker ) );\n\n\tpublic bool CanHire( WorkerType worker ) => !IsWorkerMaxed( worker ) && Coins >= WorkerCost( worker );\n\n\t/// <summary>What one worker is currently producing.</summary>\n\tpublic double WorkerOutput( WorkerType worker ) => worker.OutputAt( WorkerLevel( worker ) );\n\n\t/// <summary>Everything the crew makes between them, litres per second.</summary>\n\tpublic double WorkerWaterPerSecond\n\t{\n\t\tget\n\t\t{\n\t\t\tdouble total = 0;\n\n\t\t\tforeach ( var worker in WorkerType.All )\n\t\t\t{\n\t\t\t\ttotal += WorkerOutput( worker );\n\t\t\t}\n\n\t\t\treturn total;\n\t\t}\n\t}\n\n\t/// <summary>How many of the fifteen are on the payroll.</summary>\n\tpublic int HiredCount\n\t{\n\t\tget\n\t\t{\n\t\t\tvar count = 0;\n\n\t\t\tforeach ( var worker in WorkerType.All )\n\t\t\t{\n\t\t\t\tif ( IsHired( worker ) ) count++;\n\t\t\t}\n\n\t\t\treturn count;\n\t\t}\n\t}\n\n\t/// <summary>The pump and the crew together - what the tank actually fills at.</summary>\n\tpublic double TotalWaterPerSecond => CurrentPump.PerSecond + WorkerWaterPerSecond;\n\n\t/// <summary>Fired after hiring or promoting someone.</summary>\n\tpublic Action<WorkerType> Hired { get; set; }\n\n\t/// <summary>Hire a worker, or promote one you already have.</summary>\n\tpublic bool BuyWorker( WorkerType worker )\n\t{\n\t\tif ( IsWorkerMaxed( worker ) )\n\t\t{\n\t\t\tRefuse( $\"{worker.Name} can't be promoted any further\" );\n\t\t\treturn false;\n\t\t}\n\n\t\tvar cost = WorkerCost( worker );\n\t\tif ( Coins < cost )\n\t\t{\n\t\t\tRefuse( $\"Need {Numbers.Short( cost - Coins )} more coins\" );\n\t\t\treturn false;\n\t\t}\n\n\t\tCoins -= cost;\n\t\tworkerLevels[worker.Index]++;\n\n\t\tPlay( UpgradeSound );\n\t\tHired?.Invoke( worker );\n\t\tSave();\n\n\t\treturn true;\n\t}\n}\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "UI/TreePanel.razor",
            "FileName": "TreePanel.razor",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 343538,
            "IsPrivate": false,
            "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": "UI/GameHud.razor",
            "FileName": "GameHud.razor",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 343538,
            "IsPrivate": false,
            "Code": "@using System\n@using System.Globalization\n@using Sandbox\n@using Sandbox.UI\n@namespace DesertPump\n@inherits PanelComponent\n\n<div class=\"hud @SceneClass @( ShakeClass )\">\n\n\t<div class=\"sky\"></div>\n\t<div class=\"sun\">\n\t\t<div class=\"sun-rays\"></div>\n\t</div>\n\t<div class=\"stars\"></div>\n\n\t<div class=\"clouds\">\n\t\t<div class=\"cloud c1\"></div>\n\t\t<div class=\"cloud c2\"></div>\n\t\t<div class=\"cloud c3\"></div>\n\t</div>\n\n\t<div class=\"floor\"></div>\n\t<div class=\"dune far\"></div>\n\t<div class=\"dune near\"></div>\n\t<div class=\"shimmer\"></div>\n\t<div class=\"scenery\"></div>\n\n\t<div class=\"motes\">\n\t\t<div class=\"mote m1\"></div>\n\t\t<div class=\"mote m2\"></div>\n\t\t<div class=\"mote m3\"></div>\n\t\t<div class=\"mote m4\"></div>\n\t\t<div class=\"mote m5\"></div>\n\t\t<div class=\"mote m6\"></div>\n\t\t<div class=\"mote m7\"></div>\n\t\t<div class=\"mote m8\"></div>\n\t</div>\n\n\t@if ( State is null )\n\t{\n\t\t<div class=\"missing\">\n\t\t\t<div class=\"missing-title\">No Desert Pump Game in the scene</div>\n\t\t\t<div class=\"missing-body\">Add a \"Desert Pump Game\" component to any GameObject.</div>\n\t\t</div>\n\t}\n\telse\n\t{\n\t\t<div class=\"topbar\">\n\n\t\t\t<div class=\"stat coins @( CoinPopping ? \"pop\" : \"\" )\">\n\t\t\t\t<div class=\"coin\"></div>\n\t\t\t\t<div class=\"value\">@Numbers.Short( State.Coins )</div>\n\t\t\t\t@if ( CoinPopping )\n\t\t\t\t{\n\t\t\t\t\t<div class=\"coinflash\"></div>\n\t\t\t\t}\n\t\t\t</div>\n\n\t\t\t<div class=\"stat tankstat @( State.TankFull ? \"full\" : \"\" )\">\n\t\t\t\t<div class=\"tank\">\n\t\t\t\t\t<div class=\"tank-fill\" style=\"@FillStyle()\">\n\t\t\t\t\t\t<div class=\"tank-wave\"></div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"tank-shine\"></div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"tank-text\">\n\t\t\t\t\t<div class=\"value\">@Numbers.Short( State.Water )<span class=\"unit\"> / @Numbers.Short( State.TankCapacity )L</span></div>\n\t\t\t\t\t<div class=\"sub\">@RateText()</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div class=\"spacer\"></div>\n\n\t\t\t<div class=\"iconbutton shopbtn @( AnyUpgradeAffordable ? \"highlight\" : \"\" )\" onclick=\"@OpenShop\">\n\t\t\t\t<i>storefront</i>\n\t\t\t\t@if ( AnyUpgradeAffordable )\n\t\t\t\t{\n\t\t\t\t\t<div class=\"dot\"></div>\n\t\t\t\t}\n\t\t\t</div>\n\n\t\t\t<div class=\"iconbutton @( State.Muted ? \"muted\" : \"\" )\" onclick=\"@ToggleMute\">\n\t\t\t\t<i>@( State.Muted ? \"volume_off\" : \"volume_up\" )</i>\n\t\t\t</div>\n\n\t\t\t<div class=\"iconbutton\" onclick=\"@OpenSettings\">\n\t\t\t\t<i>settings</i>\n\t\t\t</div>\n\n\t\t</div>\n\n\t\t<div class=\"stage\">\n\n\t\t\t<div class=\"pumpwrap\">\n\n\t\t\t\t<div class=\"pumparea @PumpAreaClass()\" onclick=\"@DoPump\">\n\t\t\t\t\t<div class=\"glow\" style=\"@GlowStyle()\"></div>\n\t\t\t\t\t<div class=\"heatring\"></div>\n\t\t\t\t\t<PumpArt TierIndex=\"@State.PumpLevel\" Artwork=\"@ArtworkFor( State.PumpLevel )\"></PumpArt>\n\t\t\t\t\t<div class=\"ground\"></div>\n\t\t\t\t</div>\n\n\t\t\t\t@if ( ShowClickRing )\n\t\t\t\t{\n\t\t\t\t\t<div class=\"clickring\"></div>\n\t\t\t\t}\n\n\t\t\t\t@if ( ShowSurgeBurst )\n\t\t\t\t{\n\t\t\t\t\t<div class=\"surgering\"></div>\n\t\t\t\t}\n\n\t\t\t\t<div class=\"fx\">\n\t\t\t\t\t@foreach ( var p in particles )\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"particle @p.Kind\" style=\"@ParticleStyle( p )\"></div>\n\t\t\t\t\t}\n\n\t\t\t\t\t@foreach ( var f in floaters )\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"floater @f.Kind\" style=\"@FloaterStyle( f )\">@f.Text</div>\n\t\t\t\t\t}\n\t\t\t\t</div>\n\n\t\t\t</div>\n\n\t\t\t<div class=\"pressure s@( State.PressureStage ) @( State.Pressure > 0.02f ? \"\" : \"cold\" )\">\n\t\t\t\t<div class=\"pressure-bar\">\n\t\t\t\t\t<div class=\"pressure-fill\" style=\"@PressureFillStyle()\"></div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"pressure-label\">\n\t\t\t\t\t<span class=\"pressure-name\">PRESSURE</span>\n\t\t\t\t\t<span class=\"pressure-value\">@State.PressureBonusText</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div class=\"nameplate\">\n\t\t\t\t<div class=\"tierno\">@State.CurrentPump.Position &middot; @State.Tier.Name.ToUpper()</div>\n\t\t\t\t<div class=\"name\">@State.CurrentPump.Name</div>\n\t\t\t\t<div class=\"tagline\">@State.Tier.Tagline</div>\n\t\t\t\t<div class=\"tierbar\">\n\t\t\t\t\t<div class=\"tierfill\" style=\"@TierFillStyle()\"></div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"perclick\">[email protected]( State.LitresPerClick )L per pump - @Numbers.Short( State.PricePerLitre ) @Numbers.Plural( State.PricePerLitre, \"coin\", \"coins\" ) per litre</div>\n\t\t\t</div>\n\n\t\t\t@if ( ShowToast )\n\t\t\t{\n\t\t\t\t<div class=\"toast\">@toast</div>\n\t\t\t}\n\n\t\t\t@if ( ShowIdleNote )\n\t\t\t{\n\t\t\t\t<div class=\"idlenote\"><i>schedule</i> The pump made @Numbers.Short( idleNotice )L while you were away</div>\n\t\t\t}\n\n\t\t\t<div class=\"treearea\">\n\n\t\t\t\t<div class=\"tree @( State.TreeIsGrowing ? \"growing\" : \"\" )\" onclick=\"@DoWaterTree\"\n\t\t\t\t\t style=\"background-image: url( '/ui/tree/tree_@(State.TreeArtStage).svg' );\">\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"treeinfo\">\n\t\t\t\t\t<div class=\"treeheight\"><i>park</i> @State.TreeHeight @( State.TreeHeight == 1 ? \"stage\" : \"stages\" )</div>\n\n\t\t\t\t\t@if ( State.TreeIsGrowing )\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"treestatus growing\">Growing - @State.TreeTimeLeft</div>\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"treebar\">\n\t\t\t\t\t\t\t<div class=\"treefill\" style=\"@TreeFillStyle()\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"treestatus\">@Numbers.Short( State.TreeWater )/@Numbers.Short( State.TreeWaterNeeded )L</div>\n\t\t\t\t\t}\n\n\t\t\t\t\t<div class=\"treebtn @( State.CanWaterTree ? \"\" : \"disabled\" )\" onclick=\"@DoWaterTree\">\n\t\t\t\t\t\t<i>water_drop</i> WATER\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"treebtn board\" onclick=\"@OpenTree\">\n\t\t\t\t\t\t<i>leaderboard</i> BOARD\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t</div>\n\n\t\t\t@* Input.Pressed is edge triggered, so SPACE is one pump per press - holding it\n\t\t\t   does nothing and telling the player otherwise reads as the game being broken. *@\n\t\t\t<div class=\"hint\">Click the pump or tap SPACE - keep a rhythm going to build pressure</div>\n\n\t\t\t@* Last in the stage on purpose. It spawns at a random spot whose range\n\t\t\t   overlaps the tree, and drawn any earlier the tree paints over it and eats\n\t\t\t   the click - so the gusher silently waters the tree and then expires. It is\n\t\t\t   a rare, timed reward; nothing in here should ever be allowed on top of it. *@\n\t\t\t@if ( State.GusherActive )\n\t\t\t{\n\t\t\t\t<div class=\"gusher\" style=\"@GusherStyle()\" onclick=\"@ClaimGusher\">\n\t\t\t\t\t<div class=\"gusher-drop\"></div>\n\t\t\t\t\t<div class=\"gusher-ring\"></div>\n\t\t\t\t\t<div class=\"gusher-ring two\"></div>\n\t\t\t\t</div>\n\t\t\t}\n\n\t\t</div>\n\n\t\t@if ( State.CanClaimDaily )\n\t\t{\n\t\t\t<div class=\"daily\" onclick=\"@ClaimDaily\">\n\t\t\t\t<div class=\"daily-icon\"><i>redeem</i></div>\n\t\t\t\t<div class=\"daily-text\">\n\t\t\t\t\t<div class=\"daily-title\">DAILY BONUS READY</div>\n\t\t\t\t\t<div class=\"daily-sub\">@Numbers.Short( State.DailyReward ) coins &middot; day @State.ProjectedDailyStreak streak</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t}\n\n\t\t<div class=\"actions\">\n\n\t\t\t<div class=\"bigbutton sell @( State.CanSell ? \"\" : \"disabled\" )\" onclick=\"@DoSell\">\n\t\t\t\t<div class=\"btn-icon\"><i>local_drink</i></div>\n\t\t\t\t<div class=\"btn-text\">\n\t\t\t\t\t<div class=\"btn-title\">SELL WATER</div>\n\t\t\t\t\t<div class=\"btn-sub\">[email protected]( State.SaleValue ) coins</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"shine\"></div>\n\t\t\t</div>\n\n\t\t\t@if ( State.IsMaxed )\n\t\t\t{\n\t\t\t\t<div class=\"bigbutton buy maxed\">\n\t\t\t\t\t<div class=\"btn-icon\"><i>emoji_events</i></div>\n\t\t\t\t\t<div class=\"btn-text\">\n\t\t\t\t\t\t<div class=\"btn-title\">ALL PUMPS OWNED</div>\n\t\t\t\t\t\t<div class=\"btn-sub\">the desert is yours</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\t<div class=\"bigbutton buy @( State.CanUpgrade ? \"ready\" : \"disabled\" )\" onclick=\"@DoBuy\">\n\t\t\t\t\t<div class=\"btn-icon\"><i>upgrade</i></div>\n\t\t\t\t\t<div class=\"btn-text\">\n\t\t\t\t\t\t<div class=\"btn-title\">BUY @State.NextPump.Name.ToUpper()</div>\n\t\t\t\t\t\t<div class=\"btn-sub\">@Numbers.Short( State.NextPump.Cost ) coins</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"shine\"></div>\n\t\t\t\t</div>\n\t\t\t}\n\n\t\t</div>\n\n\t\t<div class=\"collection\">\n\t\t\t@foreach ( var pump in VisiblePumps )\n\t\t\t{\n\t\t\t\t<div class=\"slot @SlotClass( pump )\">\n\t\t\t\t\t<div class=\"slotart\">\n\t\t\t\t\t\t<PumpArt TierIndex=\"@pump.Index\"\n\t\t\t\t\t\t\t\t Silhouette=\"@( pump.Index > State.PumpLevel )\"\n\t\t\t\t\t\t\t\t Artwork=\"@ArtworkFor( pump.Index )\"></PumpArt>\n\t\t\t\t\t\t@if ( pump.Index > State.PumpLevel )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t<div class=\"question\">?</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"slotlabel\">@SlotLabel( pump )</div>\n\t\t\t\t\t@if ( pump.Index <= State.PumpLevel )\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"tick\"><i>check</i></div>\n\t\t\t\t\t}\n\t\t\t\t</div>\n\t\t\t}\n\t\t</div>\n\n\t\t@if ( ShowCelebration )\n\t\t{\n\t\t\t<div class=\"celebrate\">\n\t\t\t\t<div class=\"celebrate-ring\"></div>\n\t\t\t\t<div class=\"celebrate-ring two\"></div>\n\t\t\t\t<div class=\"celebrate-card\">\n\t\t\t\t\t<div class=\"celebrate-kicker\">NEW PUMP INSTALLED</div>\n\t\t\t\t\t<div class=\"celebrate-name\">@celebrateName</div>\n\t\t\t\t\t<div class=\"celebrate-sub\">@celebrateSub</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t}\n\n\t\t@if ( ShowSurgeFlash )\n\t\t{\n\t\t\t<div class=\"surgeflash\"></div>\n\t\t}\n\n\t\t@if ( showShop )\n\t\t{\n\t\t\t<ShopPanel OnClose=\"@CloseOverlays\"></ShopPanel>\n\t\t}\n\n\t\t@if ( showSettings )\n\t\t{\n\t\t\t<SettingsPanel OnClose=\"@CloseOverlays\"></SettingsPanel>\n\t\t}\n\n\t\t@if ( showTree )\n\t\t{\n\t\t\t<TreePanel OnClose=\"@CloseOverlays\"></TreePanel>\n\t\t}\n\t}\n\n</div>\n\n@code\n{\n\t/// <summary>Optional - left empty, the HUD finds the game itself.</summary>\n\t[Property] public DesertPumpGame Target { get; set; }\n\n\t/// <summary>\n\t/// The real pump artwork, one texture per tier in the same order as\n\t/// <see cref=\"PumpTier.All\"/>. Any slot left empty falls back to placeholder art.\n\t/// </summary>\n\t[Property] public List<Texture> PumpArtwork { get; set; } = new();\n\n\tDesertPumpGame State => Target.IsValid() ? Target : DesertPumpGame.Current;\n\n\tconst float FloaterLife = 1.1f;\n\tconst float SurgeFloaterLife = 1.5f;\n\tconst float ToastLife = 2.2f;\n\tconst float IdleNoticeLife = 8f;\n\tconst float PumpAnimLife = 0.16f;\n\tconst float ClickRingLife = 0.42f;\n\tconst float SurgeFlashLife = 0.4f;\n\tconst float SurgeBurstLife = 0.6f;\n\tconst float CoinPopLife = 0.36f;\n\tconst float CelebrationLife = 2.6f;\n\tconst float ShakeLife = 0.32f;\n\n\t/// <summary>Pixels per second squared pulling the burst particles back down.</summary>\n\tconst float Gravity = 1150f;\n\n\t/// <summary>Must match the width of .floater in the stylesheet - see FloaterStyle.</summary>\n\tconst float FloaterWidth = 300f;\n\n\t/// <summary>Above this the whole effects layer stops spawning - a fast clicker can outrun it.</summary>\n\tconst int MaxParticles = 40;\n\n\tclass Floater\n\t{\n\t\tpublic string Text;\n\t\tpublic string Kind;\n\t\tpublic float X;\n\t\tpublic float Drift;\n\t\tpublic float Life;\n\t\tpublic RealTimeSince Age;\n\t}\n\n\t/// <summary>\n\t/// One piece of the burst. Positions are integrated in C# rather than handed to a CSS\n\t/// keyframe because each one needs its own arc - see <see cref=\"ParticleStyle\"/>.\n\t/// </summary>\n\tclass Particle\n\t{\n\t\tpublic string Kind;\n\t\tpublic float X;\n\t\tpublic float Y;\n\t\tpublic float Vx;\n\t\tpublic float Vy;\n\t\tpublic float Size;\n\t\tpublic float Spin;\n\t\tpublic float Life;\n\t\tpublic RealTimeSince Age;\n\t}\n\n\treadonly List<Floater> floaters = new();\n\treadonly List<Particle> particles = new();\n\n\tstring toast;\n\tRealTimeSince toastAge;\n\n\tdouble idleNotice;\n\tRealTimeSince idleAge;\n\n\t// Every one-shot effect below is gated on \"has it been less than N seconds since\n\t// this fired\". Left at the default timestamp of zero that reads as \"it fired at\n\t// engine time zero\" - fine in the editor, which has been up for hours, but in a\n\t// fresh build RealTime.Now starts near zero too and the first frame would come up\n\t// mid surge-flash. Start them all firmly in the past instead.\n\tconst float LongAgo = 9999f;\n\n\tRealTimeSince timeSincePump = LongAgo;\n\tRealTimeSince clickRingAge = LongAgo;\n\tRealTimeSince surgeAge = LongAgo;\n\tRealTimeSince coinPopAge = LongAgo;\n\tRealTimeSince celebrateAge = LongAgo;\n\tRealTimeSince shakeAge = LongAgo;\n\n\tstring celebrateName;\n\tstring celebrateSub;\n\n\t/// <summary>Set when a shake is wanted, and which of the two flavours.</summary>\n\tstring shakeKind;\n\n\t/// <summary>Bumped every frame so the particles get rebuilt while they're moving.</summary>\n\tint animTick;\n\n\tbool hooked;\n\n\tbool showShop;\n\tbool showSettings;\n\tbool showTree;\n\n\t/// <summary>Puts a nudge on the shop button when something in there is affordable.</summary>\n\tbool AnyUpgradeAffordable =>\n\t\tState is not null && UpgradeTrack.All.Any( x => State.CanBuy( x.Kind ) );\n\n\t/// <summary>\n\t/// The strip along the bottom shows the tier you're in, not all 108 pumps - nine\n\t/// slots you can actually finish is a far better carrot than a hundred you can't.\n\t/// </summary>\n\tIEnumerable<PumpModel> VisiblePumps\n\t{\n\t\tget\n\t\t{\n\t\t\tif ( State is null ) return Enumerable.Empty<PumpModel>();\n\n\t\t\tvar first = State.Tier.FirstPumpIndex;\n\t\t\treturn PumpModel.All.Skip( first ).Take( PumpTier.PumpsPerTier );\n\t\t}\n\t}\n\n\tstring SceneClass => $\"scene-{State?.SelectedBackground ?? BackgroundStyle.DefaultId}\";\n\n\t// ---- one-shot effect gates ---------------------------------------------\n\n\t/// <summary>\n\t/// The settings toggle that governs all the per-action juice - numbers, droplets,\n\t/// coins, rings, flashes and the shake. The new-pump card is deliberately left out\n\t/// of it: that one is a milestone rather than noise, and a player who turned the\n\t/// effects down still wants to be told they bought something.\n\t/// </summary>\n\tstatic bool EffectsOn => PumpSettings.Current.ShowFloatingNumbers;\n\n\tbool ShowClickRing => EffectsOn && clickRingAge < ClickRingLife;\n\tbool ShowSurgeFlash => EffectsOn && surgeAge < SurgeFlashLife;\n\tbool ShowSurgeBurst => EffectsOn && surgeAge < SurgeBurstLife;\n\tbool CoinPopping => EffectsOn && coinPopAge < CoinPopLife;\n\tbool ShowCelebration => celebrateName is not null && celebrateAge < CelebrationLife;\n\n\t/// <summary>\n\t/// The shake is a class on the root rather than an inline transform, so the whole\n\t/// keyframe runs on the GPU and the C# side only has to say when it starts.\n\t/// </summary>\n\tstring ShakeClass => shakeAge < ShakeLife ? $\"shake-{shakeKind}\" : \"\";\n\n\tvoid OpenShop()\n\t{\n\t\tshowShop = !showShop;\n\t\tshowSettings = false;\n\t}\n\n\tvoid OpenSettings()\n\t{\n\t\tshowSettings = !showSettings;\n\t\tshowShop = false;\n\t}\n\n\tvoid OpenTree()\n\t{\n\t\tshowTree = !showTree;\n\t\tshowShop = false;\n\t\tshowSettings = false;\n\t}\n\n\t/// <summary>Open a panel from the console, so UI can be inspected without clicking.</summary>\n\tpublic void ShowPanel( string which )\n\t{\n\t\tshowShop = which is \"shop\" or \"bg\" or \"workers\";\n\t\tshowSettings = which == \"settings\";\n\t\tshowTree = which == \"tree\";\n\n\t\tStateHasChanged();\n\t}\n\n\tvoid CloseOverlays()\n\t{\n\t\tshowShop = false;\n\t\tshowSettings = false;\n\t\tshowTree = false;\n\t}\n\n\tstring TreeFillStyle() => $\"width: {N( State.TreeFraction * 100f )}%;\";\n\n\tvoid DoWaterTree()\n\t{\n\t\tvar poured = State?.WaterTree() ?? 0;\n\t\tif ( poured <= 0 ) return;\n\n\t\tSpawn( $\"-{Numbers.Short( poured )}L\", \"water\" );\n\n\t\ttoast = State.TreeIsGrowing\n\t\t\t? $\"Tree is growing - back in {State.TreeTimeLeft}\"\n\t\t\t: $\"{Numbers.Short( State.TreeWaterRemaining )}L to go\";\n\t\ttoastAge = 0;\n\t}\n\n\tvoid OnTreeGrew( int height )\n\t{\n\t\ttoast = $\"Your tree grew to {height} {( height == 1 ? \"stage\" : \"stages\" )}\";\n\t\ttoastAge = 0;\n\t}\n\n\tbool ShowToast => !string.IsNullOrEmpty( toast ) && toastAge < ToastLife;\n\tbool ShowIdleNote => idleNotice > 0 && idleAge < IdleNoticeLife;\n\n\t/// <summary>\n\t/// The panel a PanelComponent builds for itself defaults to pointer-events: none,\n\t/// and no stylesheet rule can reach it - it has no class and a generated type name.\n\t/// Left alone it makes every button underneath it dead to the mouse.\n\t/// </summary>\n\tprotected override void OnTreeFirstBuilt()\n\t{\n\t\tbase.OnTreeFirstBuilt();\n\n\t\tPanel.Style.PointerEvents = PointerEvents.All;\n\t}\n\n\tprotected override void OnStart()\n\t{\n\t\tHook();\n\n\t\tif ( State is not null && State.IdleCatchUp > 0 )\n\t\t{\n\t\t\tidleNotice = State.IdleCatchUp;\n\t\t\tidleAge = 0;\n\t\t}\n\t}\n\n\tprotected override void OnDestroy()\n\t{\n\t\tUnhook();\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\t// The game component can come alive after us, so keep trying until it's there.\n\t\tif ( !hooked ) Hook();\n\n\t\tfloaters.RemoveAll( x => x.Age > x.Life );\n\t\tparticles.RemoveAll( x => x.Age > x.Life );\n\n\t\tif ( floaters.Count > 0 || particles.Count > 0 || timeSincePump < PumpAnimLife )\n\t\t{\n\t\t\tanimTick = (int)(RealTime.Now * 30f);\n\t\t}\n\t}\n\n\tprotected override int BuildHash()\n\t{\n\t\tvar state = State;\n\t\tif ( state is null ) return 0;\n\n\t\t// The counts have to be in here, not just animTick. animTick only advances while\n\t\t// something is animating, so on the frame the last floater is removed it freezes\n\t\t// - and if nothing else in the hash moved, the tree never rebuilds and that\n\t\t// floater stays on screen forever, stuck at whatever opacity it last drew at.\n\t\t// At pump 1 there is no idle income to accidentally cover for it.\n\t\t// ShowSurgeFlash belongs here for the same reason the counts do - it is a panel\n\t\t// that has to come off the screen, and nothing else in the hash is guaranteed to\n\t\t// have moved on the frame it expires. Combine takes eight, so the last two share\n\t\t// a nested one rather than being OR'd together, which would hide a swap.\n\t\tvar effects = HashCode.Combine(\n\t\t\tfloaters.Count, particles.Count, animTick, ShakeClass,\n\t\t\tShowClickRing, ShowSurgeBurst, ShowSurgeFlash,\n\t\t\tHashCode.Combine( CoinPopping, ShowCelebration ) );\n\n\t\tvar overlays = HashCode.Combine(\n\t\t\tshowShop, showSettings, showTree, ShowToast, ShowIdleNote,\n\t\t\tAnyUpgradeAffordable, state.SelectedBackground, state.CanClaimDaily );\n\n\t\tvar world = HashCode.Combine(\n\t\t\tstate.GusherActive, state.TreeHeight, Numbers.Bucket( state.TreeWater ),\n\t\t\t(int)state.TreeSecondsLeft );\n\n\t\t// Numbers go through Numbers.Bucket rather than a cast. Coins reach 2.5e51 by\n\t\t// the last pump, and a double that big cast to long lands on the same value\n\t\t// every time - so the hash would stop moving and the wallet would freeze on\n\t\t// screen from tier 9 onward. See the remarks on Bucket.\n\t\treturn HashCode.Combine(\n\t\t\tNumbers.Bucket( state.Coins ),\n\t\t\tNumbers.Bucket( state.Water ),\n\t\t\tstate.PumpLevel,\n\t\t\tstate.Muted,\n\t\t\t// Twenty bands rather than the raw float - the gauge still reads as smooth\n\t\t\t// and the tree isn't rebuilt on every frame that pressure bleeds off.\n\t\t\t(int)(state.Pressure * 20f),\n\t\t\teffects,\n\t\t\toverlays,\n\t\t\tworld );\n\t}\n\n\tvoid Hook()\n\t{\n\t\tvar state = State;\n\t\tif ( state is null || hooked ) return;\n\n\t\tstate.Pumped += OnPumped;\n\t\tstate.Surged += OnSurged;\n\t\tstate.Sold += OnSold;\n\t\tstate.Upgraded += OnUpgraded;\n\t\tstate.Refused += OnRefused;\n\t\tstate.Purchased += OnPurchased;\n\t\tstate.TreeGrew += OnTreeGrew;\n\n\t\thooked = true;\n\t}\n\n\tvoid Unhook()\n\t{\n\t\tvar state = State;\n\t\tif ( state is null || !hooked ) return;\n\n\t\tstate.Pumped -= OnPumped;\n\t\tstate.Surged -= OnSurged;\n\t\tstate.Sold -= OnSold;\n\t\tstate.Upgraded -= OnUpgraded;\n\t\tstate.Refused -= OnRefused;\n\t\tstate.Purchased -= OnPurchased;\n\t\tstate.TreeGrew -= OnTreeGrew;\n\n\t\thooked = false;\n\t}\n\n\tvoid DoPump() => State?.Pump();\n\tvoid DoSell() => State?.SellAll();\n\tvoid DoBuy() => State?.BuyNextPump();\n\tvoid ToggleMute() => State?.ToggleMute();\n\n\tvoid OnPumped( double litres )\n\t{\n\t\ttimeSincePump = 0;\n\t\tclickRingAge = 0;\n\n\t\tSpawn( $\"+{Numbers.Short( litres )}L\", \"water\" );\n\n\t\t// More water on screen the harder you're going, so the gauge isn't the only\n\t\t// thing telling you pressure is up.\n\t\tvar count = 4 + (int)(State.Pressure * 5f);\n\t\tSprayDroplets( count );\n\t}\n\n\tvoid OnSurged( double litres )\n\t{\n\t\ttimeSincePump = 0;\n\t\tclickRingAge = 0;\n\t\tsurgeAge = 0;\n\n\t\tShake( \"hard\" );\n\t\tSpawn( $\"SURGE  +{Numbers.Short( litres )}L\", \"surge\" );\n\t\tSprayDroplets( 14 );\n\t\tSpawnSparks( 12 );\n\t}\n\n\tvoid OnSold( double coins )\n\t{\n\t\tSpawn( $\"+{Numbers.Short( coins )}\", \"coin\" );\n\t\tSpawnCoins( 10 );\n\n\t\tcoinPopAge = 0;\n\t\ttoast = null;\n\t}\n\n\tvoid OnUpgraded( PumpModel pump )\n\t{\n\t\tcelebrateName = pump.Name;\n\t\tcelebrateSub = pump.Rank == 1\n\t\t\t? $\"{pump.Tier.Name} - {pump.Tier.Tagline}\"\n\t\t\t: $\"{Numbers.Short( pump.PerClick )}L per pump  -  {Numbers.Short( pump.Tank )}L tank\";\n\t\tcelebrateAge = 0;\n\n\t\tShake( \"soft\" );\n\t\tSpawnSparks( 18 );\n\n\t\ttoast = null;\n\t\tidleNotice = 0;\n\t}\n\n\tvoid OnRefused( string reason )\n\t{\n\t\ttoast = reason;\n\t\ttoastAge = 0;\n\t}\n\n\tvoid OnPurchased( UpgradeTrack track )\n\t{\n\t\ttoast = $\"{track.Name} upgraded\";\n\t\ttoastAge = 0;\n\t}\n\n\tvoid Shake( string kind )\n\t{\n\t\tif ( !PumpSettings.Current.ShowFloatingNumbers )\n\t\t\treturn;\n\n\t\tshakeKind = kind;\n\t\tshakeAge = 0;\n\t}\n\n\tvoid Spawn( string text, string kind )\n\t{\n\t\tif ( !PumpSettings.Current.ShowFloatingNumbers )\n\t\t\treturn;\n\n\t\t// Keep the list short - a fast clicker can outrun the fade otherwise.\n\t\tif ( floaters.Count > 12 ) floaters.RemoveAt( 0 );\n\n\t\tfloaters.Add( new Floater\n\t\t{\n\t\t\tText = text,\n\t\t\tKind = kind,\n\t\t\tX = kind == \"surge\" ? Rand( -30f, 30f ) : Rand( -70f, 70f ),\n\t\t\tDrift = Rand( -18f, 18f ),\n\t\t\tLife = kind == \"surge\" ? SurgeFloaterLife : FloaterLife,\n\t\t\tAge = 0\n\t\t} );\n\t}\n\n\t/// <summary>Water thrown up out of the pump head.</summary>\n\tvoid SprayDroplets( int count )\n\t{\n\t\tfor ( var i = 0; i < count; i++ )\n\t\t{\n\t\t\tAdd( new Particle\n\t\t\t{\n\t\t\t\tKind = \"drop\",\n\t\t\t\tX = Rand( -26f, 26f ),\n\t\t\t\tY = Rand( -96f, -60f ),\n\t\t\t\tVx = Rand( -280f, 280f ),\n\t\t\t\tVy = Rand( -440f, -230f ),\n\t\t\t\tSize = Rand( 7f, 16f ),\n\t\t\t\tSpin = 0f,\n\t\t\t\tLife = Rand( 0.55f, 0.85f ),\n\t\t\t\tAge = 0\n\t\t\t} );\n\t\t}\n\t}\n\n\t/// <summary>Coins tossed out on a sale.</summary>\n\tvoid SpawnCoins( int count )\n\t{\n\t\tfor ( var i = 0; i < count; i++ )\n\t\t{\n\t\t\tAdd( new Particle\n\t\t\t{\n\t\t\t\tKind = \"coinbit\",\n\t\t\t\tX = Rand( -40f, 40f ),\n\t\t\t\tY = Rand( -30f, 20f ),\n\t\t\t\tVx = Rand( -240f, 240f ),\n\t\t\t\tVy = Rand( -520f, -320f ),\n\t\t\t\tSize = Rand( 14f, 24f ),\n\t\t\t\tSpin = Rand( -420f, 420f ),\n\t\t\t\tLife = Rand( 0.75f, 1.05f ),\n\t\t\t\tAge = 0\n\t\t\t} );\n\t\t}\n\t}\n\n\t/// <summary>The flat radial burst a surge or a new pump throws off.</summary>\n\tvoid SpawnSparks( int count )\n\t{\n\t\tfor ( var i = 0; i < count; i++ )\n\t\t{\n\t\t\tvar angle = (i / (float)count) * MathF.PI * 2f + Rand( -0.2f, 0.2f );\n\t\t\tvar speed = Rand( 380f, 620f );\n\n\t\t\tAdd( new Particle\n\t\t\t{\n\t\t\t\tKind = \"spark\",\n\t\t\t\tX = 0f,\n\t\t\t\tY = -40f,\n\t\t\t\tVx = MathF.Cos( angle ) * speed,\n\t\t\t\tVy = MathF.Sin( angle ) * speed,\n\t\t\t\tSize = Rand( 8f, 15f ),\n\t\t\t\tSpin = Rand( -240f, 240f ),\n\t\t\t\tLife = Rand( 0.45f, 0.7f ),\n\t\t\t\tAge = 0\n\t\t\t} );\n\t\t}\n\t}\n\n\tvoid Add( Particle particle )\n\t{\n\t\tif ( !PumpSettings.Current.ShowFloatingNumbers )\n\t\t\treturn;\n\n\t\t// Drop the oldest rather than refusing the new one, so a burst always reads as\n\t\t// a burst even when the screen is already busy.\n\t\tif ( particles.Count >= MaxParticles )\n\t\t\tparticles.RemoveAt( 0 );\n\n\t\tparticles.Add( particle );\n\t}\n\n\tTexture ArtworkFor( int index )\n\t{\n\t\tif ( PumpArtwork is null ) return null;\n\t\tif ( index < 0 || index >= PumpArtwork.Count ) return null;\n\n\t\treturn PumpArtwork[index];\n\t}\n\n\tstring RateText()\n\t{\n\t\tvar perSecond = State.TotalWaterPerSecond;\n\n\t\treturn perSecond > 0\n\t\t\t? $\"+{Numbers.Rate( perSecond )}L/s while idle\"\n\t\t\t: \"hand powered\";\n\t}\n\n\tstring PumpAreaClass()\n\t{\n\t\tvar classes = timeSincePump < PumpAnimLife ? \"pumping\" : \"\";\n\t\tif ( State.TankFull ) classes += \" full\";\n\t\tif ( State.PressureHot ) classes += \" hot\";\n\n\t\treturn classes;\n\t}\n\n\t/// <summary>The pump's halo brightens and widens with pressure.</summary>\n\tstring GlowStyle()\n\t{\n\t\tvar p = State.Pressure;\n\t\tvar size = 78f + p * 14f;\n\t\tvar alpha = 0.14f + p * 0.22f;\n\n\t\treturn $\"width: {N( size )}%; height: {N( size )}%; \" +\n\t\t\t$\"background-color: rgba( 255, 255, 255, {N( alpha )} );\";\n\t}\n\n\tstring PressureFillStyle() => $\"width: {N( State.Pressure * 100f )}%;\";\n\n\tstring SlotClass( PumpModel tier )\n\t{\n\t\tif ( tier.Index <= State.PumpLevel ) return \"owned\";\n\n\t\treturn tier.Index == State.PumpLevel + 1 ? \"locked next\" : \"locked\";\n\t}\n\n\tstring SlotLabel( PumpModel tier )\n\t{\n\t\treturn tier.Index <= State.PumpLevel\n\t\t\t? tier.Name\n\t\t\t: $\"{Numbers.Short( tier.Cost )} {Numbers.Plural( tier.Cost, \"coin\", \"coins\" )}\";\n\t}\n\n\tstring FillStyle() => $\"height: {N( State.TankFraction * 100f )}%;\";\n\n\tstring TierFillStyle() => $\"width: {N( State.TierProgress * 100f )}%;\";\n\n\tstring GusherStyle() =>\n\t\t$\"left: {N( State.GusherX * 100f )}%; top: {N( State.GusherY * 100f )}%;\";\n\n\tvoid ClaimGusher()\n\t{\n\t\tvar won = State?.ClaimGusher() ?? 0;\n\t\tif ( won <= 0 ) return;\n\n\t\tSpawn( $\"+{Numbers.Short( won )}\", \"coin\" );\n\t\tSpawnCoins( 14 );\n\t\tcoinPopAge = 0;\n\n\t\ttoast = \"Gusher! Free coins\";\n\t\ttoastAge = 0;\n\t}\n\n\tvoid ClaimDaily()\n\t{\n\t\tvar won = State?.ClaimDaily() ?? 0;\n\t\tif ( won <= 0 ) return;\n\n\t\tSpawn( $\"+{Numbers.Short( won )}\", \"coin\" );\n\t\tSpawnCoins( 16 );\n\t\tcoinPopAge = 0;\n\n\t\ttoast = $\"Day {State.DailyStreak} streak - come back tomorrow\";\n\t\ttoastAge = 0;\n\t}\n\n\tstring FloaterStyle( Floater f )\n\t{\n\t\tvar t = Math.Clamp( (float)f.Age / f.Life, 0f, 1f );\n\t\tvar rise = 70f + t * 110f;\n\t\tvar fade = 1f - t * t;\n\n\t\t// Surge numbers punch out to full size and settle, so they read as an event\n\t\t// rather than as one more of the numbers already streaming past.\n\t\tvar scale = f.Kind == \"surge\"\n\t\t\t? 1f + (1f - MathF.Min( t * 5f, 1f )) * 0.5f\n\t\t\t: 1f;\n\n\t\t// Centred on the anchor by hand: the panel is a fixed 300px wide with centred\n\t\t// text, so half its width comes back off the left. Text has no width to measure.\n\t\treturn $\"left: {N( f.X + f.Drift * t - FloaterWidth * 0.5f )}px; top: {N( -rise )}px; \" +\n\t\t\t$\"opacity: {N( fade )}; transform: scale( {N( scale )} );\";\n\t}\n\n\tstring ParticleStyle( Particle p )\n\t{\n\t\tvar t = (float)p.Age;\n\t\tvar k = Math.Clamp( t / p.Life, 0f, 1f );\n\n\t\t// Sparks fly flat - they're a burst of light, not something thrown in the air.\n\t\tvar gravity = p.Kind == \"spark\" ? 0f : Gravity;\n\n\t\tvar x = p.X + p.Vx * t;\n\t\tvar y = p.Y + p.Vy * t + 0.5f * gravity * t * t;\n\n\t\tvar fade = 1f - k * k;\n\t\tvar scale = p.Kind == \"spark\" ? 1f - k * 0.7f : 1f;\n\n\t\treturn $\"left: {N( x - p.Size * 0.5f )}px; top: {N( y - p.Size * 0.5f )}px; \" +\n\t\t\t$\"width: {N( p.Size )}px; height: {N( p.Size )}px; opacity: {N( fade )}; \" +\n\t\t\t$\"transform: rotate( {N( p.Spin * t )}deg ) scale( {N( scale )} );\";\n\t}\n\n\t/// <summary>Style strings need dots for decimals, whatever the machine's locale says.</summary>\n\tstatic string N( float value ) => value.ToString( \"0.##\", CultureInfo.InvariantCulture );\n\n\tstatic float Rand( float min, float max ) => min + (float)System.Random.Shared.NextDouble() * (max - min);\n}\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "UI/PumpArt.razor",
            "FileName": "PumpArt.razor",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 343538,
            "IsPrivate": false,
            "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 @( 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=\"drawing\" style=\"background-image: url( '@ArtPath' );\"></div>\r\n\t}\r\n\r\n</root>\r\n\r\n@code\r\n{\r\n\t/// <summary>Index into <see cref=\"PumpModel.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>Overrides the drawn artwork entirely, if you'd rather use your own.</summary>\r\n\tpublic Texture Artwork { get; set; }\r\n\r\n\tPumpModel Pump => PumpModel.Get( TierIndex );\r\n\r\n\t/// <summary>\r\n\t/// One illustration per pump - all 108 of them, and no two the same machine. The\r\n\t/// tier owns the palette and what the thing stands on; everything else about a pump\r\n\t/// is drawn for the name it carries. See tools/make_pumps.py.\r\n\t/// </summary>\r\n\tstring ArtPath => $\"/ui/pumps/pump_{Pump.Index + 1:000}.svg\";\r\n\r\n\t// There used to be a 0.92-1.0 scale ramp by rank here, from back when the nine\r\n\t// pumps in a tier were one drawing and the ramp was doing the work of selling a\r\n\t// purchase. Every drawing includes the ground it stands on, so scaling the image\r\n\t// scaled the ground with it and the desert changed size when you bought a pump.\r\n\t// The drawings differ on their own now, so the ramp is gone and the horizon holds.\r\n\r\n\tprotected override int BuildHash() => HashCode.Combine( TierIndex, Silhouette, Artwork );\r\n}\r\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "styles/form/_dropdown.scss",
            "FileName": "_dropdown.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 343538,
            "IsPrivate": false,
            "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": 343538,
            "IsPrivate": false,
            "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": 343538,
            "IsPrivate": false,
            "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": 343538,
            "IsPrivate": false,
            "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.clouds,\r\n.motes,\r\n.shimmer,\r\n.fx\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\talign-items: center;\r\n\tjustify-content: center;\r\n\tanimation: sun-breathe 9s ease-in-out infinite;\r\n}\r\n\r\n// A slowly turning square behind the disc. One panel, and it reads as light bending\r\n// off something very bright - eight individual ray divs looked like a cartwheel.\r\n.sun-rays\r\n{\r\n\tposition: absolute;\r\n\twidth: 230%;\r\n\theight: 230%;\r\n\tborder-radius: 22%;\r\n\tbackground-color: rgba( 255, 240, 180, 0.10 );\r\n\tanimation: sun-turn 70s linear infinite;\r\n}\r\n\r\n@keyframes sun-turn\r\n{\r\n\t0% { transform: rotate( 0deg ); }\r\n\t100% { transform: rotate( 360deg ); }\r\n}\r\n\r\n// Scale, not blur radius. The disc and its shadow are drawn once and the transform\r\n// moves the result, where animating the shadow re-blurs a 170px quad every frame for\r\n// as long as the game is running - and the sun is on screen the whole time.\r\n@keyframes sun-breathe\r\n{\r\n\t0% { transform: scale( 1 ); }\r\n\t50% { transform: scale( 1.07 ); }\r\n\t100% { transform: scale( 1 ); }\r\n}\r\n\r\n// ---------------------------------------------------------------- clouds\r\n\r\n.clouds\r\n{\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tleft: 0;\r\n\twidth: 100%;\r\n\theight: 55%;\r\n\toverflow: hidden;\r\n}\r\n\r\n// Soft ellipses with a wide shadow rather than drawn puffs - at this scale the shadow\r\n// is what sells it, and it costs one panel each.\r\n.cloud\r\n{\r\n\tposition: absolute;\r\n\tborder-radius: 50%;\r\n\tbackground-color: rgba( 255, 255, 255, 0.5 );\r\n\tbox-shadow: 0px 0px 50px 24px rgba( 255, 255, 255, 0.3 );\r\n\r\n\t&.c1\r\n\t{\r\n\t\ttop: 12%;\r\n\t\twidth: 260px;\r\n\t\theight: 54px;\r\n\t\tanimation: drift-a 96s linear infinite;\r\n\t}\r\n\r\n\t&.c2\r\n\t{\r\n\t\ttop: 30%;\r\n\t\twidth: 180px;\r\n\t\theight: 38px;\r\n\t\topacity: 0.7;\r\n\t\tanimation: drift-b 138s linear infinite;\r\n\t}\r\n\r\n\t&.c3\r\n\t{\r\n\t\ttop: 5%;\r\n\t\twidth: 340px;\r\n\t\theight: 62px;\r\n\t\topacity: 0.45;\r\n\t\tanimation: drift-c 176s linear infinite;\r\n\t}\r\n}\r\n\r\n@keyframes drift-a\r\n{\r\n\t0% { left: -22%; }\r\n\t100% { left: 108%; }\r\n}\r\n\r\n@keyframes drift-b\r\n{\r\n\t0% { left: -16%; }\r\n\t100% { left: 104%; }\r\n}\r\n\r\n@keyframes drift-c\r\n{\r\n\t0% { left: -30%; }\r\n\t100% { left: 112%; }\r\n}\r\n\r\n// ---------------------------------------------------------------- dust\r\n\r\n.motes\r\n{\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tleft: 0;\r\n\twidth: 100%;\r\n\theight: 100%;\r\n\toverflow: hidden;\r\n}\r\n\r\n// Specks of sand on the wind. Purely atmosphere, and the one thing on screen that\r\n// keeps moving when the player has stopped.\r\n.mote\r\n{\r\n\tposition: absolute;\r\n\twidth: 5px;\r\n\theight: 5px;\r\n\tborder-radius: 100%;\r\n\tbackground-color: rgba( 255, 244, 214, 0.55 );\r\n\r\n\t&.m1 { top: 42%; width: 4px; height: 4px; animation: mote-a 17s linear infinite; }\r\n\t&.m2 { top: 58%; animation: mote-b 23s linear infinite; }\r\n\t&.m3 { top: 71%; width: 3px; height: 3px; animation: mote-c 14s linear infinite; }\r\n\t&.m4 { top: 36%; animation: mote-b 29s linear infinite; }\r\n\t&.m5 { top: 64%; width: 6px; height: 6px; animation: mote-a 21s linear infinite; }\r\n\t&.m6 { top: 79%; width: 4px; height: 4px; animation: mote-c 26s linear infinite; }\r\n\t&.m7 { top: 50%; width: 3px; height: 3px; animation: mote-b 19s linear infinite; }\r\n\t&.m8 { top: 86%; animation: mote-a 31s linear infinite; }\r\n}\r\n\r\n@keyframes mote-a\r\n{\r\n\t0% { left: -3%; opacity: 0; transform: translateY( 0px ); }\r\n\t14% { opacity: 0.8; }\r\n\t86% { opacity: 0.8; }\r\n\t100% { left: 103%; opacity: 0; transform: translateY( -46px ); }\r\n}\r\n\r\n@keyframes mote-b\r\n{\r\n\t0% { left: -3%; opacity: 0; transform: translateY( 0px ); }\r\n\t18% { opacity: 0.6; }\r\n\t82% { opacity: 0.6; }\r\n\t100% { left: 103%; opacity: 0; transform: translateY( 30px ); }\r\n}\r\n\r\n@keyframes mote-c\r\n{\r\n\t0% { left: -3%; opacity: 0; transform: translateY( 0px ); }\r\n\t20% { opacity: 0.9; }\r\n\t80% { opacity: 0.9; }\r\n\t100% { left: 103%; opacity: 0; transform: translateY( -18px ); }\r\n}\r\n\r\n// ---------------------------------------------------------------- heat haze\r\n\r\n// A band of light sitting on the horizon, breathing. Off by default and turned up by\r\n// the scenes where the air would actually be shaking.\r\n.shimmer\r\n{\r\n\tposition: absolute;\r\n\tbottom: 8%;\r\n\tleft: 0;\r\n\twidth: 100%;\r\n\theight: 22%;\r\n\topacity: 0;\r\n\tbackground-image: linear-gradient( to top, rgba( 255, 255, 255, 0.22 ) 0%, rgba( 255, 255, 255, 0 ) 100% );\r\n\tanimation: haze 7s ease-in-out infinite;\r\n}\r\n\r\n// Deliberately does not touch opacity - that is the switch each scene uses to turn the\r\n// haze on, and a keyframe writing it would override the switch and show it everywhere.\r\n@keyframes haze\r\n{\r\n\t0% { transform: scaleY( 1 ); }\r\n\t50% { transform: scaleY( 1.3 ); }\r\n\t100% { transform: scaleY( 1 ); }\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\tposition: relative;\r\n\ttransition: transform 0.12s ease-out;\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\t// Mounted only for the moment after a sale, so it starts its animation fresh every\r\n\t// time. Toggling a class on a panel that is already there does not reliably restart\r\n\t// one, and a payout that doesn't visibly land is the whole point of this.\r\n\t.coinflash\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\tborder-radius: 18px;\r\n\t\tborder: 2px solid $gold;\r\n\t\tanimation: coin-flash 0.36s ease-out;\r\n\t}\r\n\r\n\t&.pop\r\n\t{\r\n\t\ttransform: scale( 1.06 );\r\n\t}\r\n}\r\n\r\n@keyframes coin-flash\r\n{\r\n\t0% { opacity: 0.95; transform: scale( 1 ); }\r\n\t100% { opacity: 0; transform: scale( 1.18 ); }\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\tposition: relative;\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\tposition: relative;\r\n\t}\r\n\r\n\t// A pale disc riding the surface, wider than the tank so only its curve shows. It\r\n\t// rocks side to side, which turns a rectangle sliding up into something with liquid in it.\r\n\t.tank-wave\r\n\t{\r\n\t\tposition: absolute;\r\n\t\ttop: -5px;\r\n\t\tleft: -30%;\r\n\t\twidth: 160%;\r\n\t\theight: 10px;\r\n\t\tborder-radius: 50%;\r\n\t\tbackground-color: #8fd8ff;\r\n\t\tanimation: tank-rock 1.9s ease-in-out infinite;\r\n\t}\r\n\r\n\t// Fixed glass highlight down the left, so the tank reads as a cylinder.\r\n\t.tank-shine\r\n\t{\r\n\t\tposition: absolute;\r\n\t\ttop: 8%;\r\n\t\tleft: 14%;\r\n\t\twidth: 5px;\r\n\t\theight: 60%;\r\n\t\tborder-radius: 999px;\r\n\t\tbackground-color: rgba( 255, 255, 255, 0.35 );\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 .tank-wave\r\n\t{\r\n\t\tbackground-color: #ffb98a;\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\t// A full tank is wasted pumping, so the whole readout nags rather than sitting there.\r\n\t&.full\r\n\t{\r\n\t\tanimation: tank-nag 1.1s ease-in-out infinite;\r\n\t}\r\n}\r\n\r\n@keyframes tank-rock\r\n{\r\n\t0% { transform: translateX( -4px ) rotate( -2deg ); }\r\n\t50% { transform: translateX( 4px ) rotate( 2deg ); }\r\n\t100% { transform: translateX( -4px ) rotate( -2deg ); }\r\n}\r\n\r\n@keyframes tank-nag\r\n{\r\n\t0% { box-shadow: 0px 0px 0px 0px rgba( 255, 139, 74, 0.5 ); }\r\n\t50% { box-shadow: 0px 0px 18px 3px rgba( 255, 139, 74, 0.55 ); }\r\n\t100% { box-shadow: 0px 0px 0px 0px rgba( 255, 139, 74, 0.5 ); }\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\tposition: relative;\r\n\tmargin-left: 10px;\r\n\ttransition: transform 0.09s ease-out, background-color 0.12s linear;\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\ttransform: translateY( -2px );\r\n\t}\r\n\r\n\t&:active\r\n\t{\r\n\t\ttransform: translateY( 1px );\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\t// Something in the shop is affordable. A dot in the corner rather than a colour\r\n\t// change on the button, so it reads the way an unread badge does everywhere else.\r\n\t.dot\r\n\t{\r\n\t\tposition: absolute;\r\n\t\ttop: -4px;\r\n\t\tright: -4px;\r\n\t\twidth: 16px;\r\n\t\theight: 16px;\r\n\t\tborder-radius: 100%;\r\n\t\tbackground-color: $gold;\r\n\t\tborder: 2px solid rgba( 18, 22, 32, 0.9 );\r\n\t}\r\n\r\n\t&.highlight\r\n\t{\r\n\t\tanimation: btn-nudge 2.4s ease-in-out infinite;\r\n\t}\r\n}\r\n\r\n@keyframes btn-nudge\r\n{\r\n\t0% { transform: translateY( 0px ); }\r\n\t8% { transform: translateY( -5px ); }\r\n\t16% { transform: translateY( 0px ); }\r\n\t24% { transform: translateY( -3px ); }\r\n\t32% { transform: translateY( 0px ); }\r\n\t100% { transform: translateY( 0px ); }\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// Wraps the pump so the effects layer has something the exact size and place of the\r\n// pump to anchor to. Anchoring to .stage instead put every burst too low, because the\r\n// nameplate under the pump pushes the pump above the stage's centre.\r\n.pumpwrap\r\n{\r\n\tposition: relative;\r\n\talign-items: center;\r\n\tjustify-content: center;\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// Size and brightness come in as an inline style - they track pressure.\r\n\t.glow\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tborder-radius: 100%;\r\n\t\tbackground-color: rgba( 255, 255, 255, 0.14 );\r\n\t\ttransition: width 0.2s ease-out, height 0.2s ease-out;\r\n\t}\r\n\r\n\t// Only shows once pressure is pinned. The ring, not the glow, is what tells you\r\n\t// you're at the top - a glow that just keeps brightening has no ceiling to read.\r\n\t.heatring\r\n\t{\r\n\t\tposition: absolute;\r\n\t\twidth: 86%;\r\n\t\theight: 86%;\r\n\t\tborder-radius: 100%;\r\n\t\tborder: 3px solid rgba( 255, 190, 90, 0.85 );\r\n\t\topacity: 0;\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\t&.hot\r\n\t{\r\n\t\t.heatring\r\n\t\t{\r\n\t\t\topacity: 1;\r\n\t\t\tanimation: heat-ring 1.05s ease-out infinite;\r\n\t\t}\r\n\r\n\t\t.pumpart\r\n\t\t{\r\n\t\t\tanimation: hot-throb 0.62s ease-in-out infinite;\r\n\t\t}\r\n\t}\r\n}\r\n\r\n@keyframes heat-ring\r\n{\r\n\t0% { transform: scale( 0.94 ); opacity: 0.9; }\r\n\t100% { transform: scale( 1.18 ); opacity: 0; }\r\n}\r\n\r\n@keyframes hot-throb\r\n{\r\n\t0% { transform: scale( 1 ); }\r\n\t50% { transform: scale( 1.035 ); }\r\n\t100% { transform: scale( 1 ); }\r\n}\r\n\r\n// ---------------------------------------------------------------- effects layer\r\n\r\n// A zero-size anchor dead centre on the pump. Everything in here positions itself off\r\n// it with margins, which is why the particle code can work in plain pixel offsets.\r\n.fx\r\n{\r\n\tposition: absolute;\r\n\tleft: 50%;\r\n\ttop: 50%;\r\n\twidth: 0px;\r\n\theight: 0px;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n}\r\n\r\n.particle\r\n{\r\n\tposition: absolute;\r\n\tpointer-events: none;\r\n\r\n\t// Water thrown up out of the pump head.\r\n\t&.drop\r\n\t{\r\n\t\tborder-radius: 50%;\r\n\t\tbackground-color: #9fe0ff;\r\n\t\tborder: 2px solid rgba( 255, 255, 255, 0.75 );\r\n\t}\r\n\r\n\t// Coins off a sale. Round, gold, and spinning - the spin comes in inline.\r\n\t&.coinbit\r\n\t{\r\n\t\tborder-radius: 100%;\r\n\t\tbackground-color: $gold;\r\n\t\tborder: 3px solid #a8791a;\r\n\t}\r\n\r\n\t// The flat radial burst on a surge or a new pump.\r\n\t&.spark\r\n\t{\r\n\t\tborder-radius: 100%;\r\n\t\tbackground-color: #fff6d8;\r\n\t\tbox-shadow: 0px 0px 14px 4px rgba( 255, 214, 120, 0.75 );\r\n\t}\r\n}\r\n\r\n// A ring thrown off the pump on every click. Mounted fresh each time so it always\r\n// plays from the start.\r\n// Centred by hand with half its size off the top and left. The layout engine will\r\n// centre an absolutely positioned child off the parent's alignment, but only while no\r\n// inset is set - too subtle a rule to hang the whole effects layer on.\r\n.clickring\r\n{\r\n\tposition: absolute;\r\n\tleft: 50%;\r\n\ttop: 50%;\r\n\tmargin-left: -105px;\r\n\tmargin-top: -105px;\r\n\twidth: 210px;\r\n\theight: 210px;\r\n\tborder-radius: 100%;\r\n\tborder: 4px solid rgba( 190, 235, 255, 0.75 );\r\n\tpointer-events: none;\r\n\tanimation: click-ring 0.42s ease-out;\r\n}\r\n\r\n@keyframes click-ring\r\n{\r\n\t0% { transform: scale( 0.42 ); opacity: 0.85; }\r\n\t100% { transform: scale( 1.3 ); opacity: 0; }\r\n}\r\n\r\n.surgering\r\n{\r\n\tposition: absolute;\r\n\tleft: 50%;\r\n\ttop: 50%;\r\n\tmargin-left: -150px;\r\n\tmargin-top: -150px;\r\n\twidth: 300px;\r\n\theight: 300px;\r\n\tborder-radius: 100%;\r\n\tborder: 6px solid rgba( 255, 226, 150, 0.9 );\r\n\tbox-shadow: 0px 0px 40px 10px rgba( 255, 200, 90, 0.5 );\r\n\tpointer-events: none;\r\n\tanimation: surge-ring 0.6s ease-out;\r\n}\r\n\r\n@keyframes surge-ring\r\n{\r\n\t0% { transform: scale( 0.3 ); opacity: 1; }\r\n\t100% { transform: scale( 1.7 ); opacity: 0; }\r\n}\r\n\r\n// A wash of warm light over the whole screen for a fifth of a second.\r\n.surgeflash\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-color: rgba( 255, 220, 150, 0.4 );\r\n\tpointer-events: none;\r\n\tanimation: surge-flash 0.4s ease-out;\r\n}\r\n\r\n@keyframes surge-flash\r\n{\r\n\t0% { opacity: 0.75; }\r\n\t100% { opacity: 0; }\r\n}\r\n\r\n// ---------------------------------------------------------------- shake\r\n\r\n// The stage shakes, not the whole HUD. Moving the root would drag the sky and the\r\n// dunes with it and show bare edges at the screen border.\r\n.hud.shake-soft .stage\r\n{\r\n\tanimation: shake-soft 0.32s ease-out;\r\n}\r\n\r\n.hud.shake-hard .stage\r\n{\r\n\tanimation: shake-hard 0.32s ease-out;\r\n}\r\n\r\n@keyframes shake-soft\r\n{\r\n\t0% { transform: translateX( 0px ) translateY( 0px ); }\r\n\t25% { transform: translateX( -4px ) translateY( 2px ); }\r\n\t50% { transform: translateX( 3px ) translateY( -2px ); }\r\n\t75% { transform: translateX( -2px ) translateY( 1px ); }\r\n\t100% { transform: translateX( 0px ) translateY( 0px ); }\r\n}\r\n\r\n@keyframes shake-hard\r\n{\r\n\t0% { transform: translateX( 0px ) translateY( 0px ); }\r\n\t15% { transform: translateX( -9px ) translateY( 4px ); }\r\n\t32% { transform: translateX( 8px ) translateY( -5px ); }\r\n\t50% { transform: translateX( -6px ) translateY( 3px ); }\r\n\t70% { transform: translateX( 4px ) translateY( -2px ); }\r\n\t85% { transform: translateX( -2px ) translateY( 1px ); }\r\n\t100% { transform: translateX( 0px ) translateY( 0px ); }\r\n}\r\n\r\n// ---------------------------------------------------------------- pressure\r\n\r\n// Sits between the pump and the nameplate. Always laid out, faded out when cold, so\r\n// showing and hiding it never shunts the nameplate up and down the screen.\r\n.pressure\r\n{\r\n\tflex-direction: column;\r\n\talign-items: center;\r\n\tmargin-top: 6px;\r\n\ttransition: opacity 0.25s linear;\r\n\r\n\t&.cold { opacity: 0.25; }\r\n\r\n\t.pressure-bar\r\n\t{\r\n\t\twidth: 240px;\r\n\t\theight: 10px;\r\n\t\tborder-radius: 999px;\r\n\t\tbackground-color: rgba( 0, 0, 0, 0.45 );\r\n\t\tborder: 1px solid rgba( 255, 255, 255, 0.22 );\r\n\t\toverflow: hidden;\r\n\t}\r\n\r\n\t.pressure-fill\r\n\t{\r\n\t\theight: 100%;\r\n\t\tbackground-color: $water;\r\n\t\ttransition: width 0.1s linear, background-color 0.3s linear;\r\n\t}\r\n\r\n\t.pressure-label\r\n\t{\r\n\t\tmargin-top: 4px;\r\n\t\talign-items: center;\r\n\t}\r\n\r\n\t.pressure-name\r\n\t{\r\n\t\tfont-size: 11px;\r\n\t\tletter-spacing: 3px;\r\n\t\topacity: 0.7;\r\n\t\tmargin-right: 8px;\r\n\t}\r\n\r\n\t.pressure-value\r\n\t{\r\n\t\tfont-size: 14px;\r\n\t\tfont-weight: 800;\r\n\t\tcolor: #bfe9ff;\r\n\t}\r\n\r\n\t// Four bands rather than a continuous ramp - a colour you can name is a colour you\r\n\t// can chase, and the game tells you which band you're in without a number.\r\n\t&.s1 .pressure-fill { background-color: #6fd3a0; }\r\n\t&.s1 .pressure-value { color: #9ff0c6; }\r\n\r\n\t&.s2 .pressure-fill { background-color: #ffb54a; }\r\n\t&.s2 .pressure-value { color: #ffd08a; }\r\n\r\n\t&.s3\r\n\t{\r\n\t\t.pressure-fill\r\n\t\t{\r\n\t\t\tbackground-color: #fff0c0;\r\n\t\t\tbox-shadow: 0px 0px 14px 3px rgba( 255, 200, 90, 0.8 );\r\n\t\t}\r\n\r\n\t\t.pressure-value\r\n\t\t{\r\n\t\t\tcolor: #fff2cc;\r\n\t\t}\r\n\r\n\t\t.pressure-bar\r\n\t\t{\r\n\t\t\tborder-color: rgba( 255, 214, 120, 0.9 );\r\n\t\t\tanimation: pressure-max 0.8s ease-in-out infinite;\r\n\t\t}\r\n\t}\r\n}\r\n\r\n@keyframes pressure-max\r\n{\r\n\t0% { box-shadow: 0px 0px 0px 0px rgba( 255, 200, 90, 0.6 ); }\r\n\t50% { box-shadow: 0px 0px 16px 3px rgba( 255, 200, 90, 0.6 ); }\r\n\t100% { box-shadow: 0px 0px 0px 0px rgba( 255, 200, 90, 0.6 ); }\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// Lives in .fx now, so left/top come in inline off the pump's centre. The fixed width\r\n// with centred text is what lets the code place it without measuring the string - keep\r\n// it in step with FloaterWidth in GameHud.razor.\r\n.floater\r\n{\r\n\tposition: absolute;\r\n\twidth: 300px;\r\n\ttext-align: center;\r\n\tjustify-content: center;\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\t&.surge\r\n\t{\r\n\t\tfont-size: 40px;\r\n\t\tcolor: #fff4d2;\r\n\t\tletter-spacing: 2px;\r\n\t\ttext-shadow: 0px 3px 0px rgba( 120, 60, 0, 0.55 );\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\talign-items: center;\r\n\tborder: 2px solid rgba( 140, 210, 255, 0.5 );\r\n\r\n\ti\r\n\t{\r\n\t\tfont-family: Material Icons;\r\n\t\tfont-size: 20px;\r\n\t\tmargin-right: 8px;\r\n\t}\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\tposition: relative;\r\n\toverflow: hidden;\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// A band of light that sweeps across the face. Parked off the left edge and only\r\n\t// sent across when the button is actually worth pressing.\r\n\t.shine\r\n\t{\r\n\t\tposition: absolute;\r\n\t\ttop: 0;\r\n\t\tleft: -40%;\r\n\t\twidth: 30%;\r\n\t\theight: 100%;\r\n\t\tbackground-image: linear-gradient( to right, rgba( 255, 255, 255, 0 ) 0%, rgba( 255, 255, 255, 0.35 ) 50%, rgba( 255, 255, 255, 0 ) 100% );\r\n\t\topacity: 0;\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\r\n\t\t&:not(.disabled) .shine\r\n\t\t{\r\n\t\t\topacity: 1;\r\n\t\t\tanimation: shine-sweep 3.4s linear infinite;\r\n\t\t}\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// Affordable. This is the moment the whole loop is pointed at, so it gets the\r\n\t// brightest thing on screen after the pump itself.\r\n\t&.buy.ready\r\n\t{\r\n\t\tanimation: buy-ready 1.6s ease-in-out infinite;\r\n\r\n\t\t.shine\r\n\t\t{\r\n\t\t\topacity: 1;\r\n\t\t\tanimation: shine-sweep 2.2s linear infinite;\r\n\t\t}\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@keyframes shine-sweep\r\n{\r\n\t0% { left: -40%; }\r\n\t40% { left: 110%; }\r\n\t100% { left: 110%; }\r\n}\r\n\r\n@keyframes buy-ready\r\n{\r\n\t0% { box-shadow: 0px 0px 0px 0px rgba( 245, 197, 66, 0.55 ); }\r\n\t50% { box-shadow: 0px 0px 30px 6px rgba( 245, 197, 66, 0.5 ); }\r\n\t100% { box-shadow: 0px 0px 0px 0px rgba( 245, 197, 66, 0.55 ); }\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\tposition: relative;\r\n\ttransition: transform 0.12s ease-out, background-color 0.2s linear;\r\n\r\n\t&:hover\r\n\t{\r\n\t\ttransform: translateY( -4px );\r\n\t}\r\n\r\n\t.tick\r\n\t{\r\n\t\tposition: absolute;\r\n\t\ttop: 6px;\r\n\t\tright: 6px;\r\n\t\twidth: 22px;\r\n\t\theight: 22px;\r\n\t\tborder-radius: 100%;\r\n\t\tbackground-color: #4f9a45;\r\n\t\talign-items: center;\r\n\t\tjustify-content: 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: 15px;\r\n\t\t}\r\n\t}\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\tbox-shadow: 0px 0px 16px 2px rgba( 245, 197, 66, 0.4 );\r\n\t\tanimation: slot-next 2s ease-in-out infinite;\r\n\t}\r\n}\r\n\r\n// The glow is a fixed shadow and the pulse is a colour. Animating the blur radius\r\n// instead makes the renderer rebuild a blurred quad on every frame of every second the\r\n// game is open, and this one is always on because there is always a next pump.\r\n//\r\n// The border, specifically. .slot animates neither - it transitions transform for its\r\n// hover lift and background-color for its owned state, and a keyframe on either of\r\n// those would fight something: an animation outranks the hover for as long as it runs,\r\n// which would have quietly killed the lift on the one slot the player is most likely\r\n// to point at.\r\n@keyframes slot-next\r\n{\r\n\t0% { border-color: rgba( 245, 197, 66, 0.45 ); }\r\n\t50% { border-color: rgba( 255, 226, 140, 1 ); }\r\n\t100% { border-color: rgba( 245, 197, 66, 0.45 ); }\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: 118px;\r\n\theight: 150px;\r\n\tposition: relative;\r\n\tcursor: pointer;\r\n\tpointer-events: all;\r\n\tbackground-repeat: no-repeat;\r\n\tbackground-position: bottom center;\r\n\tbackground-size: contain;\r\n\ttransition: transform 0.25s ease-out;\r\n\r\n\t&:hover { transform: scale( 1.06 ); }\r\n\r\n\t&.growing { animation: tree-sway 3.4s ease-in-out infinite; }\r\n}\r\n\r\n@keyframes tree-sway\r\n{\r\n\t0% { transform: rotate( -1.5deg ); }\r\n\t50% { transform: rotate( 1.5deg ); }\r\n\t100% { transform: rotate( -1.5deg ); }\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\r\n// ---- Sandstorm: ochre haze, the horizon eaten by dust\r\n\r\n.scene-sandstorm\r\n{\r\n\t.sky { background-image: linear-gradient( to bottom, #d9ae6b 0%, #c99a52 45%, #a97c3c 78%, #7a5326 100% ); }\r\n\t.sun { background-color: #f0d9a6; box-shadow: 0px 0px 130px 50px rgba( 240, 217, 166, 0.3 ); }\r\n\t.dune.far { background-color: #a9803f; }\r\n\t.dune.near { background-color: #96702f; }\r\n\t.floor { background-color: #7a5326; }\r\n\t.scenery\r\n\t{\r\n\t\topacity: 1;\r\n\t\tbackground-image: linear-gradient( to top, rgba( 200, 160, 90, 0.4 ) 0%, rgba( 200, 160, 90, 0 ) 60% );\r\n\t}\r\n}\r\n\r\n// ---- Golden Hour: the twenty minutes a day the desert looks kind\r\n\r\n.scene-golden\r\n{\r\n\t.sky { background-image: linear-gradient( to bottom, #5b3a86 0%, #c1476a 38%, #f2884b 74%, #f7b167 100% ); }\r\n\t.sun { background-color: #fff0c4; box-shadow: 0px 0px 120px 44px rgba( 255, 200, 120, 0.5 ); }\r\n\t.dune.far { background-color: #a4553f; }\r\n\t.dune.near { background-color: #8c4436; }\r\n\t.floor { background-color: #6d3630; }\r\n}\r\n\r\n// ---- Starfield: no moon, no cloud, rather a lot of sky\r\n\r\n.scene-starfield\r\n{\r\n\t.sky { background-image: linear-gradient( to bottom, #04060f 0%, #070b1c 40%, #18213f 75%, #2b3560 100% ); }\r\n\t.sun\r\n\t{\r\n\t\tbackground-color: #dfe6ff;\r\n\t\twidth: 90px;\r\n\t\theight: 90px;\r\n\t\tbox-shadow: 0px 0px 70px 22px rgba( 223, 230, 255, 0.35 );\r\n\t}\r\n\t.dune.far { background-color: #1b2340; }\r\n\t.dune.near { background-color: #141a30; }\r\n\t.floor { background-color: #0d1122; }\r\n\t.stars\r\n\t{\r\n\t\topacity: 1;\r\n\t\tbackground-image: linear-gradient( to bottom, rgba( 255, 255, 255, 0.22 ) 0%, rgba( 255, 255, 255, 0 ) 75% );\r\n\t}\r\n}\r\n\r\n// ---- Aurora: green light over the ice\r\n\r\n.scene-aurora\r\n{\r\n\t.sky { background-image: linear-gradient( to bottom, #04121f 0%, #0b2c3a 35%, #1c6b63 70%, #3fd6a0 100% ); }\r\n\t.sun { background-color: #b9f5e2; box-shadow: 0px 0px 110px 40px rgba( 100, 230, 190, 0.4 ); }\r\n\t.dune.far { background-color: #1d5a5a; }\r\n\t.dune.near { background-color: #16414a; }\r\n\t.floor { background-color: #0f2c35; }\r\n\t.stars\r\n\t{\r\n\t\topacity: 1;\r\n\t\tbackground-image: linear-gradient( to bottom, rgba( 120, 255, 210, 0.18 ) 0%, rgba( 120, 255, 210, 0 ) 70% );\r\n\t}\r\n\t.scenery\r\n\t{\r\n\t\topacity: 1;\r\n\t\tbackground-image: linear-gradient( to top, rgba( 63, 214, 160, 0.3 ) 0%, rgba( 63, 214, 160, 0 ) 45% );\r\n\t}\r\n}\r\n\r\n// ---- Volcanic: warm rock, water comes up boiling\r\n\r\n.scene-volcanic\r\n{\r\n\t.sky { background-image: linear-gradient( to bottom, #120404 0%, #1a0806 40%, #7d2412 78%, #e2542a 100% ); }\r\n\t.sun { background-color: #ff8a4a; box-shadow: 0px 0px 130px 46px rgba( 226, 84, 42, 0.5 ); }\r\n\t.dune.far { background-color: #3a1410; }\r\n\t.dune.near { background-color: #2a0d0a; }\r\n\t.floor { background-color: #1c0806; }\r\n\t.scenery\r\n\t{\r\n\t\topacity: 1;\r\n\t\tbackground-image: linear-gradient( to top, rgba( 226, 84, 42, 0.45 ) 0%, rgba( 226, 84, 42, 0 ) 40% );\r\n\t}\r\n}\r\n\r\n// ---- Crystal Cavern: deep enough that the walls glow\r\n\r\n.scene-crystal\r\n{\r\n\t.sky { background-image: linear-gradient( to bottom, #0a0620 0%, #160d33 40%, #3f2a70 76%, #7f6ae0 100% ); }\r\n\t.sun { background-color: #cfc2ff; box-shadow: 0px 0px 120px 42px rgba( 160, 140, 255, 0.45 ); }\r\n\t.dune.far { background-color: #2e2059; }\r\n\t.dune.near { background-color: #241947; }\r\n\t.floor { background-color: #170f2e; }\r\n\t.stars\r\n\t{\r\n\t\topacity: 1;\r\n\t\tbackground-image: linear-gradient( to bottom, rgba( 190, 170, 255, 0.2 ) 0%, rgba( 190, 170, 255, 0 ) 70% );\r\n\t}\r\n}\r\n\r\n// ---------------------------------------------------------------- new pump\r\n\r\n// Buying a pump is the reward the whole loop points at, and it used to be a line of\r\n// toast text. Two and a half seconds of the screen stopping to say so instead.\r\n.celebrate\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: none;\r\n\tanimation: celebrate-fade 2.6s ease-out;\r\n\r\n\t.celebrate-ring\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tleft: 50%;\r\n\t\ttop: 50%;\r\n\t\tmargin-left: -210px;\r\n\t\tmargin-top: -210px;\r\n\t\twidth: 420px;\r\n\t\theight: 420px;\r\n\t\tborder-radius: 100%;\r\n\t\tborder: 8px solid rgba( 255, 226, 150, 0.85 );\r\n\t\tanimation: celebrate-ring 1.1s ease-out;\r\n\r\n\t\t&.two\r\n\t\t{\r\n\t\t\tmargin-left: -130px;\r\n\t\t\tmargin-top: -130px;\r\n\t\t\twidth: 260px;\r\n\t\t\theight: 260px;\r\n\t\t\tborder-width: 5px;\r\n\t\t\tborder-color: rgba( 255, 255, 255, 0.8 );\r\n\t\t\tanimation: celebrate-ring 1.5s ease-out;\r\n\t\t}\r\n\t}\r\n\r\n\t.celebrate-card\r\n\t{\r\n\t\tflex-direction: column;\r\n\t\talign-items: center;\r\n\t\tbackground-color: rgba( 16, 20, 30, 0.86 );\r\n\t\tborder: 3px solid $gold;\r\n\t\tborder-radius: 24px;\r\n\t\tpadding: 20px 46px;\r\n\t\tanimation: celebrate-card 2.6s ease-out;\r\n\t}\r\n\r\n\t.celebrate-kicker\r\n\t{\r\n\t\tfont-size: 14px;\r\n\t\tletter-spacing: 5px;\r\n\t\tcolor: $gold;\r\n\t\tfont-weight: 700;\r\n\t}\r\n\r\n\t.celebrate-name\r\n\t{\r\n\t\tfont-size: 46px;\r\n\t\tfont-weight: 800;\r\n\t\tmargin-top: 2px;\r\n\t\ttext-shadow: 0px 3px 0px rgba( 0, 0, 0, 0.45 );\r\n\t}\r\n\r\n\t.celebrate-sub\r\n\t{\r\n\t\tfont-size: 16px;\r\n\t\topacity: 0.8;\r\n\t\tmargin-top: 2px;\r\n\t}\r\n}\r\n\r\n@keyframes celebrate-fade\r\n{\r\n\t0% { opacity: 1; }\r\n\t80% { opacity: 1; }\r\n\t100% { opacity: 0; }\r\n}\r\n\r\n@keyframes celebrate-ring\r\n{\r\n\t0% { transform: scale( 0.2 ); opacity: 0.95; }\r\n\t100% { transform: scale( 1.6 ); opacity: 0; }\r\n}\r\n\r\n@keyframes celebrate-card\r\n{\r\n\t0% { transform: scale( 0.6 ); opacity: 0; }\r\n\t9% { transform: scale( 1.08 ); opacity: 1; }\r\n\t16% { transform: scale( 1 ); opacity: 1; }\r\n\t86% { transform: scale( 1 ); opacity: 1; }\r\n\t100% { transform: scale( 1 ); opacity: 0; }\r\n}\r\n\r\n// ---------------------------------------------------------------- scene weather\r\n\r\n// The ambient layers are shared markup, so each scene only has to say what its own\r\n// air is doing. Dawn is the baseline: clouds, a little haze off the sand.\r\n\r\n.scene-dawn\r\n{\r\n\t.shimmer { opacity: 0.7; }\r\n}\r\n\r\n.scene-dusk\r\n{\r\n\t.cloud { background-color: rgba( 120, 70, 90, 0.5 ); box-shadow: 0px 0px 50px 24px rgba( 120, 70, 90, 0.3 ); }\r\n\t.mote { background-color: rgba( 255, 190, 190, 0.4 ); }\r\n\t.shimmer { opacity: 0.25; }\r\n}\r\n\r\n.scene-oasis\r\n{\r\n\t.cloud { background-color: rgba( 255, 255, 255, 0.7 ); }\r\n\t// Seeds and pollen rather than sand, drifting rather than blowing through.\r\n\t.mote { background-color: rgba( 220, 255, 190, 0.7 ); }\r\n\t.shimmer { opacity: 0; }\r\n}\r\n\r\n.scene-neon\r\n{\r\n\t.cloud { background-color: rgba( 255, 120, 190, 0.28 ); box-shadow: 0px 0px 60px 30px rgba( 255, 95, 162, 0.22 ); }\r\n\t.mote { background-color: rgba( 255, 150, 220, 0.75 ); }\r\n\t.shimmer { opacity: 0.4; }\r\n}\r\n\r\n// Enough dust in the air that the clouds have gone and the haze has taken over.\r\n.scene-sandstorm\r\n{\r\n\t.cloud { background-color: rgba( 220, 190, 140, 0.4 ); box-shadow: 0px 0px 70px 40px rgba( 200, 160, 90, 0.3 ); }\r\n\t.mote { background-color: rgba( 255, 230, 180, 0.85 ); width: 7px; height: 7px; }\r\n\t.shimmer { opacity: 1; }\r\n}\r\n\r\n.scene-golden\r\n{\r\n\t.cloud { background-color: rgba( 255, 210, 180, 0.55 ); box-shadow: 0px 0px 60px 26px rgba( 255, 170, 120, 0.35 ); }\r\n\t.mote { background-color: rgba( 255, 220, 170, 0.7 ); }\r\n\t.shimmer { opacity: 0.6; }\r\n}\r\n\r\n// No cloud and no wind - the motes hold still enough to read as stars.\r\n.scene-starfield\r\n{\r\n\t.clouds { opacity: 0; }\r\n\t.mote { background-color: rgba( 235, 240, 255, 0.9 ); }\r\n\t.shimmer { opacity: 0; }\r\n}\r\n\r\n.scene-aurora\r\n{\r\n\t.cloud { background-color: rgba( 120, 255, 210, 0.2 ); box-shadow: 0px 0px 70px 34px rgba( 100, 230, 190, 0.22 ); }\r\n\t.mote { background-color: rgba( 235, 255, 250, 0.85 ); width: 6px; height: 6px; }\r\n\t.shimmer { opacity: 0.3; }\r\n}\r\n\r\n// Embers, and air you can see moving.\r\n.scene-volcanic\r\n{\r\n\t.cloud { background-color: rgba( 90, 40, 30, 0.5 ); box-shadow: 0px 0px 70px 34px rgba( 226, 84, 42, 0.25 ); }\r\n\t.mote { background-color: rgba( 255, 160, 90, 0.9 ); box-shadow: 0px 0px 8px 2px rgba( 255, 120, 50, 0.6 ); }\r\n\t.shimmer { opacity: 1; }\r\n}\r\n\r\n.scene-crystal\r\n{\r\n\t.clouds { opacity: 0; }\r\n\t.mote { background-color: rgba( 210, 190, 255, 0.85 ); box-shadow: 0px 0px 8px 2px rgba( 170, 140, 255, 0.5 ); }\r\n\t.shimmer { opacity: 0.35; }\r\n}\r\n"
        },
        {
            "Ident": "fpkreastudios.desertpump",
            "Path": "Game/UpgradeTrack.cs",
            "FileName": "UpgradeTrack.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 343538,
            "IsPrivate": false,
            "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"
        }
    ]
}