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