{"TotalCount":78,"Files":[{"Ident":"idkman.monolith","Path":"Game/Interceptor.cs","FileName":"Interceptor.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"namespace Monolith;\n\n/// \u003Csummary\u003E\n/// A drifting obstacle that positions itself between you and the shape and eats your shots.\n///\n/// It cannot hurt you. Its whole job is to make aiming a decision: shots spent on an\n/// interceptor are shots not spent on the rock, but leaving them alive means a steadily larger\n/// share of your fire never lands. Killing one pays out, so clearing them is worth doing rather\n/// than merely necessary.\n/// \u003C/summary\u003E\npublic sealed class Interceptor : Component\n{\n\tprivate static readonly List\u003CInterceptor\u003E all = new();\n\tpublic static IReadOnlyList\u003CInterceptor\u003E All =\u003E all;\n\n\tprivate static Model shellModel;\n\n\t[Property] public float Radius { get; set; } = 34f;\n\n\tpublic int Health { get; private set; } = Tuning.InterceptorHealth;\n\n\tprivate Vector3 driftTarget;\n\tprivate Vector3 velocity;\n\tprivate GameTimeSince timeSinceRetarget;\n\tprivate ModelRenderer renderer;\n\tprivate GameTimeSince timeSinceHit = 99f;\n\n\tpublic static Interceptor Spawn( Scene scene, Vector3 position )\n\t{\n\t\tif ( scene == null ) return null;\n\n\t\tvar obj = new GameObject( true, \u0022Interceptor\u0022 );\n\t\tobj.NetworkMode = NetworkMode.Never;\n\t\tobj.WorldPosition = position;\n\t\tobj.WorldRotation = Rotation.Random;\n\n\t\tvar interceptor = obj.AddComponent\u003CInterceptor\u003E();\n\t\tobj.WorldScale = interceptor.Radius * 2f;\n\n\t\tvar mr = obj.AddComponent\u003CModelRenderer\u003E();\n\t\tmr.Model = GetShellModel();\n\t\tmr.Tint = new Color( 0.85f, 0.20f, 0.08f );\n\n\t\tinterceptor.renderer = mr;\n\t\treturn interceptor;\n\t}\n\n\tprotected override void OnEnabled() =\u003E all.Add( this );\n\tprotected override void OnDisabled() =\u003E all.Remove( this );\n\n\tprotected override void OnUpdate()\n\t{\n\t\t// Frozen while a blocking screen or the pause menu is up. See GameTime.\n\t\tif ( GameTime.Paused )\n\t\t\treturn;\n\n\t\tvar manager = MonolithManager.Instance;\n\t\tif ( !manager.IsValid() || manager.World == null )\n\t\t\treturn;\n\n\t\t// Retarget periodically to a point between the player and the shape, which is what\n\t\t// makes them read as deliberately getting in the way rather than milling about.\n\t\tif ( timeSinceRetarget \u003E Tuning.InterceptorRetargetSeconds )\n\t\t{\n\t\t\ttimeSinceRetarget = 0;\n\t\t\tdriftTarget = PickBlockingPosition( manager );\n\t\t}\n\n\t\tvar toTarget = driftTarget - WorldPosition;\n\n\t\tif ( toTarget.Length \u003E 1f )\n\t\t\tvelocity = velocity.LerpTo( toTarget.Normal * Tuning.InterceptorSpeed, 1.6f * Time.Delta );\n\n\t\tWorldPosition \u002B= velocity * Time.Delta;\n\t\tWorldRotation *= Rotation.From( 40f * Time.Delta, 55f * Time.Delta, 0f );\n\n\t\t// Flash on hit so damage is legible without a health bar.\n\t\tif ( renderer.IsValid() )\n\t\t{\n\t\t\tfloat flash = timeSinceHit \u003C 0.12f ? 1f : 0f;\n\t\t\tfloat wear = Health / (float)Tuning.InterceptorHealth;\n\n\t\t\trenderer.Tint = Color.Lerp(\n\t\t\t\tnew Color( 0.45f, 0.09f, 0.03f ),\n\t\t\t\tnew Color( 1f, 0.35f, 0.12f ),\n\t\t\t\twear ) \u002B new Color( flash, flash * 0.8f, flash * 0.5f );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003ESomewhere on the line between the player and the shape, offset a little.\u003C/summary\u003E\n\tprivate Vector3 PickBlockingPosition( MonolithManager manager )\n\t{\n\t\tvar bounds = manager.World.WorldBounds;\n\n\t\tvar player = Scene.GetAllComponents\u003CPlayerMovement\u003E().FirstOrDefault();\n\t\tvar from = player.IsValid() ? player.WorldPosition : bounds.Center \u002B Vector3.Backward * 800f;\n\n\t\tfloat t = Game.Random.Float( 0.35f, 0.75f );\n\t\tvar onLine = Vector3.Lerp( from, bounds.Center, t );\n\n\t\treturn onLine \u002B Vector3.Random.Normal * Game.Random.Float( 60f, 260f );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Sphere test against every live interceptor for one projectile step. Linear, but the\n\t/// population is capped in the low tens so it is far cheaper than a physics query.\n\t/// \u003C/summary\u003E\n\tpublic static bool TryIntercept( Scene scene, Vector3 start, Vector3 direction, float distance,\n\t\tout Interceptor hit )\n\t{\n\t\thit = null;\n\t\tfloat best = float.MaxValue;\n\n\t\tfor ( int i = 0; i \u003C all.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar candidate = all[i];\n\t\t\tif ( !candidate.IsValid() ) continue;\n\n\t\t\tvar toCentre = candidate.WorldPosition - start;\n\t\t\tfloat along = Vector3.Dot( toCentre, direction );\n\n\t\t\t// Allow a small negative so a projectile spawned just inside still registers.\n\t\t\tif ( along \u003C -candidate.Radius || along \u003E distance \u002B candidate.Radius )\n\t\t\t\tcontinue;\n\n\t\t\tfloat perpSq = toCentre.LengthSquared - along * along;\n\t\t\tif ( perpSq \u003E candidate.Radius * candidate.Radius )\n\t\t\t\tcontinue;\n\n\t\t\tif ( along \u003C best )\n\t\t\t{\n\t\t\t\tbest = along;\n\t\t\t\thit = candidate;\n\t\t\t}\n\t\t}\n\n\t\treturn hit != null;\n\t}\n\n\tpublic void TakeHit( int damage )\n\t{\n\t\tHealth -= damage;\n\t\ttimeSinceHit = 0;\n\n\t\tif ( Health \u003E 0 )\n\t\t\treturn;\n\n\t\tBlastEffect.Spawn( Scene, WorldPosition, 220f, true );\n\t\tDebris.Burst( Scene, WorldPosition, 130f, true );\n\n\t\tvar progress = PlayerProgress.Local;\n\n\t\tif ( progress.IsValid() )\n\t\t{\n\t\t\tprogress.AwardDust( Tuning.InterceptorDustReward );\n\t\t\tprogress.Data.InterceptorKills\u002B\u002B;\n\t\t\tprogress.AddResonance();\n\t\t}\n\n\t\tGameObject.Destroy();\n\t}\n\n\tprivate static Model GetShellModel()\n\t{\n\t\tif ( shellModel != null )\n\t\t\treturn shellModel;\n\n\t\t// An octahedron: distinct from every cube in the scene at a glance, and cheap.\n\t\tvar vb = new VertexBuffer();\n\t\tvb.Init( true );\n\n\t\tVector3[] tips =\n\t\t{\n\t\t\tVector3.Up * 0.5f, Vector3.Down * 0.5f,\n\t\t};\n\n\t\tVector3[] ring =\n\t\t{\n\t\t\tVector3.Forward * 0.5f, Vector3.Right * 0.5f,\n\t\t\tVector3.Backward * 0.5f, Vector3.Left * 0.5f,\n\t\t};\n\n\t\tint index = 0;\n\n\t\tforeach ( var tip in tips )\n\t\t{\n\t\t\tfor ( int i = 0; i \u003C ring.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar a = ring[i];\n\t\t\t\tvar b = ring[(i \u002B 1) % ring.Length];\n\n\t\t\t\t// Wind so both caps face outwards.\n\t\t\t\tvar (p1, p2) = tip.z \u003E 0 ? (a, b) : (b, a);\n\t\t\t\tvar normal = Vector3.Cross( p2 - tip, p1 - tip ).Normal;\n\t\t\t\tvar tangent = (p1 - tip).Normal;\n\n\t\t\t\tvb.Add( new Vertex( tip, normal, tangent, new Vector4( 0.5f, 0, 0, 0 ) ) );\n\t\t\t\tvb.Add( new Vertex( p1, normal, tangent, new Vector4( 0, 1, 0, 0 ) ) );\n\t\t\t\tvb.Add( new Vertex( p2, normal, tangent, new Vector4( 1, 1, 0, 0 ) ) );\n\n\t\t\t\tvb.AddRawIndex( index \u002B 0 );\n\t\t\t\tvb.AddRawIndex( index \u002B 1 );\n\t\t\t\tvb.AddRawIndex( index \u002B 2 );\n\n\t\t\t\tindex \u002B= 3;\n\t\t\t}\n\t\t}\n\n\t\tvar mesh = new Mesh( Material.Load( \u0022materials/default.vmat\u0022 ) );\n\t\tmesh.CreateBuffers( vb );\n\n\t\tshellModel = new ModelBuilder().AddMesh( mesh ).Create();\n\t\treturn shellModel;\n\t}\n}\n"},{"Ident":"idkman.monolith","Path":"UI/Hud.razor","FileName":"Hud.razor","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"@using Sandbox\n@using Sandbox.UI\n@using System\n@using System.Linq\n@inherits PanelComponent\n@namespace Monolith\n\n\u003Croot class=\u0022hud @(PhotoHidden ? \u0022photo-hidden\u0022 : \u0022\u0022)\u0022\u003E\n\n\t@if ( Progress != null )\n\t{\n\t\t@if ( Manager != null \u0026\u0026 Manager.StageJustCleared )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022cleared\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022big\u0022\u003E@Manager.LastClearedName BROKEN\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022next\u0022\u003E@StageName condensing\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t}\n\n\t\t\u003Cdiv class=\u0022topbar\u0022\u003E\n\t\t\t@* The shared Monolith is NOT a rung on the solo ladder, so it must not borrow the\n\t\t\t   ladder\u0027s numbering. It was showing \u0022STAGE 48\u0022 because Tier is still whatever solo\n\t\t\t   stage you left, which made a separate destination look like more of the same. *@\n\t\t\t\u003Cdiv class=\u0022tier\u0022\u003E@TierLine\u003C/div\u003E\n\t\t\t\u003Cdiv class=\u0022bar\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022fill\u0022 style=\u0022width: @(ClearedPercent)%;\u0022\u003E\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t\t\u003Cdiv class=\u0022counts\u0022\u003E\n\t\t\t\t\u003Cspan class=\u0022remaining\u0022\u003E@Num.Short( Remaining )\u003C/span\u003E\n\t\t\t\t\u003Cspan class=\u0022sep\u0022\u003E/\u003C/span\u003E\n\t\t\t\t\u003Cspan class=\u0022total\u0022\u003E@Num.Short( Total )\u003C/span\u003E\n\t\t\t\t\u003Cspan class=\u0022pct\u0022\u003E@ClearedPercent.ToString( \u00220.000\u0022 )% cleared\u003C/span\u003E\n\t\t\t\u003C/div\u003E\n\t\t\u003C/div\u003E\n\n\t\t\u003Cdiv class=\u0022wallet\u0022\u003E\n\t\t\t\u003Cdiv class=\u0022dust\u0022\u003E\n\t\t\t\t\u003Cspan class=\u0022label\u0022\u003EDUST\u003C/span\u003E\n\t\t\t\t\u003Cspan class=\u0022value\u0022\u003E@Num.Short( Progress.Data.Dust )\u003C/span\u003E\n\t\t\t\u003C/div\u003E\n\t\t\t\u003Cdiv class=\u0022line\u0022\u003E\n\t\t\t\t\u003Cspan class=\u0022label\u0022\u003ECubes mined\u003C/span\u003E\n\t\t\t\t\u003Cspan class=\u0022value\u0022\u003E@Num.Short( Progress.Data.LifetimeCubes )\u003C/span\u003E\n\t\t\t\u003C/div\u003E\n\t\t\t\u003Cdiv class=\u0022line\u0022\u003E\n\t\t\t\t\u003Cspan class=\u0022label\u0022\u003ECores\u003C/span\u003E\n\t\t\t\t\u003Cspan class=\u0022value\u0022\u003E@Progress.Data.Cores (x@(Progress.CoreMultiplier.ToString( \u00220.00\u0022 )))\u003C/span\u003E\n\t\t\t\u003C/div\u003E\n\t\t\t\u003Cdiv class=\u0022line\u0022\u003E\n\t\t\t\t\u003Cspan class=\u0022label\u0022\u003EShots\u003C/span\u003E\n\t\t\t\t\u003Cspan class=\u0022value\u0022\u003E@($\u0022x{Progress.ProjectileCount}\u0022)\u003C/span\u003E\n\t\t\t\u003C/div\u003E\n\t\t\t\u003Cdiv class=\u0022line timer\u0022\u003E\n\t\t\t\t\u003Cspan class=\u0022label\u0022\u003E@(InMonolith ? \u0022Monolith run\u0022 : \u0022Ladder run\u0022)\u003C/span\u003E\n\t\t\t\t\u003Cspan class=\u0022value\u0022\u003E@Num.Duration( InMonolith ? (Manager?.MonolithSeconds ?? 0f) : Progress.RunSeconds )\u003C/span\u003E\n\t\t\t\u003C/div\u003E\n\t\t\u003C/div\u003E\n\n\t\t@if ( Leech.All.Count \u003E 0 )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022leeches\u0022\u003E\n\t\t\t\t@Leech.All.Count LEECH@(Leech.All.Count == 1 ? \u0022\u0022 : \u0022ES\u0022)\n\t\t\t\t\u0026nbsp;\u0026middot;\u0026nbsp; -@((Leech.TotalSiphon * 100f).ToString( \u00220\u0022 ))% dust\n\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003Eshoot them to take it back, with interest\u003C/span\u003E\n\t\t\t\u003C/div\u003E\n\t\t}\n\n\t\t@* No Spotter warning here on purpose. The threat is told entirely by its light: the\n\t\t   beam snapping onto you, reddening, tightening and pulsing faster as the lock fills.\n\t\t   A text overlay would let you read the HUD instead of reading the world.\n\n\t\t   The LOSS is different, and is the one place text earns its keep. A threat you can see\n\t\t   coming should be read from the world; a stage that has already reset underneath you\n\t\t   has no world-state left to read, and without this the most likely interpretation was\n\t\t   that the game had glitched. *@\n\t\t@* WELCOME PAGES. Shown once, on a save that has never been played.\n\n\t\t   Everything in this game is taught by the world rather than by text (GOALS 6b), which\n\t\t   works for a threat you can watch and fails completely for a threat you have not met\n\t\t   yet. Five pages is the smallest thing that stops a new player being killed by a\n\t\t   Spotter, or by the floor, without ever learning what either one was. *@\n\t\t@if ( ShowTutorial )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022overlay\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022sheet\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022sheet-eyebrow\u0022\u003E@($\u0022{TutorialPage \u002B 1} / {TutorialPages}\u0022)\u003C/div\u003E\n\n\t\t\t\t\t@if ( TutorialPage == 0 )\n\t\t\t\t\t{\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-title\u0022\u003EMONOLITH\u003C/div\u003E\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-lead\u0022\u003EA shape of solid cubes hangs in the void. Take it apart.\u003C/div\u003E\n\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-rows\u0022\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022srow\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022icon rock\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022stext\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cb\u003EMine it\u003C/b\u003E\n\t\t\t\t\t\t\t\t\tHold left click. Every cube you break pays DUST, and dust buys\n\t\t\t\t\t\t\t\t\tupgrades that break more cubes. Clear the shape and the next\n\t\t\t\t\t\t\t\t\tone condenses in its place.\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022srow\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022icon ladder\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022stext\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cb\u003EThe ladder is 100 stages\u003C/b\u003E\n\t\t\t\t\t\t\t\t\tEach is a little bigger than the last. Reaching the end unlocks\n\t\t\t\t\t\t\t\t\tCOLLAPSE, which resets your dust and upgrades but pays permanent\n\t\t\t\t\t\t\t\t\tcores. You come back stronger and climb faster.\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t}\n\t\t\t\t\telse if ( TutorialPage == 1 )\n\t\t\t\t\t{\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-title\u0022\u003EYOUR TWO WEAPONS\u003C/div\u003E\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-lead\u0022\u003EOne is constant. One is a decision.\u003C/div\u003E\n\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-rows\u0022\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022srow\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022icon bolt\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022stext\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cb\u003ELeft click \u0026middot; the drill\u003C/b\u003E\n\t\t\t\t\t\t\t\t\tA stream of bolts that travel. They can be blocked, and they\n\t\t\t\t\t\t\t\t\tcan hit things other than rock, so you can shoot down what is\n\t\t\t\t\t\t\t\t\tshooting at you.\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022srow\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022icon bore\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022stext\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cb\u003ERight click \u0026middot; the charge\u003C/b\u003E\n\t\t\t\t\t\t\t\t\tFires a charge that bores INTO the shape. Left click sets it\n\t\t\t\t\t\t\t\t\toff wherever it has reached. It removes a share of the whole\n\t\t\t\t\t\t\t\t\tstage, so it never stops mattering, and burying it deeper\n\t\t\t\t\t\t\t\t\tmeans more of the blast lands in rock instead of air.\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022srow\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022icon rocket\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022stext\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cb\u003EShoot the FLOOR to rocket jump\u003C/b\u003E\n\t\t\t\t\t\t\t\t\tA charge that meets the ground goes off on its own. No\n\t\t\t\t\t\t\t\t\tsecond click and no timing. It throws you, hardest when it\n\t\t\t\t\t\t\t\t\tlands closest, and it still mines whatever was beside it,\n\t\t\t\t\t\t\t\t\tthough less than a charge you let bore in first.\n\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022c-o\u0022\u003EGoing faster costs you a charge.\u003C/span\u003E\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022srow\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022icon volatile\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022stext\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cb\u003ERed cubes are volatile\u003C/b\u003E\n\t\t\t\t\t\t\t\t\tHit one squarely and it detonates. The only large explosion\n\t\t\t\t\t\t\t\t\tordinary fire can produce, so they are worth aiming at, but\n\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022c-r\u0022\u003Ethe blast throws burning chunks of rock outward\n\t\t\t\t\t\t\t\t\tand the pieces come down around YOU.\u003C/span\u003E Each one marks the\n\t\t\t\t\t\t\t\t\tground with a ring before it lands. Rings creep toward you, so\n\t\t\t\t\t\t\t\t\tstanding still never works, but they creep slowly, so walking\n\t\t\t\t\t\t\t\t\tout always does. Jumping does not: the blast reaches higher\n\t\t\t\t\t\t\t\t\tthan you can. The real cost is WHERE it makes you go.\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t}\n\t\t\t\t\telse if ( TutorialPage == 2 )\n\t\t\t\t\t{\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-title\u0022\u003EWHAT IS OUT THERE\u003C/div\u003E\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-lead\u0022\u003E\n\t\t\t\t\t\t\tNothing can kill you. The Spotter can take the stage back.\n\t\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-rows\u0022\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022srow\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022icon spotter\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022stext\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cb\u003ESpotter \u0026middot; the searchlight\u003C/b\u003E\n\t\t\t\t\t\t\t\t\tSweeps a wide beam. If it finds you it locks on and a line\n\t\t\t\t\t\t\t\t\tattaches, and it changes colour as the lock fills:\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ramp\u0022\u003E\n\t\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022c-y\u0022\u003EYELLOW\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022arrow\u0022\u003E\u0026rsaquo;\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022c-o\u0022\u003EORANGE\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022arrow\u0022\u003E\u0026rsaquo;\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022c-r\u0022\u003ERED, THE LAST SECOND\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\t\tLet it finish and the stage restarts. Break line of sight behind\n\t\t\t\t\t\t\t\t\tthe shape, outrun the beam, or shoot it down.\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022srow\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022icon sentinel\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022stext\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cb\u003ESentinel \u0026middot; the turret\u003C/b\u003E\n\t\t\t\t\t\t\t\t\tTurns to face you, glows brighter as it charges, then fires a\n\t\t\t\t\t\t\t\t\tslow pink orb. Dodge it, shoot the orb, or kill the sentinel.\n\t\t\t\t\t\t\t\t\tIt also radios your position to Spotters, so leaving one alive\n\t\t\t\t\t\t\t\t\tis how you get found.\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022srow\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022icon anchor\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022stext\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cb\u003EAnchor \u0026middot; the tether\u003C/b\u003E\n\t\t\t\t\t\t\t\t\tFires a green cable that halves your speed. You cannot outrun\n\t\t\t\t\t\t\t\t\tit and cover will not save you. Shoot the far end.\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022srow\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022icon crawler\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022stext\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cb\u003ECrawler \u0026middot; the one on the floor\u003C/b\u003E\n\t\t\t\t\t\t\t\t\tRuns at you across the ground and gets FASTER the longer it\n\t\t\t\t\t\t\t\t\tchases, glowing from ember to white hot as it winds up. A fresh\n\t\t\t\t\t\t\t\t\tone you can walk away from; a wound-up one needs a sprint. If\n\t\t\t\t\t\t\t\t\tit reaches you it takes the stage. Cut any Anchor cable first:\n\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022c-r\u0022\u003Etethered, you cannot outrun a crawler at all.\u003C/span\u003E\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t}\n\t\t\t\t\telse if ( TutorialPage == 3 )\n\t\t\t\t\t{\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-title\u0022\u003ETHE FLOOR IS A CLOCK\u003C/div\u003E\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-lead\u0022\u003E\n\t\t\t\t\t\t\tStanding still is the only thing that is always wrong.\n\t\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-rows\u0022\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022srow\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022icon warn\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022stext\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cb\u003EHalf the floor turns on you\u003C/b\u003E\n\t\t\t\t\t\t\t\t\tEvery three seconds a new half lights up. It goes\n\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022c-o\u0022\u003EAMBER\u003C/span\u003E first, and that is your one\n\t\t\t\t\t\t\t\t\tsecond to be somewhere else. Then it goes red and takes\n\t\t\t\t\t\t\t\t\tthe stage.\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022srow\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022icon dead\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022stext\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cb\u003EThe raised violet blocks never turn off\u003C/b\u003E\n\t\t\t\t\t\t\t\t\tA few squares stand proud of the floor and glow violet.\n\t\t\t\t\t\t\t\t\tLethal permanently: no warning, no cycle, no second\n\t\t\t\t\t\t\t\t\tchance. They are the holes in whatever route you were\n\t\t\t\t\t\t\t\t\tplanning, and they are the only thing out here that is\n\t\t\t\t\t\t\t\t\tnot orange, so learn them at a glance.\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022srow\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022icon air\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022stext\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cb\u003EIn the air you are safe\u003C/b\u003E\n\t\t\t\t\t\t\t\t\tThe floor can only take you while you are standing on it.\n\t\t\t\t\t\t\t\t\tOne ordinary jump clears one tile, a sprint clears two, a\n\t\t\t\t\t\t\t\t\tgood hop chain clears three.\n\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022c-y\u0022\u003EEvery movement trick you have is a way\n\t\t\t\t\t\t\t\t\tof being off the ground for longer.\u003C/span\u003E\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-title\u0022\u003EAND THE REST\u003C/div\u003E\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-lead\u0022\u003EThings that cost you, and things that pay you.\u003C/div\u003E\n\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-rows\u0022\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022srow\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022icon barrier\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022stext\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cb\u003EBarriers\u003C/b\u003E\n\t\t\t\t\t\t\t\t\tFrames that orbit the shape and eat any shot fired INTO it.\n\t\t\t\t\t\t\t\t\tThey cannot be destroyed. Move, or wait for the gap.\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022srow\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022icon leech\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022stext\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cb\u003ELeeches\u003C/b\u003E\n\t\t\t\t\t\t\t\t\tClamp to the shape and siphon your income. They never touch\n\t\t\t\t\t\t\t\t\tyour cubes. Pop one and it pays back everything it took, with\n\t\t\t\t\t\t\t\t\tinterest, so letting one feed first is a real choice.\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022srow\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022icon move\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022stext\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cb\u003EMove like it matters\u003C/b\u003E\n\t\t\t\t\t\t\t\t\tSpeed comes from TIMING your jumps, not from steering: press\n\t\t\t\t\t\t\t\t\tjump just as you land for a much bigger hop. A second jump in\n\t\t\t\t\t\t\t\t\tthe air dashes you along your current direction, which is how\n\t\t\t\t\t\t\t\t\tyou break a beam.\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t}\n\n\t\t\t\t\t\u003Cdiv class=\u0022sheet-actions\u0022\u003E\n\t\t\t\t\t\t@if ( TutorialPage \u003E 0 )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-btn ghost\u0022 onclick=@TutorialBack\u003EBACK\u003C/div\u003E\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-btn\u0022 onclick=@TutorialNext\u003E\n\t\t\t\t\t\t\t@(TutorialPage \u003C TutorialPages - 1 ? \u0022NEXT\u0022 : \u0022BEGIN\u0022)\n\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t}\n\t\telse if ( ShowStart )\n\t\t{\n\t\t\t@* START SCREEN. Where every session begins, fresh or returning.\n\n\t\t\t   Note for editors: an else must touch the closing brace of its if, a Razor comment\n\t\t\t   between the two breaks the chain, and there is no such thing as an at-else because\n\t\t\t   the at sign was already consumed by the opening if.\n\n\t\t\t   Also, and this is what broke the build a second time: a Razor comment CANNOT\n\t\t\t   contain the two character sequence that closes one. Writing it inside the prose\n\t\t\t   ended the comment early and the remaining words were compiled as C#. *@\n\t\t\t\u003Cdiv class=\u0022overlay\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022sheet start\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022sheet-title\u0022\u003EMONOLITH\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022sheet-lead\u0022\u003E\n\t\t\t\t\t\t@if ( Progress.IsFreshSave )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t@(\u0022A new descent.\u0022)\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t@($\u0022Welcome back. {Num.Short( Progress.Data.LifetimeCubes )} cubes mined, \u0022\n\t\t\t\t\t\t\t\t\u002B $\u0022{Progress.Data.Collapses} collapses, {Progress.Data.Cores} cores.\u0022)\n\t\t\t\t\t\t}\n\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\u003Cdiv class=\u0022sheet-actions column\u0022\u003E\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-btn big\u0022 onclick=@StartLadder\u003E\n\t\t\t\t\t\t\t@(Progress.IsFreshSave ? \u0022START THE LADDER\u0022 : \u0022CONTINUE THE LADDER\u0022)\n\t\t\t\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003E\n\t\t\t\t\t\t\t\t@($\u0022stage {Math.Max( 1, Progress.Data.HighestStage )} of {Tuning.PrestigeStageRequirement}\u0022\n\t\t\t\t\t\t\t\t\t\u002B \u0022 \u00B7 climb, upgrade, collapse\u0022)\n\t\t\t\t\t\t\t\u003C/span\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-btn big @(Progress.IsFreshSave ? \u0022locked\u0022 : \u0022\u0022)\u0022\n\t\t\t\t\t\t\t onclick=@StartMonolith\u003E\n\t\t\t\t\t\t\tTHE MONOLITH\n\t\t\t\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003E\n\t\t\t\t\t\t\t\t@if ( Progress.IsFreshSave )\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t@(\u0022the shared rock, 16.7 million cubes. Climb a little first: you will not dent it yet.\u0022)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t@(\u0022the shared rock, 16.7 million cubes. Felling it pays cores and prestige.\u0022)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\u003C/span\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-btn ghost\u0022 onclick=@ReopenTutorial\u003EHOW TO PLAY\u003C/div\u003E\n\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t}\n\n\t\t@* PAUSE MENU. The way out, which the game did not have at all: once you were in, the only\n\t\t   exit was killing the process. Also the only place that tells you the world is frozen. *@\n\t\t@if ( ShowPause )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022overlay transparent\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022sheet pause\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022sheet-title\u0022\u003EPAUSED\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022sheet-lead\u0022\u003E@PauseLead\u003C/div\u003E\n\n\t\t\t\t\t\u003Cdiv class=\u0022sheet-actions column\u0022\u003E\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-btn big @(IsPauseSelected( 0 ) ? \u0022selected\u0022 : \u0022\u0022)\u0022\n\t\t\t\t\t\t\t onclick=@Resume\u003E\n\t\t\t\t\t\t\tRESUME\n\t\t\t\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003Eor press escape\u003C/span\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\t\u003Cdiv class=\u0022toggle @(IsPauseSelected( 1 ) ? \u0022selected\u0022 : \u0022\u0022)\u0022\n\t\t\t\t\t\t\t onclick=@ToggleMusic\u003E\n\t\t\t\t\t\t\t\u003Cspan class=\u0022name\u0022\u003EMUSIC\u003C/span\u003E\n\t\t\t\t\t\t\t\u003Cspan class=\u0022state @(AudioSettings.MusicMuted ? \u0022off\u0022 : \u0022on\u0022)\u0022\u003E\n\t\t\t\t\t\t\t\t@(AudioSettings.MusicMuted ? \u0022OFF\u0022 : \u0022ON\u0022)\n\t\t\t\t\t\t\t\u003C/span\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\t\u003Cdiv class=\u0022toggle @(IsPauseSelected( 2 ) ? \u0022selected\u0022 : \u0022\u0022)\u0022\n\t\t\t\t\t\t\t onclick=@ToggleEffects\u003E\n\t\t\t\t\t\t\t\u003Cspan class=\u0022name\u0022\u003ESOUND EFFECTS\u003C/span\u003E\n\t\t\t\t\t\t\t\u003Cspan class=\u0022state @(AudioSettings.EffectsMuted ? \u0022off\u0022 : \u0022on\u0022)\u0022\u003E\n\t\t\t\t\t\t\t\t@(AudioSettings.EffectsMuted ? \u0022OFF\u0022 : \u0022ON\u0022)\n\t\t\t\t\t\t\t\u003C/span\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-btn ghost @(IsPauseSelected( 3 ) ? \u0022selected\u0022 : \u0022\u0022)\u0022\n\t\t\t\t\t\t\t onclick=@ReopenTutorial\u003EHOW TO PLAY\u003C/div\u003E\n\n\t\t\t\t\t\t\u003Cdiv class=\u0022sheet-btn ghost @(IsPauseSelected( 4 ) ? \u0022selected\u0022 : \u0022\u0022)\u0022\n\t\t\t\t\t\t\t onclick=@QuitGame\u003E\n\t\t\t\t\t\t\tQUIT\n\t\t\t\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003Eyour progress is saved first\u003C/span\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t}\n\n\t\t@* The end of the ladder. Stops the run and asks for a decision, instead of silently\n\t\t   condensing stage 101 as if nothing had happened. *@\n\t\t@if ( Manager != null \u0026\u0026 Manager.LadderComplete )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022ladder-done\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022done-title\u0022\u003ELADDER COMPLETE\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022done-sub\u0022\u003E@($\u0022all {Tuning.PrestigeStageRequirement} stages\u0022)\u003C/div\u003E\n\n\t\t\t\t\u003Cdiv class=\u0022done-time\u0022\u003E@Num.Duration( Manager.LadderCompleteSeconds )\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022done-rank\u0022\u003E@LadderRankLine\u003C/div\u003E\n\n\t\t\t\t\u003Cdiv class=\u0022done-actions\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022done-btn\u0022 onclick=@CollapseFromSummary\u003E\n\t\t\t\t\t\tCOLLAPSE\n\t\t\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003E\n\t\t\t\t\t\t\t@($\u0022reset to stage 1 and bank {Progress?.PendingCores ?? 0} cores, kept forever\u0022)\n\t\t\t\t\t\t\u003C/span\u003E\n\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022done-btn\u0022 onclick=@GoToMonolith\u003E\n\t\t\t\t\t\tTHE MONOLITH\n\t\t\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003E\n\t\t\t\t\t\t\ttake your upgrades to the shared rock and keep this run alive\n\t\t\t\t\t\t\u003C/span\u003E\n\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t}\n\n\t\t@if ( StageLostEffect.Intensity \u003E 0f )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022stage-lost\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022lost-title\u0022\u003ESTAGE LOST\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022lost-sub\u0022\u003Ea Spotter had you. nothing earned was taken.\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t}\n\n\t\t@if ( Manager != null \u0026\u0026 Manager.ShieldActive )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022shield-banner\u0022\u003E\n\t\t\t\tSHIELDED \u0026mdash; DRONES JAMMED \u0026nbsp;@Manager.ShieldSecondsLeft.ToString( \u00220\u0022 )s\n\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003Eshoot the node above the shape\u003C/span\u003E\n\t\t\t\u003C/div\u003E\n\t\t}\n\t\telse if ( Manager != null \u0026\u0026 Manager.ShieldBroken )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022shield-banner broken\u0022\u003E\n\t\t\t\tNODE DOWN \u0026mdash; DRONES FREE \u0026nbsp;@Manager.ShieldBreakSecondsLeft.ToString( \u00220.0\u0022 )s\n\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003Eit recharges\u003C/span\u003E\n\t\t\t\u003C/div\u003E\n\t\t}\n\n\t\t@if ( Progress.InHollowRun )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022hollow-banner\u0022\u003E\n\t\t\t\tHOLLOW RUN \u0026middot; @HollowRuns.Get( Progress.Hollow )?.Name\n\t\t\t\u003C/div\u003E\n\t\t}\n\n\t\t@* Hidden while the panel is up. It is a live combat readout and it rendered straight\n\t\t   through the upgrade list, on top of the text you were trying to read. *@\n\t\t@if ( Progress.Resonance \u003E 0 \u0026\u0026 !PanelOpen )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022resonance\u0022\u003E\n\t\t\t\t\t@* Explicit expression, not an implicit one. \u0022x@Foo.ToString(...) TEXT\u0022 is\n\t\t\t\t\t   fragile in Razor: the parser can swallow or mangle the surrounding\n\t\t\t\t\t   literal, which is how this rendered as garbled text. *@\n\t\t\t\t\u003Cdiv class=\u0022stacks\u0022\u003E@($\u0022RESONANCE x{Progress.ResonanceMultiplier:0.0}\u0022)\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022rbar\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022fill\u0022\n\t\t\t\t\t\t style=\u0022width: @((Progress.ResonanceRemaining / Tuning.ResonanceWindow * 100f).ToString( \u00220.0\u0022 ))%;\u0022\u003E\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022count\u0022\u003E@Progress.Resonance stacks\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t}\n\n\t\t@if ( Progress.LastMark != null \u0026\u0026 Progress.TimeSinceMark \u003C 4f )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022mark-toast\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022head\u0022\u003EMARK EARNED\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022name\u0022\u003E@Progress.LastMark.Name\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t}\n\n\t\t@if ( Residue )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022residue\u0022\u003E\n\t\t\t\t@Remaining @(Remaining == 1 ? \u0022CUBE\u0022 : \u0022CUBES\u0022) LEFT\n\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003E@ResidueHint\u003C/span\u003E\n\t\t\t\u003C/div\u003E\n\t\t}\n\n\t\t@* BURNING. Standing on a live tile no longer kills instantly, so there has to be\n\t\t   something telling you the clock is running. Drawn as a border closing in from the\n\t\t   edges rather than as a bar, because a bar is something you have to look AT and the\n\t\t   whole point is that you are looking at the rock. *@\n\t\t@if ( Burning )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022burn\u0022 style=\u0022opacity: @(BurnAmount.ToString( \u00220.00\u0022 ));\u0022\u003E\u003C/div\u003E\n\t\t\t\u003Cdiv class=\u0022burn-word\u0022 style=\u0022opacity: @(BurnAmount.ToString( \u00220.00\u0022 ));\u0022\u003EMOVE\u003C/div\u003E\n\t\t}\n\n\t\t@if ( !PanelOpen \u0026\u0026 !BlockingScreen )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022crosshair @(Aiming ? \u0022on-target\u0022 : \u0022\u0022)\u0022\u003E\u003C/div\u003E\n\n\t\t\t@* The charge is buried inside solid rock, so nothing in the world can show you\n\t\t\t   where it is. Project it onto the screen instead: this is the only reliable way\n\t\t\t   to know what you are about to detonate. *@\n\t\t\t@if ( ChargeOnScreen )\n\t\t\t{\n\t\t\t\t\u003Cdiv class=\u0022charge-marker\u0022 style=\u0022left: @(ChargeScreen.x * 100f)%; top: @(ChargeScreen.y * 100f)%;\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022ring\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022label\u0022\u003E@((miner.DrillQuality * 100f).ToString( \u00220\u0022 ))% buried\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t}\n\t\t\telse if ( miner != null \u0026\u0026 miner.ChargeLive )\n\t\t\t{\n\t\t\t\t\u003Cdiv class=\u0022charge-offscreen\u0022\u003ECHARGE IS BEHIND YOU\u003C/div\u003E\n\t\t\t}\n\n\t\t\t@if ( miner != null )\n\t\t\t{\n\t\t\t\t\u003Cdiv class=\u0022charge @ChargeClass\u0022\u003E\n\n\t\t\t\t\t@* Track A: the charge in the rock, waiting for a left click. *@\n\t\t\t\t\t@if ( miner.ChargeLive )\n\t\t\t\t\t{\n\t\t\t\t\t\t\u003Cdiv class=\u0022live\u0022\u003E\n\t\t\t\t\t\t\tCHARGE IN THE ROCK \u0026mdash; \u003Cb\u003ELMB\u003C/b\u003E to detonate\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022depth\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022fill\u0022 style=\u0022width: @((miner.DrillQuality * 100f).ToString( \u00220\u0022 ))%;\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t}\n\n\t\t\t\t\t@* Track B: the reload, entirely independent of the above. *@\n\t\t\t\t\t@if ( miner.ChargeReady )\n\t\t\t\t\t{\n\t\t\t\t\t\t\u003Cdiv class=\u0022ready-line\u0022\u003E\u003Cb\u003ERMB\u003C/b\u003E to fire a charge\u003C/div\u003E\n\t\t\t\t\t}\n\t\t\t\t\telse if ( miner.ReloadAttemptOpen )\n\t\t\t\t\t{\n\t\t\t\t\t\t@* The 2 second minigame. Target position is rerolled every shot. *@\n\t\t\t\t\t\t\u003Cdiv class=\u0022reload attempt\u0022\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022bar\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022window\u0022\n\t\t\t\t\t\t\t\t\t style=\u0022left: @((miner.ReloadWindowStart * 100f).ToString( \u00220.0\u0022 ))%;\n\t\t\t\t\t\t\t\t\t\t\twidth: @((miner.ReloadWindowWidth * 100f).ToString( \u00220.0\u0022 ))%;\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022playhead\u0022\n\t\t\t\t\t\t\t\t\t style=\u0022left: @((miner.ReloadAttemptProgress * 100f).ToString( \u00220.0\u0022 ))%;\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003E\n\t\t\t\t\t\t\t\t@if ( miner.InActiveReloadWindow )\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\u003Cb\u003ERMB NOW\u003C/b\u003E\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t@(\u0022hit the mark to reload instantly\u0022)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\u003C/span\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\t\u003Cdiv class=\u0022reload\u0022\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022bar\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022fill\u0022 style=\u0022width: @((miner.ReloadProgress * 100f).ToString( \u00220.0\u0022 ))%;\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003E@(\u0022reloading \u0022 \u002B miner.ChargeCooldownLeft.ToString( \u00220.0\u0022 ) \u002B \u0022s\u0022)\u003C/span\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t}\n\t\t\t\t\u003C/div\u003E\n\t\t\t}\n\n\t\t\t@* Prompts follow the DEVICE.\n\t\t\t   \u0060Input.GetGlyph( action, ... )\u0060 returns the icon for whatever the action is bound\n\t\t\t   to on the connected controller, so an Xbox player sees A where a PlayStation\n\t\t\t   player sees Cross, from one binding. With no pad it returns an UNBOUND glyph, so\n\t\t\t   the keyboard branch stays as text rather than showing a row of question marks. *@\n\t\t\t@if ( UsingController )\n\t\t\t{\n\t\t\t\t\u003Cdiv class=\u0022hint pad\u0022\u003E\n\t\t\t\t\t\u003CImage Texture=@Glyph( \u0022Attack1\u0022 ) /\u003E \u003Cspan\u003Emine\u003C/span\u003E\n\t\t\t\t\t\u003CImage Texture=@Glyph( \u0022Attack2\u0022 ) /\u003E \u003Cspan\u003Echarge\u003C/span\u003E\n\t\t\t\t\t\u003CImage Texture=@Glyph( \u0022Jump\u0022 ) /\u003E \u003Cspan\u003Ejump / hop\u003C/span\u003E\n\t\t\t\t\t\u003CImage Texture=@Glyph( \u0022Run\u0022 ) /\u003E \u003Cspan\u003Esprint\u003C/span\u003E\n\t\t\t\t\t\u003CImage Texture=@Glyph( \u0022Score\u0022 ) /\u003E \u003Cspan\u003Eupgrades\u003C/span\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\t\u003Cdiv class=\u0022hint\u0022\u003E\n\t\t\t\t\t\u003Cb\u003ELMB\u003C/b\u003E mine \u0026nbsp; \u003Cb\u003ERMB\u003C/b\u003E charge \u0026nbsp; \u003Cb\u003EWASD\u003C/b\u003E move \u0026nbsp;\n\t\t\t\t\t\u003Cb\u003ESPACE\u003C/b\u003E jump / double jump \u0026nbsp; \u003Cb\u003ESHIFT\u003C/b\u003E sprint \u0026nbsp; \u003Cb\u003ETAB\u003C/b\u003E upgrades\n\t\t\t\t\u003C/div\u003E\n\t\t\t}\n\t\t}\n\n\t\t@if ( PanelOpen )\n\t\t{\n\t\t\t@* Full-screen catcher BEHIND the panel. Clicking the play field closes the menu and\n\t\t\t   hands the mouse back to shooting, alongside TAB, ESC and WASD.\n\n\t\t\t   It works because it sits earlier in the tree with a lower z-index, so the panel is\n\t\t\t   on top and still receives its own clicks. Putting the handler on a backdrop drawn\n\t\t\t   OVER the panel would eat every purchase. *@\n\t\t\t\u003Cdiv class=\u0022panel-backdrop\u0022 onclick=@ClosePanel\u003E\u003C/div\u003E\n\n\t\t\t\u003Cdiv class=\u0022panel\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022panel-head\u0022\u003E\n\t\t\t\t\t\u003Cspan class=\u0022title @(Tab == 0 ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@( () =\u003E SetTab( 0 ) )\u003EUPGRADES\u003C/span\u003E\n\t\t\t\t\t@if ( CoresUnlocked )\n\t\t\t\t\t{\n\t\t\t\t\t\t\u003Cspan class=\u0022title @(Tab == 1 ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@( () =\u003E SetTab( 1 ) )\u003ECORES\u003C/span\u003E\n\t\t\t\t\t}\n\t\t\t\t\t\u003Cspan class=\u0022title @(Tab == 2 ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@( () =\u003E SetTab( 2 ) )\u003EMARKS\u003C/span\u003E\n\t\t\t\t\t\u003Cspan class=\u0022title @(Tab == 3 ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@( () =\u003E SetTab( 3 ) )\u003ERANKS\u003C/span\u003E\n\t\t\t\t\t\u003Cspan class=\u0022dust-inline\u0022\u003E@Num.Short( Progress.Data.Dust ) dust\u003C/span\u003E\n\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t@* Pad controls are only discoverable if they are shown. A mouse player already\n\t\t\t\t   knows to click, so this appears for controllers only. *@\n\t\t\t\t@if ( UsingController )\n\t\t\t\t{\n\t\t\t\t\t\u003Cdiv class=\u0022pad-hints\u0022\u003E\n\t\t\t\t\t\t\u003CImage Texture=@Glyph( \u0022TabPrev\u0022 ) /\u003E\u003CImage Texture=@Glyph( \u0022TabNext\u0022 ) /\u003E\n\t\t\t\t\t\t\u003Cspan\u003Etabs\u003C/span\u003E\n\t\t\t\t\t\t\u003CImage Texture=@Glyph( \u0022UiConfirm\u0022 ) /\u003E\u003Cspan\u003Ebuy\u003C/span\u003E\n\t\t\t\t\t\t\u003CImage Texture=@Glyph( \u0022UiAlt\u0022 ) /\u003E\u003Cspan\u003Ebuy max\u003C/span\u003E\n\t\t\t\t\t\t\u003CImage Texture=@Glyph( \u0022UiBack\u0022 ) /\u003E\u003Cspan\u003Eclose\u003C/span\u003E\n\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t}\n\n\t\t\t\t@if ( Tab == 1 )\n\t\t\t\t{\n\t\t\t\t\t\u003Cdiv class=\u0022upgrades\u0022\u003E\n\t\t\t\t\t\t\u003Cdiv class=\u0022cores-head\u0022\u003E\n\t\t\t\t\t\t\t\u003Cb\u003E@Progress.AvailableCores\u003C/b\u003E cores unspent\n\t\t\t\t\t\t\t\u0026nbsp;\u0026middot;\u0026nbsp; @Progress.Data.Cores earned\n\t\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\t@* None of this was explained anywhere, so it read as three unrelated\n\t\t\t\t\t\t   currencies. Each block now says what the thing IS before it offers\n\t\t\t\t\t\t   you anything to spend. *@\n\t\t\t\t\t\t\u003Cdiv class=\u0022primer\u0022\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ptitle\u0022\u003EWHAT ARE CORES?\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ptext\u0022\u003E\n\t\t\t\t\t\t\t\t@($\u0022Cores are permanent. You earn 1 for every {Tuning.StagesPerCore} stages you reached when you COLLAPSE, which resets your dust and upgrade levels back to zero and puts you at stage 1 again. Clearing all {Tuning.PrestigeStageRequirement} stages is worth {Tuning.PrestigeStageRequirement / Tuning.StagesPerCore} cores.\u0022)\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ptext\u0022\u003E\n\t\t\t\t\t\t\t\t@($\u0022You spend them below. Nothing here is lost on a later Collapse, so the point of resetting is that you come back permanently stronger and climb the same ladder faster. You have collapsed {Progress.Data.Collapses} times.\u0022)\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ptext\u0022\u003E\n\t\t\t\t\t\t\t\t@(Progress.CanCollapse\n\t\t\t\t\t\t\t\t\t? \u0022You can Collapse right now.\u0022\n\t\t\t\t\t\t\t\t\t: $\u0022Collapse unlocks at stage {Tuning.PrestigeStageRequirement}. You have reached {Progress.Data.HighestStage}, so {Progress.StagesToCollapse} to go.\u0022)\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\t@* Slag accrues on a real clock, so it is worth showing even at zero. *@\n\t\t\t\t\t\t\u003Cdiv class=\u0022slag\u0022\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022row\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cspan class=\u0022name\u0022\u003ESLAG \u0026nbsp; \u003Cb\u003E@Progress.Data.Slag\u003C/b\u003E / @Tuning.SlagMax\u003C/span\u003E\n\t\t\t\t\t\t\t\t\u003Cspan class=\u0022level\u0022\u003E\n\t\t\t\t\t\t\t\t\t@(Progress.Data.Slag \u003E= Tuning.SlagMax\n\t\t\t\t\t\t\t\t\t\t? \u0022full\u0022\n\t\t\t\t\t\t\t\t\t\t: \u0022next in \u0022 \u002B Num.Duration( Progress.MinutesToNextSlag * 60.0 ))\n\t\t\t\t\t\t\t\t\u003C/span\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022desc\u0022\u003E\n\t\t\t\t\t\t\t\t@($\u0022One Slag every {Tuning.SlagMinutesEach / 60.0:0.#} hours of real time, capped at {Tuning.SlagMax}. It accrues while the game is shut, so there is a reason to open it on a day you do not intend to grind. It is NOT earned by playing.\u0022)\n\t\t\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022slag-actions\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022slag-btn @(Progress.Data.Slag \u003E= Tuning.SlagCostFracture ? \u0022on\u0022 : \u0022\u0022)\u0022\n\t\t\t\t\t\t\t\t\t onclick=@Fracture\u003E\n\t\t\t\t\t\t\t\t\tFRACTURE \u003Cb\u003E@Tuning.SlagCostFracture\u003C/b\u003E\n\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003E\n\t\t\t\t\t\t\t\t\t\t@($\u0022instantly destroys {Tuning.FractureFraction * 100f:0}% of the cubes left in the current shape ({Num.Short( Remaining * Tuning.FractureFraction )} right now) and pays you the dust for them\u0022)\n\t\t\t\t\t\t\t\t\t\u003C/span\u003E\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022slag-btn @(Progress.Data.Slag \u003E= Tuning.SlagCostReroll \u0026\u0026 !InMonolith ? \u0022on\u0022 : \u0022\u0022)\u0022\n\t\t\t\t\t\t\t\t\t onclick=@Reroll\u003E\n\t\t\t\t\t\t\t\t\tREROLL \u003Cb\u003E@Tuning.SlagCostReroll\u003C/b\u003E\n\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003E\n\t\t\t\t\t\t\t\t\t\trebuilds this stage as a different shape at full size. For\n\t\t\t\t\t\t\t\t\t\twhen you have drawn a shape you find awkward to clear\n\t\t\t\t\t\t\t\t\t\u003C/span\u003E\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\t@* Hollow runs: restriction plus Collapse, paying Cores only. *@\n\t\t\t\t\t\t\u003Cdiv class=\u0022hollow\u0022\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022row\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cspan class=\u0022name\u0022\u003EHOLLOW RUNS\u003C/span\u003E\n\t\t\t\t\t\t\t\t\u003Cspan class=\u0022level\u0022\u003E\n\t\t\t\t\t\t\t\t\t@(Progress.InHollowRun\n\t\t\t\t\t\t\t\t\t\t? \u0022active: \u0022 \u002B HollowRuns.Get( Progress.Hollow )?.Name\n\t\t\t\t\t\t\t\t\t\t: \u0022\u002B\u0022 \u002B Tuning.HollowRunCoreReward \u002B \u0022 cores each\u0022)\n\t\t\t\t\t\t\t\t\u003C/span\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022desc\u0022\u003ECollapse under a restriction. Pays cores and nothing else.\u003C/div\u003E\n\n\t\t\t\t\t\t\t@foreach ( var run in HollowRuns.All )\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tvar cleared = Progress.HasClearedHollow( run.Kind );\n\t\t\t\t\t\t\t\tvar active = Progress.Hollow == run.Kind;\n\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022hollow-btn @(cleared ? \u0022cleared\u0022 : \u0022\u0022) @(active ? \u0022active\u0022 : \u0022\u0022) @(Progress.CanCollapse \u0026\u0026 !Progress.InHollowRun ? \u0022on\u0022 : \u0022\u0022)\u0022\n\t\t\t\t\t\t\t\t\t onclick=@( () =\u003E StartHollow( run.Kind ) )\u003E\n\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022hname\u0022\u003E@run.Name @(cleared ? \u0022 - cleared\u0022 : \u0022\u0022)\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003E@run.Rule\u003C/span\u003E\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\t@for ( int i = 0; i \u003C CoreTree.All.Length; i\u002B\u002B )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar node = CoreTree.All[i];\n\n\t\t\t\t\t\t\tvar lvl = Progress.CoreLevel( node.Kind );\n\t\t\t\t\t\t\tvar cost = Progress.CoreCost( node.Kind );\n\t\t\t\t\t\t\tvar affordable = Progress.CanBuyCore( node.Kind );\n\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022upgrade core @(affordable ? \u0022affordable\u0022 : \u0022\u0022) @(IsSelected( i ) ? \u0022selected\u0022 : \u0022\u0022)\u0022\n\t\t\t\t\t\t\t\t onclick=@( () =\u003E BuyCore( node.Kind ) )\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022body\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022row\u0022\u003E\n\t\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022name\u0022\u003E@node.Name\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022level\u0022\u003ELv @lvl\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022desc\u0022\u003E@node.Description\u003C/div\u003E\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022gain\u0022\u003E@GainText( node, lvl )\u003C/div\u003E\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022row\u0022\u003E\n\t\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022current\u0022\u003E@(lvl \u003E 0 ? \u0022now: \u0022 \u002B FormatText( node, lvl ) : \u0022not taken\u0022)\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022cost\u0022\u003E@cost @(cost == 1 ? \u0022core\u0022 : \u0022cores\u0022)\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t}\n\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t}\n\t\t\t\telse if ( Tab == 2 )\n\t\t\t\t{\n\t\t\t\t\t\u003Cdiv class=\u0022upgrades\u0022\u003E\n\t\t\t\t\t\t\u003Cdiv class=\u0022cores-head\u0022\u003E\n\t\t\t\t\t\t\t\u003Cb\u003E@Progress.MarksEarned\u003C/b\u003E / @Marks.All.Length marks\n\t\t\t\t\t\t\t\u0026nbsp;\u0026middot;\u0026nbsp; @($\u0022x{Progress.MarkMultiplier:0.00} to everything\u0022)\n\t\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\t\u003Cdiv class=\u0022primer\u0022\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ptitle\u0022\u003EWHAT ARE MARKS?\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ptext\u0022\u003E\n\t\t\t\t\t\t\t\t@($\u0022One-off achievements. Each one you earn adds a permanent \u002B{Tuning.MarkPowerEach * 100f:0}% to ALL dust you ever collect, and they are never lost, not even to a Collapse. All {Marks.All.Length} of them is x{1f \u002B Marks.All.Length * Tuning.MarkPowerEach:0.00}.\u0022)\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ptext\u0022\u003E\n\t\t\t\t\t\t\t\tYou do not buy them and you cannot choose them. They are a\n\t\t\t\t\t\t\t\tbackground hum of progress that rewards playing in different\n\t\t\t\t\t\t\t\tways rather than doing the same thing faster.\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\t@foreach ( var mark in Marks.All )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar got = Progress.HasMark( mark.Id );\n\t\t\t\t\t\t\tvar pct = got ? 1f : Math.Clamp( mark.Progress?.Invoke( Progress ) ?? 0f, 0f, 1f );\n\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022upgrade mark @(got ? \u0022earned\u0022 : \u0022\u0022)\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022body\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022row\u0022\u003E\n\t\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022name\u0022\u003E@mark.Name\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022level\u0022\u003E@(got ? \u0022EARNED\u0022 : (pct * 100f).ToString( \u00220\u0022 ) \u002B \u0022%\u0022)\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022desc\u0022\u003E@mark.Hint\u003C/div\u003E\n\t\t\t\t\t\t\t\t\t@if ( !got )\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022markbar\u0022\u003E\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022fill\u0022 style=\u0022width: @((pct * 100f).ToString( \u00220.0\u0022 ))%;\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t}\n\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t}\n\t\t\t\telse if ( Tab == 3 )\n\t\t\t\t{\n\t\t\t\t\t\u003Cdiv class=\u0022boards\u0022\u003E\n\t\t\t\t\t\t@foreach ( var board in Boards )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022board\u0022\u003E\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022board-title\u0022\u003E@board.Title\u003C/div\u003E\n\n\t\t\t\t\t\t\t\t@if ( board.Loading )\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022board-note\u0022\u003Eloading...\u003C/div\u003E\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\telse if ( board.Error != null )\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022board-note error\u0022\u003E@board.Error\u003C/div\u003E\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\telse if ( board.Rows.Count == 0 )\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022board-note\u0022\u003E\n\t\t\t\t\t\t\t\t\t\tNo entries yet. Global boards only populate once the\n\t\t\t\t\t\t\t\t\t\tgame is published, so this stays empty while it runs\n\t\t\t\t\t\t\t\t\t\tlocally.\n\t\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t@foreach ( var row in board.Rows )\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022rank-row @(row.IsMe ? \u0022me\u0022 : \u0022\u0022)\u0022\u003E\n\t\t\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022rank\u0022\u003E#@row.Rank\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022who\u0022\u003E@row.Name\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022score\u0022\u003E@row.Value\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\u003Cdiv class=\u0022refresh\u0022 onclick=@RefreshBoards\u003EREFRESH\u003C/div\u003E\n\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\n\t\t\t\t\u003Cdiv class=\u0022upgrades\u0022\u003E\n\t\t\t\t\t@for ( int i = 0; i \u003C Upgrades.All.Length; i\u002B\u002B )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar def = Upgrades.All[i];\n\n\t\t\t\t\t\tvar level = Progress.LevelOf( def.Kind );\n\t\t\t\t\t\tvar cost = Progress.CostOf( def.Kind );\n\t\t\t\t\t\tvar affordable = Progress.CanAfford( def.Kind );\n\n\t\t\t\t\t\tvar bulk = Progress.MaxAffordable( def.Kind );\n\n\t\t\t\t\t\t\u003Cdiv class=\u0022upgrade @(affordable ? \u0022affordable\u0022 : \u0022\u0022) @(IsSelected( i ) ? \u0022selected\u0022 : \u0022\u0022)\u0022\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022body\u0022 onclick=@( () =\u003E Buy( def.Kind ) )\u003E\n\t\t\t\t\t\t\t\t@* Header: what it is, and how deep you already are. *@\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022head\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022glyph @(def.Kind.ToString().ToLower())\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022name\u0022\u003E@def.Name\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022level\u0022\u003E@level\u003C/span\u003E\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\t\t\t@* The one line that answers \u0022what do I get\u0022. Given the most weight in the card,\n\t\t\t\t\t\t\t\t   because it is the only part anyone actually reads twice. *@\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022gain\u0022\u003E@GainText( def, level )\u003C/div\u003E\n\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022desc\u0022\u003E@def.Description\u003C/div\u003E\n\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022foot\u0022\u003E\n\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022current\u0022\u003E@FormatText( def, level )\u003C/span\u003E\n\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022cost\u0022\u003E@Num.Short( cost )\u003C/span\u003E\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\t\t\t@if ( bulk \u003E 1 )\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022buymax\u0022 onclick=@( () =\u003E BuyMax( def.Kind ) )\u003E\n\t\t\t\t\t\t\t\t\tBUY MAX \u0026nbsp;\u003Cb\u003E\u002B@bulk\u003C/b\u003E\n\t\t\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t}\n\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\u003Cdiv class=\u0022travel @(InMonolith ? \u0022back\u0022 : \u0022\u0022)\u0022 onclick=@ToggleMonolith\u003E\n\t\t\t\t\t@if ( InMonolith )\n\t\t\t\t\t{\n\t\t\t\t\t\t\u003Cspan class=\u0022title\u0022\u003ERETURN TO THE LADDER\u003C/span\u003E\n\t\t\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003EStage @(Tier \u002B 1) \u0026middot; @Stages.NameFor( Tier )\u003C/span\u003E\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\t\u003Cspan class=\u0022title\u0022\u003EGO TO THE MONOLITH\u003C/span\u003E\n\t\t\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003EShared. Open to anyone. It will not care that you are here yet.\u003C/span\u003E\n\t\t\t\t\t}\n\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t\u003Cdiv class=\u0022collapse @(Progress.CanCollapse ? \u0022ready\u0022 : \u0022\u0022)\u0022 onclick=@Collapse\u003E\n\t\t\t\t\t\u003Cspan class=\u0022title\u0022\u003ECOLLAPSE\u003C/span\u003E\n\t\t\t\t\t@if ( Progress.CanCollapse )\n\t\t\t\t\t{\n\t\t\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003E\n\t\t\t\t\t\t\tBack to stage 1 for \u002B@Progress.PendingCores cores,\n\t\t\t\t\t\t\tpermanently x@((1f \u002B (Progress.Data.Cores \u002B Progress.PendingCores) * Tuning.CorePowerPerCore).ToString( \u00220.00\u0022 ))\n\t\t\t\t\t\t\u003C/span\u003E\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\t\u003Cspan class=\u0022sub\u0022\u003E\n\t\t\t\t\t\t\t@Progress.StagesToCollapse more stages\n\t\t\t\t\t\t\t(reach @Tuning.PrestigeStageRequirement)\n\t\t\t\t\t\t\u003C/span\u003E\n\t\t\t\t\t}\n\t\t\t\t\u003C/div\u003E\n\n\t\t\t\t}\n\t\t\t\u003C/div\u003E\n\t\t}\n\t}\n\n\u003C/root\u003E\n\n@code\n{\n\t/// \u003Csummary\u003ETrue while the upgrade panel owns the mouse, so the Miner holds fire.\u003C/summary\u003E\n\tpublic static bool CursorVisible { get; private set; }\n\n\tprivate bool PanelOpen;\n\n\t/// \u003Csummary\u003E0 = upgrades, 1 = core tree, 2 = leaderboards.\u003C/summary\u003E\n\tprivate int Tab;\n\n\t// Deliberately ONE board. Ranking solo progress too would tell players the solo ladder is\n\t// the destination; the only thing worth a global rank is what you take off the Monolith.\n\tprivate readonly LeaderboardData[] Boards =\n\t{\n\t\tnew( \u0022CUBES REMOVED FROM THE MONOLITH\u0022, MonolithStats.StatCubes ),\n\t\tnew( \u0022MOST PRESTIGES\u0022, MonolithStats.StatBestCollapses ),\n\t\tnew( \u0022FASTEST FULL LADDER\u0022, MonolithStats.StatFastestLadder ) { LowestWins = true },\n\t\tnew( \u0022FASTEST MONOLITH FELLING\u0022, MonolithStats.StatFastestMonolith ) { LowestWins = true },\n\t};\n\n\t// ---------------------------------------------------------------- welcome and start\n\n\t/// \u003Csummary\u003ENumber of welcome pages.\u003C/summary\u003E\n\tprivate const int TutorialPages = 5;\n\n\tprivate int TutorialPage;\n\n\t/// \u003Csummary\u003ESet once the start screen has been dismissed, for this session only.\u003C/summary\u003E\n\tprivate bool started;\n\n\t/// \u003Csummary\u003EOpened from the start screen, so the pages can be re-read on purpose.\u003C/summary\u003E\n\tprivate bool tutorialReopened;\n\n\t/// \u003Csummary\u003E\n\t/// The welcome pages show automatically on a save that has not seen them, and on demand from\n\t/// the start screen. Waits for Progress to exist, since it is the thing that remembers.\n\t/// \u003C/summary\u003E\n\tprivate bool ShowTutorial\n\t\t=\u003E Progress != null \u0026\u0026 (tutorialReopened || !Progress.Data.SeenTutorial);\n\n\t/// \u003Csummary\u003EThe start screen sits between the welcome pages and play, once per session.\u003C/summary\u003E\n\tprivate bool ShowStart =\u003E Progress != null \u0026\u0026 !started;\n\n\t/// \u003Csummary\u003ESet while the pause menu is up. Session only, never saved.\u003C/summary\u003E\n\tprivate bool paused;\n\n\t/// \u003Csummary\u003E\n\t/// The pause menu, suppressed behind the welcome and start sheets so escape cannot stack two\n\t/// full-screen sheets on top of each other.\n\t/// \u003C/summary\u003E\n\tprivate bool ShowPause =\u003E paused \u0026\u0026 !ShowTutorial \u0026\u0026 !ShowStart;\n\n\t/// \u003Csummary\u003E\n\t/// True while any full-screen sheet is up. Firing, movement and the upgrade panel are all\n\t/// suppressed by the same flag that suppresses them for the upgrade panel, so nothing new\n\t/// had to learn about these screens.\n\t/// \u003C/summary\u003E\n\tprivate bool BlockingScreen =\u003E ShowTutorial || ShowStart || ShowPause;\n\n\t/// \u003Csummary\u003E\n\t/// Says whether the world actually stopped. Worth stating rather than assuming, because the\n\t/// answer changes with the session: solo really does freeze, and in company it cannot.\n\t/// \u003C/summary\u003E\n\tprivate string PauseLead\n\t\t=\u003E GameTime.Paused\n\t\t\t? \u0022The monolith waits.\u0022\n\t\t\t: \u0022Others are still playing, so the world keeps going.\u0022;\n\n\tprivate void Resume()\n\t{\n\t\tAudio.Close();\n\t\tpaused = false;\n\t\tPauseSelected = 0;\n\t}\n\n\t/// \u003Csummary\u003EHighlighted row of the pause menu: resume, how to play, quit.\u003C/summary\u003E\n\tprivate int PauseSelected;\n\n\tprivate const int PauseItems = 5;\n\n\t/// \u003Csummary\u003E\n\t/// The mute switches. Both play their own confirmation, which is the only feedback that makes\n\t/// sense here: turning effects ON should make a sound, and turning them OFF should be the\n\t/// last sound you hear. The music toggle is silent because the music itself is the answer.\n\t/// \u003C/summary\u003E\n\tprivate void ToggleMusic()\n\t{\n\t\tAudioSettings.ToggleMusic();\n\t\tAudio.PlayUi( Audio.UiPress, 0.5f, 1f );\n\t}\n\n\tprivate void ToggleEffects()\n\t{\n\t\t// Ordered so the click lands while effects are still audible when switching OFF, and\n\t\t// after they are back when switching ON. Either way you hear exactly one.\n\t\tif ( AudioSettings.EffectsMuted )\n\t\t{\n\t\t\tAudioSettings.ToggleEffects();\n\t\t\tAudio.PlayUi( Audio.UiPress, 0.6f, 1.1f );\n\t\t\treturn;\n\t\t}\n\n\t\tAudio.PlayUi( Audio.UiPress, 0.6f, 0.9f );\n\t\tAudioSettings.ToggleEffects();\n\t}\n\n\tprivate bool IsPauseSelected( int index )\n\t\t=\u003E UsingController \u0026\u0026 ShowPause \u0026\u0026 index == PauseSelected;\n\n\t/// \u003Csummary\u003E\n\t/// Stick and dpad through the pause menu, A to pick, B to resume.\n\t///\n\t/// Not optional the way panel navigation was. The pause menu is the only exit from the game,\n\t/// and on a Steam Deck there is no mouse to reach QUIT with, so a mouse-only pause menu would\n\t/// leave a handheld player with no way out at all.\n\t/// \u003C/summary\u003E\n\tprivate void UpdatePauseSelection()\n\t{\n\t\tfloat move = Input.AnalogMove.x;\n\n\t\tif ( MathF.Abs( move ) \u003C 0.5f )\n\t\t\ttimeSinceNav = NavRepeat;\n\t\telse if ( timeSinceNav \u003E= NavRepeat )\n\t\t{\n\t\t\ttimeSinceNav = 0f;\n\t\t\tPauseSelected = (PauseSelected \u002B (move \u003E 0f ? -1 : 1) \u002B PauseItems) % PauseItems;\n\t\t\tAudio.Navigate();\n\t\t}\n\n\t\t// B always resumes, wherever the highlight happens to be sitting.\n\t\tif ( Input.Pressed( \u0022UiBack\u0022 ) )\n\t\t{\n\t\t\tResume();\n\t\t\treturn;\n\t\t}\n\n\t\tif ( !Input.Pressed( \u0022UiConfirm\u0022 ) )\n\t\t\treturn;\n\n\t\tswitch ( PauseSelected )\n\t\t{\n\t\t\tcase 0: Resume(); break;\n\t\t\tcase 1: ToggleMusic(); break;\n\t\t\tcase 2: ToggleEffects(); break;\n\t\t\tcase 3: ReopenTutorial(); break;\n\t\t\tcase 4: QuitGame(); break;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Saves before leaving. The autosave runs on a fifteen second timer, so quitting without\n\t/// this can silently discard up to fifteen seconds of mining, which is exactly the kind of\n\t/// loss that makes a player distrust a game that never technically crashed.\n\t/// \u003C/summary\u003E\n\tprivate void QuitGame()\n\t{\n\t\tAudio.PlayUi( Audio.UiPress, 0.8f, 0.8f );\n\n\t\tProgress?.Save();\n\n\t\ttry { Game.Close(); }\n\t\tcatch ( Exception e ) { Log.Warning( $\u0022Could not close the game: {e.Message}\u0022 ); }\n\t}\n\n\tprivate void TutorialNext()\n\t{\n\t\tAudio.Forward();\n\n\t\tif ( TutorialPage \u003C TutorialPages - 1 )\n\t\t{\n\t\t\tTutorialPage\u002B\u002B;\n\t\t\treturn;\n\t\t}\n\n\t\t// Recorded on FINISHING rather than on opening, so quitting midway shows them again.\n\t\tProgress?.MarkTutorialSeen();\n\n\t\tTutorialPage = 0;\n\t\ttutorialReopened = false;\n\t}\n\n\tprivate void TutorialBack()\n\t{\n\t\tAudio.Back();\n\t\tTutorialPage = Math.Max( 0, TutorialPage - 1 );\n\t}\n\n\tprivate void ReopenTutorial()\n\t{\n\t\tAudio.Open();\n\t\tTutorialPage = 0;\n\t\ttutorialReopened = true;\n\t}\n\n\tprivate void StartLadder()\n\t{\n\t\tAudio.Forward();\n\n\t\tif ( InMonolith )\n\t\t\tManager?.SetMonolithMode( false );\n\n\t\tstarted = true;\n\t\tProgress?.StartRunTimer();\n\t}\n\n\tprivate void StartMonolith()\n\t{\n\t\t// A brand new save cannot meaningfully touch 16.7M cubes, so the button says so and\n\t\t// declines rather than dropping the player somewhere hopeless with no explanation.\n\t\tif ( Progress?.IsFreshSave ?? true )\n\t\t{\n\t\t\tAudio.Refused();\n\t\t\treturn;\n\t\t}\n\n\t\tAudio.PlayUi( Audio.UiPress, 0.8f, 1.1f );\n\n\t\tManager?.SetMonolithMode( true );\n\t\tstarted = true;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// One escape key, several meanings, resolved in a fixed order so it is always predictable.\n\t///\n\t/// The engine treats escape and the pad START button as the same press, so this gives the\n\t/// controller a pause button without any binding of our own.\n\t///\n\t/// Escape never dismisses the first-run welcome. It is four pages long, it is the only\n\t/// explanation of the game, and the button that skips it should not also be the button\n\t/// everyone mashes. Reopened on purpose from a menu, it closes on escape like anything else.\n\t/// \u003C/summary\u003E\n\tprivate void UpdateEscape()\n\t{\n\t\tif ( !Input.EscapePressed )\n\t\t\treturn;\n\n\t\tif ( ShowTutorial )\n\t\t{\n\t\t\tif ( tutorialReopened )\n\t\t\t{\n\t\t\t\tTutorialPage = 0;\n\t\t\t\ttutorialReopened = false;\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\t// Nothing to back out to, and quitting has its own button once you are in.\n\t\tif ( ShowStart )\n\t\t\treturn;\n\n\t\tif ( ShowPause )\n\t\t{\n\t\t\tResume();\n\t\t\treturn;\n\t\t}\n\n\t\t// The panel is a layer over the game, so escape peels that off before it reaches the game.\n\t\tif ( PanelOpen )\n\t\t{\n\t\t\tPanelOpen = false;\n\t\t\treturn;\n\t\t}\n\n\t\t// The summary is a decision, not a screen to escape from.\n\t\tif ( Manager?.LadderComplete ?? false )\n\t\t\treturn;\n\n\t\tAudio.Open();\n\t\tpaused = true;\n\t}\n\n\t/// \u003Csummary\u003EKeyboard and pad paths for the sheets, so neither needs a mouse.\u003C/summary\u003E\n\tprivate void UpdateScreenInput()\n\t{\n\t\tif ( ShowPause )\n\t\t{\n\t\t\tUpdatePauseSelection();\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ShowTutorial )\n\t\t{\n\t\t\tif ( Input.Pressed( \u0022UiConfirm\u0022 ) || Input.Pressed( \u0022Jump\u0022 ) )\n\t\t\t\tTutorialNext();\n\t\t\telse if ( Input.Pressed( \u0022UiBack\u0022 ) )\n\t\t\t\tTutorialBack();\n\n\t\t\treturn;\n\t\t}\n\n\t\tif ( !ShowStart )\n\t\t\treturn;\n\n\t\tif ( Input.Pressed( \u0022UiConfirm\u0022 ) || Input.Pressed( \u0022Jump\u0022 ) )\n\t\t\tStartLadder();\n\t\telse if ( Input.Pressed( \u0022UiAlt\u0022 ) )\n\t\t\tStartMonolith();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// True while a controller is connected, so prompts and navigation follow the device.\n\t///\n\t/// Deliberately based on a pad being PRESENT rather than on the last input used. A Deck\n\t/// player always has one; a desktop player who owns one but is on mouse and keyboard would\n\t/// briefly see the wrong prompts, which is the lesser of the two errors and is easy to\n\t/// revisit if it grates.\n\t/// \u003C/summary\u003E\n\tprivate static bool UsingController\n\t{\n\t\tget\n\t\t{\n\t\t\ttry { return Input.ControllerCount \u003E 0; }\n\t\t\tcatch ( Exception ) { return false; }\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Glyph for an action on the connected controller. One binding, correct icon per device:\n\t/// Steam Input maps our logical \u0060A\u0060 to Cross on a DualSense and A on an Xbox pad, so nothing\n\t/// in this file ever names a physical button.\n\t/// \u003C/summary\u003E\n\tprivate static Texture Glyph( string action )\n\t{\n\t\ttry { return Input.GetGlyph( action, InputGlyphSize.Small, true ); }\n\t\tcatch ( Exception ) { return null; }\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Closes the upgrade panel. Wired to the backdrop, so clicking the play field dismisses it\n\t/// and hands the mouse straight back to shooting.\n\t/// \u003C/summary\u003E\n\tprivate void ClosePanel() =\u003E PanelOpen = false;\n\n\t/// \u003Csummary\u003E\n\t/// Moves along the tabs that are actually open, so a bumper never lands on a hidden one.\n\t///\n\t/// Indices stay fixed rather than being renumbered when Cores unlocks. Renumbering would\n\t/// silently move every hardcoded \u0060Tab == n\u0060 check in this file the day the player finishes\n\t/// their first ladder, which is the worst possible moment to introduce a bug.\n\t/// \u003C/summary\u003E\n\tprivate int StepTab( int direction )\n\t{\n\t\tvar open = OpenTabs;\n\t\tint at = Array.IndexOf( open, Tab );\n\n\t\tif ( at \u003C 0 )\n\t\t\treturn open[0];\n\n\t\treturn open[(at \u002B direction \u002B open.Length) % open.Length];\n\t}\n\n\tprivate void SetTab( int tab )\n\t{\n\t\tTab = tab;\n\n\t\t// Fetch lazily the first time the boards tab is opened, not on every HUD build.\n\t\tif ( tab == 3 \u0026\u0026 Boards.Any( b =\u003E !b.HasLoaded \u0026\u0026 !b.Loading ) )\n\t\t\tRefreshBoards();\n\t}\n\n\tprivate void BuyCore( CoreNodeKind kind )\n\t{\n\t\tProgress?.TryBuyCore( kind );\n\t}\n\n\tprivate void Fracture()\n\t{\n\t\tif ( Progress == null || Manager == null ) return;\n\t\tif ( !Progress.TrySpendSlag( Tuning.SlagCostFracture ) ) return;\n\n\t\tManager.RequestFracture();\n\t}\n\n\tprivate void StartHollow( HollowKind kind )\n\t{\n\t\tif ( Progress == null || Progress.InHollowRun || !Progress.CanCollapse )\n\t\t\treturn;\n\n\t\tProgress.BeginHollowRun( kind );\n\t}\n\n\tprivate void Reroll()\n\t{\n\t\tif ( Progress == null || Manager == null || InMonolith ) return;\n\t\tif ( !Progress.TrySpendSlag( Tuning.SlagCostReroll ) ) return;\n\n\t\tManager.RerollShape();\n\t}\n\n\tprivate void RefreshBoards()\n\t{\n\t\tforeach ( var board in Boards )\n\t\t\t_ = board.Load();\n\t}\n\n\tprivate PlayerProgress Progress =\u003E PlayerProgress.Local;\n\tprivate MonolithManager Manager =\u003E MonolithManager.Instance;\n\n\t/// \u003Csummary\u003E\n\t/// The \u0022what does the next level buy me\u0022 line, made harmless.\n\t///\n\t/// A throwing or NULL Gain used to take the whole list down with it. Razor abandons the\n\t/// render at the point of the exception, so a single bad delegate on the SECOND upgrade left\n\t/// only Rate of Fire on screen and every other upgrade simply gone. Hotload is the usual\n\t/// cause: it cannot always remap lambdas held in a static array\n\t/// (\u0022Unable to find matching substitution for a lambda method\u0022), and leaves them null.\n\t///\n\t/// One missing hint line is a cosmetic problem. An empty upgrade panel is not.\n\t/// \u003C/summary\u003E\n\tprivate static string GainText( UpgradeDef def, int level )\n\t{\n\t\ttry { return def?.Gain?.Invoke( level ) ?? \u0022\u0022; }\n\t\tcatch ( Exception ) { return \u0022\u0022; }\n\t}\n\n\tprivate static string GainText( CoreNodeDef def, int level )\n\t{\n\t\ttry { return def?.Gain?.Invoke( level ) ?? \u0022\u0022; }\n\t\tcatch ( Exception ) { return \u0022\u0022; }\n\t}\n\n\t/// \u003Csummary\u003EThe same guard for the current-value line, which has the same failure mode.\u003C/summary\u003E\n\tprivate static string FormatText( UpgradeDef def, int level )\n\t{\n\t\ttry { return def?.Format?.Invoke( level ) ?? \u0022\u0022; }\n\t\tcatch ( Exception ) { return \u0022\u0022; }\n\t}\n\n\tprivate static string FormatText( CoreNodeDef def, int level )\n\t{\n\t\ttry { return def?.Format?.Invoke( level ) ?? \u0022\u0022; }\n\t\tcatch ( Exception ) { return \u0022\u0022; }\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The headline. Counts MONOLITHS on the shared rock and STAGES on the solo ladder, because\n\t/// they are different progressions and sharing a number made the Monolith read as stage 48.\n\t/// \u003C/summary\u003E\n\tprivate string TierLine\n\t{\n\t\tget\n\t\t{\n\t\t\tif ( !InMonolith )\n\t\t\t\treturn $\u0022STAGE {Tier \u002B 1}  \u00B7  {StageName}\u0022;\n\n\t\t\tint felled = Progress?.Data.MonolithCredits ?? 0;\n\t\t\treturn $\u0022MONOLITH {felled \u002B 1}\u0022;\n\t\t}\n\t}\n\n\tprivate long Remaining =\u003E Manager?.World?.SolidCount ?? 0;\n\tprivate long Total =\u003E Manager?.World?.InitialCount ?? 0;\n\tprivate int Tier =\u003E Manager?.Tier ?? 0;\n\tprivate string StageName =\u003E Manager?.StageName ?? \u0022\u0022;\n\t/// \u003Csummary\u003EHow close the player is to burning on a live floor tile, 0 to 1.\u003C/summary\u003E\n\tprivate static float BurnAmount =\u003E FloorHazard.BurnFraction;\n\n\tprivate static bool Burning =\u003E BurnAmount \u003E 0.02f;\n\n\t/// \u003Csummary\u003E\n\t/// Hides the whole HUD for a clean screenshot. Driven by \u003Csee cref=\u0022PhotoMode\u0022/\u003E.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// A CLASS rather than disabling the component. \u0060OnUpdate\u0060 decides the pause, manages the\n\t/// cursor and projects the charge marker, so switching the component off would quietly change\n\t/// how the game behaves in exactly the frames being photographed.\n\t/// \u003C/remarks\u003E\n\tpublic static bool PhotoHidden { get; set; }\n\n\tprivate bool Residue =\u003E Remaining \u003E 0 \u0026\u0026 Remaining \u003C= Tuning.ResidueRevealThreshold;\n\n\t/// \u003Csummary\u003E\n\t/// Turns the leftover cubes into a direction and a distance relative to where the camera\n\t/// is actually looking. In an empty void a compass bearing is useless; \u0022behind you\u0022 is not.\n\t/// \u003C/summary\u003E\n\tprivate string ResidueHint\n\t{\n\t\tget\n\t\t{\n\t\t\tvar centre = Manager?.ResidueCentre;\n\t\t\tvar camera = Scene?.Camera;\n\n\t\t\tif ( !centre.HasValue || !camera.IsValid() )\n\t\t\t\treturn \u0022the last of it is lit up\u0022;\n\n\t\t\tvar delta = centre.Value - camera.WorldPosition;\n\t\t\tfloat distance = delta.Length;\n\n\t\t\tif ( distance \u003C 1f )\n\t\t\t\treturn \u0022right here\u0022;\n\n\t\t\tvar dir = delta.Normal;\n\t\t\tvar rot = camera.WorldRotation;\n\n\t\t\tfloat forward = Vector3.Dot( dir, rot.Forward );\n\t\t\tfloat right = Vector3.Dot( dir, rot.Right );\n\t\t\tfloat up = Vector3.Dot( dir, rot.Up );\n\n\t\t\tstring where;\n\n\t\t\tif ( forward \u003E 0.75f ) where = \u0022dead ahead\u0022;\n\t\t\telse if ( forward \u003C -0.5f ) where = \u0022BEHIND YOU\u0022;\n\t\t\telse if ( right \u003E 0.35f ) where = \u0022to your right\u0022;\n\t\t\telse if ( right \u003C -0.35f ) where = \u0022to your left\u0022;\n\t\t\telse if ( up \u003E 0.35f ) where = \u0022above you\u0022;\n\t\t\telse if ( up \u003C -0.35f ) where = \u0022below you\u0022;\n\t\t\telse where = \u0022just off to the side\u0022;\n\n\t\t\t// Plain text, not an HTML entity: this string is escaped when rendered.\n\t\t\treturn $\u0022{where}  -  {distance:N0} units away\u0022;\n\t\t}\n\t}\n\tprivate float ClearedPercent =\u003E Total == 0 ? 0f : (float)(100.0 * (Total - Remaining) / Total);\n\n\tprivate Miner miner;\n\tprivate bool Aiming =\u003E miner?.HasAim ?? false;\n\n\t/// \u003Csummary\u003EScreen position of the live charge, 0 to 1 across the viewport.\u003C/summary\u003E\n\tprivate Vector2 ChargeScreen;\n\tprivate bool ChargeOnScreen;\n\n\t/// \u003Csummary\u003E\n\t/// Projects the live charge to screen space. Computed in OnUpdate rather than lazily in a\n\t/// property, because BuildHash reads it and must see the value for the frame it is hashing.\n\t/// \u003C/summary\u003E\n\tprivate void UpdateChargeMarker()\n\t{\n\t\tChargeOnScreen = false;\n\n\t\tif ( miner == null || !miner.ChargeLive )\n\t\t\treturn;\n\n\t\tvar camera = Scene?.Camera;\n\t\tif ( !camera.IsValid() )\n\t\t\treturn;\n\n\t\tvar world = miner.LiveChargePosition;\n\n\t\t// Reject anything behind the camera: the projection wraps round and would place the\n\t\t// marker on the opposite side of the screen, pointing you the wrong way.\n\t\tif ( Vector3.Dot( world - camera.WorldPosition, camera.WorldRotation.Forward ) \u003C= 0f )\n\t\t\treturn;\n\n\t\tChargeScreen = camera.PointToScreenNormal( world );\n\n\t\tChargeOnScreen = ChargeScreen.x \u003E= -0.02f \u0026\u0026 ChargeScreen.x \u003C= 1.02f\n\t\t\t\u0026\u0026 ChargeScreen.y \u003E= -0.02f \u0026\u0026 ChargeScreen.y \u003C= 1.02f;\n\t}\n\n\tprivate string ChargeClass\n\t{\n\t\tget\n\t\t{\n\t\t\tif ( miner == null ) return \u0022\u0022;\n\t\t\tif ( miner.ActiveReloadJustHit ) return \u0022caught\u0022;\n\t\t\tif ( miner.InActiveReloadWindow ) return \u0022window\u0022;\n\t\t\tif ( miner.ChargeLive ) return \u0022armed\u0022;\n\t\t\tif ( miner.ChargeReady ) return \u0022ready\u0022;\n\t\t\treturn \u0022\u0022;\n\t\t}\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( !miner.IsValid() )\n\t\t\tminer = Scene?.GetAllComponents\u003CMiner\u003E().FirstOrDefault();\n\n\t\tUpdateChargeMarker();\n\n\t\tUpdateEscape();\n\n\t\t// Freezing the world is decided here, in the one place that knows every sheet that can be\n\t\t// up. The tutorial used to play out over a live game: hazards hunting you and the run\n\t\t// timer counting while you read about them.\n\t\tGameTime.SetPaused( BlockingScreen );\n\n\t\t// The sheets own the input while they are up. Checked FIRST so opening the upgrade panel\n\t\t// or firing cannot happen behind them.\n\t\tif ( BlockingScreen )\n\t\t{\n\t\t\tPanelOpen = false;\n\n\t\t\tUpdateScreenInput();\n\n\t\t\tCursorVisible = true;\n\t\t\tMouse.Visibility = MouseVisibility.Visible;\n\t\t\tSetClass( \u0022panel-open\u0022, true );\n\t\t\treturn;\n\t\t}\n\n\t\tif ( Input.Pressed( \u0022Score\u0022 ) )\n\t\t\tPanelOpen = !PanelOpen;\n\n\t\tif ( PanelOpen \u0026\u0026 MovementPressed() )\n\t\t\tPanelOpen = false;\n\n\t\tUpdatePanelInput();\n\t\tUpdateSummaryInput();\n\n\t\t// The summary has buttons, so it needs the cursor exactly like the upgrade panel does.\n\t\t// Firing is suppressed for the same reason: you are being asked a question, not mining.\n\t\tbool wantsCursor = PanelOpen || (Manager?.LadderComplete ?? false);\n\n\t\tCursorVisible = wantsCursor;\n\t\tMouse.Visibility = wantsCursor ? MouseVisibility.Visible : MouseVisibility.Hidden;\n\n\t\tSetClass( \u0022panel-open\u0022, wantsCursor );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Buying was silent whether or not it worked, so a refused purchase and a successful one\n\t/// were indistinguishable without reading the dust counter. The return value was already\n\t/// there; nothing was listening to it.\n\t/// \u003C/summary\u003E\n\tprivate void Buy( UpgradeKind kind )\n\t{\n\t\tif ( Progress?.TryBuy( kind ) ?? false )\n\t\t\tAudio.Purchase();\n\t\telse\n\t\t\tAudio.Refused();\n\t}\n\n\tprivate void BuyMax( UpgradeKind kind )\n\t{\n\t\tint bought = Progress?.BuyMax( kind ) ?? 0;\n\n\t\tif ( bought \u003C= 0 )\n\t\t{\n\t\t\tAudio.Refused();\n\t\t\treturn;\n\t\t}\n\n\t\tAudio.Purchase();\n\n\t\t// A block purchase should not sound like a single one. The second, quieter note is the\n\t\t// whole difference between \u0022bought\u0022 and \u0022bought a lot\u0022.\n\t\tif ( bought \u003E 1 )\n\t\t\tAudio.PlayUi( Audio.UiReward, 0.4f, 1.35f );\n\t}\n\n\tprivate bool InMonolith =\u003E Manager?.InMonolith ?? false;\n\n\tprivate void ToggleMonolith()\n\t{\n\t\tManager?.SetMonolithMode( !InMonolith );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Any movement key closes the panel, so you never have to reach for Tab to fly.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// Jump is EXCLUDED when a controller is connected. On a pad, Jump is bound to A and A is\n\t/// also the confirm button, so leaving it in meant every attempt to buy an upgrade closed\n\t/// the panel instead. The keyboard has separate keys for the two and does not have the\n\t/// problem, which is exactly the kind of thing a keyboard-only test never surfaces.\n\t/// \u003C/remarks\u003E\n\tprivate static bool MovementPressed()\n\t{\n\t\tbool moved = Input.Down( \u0022Forward\u0022 ) || Input.Down( \u0022Backward\u0022 )\n\t\t\t|| Input.Down( \u0022Left\u0022 ) || Input.Down( \u0022Right\u0022 )\n\t\t\t|| Input.Down( \u0022Duck\u0022 );\n\n\t\tif ( !UsingController )\n\t\t\tmoved = moved || Input.Down( \u0022Jump\u0022 );\n\n\t\treturn moved;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Pad navigation for the upgrade panel. Runs only while it is open.\n\t///\n\t/// Without this a Steam Deck player has to drive a mouse cursor with the trackpad, which is\n\t/// the thing the whole controller pass exists to avoid. Bumpers cycle tabs because the\n\t/// triggers are mine and charge and the face buttons are wanted for confirm.\n\t/// \u003C/summary\u003E\n\tprivate void UpdatePanelInput()\n\t{\n\t\tif ( !PanelOpen )\n\t\t\treturn;\n\n\t\t// A progress reset can take Cores away again while the tab is open. Nothing else moves\n\t\t// the player off a tab that has stopped existing.\n\t\tif ( Tab == 1 \u0026\u0026 !CoresUnlocked )\n\t\t\tSetTab( 0 );\n\n\t\tif ( Input.Pressed( \u0022TabNext\u0022 ) )\n\t\t{\n\t\t\tSetTab( StepTab( 1 ) );\n\t\t\tSelected = 0;\n\t\t}\n\n\t\tif ( Input.Pressed( \u0022TabPrev\u0022 ) )\n\t\t{\n\t\t\tSetTab( StepTab( -1 ) );\n\t\t\tSelected = 0;\n\t\t}\n\n\t\tif ( Input.Pressed( \u0022UiBack\u0022 ) )\n\t\t{\n\t\t\tPanelOpen = false;\n\t\t\treturn;\n\t\t}\n\n\t\tUpdateSelection();\n\t}\n\n\t/// \u003Csummary\u003ETabs in the panel: Upgrades, Cores, Marks, Ranks.\u003C/summary\u003E\n\t/// \u003Csummary\u003E\n\t/// Whether the permanent Core tree is offered at all.\n\t///\n\t/// **Hidden until a run has been finished.** It was a tab on the panel from the first second\n\t/// of a brand new save, next to the upgrades you actually can buy, explaining a currency you\n\t/// cannot earn for another hundred stages. That is three screens of text about a system that\n\t/// does not apply to you yet, and it made the panel read as a specification rather than as a\n\t/// shop.\n\t///\n\t/// It appears the moment it becomes real: cores in hand, a ladder finished, or a Collapse\n\t/// already behind you. Anything you have earned stays visible forever after.\n\t/// \u003C/summary\u003E\n\tprivate bool CoresUnlocked\n\t\t=\u003E Progress != null\n\t\t\t\u0026\u0026 (Progress.Data.Collapses \u003E 0\n\t\t\t\t|| Progress.AvailableCores \u003E 0\n\t\t\t\t|| Progress.Data.Cores \u003E 0\n\t\t\t\t|| Progress.CanCollapse);\n\n\t/// \u003Csummary\u003ETabs the player can actually reach right now, in order.\u003C/summary\u003E\n\tprivate int[] OpenTabs\n\t\t=\u003E CoresUnlocked\n\t\t\t? new[] { 0, 1, 2, 3 }\n\t\t\t: new[] { 0, 2, 3 };\n\n\tprivate const int TabCount = 4;\n\n\t/// \u003Csummary\u003EIndex of the highlighted card in the current tab.\u003C/summary\u003E\n\tprivate int Selected;\n\n\tprivate TimeSince timeSinceNav;\n\n\t/// \u003Csummary\u003E\n\t/// Seconds between steps while the stick is held. Long enough that a flick moves exactly\n\t/// one row, which is what makes a list navigable rather than a slot machine.\n\t/// \u003C/summary\u003E\n\tprivate const float NavRepeat = 0.18f;\n\n\t/// \u003Csummary\u003E\n\t/// How many cards the current tab offers. Marks and Ranks are read-only, so they have no\n\t/// selection and the stick does nothing there rather than moving an invisible cursor.\n\t/// \u003C/summary\u003E\n\tprivate int SelectableCount =\u003E Tab switch\n\t{\n\t\t0 =\u003E Upgrades.All.Length,\n\t\t1 =\u003E CoreTree.All.Length,\n\t\t_ =\u003E 0,\n\t};\n\n\t/// \u003Csummary\u003E\n\t/// True if this card is the pad selection. Returns false without a controller, so the\n\t/// mouse experience is untouched and no stray highlight follows a cursor around.\n\t/// \u003C/summary\u003E\n\tprivate bool IsSelected( int index )\n\t\t=\u003E UsingController \u0026\u0026 PanelOpen \u0026\u0026 index == Selected;\n\n\t/// \u003Csummary\u003E\n\t/// Stick or dpad moves the highlight, A buys, Y buys max.\n\t///\n\t/// The stick is free for this because our movement actions are keyboard-only bindings, so\n\t/// pushing it never reaches \u0060MovementPressed\u0060 and cannot close the panel. That is a\n\t/// consequence of the binding audit rather than an accident, and it is worth knowing before\n\t/// anyone gives Forward a \u0060GamepadCode\u0060.\n\t/// \u003C/summary\u003E\n\tprivate void UpdateSelection()\n\t{\n\t\tint count = SelectableCount;\n\n\t\tif ( count \u003C= 0 )\n\t\t\treturn;\n\n\t\t// Forward on the stick is \u002Bx, and forward should move UP the list.\n\t\tfloat move = Input.AnalogMove.x;\n\n\t\tif ( MathF.Abs( move ) \u003C 0.5f )\n\t\t{\n\t\t\t// Released: arm the next step so a deliberate flick responds instantly instead of\n\t\t\t// waiting out a repeat delay it never earned.\n\t\t\ttimeSinceNav = NavRepeat;\n\t\t}\n\t\telse if ( timeSinceNav \u003E= NavRepeat )\n\t\t{\n\t\t\ttimeSinceNav = 0f;\n\t\t\tSelected = (Selected \u002B (move \u003E 0f ? -1 : 1) \u002B count) % count;\n\t\t}\n\n\t\tSelected = Math.Clamp( Selected, 0, count - 1 );\n\n\t\tif ( Input.Pressed( \u0022UiConfirm\u0022 ) )\n\t\t\tConfirmSelected();\n\n\t\tif ( Input.Pressed( \u0022UiAlt\u0022 ) )\n\t\t\tAltSelected();\n\t}\n\n\t/// \u003Csummary\u003EA on the highlighted card: buy one.\u003C/summary\u003E\n\tprivate void ConfirmSelected()\n\t{\n\t\tif ( Tab == 0 \u0026\u0026 Selected \u003C Upgrades.All.Length )\n\t\t\tBuy( Upgrades.All[Selected].Kind );\n\t\telse if ( Tab == 1 \u0026\u0026 Selected \u003C CoreTree.All.Length )\n\t\t\tBuyCore( CoreTree.All[Selected].Kind );\n\t}\n\n\t/// \u003Csummary\u003EY on the highlighted card: buy max. Cores have no bulk purchase, so it is a no-op there.\u003C/summary\u003E\n\tprivate void AltSelected()\n\t{\n\t\tif ( Tab == 0 \u0026\u0026 Selected \u003C Upgrades.All.Length )\n\t\t\tBuyMax( Upgrades.All[Selected].Kind );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Pad navigation for the ladder-complete summary, which had NO keyboard or pad path at all:\n\t/// two clickable buttons and nothing else. Confirm takes the Monolith, Back collapses, so\n\t/// the destructive one is not the default.\n\t/// \u003C/summary\u003E\n\tprivate void UpdateSummaryInput()\n\t{\n\t\tif ( !(Manager?.LadderComplete ?? false) )\n\t\t\treturn;\n\n\t\tif ( Input.Pressed( \u0022UiConfirm\u0022 ) )\n\t\t\tGoToMonolith();\n\t\telse if ( Input.Pressed( \u0022UiBack\u0022 ) )\n\t\t\tCollapseFromSummary();\n\t}\n\n\tprivate void Collapse()\n\t{\n\t\tProgress?.Collapse();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Where this run\u0027s time places on the global board. Reads the already-fetched ladder board\n\t/// rather than issuing a request, so the summary never waits on the network to appear.\n\t/// \u003C/summary\u003E\n\tprivate string LadderRankLine\n\t{\n\t\tget\n\t\t{\n\t\t\tvar board = Boards.FirstOrDefault( b =\u003E b.Title == \u0022FASTEST FULL LADDER\u0022 );\n\n\t\t\tif ( board == null || board.Rows == null || board.Rows.Count == 0 )\n\t\t\t\treturn \u0022global rank unavailable\u0022;\n\n\t\t\tfloat mine = Manager?.LadderCompleteSeconds ?? 0f;\n\n\t\t\t// Row is a STRUCT, so FirstOrDefault cannot be null-checked. Ask whether one exists\n\t\t\t// before taking it.\n\t\t\tif ( board.Rows.Any( r =\u003E r.IsMe ) )\n\t\t\t\treturn $\u0022global rank #{board.Rows.First( r =\u003E r.IsMe ).Rank}\u0022;\n\n\t\t\tint beaten = board.Rows.Count( r =\u003E r.Raw \u003E mine );\n\t\t\treturn $\u0022faster than {beaten} of the top {board.Rows.Count}\u0022;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003ECollapse from the summary. Ends the run and banks the Cores.\u003C/summary\u003E\n\tprivate void CollapseFromSummary()\n\t{\n\t\tManager?.ClearLadderComplete();\n\t\tProgress?.Collapse();\n\n\t\tAudio.Collapse();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Travel to the shared Monolith instead of collapsing. Keeps the run, and the upgrades,\n\t/// which is the whole reason a fully armed player is the one who should be going there.\n\t/// \u003C/summary\u003E\n\tprivate void GoToMonolith()\n\t{\n\t\tManager?.ClearLadderComplete();\n\t\tManager?.SetMonolithMode( true );\n\n\t\tAudio.PlayUi( Audio.UiPress, 0.8f, 1.1f );\n\t}\n\n\tprotected override int BuildHash()\n\t{\n\t\tvar p = Progress;\n\n\t\tint boardHash = 17;\n\t\tforeach ( var b in Boards )\n\t\t\tboardHash = boardHash * 31 \u002B b.Revision;\n\n\t\t// Charge state is quantised to a tenth of a second: the cooldown readout needs to tick,\n\t\t// but rebuilding the whole HUD every frame for it would be wasteful.\n\t\t// The reload minigame runs for two seconds and needs a smooth playhead, so it is\n\t\t// quantised finely while it is open and coarsely the rest of the time.\n\t\tint chargeHash = miner == null ? 0\n\t\t\t: HashCode.Combine( miner.ChargeLive, miner.ChargeReady, miner.ReloadAttemptOpen,\n\t\t\t\tminer.InActiveReloadWindow, miner.ActiveReloadJustHit,\n\t\t\t\t(int)(miner.ReloadAttemptProgress * 300f),\n\t\t\t\t(int)(miner.ReloadProgress * 100f),\n\t\t\t\tHashCode.Combine( (int)(miner.DrillQuality * 50f),\n\t\t\t\t\t(int)(ChargeScreen.x * 400f), (int)(ChargeScreen.y * 400f) ) );\n\n\t\t// While hunting the last cubes the hint depends on where you are looking, so refresh it\n\t\t// several times a second. Outside that state nothing here changes per frame.\n\t\tint residueTick = Residue ? (int)(Time.Now * 5f) : 0;\n\n\t\t// The run timer is always on screen, so the HUD needs to rebuild ten times a second\n\t\t// regardless of what else changed.\n\t\tint timerTick = (int)(Time.Now * 10f);\n\n\t\treturn System.HashCode.Combine(\n\t\t\tPanelOpen || (Manager?.LadderComplete ?? false)\n\t\t\t\t|| ShowTutorial || ShowStart || ShowPause || PhotoHidden,\n\t\t\t// Selection and device folded into the tab slot: both change what the panel draws,\n\t\t\t// and a highlight that does not repaint is a highlight that does not exist.\n\t\t\tTab ^ (Selected * 31) ^ (UsingController ? 7717 : 0) ^ (TutorialPage * 131)\n\t\t\t\t^ (PauseSelected * 1973)\n\t\t\t\t^ (AudioSettings.MusicMuted ? 2609 : 0)\n\t\t\t\t^ (AudioSettings.EffectsMuted ? 3299 : 0),\n\t\t\tAiming,\n\t\t\tRemaining ^ residueTick ^ timerTick ^ ((int)(BurnAmount * 20f) * 6151)\n\t\t\t\t^ ((p?.Resonance ?? 0) * 7919) ^ (Leech.All.Count * 104729)\n\t\t\t\t^ ((Manager?.ShieldActive ?? false) ? 31337 : 0),\n\t\t\tTier \u002B (InMonolith ? 100000 : 0),\n\t\t\tp?.Data.Dust.GetHashCode() ?? 0,\n\t\t\tboardHash ^ chargeHash,\n\t\t\tp == null ? 0 : UpgradeHash( p ) );\n\t}\n\n\tprivate static int UpgradeHash( PlayerProgress p )\n\t{\n\t\tint hash = 17;\n\t\tforeach ( var level in p.Data.UpgradeLevels )\n\t\t\thash = hash * 31 \u002B level;\n\n\t\treturn hash;\n\t}\n}\n"},{"Ident":"idkman.monolith","Path":"ui/hud.razor.scss","FileName":"hud.razor.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"// Devil Daggers palette: near-black ground, ember orange, blood red. No blues.\n$accent: #ff7a1c;\n$accent-dim: #a8410c;\n$charge: #ffd34a;\n$panel-bg: rgba(6, 4, 3, 0.94);\n$text: #f0dcc8;\n$muted: #8a6a58;\n\n.hud\n{\n\tposition: absolute;\n\twidth: 100%;\n\theight: 100%;\n\tfont-family: Consolas, monospace;\n\tcolor: $text;\n\tpointer-events: none;\n\n\t\u0026.panel-open\n\t{\n\t\tpointer-events: all;\n\t}\n\n\t// Photo mode. Opacity rather than display:none so layout, hit testing and every OnUpdate\n\t// behind it carry on exactly as they were: the overlay comes off the glass, the game does\n\t// not change.\n\t\u0026.photo-hidden { opacity: 0; }\n\n\t// ------------------------------------------------------------ top progress bar\n\n\t.topbar\n\t{\n\t\tposition: absolute;\n\t\ttop: 24px;\n\t\tleft: 50%;\n\t\ttransform: translateX( -50% );\n\t\twidth: 620px;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\n\t\t.tier\n\t\t{\n\t\t\tfont-size: 16px;\n\t\t\tletter-spacing: 6px;\n\t\t\tcolor: $accent;\n\t\t\tmargin-bottom: 6px;\n\t\t}\n\n\t\t.bar\n\t\t{\n\t\t\twidth: 100%;\n\t\t\theight: 10px;\n\t\t\tbackground-color: rgba(255, 255, 255, 0.08);\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.15);\n\t\t\toverflow: hidden;\n\n\t\t\t.fill\n\t\t\t{\n\t\t\t\theight: 100%;\n\t\t\t\tbackground-color: $accent;\n\t\t\t\ttransition: width 0.25s ease-out;\n\t\t\t}\n\t\t}\n\n\t\t.counts\n\t\t{\n\t\t\tmargin-top: 6px;\n\t\t\tfont-size: 13px;\n\t\t\tcolor: $muted;\n\t\t\tgap: 8px;\n\n\t\t\t.remaining { color: $text; }\n\t\t\t.pct { color: $accent-dim; margin-left: 12px; }\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------ stage cleared banner\n\n\t.cleared\n\t{\n\t\tposition: absolute;\n\t\ttop: 34%;\n\t\tleft: 50%;\n\t\ttransform: translateX( -50% );\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\ttext-align: center;\n\n\t\t.big\n\t\t{\n\t\t\tfont-size: 52px;\n\t\t\tletter-spacing: 10px;\n\t\t\tcolor: $charge;\n\t\t\ttext-stroke: 2px rgba(0, 0, 0, 0.55);\n\t\t}\n\n\t\t.next\n\t\t{\n\t\t\tmargin-top: 10px;\n\t\t\tfont-size: 18px;\n\t\t\tletter-spacing: 5px;\n\t\t\tcolor: $accent;\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------ wallet\n\n\t.wallet\n\t{\n\t\tposition: absolute;\n\t\ttop: 24px;\n\t\tleft: 24px;\n\t\tflex-direction: column;\n\t\tbackground-color: rgba(8, 11, 16, 0.55);\n\t\tpadding: 14px 18px;\n\t\tborder-left: 2px solid $accent;\n\n\t\t.dust\n\t\t{\n\t\t\tflex-direction: column;\n\t\t\tmargin-bottom: 8px;\n\n\t\t\t.label { font-size: 11px; letter-spacing: 4px; color: $muted; }\n\t\t\t.value { font-size: 30px; color: $accent; }\n\t\t}\n\n\t\t.line\n\t\t{\n\t\t\tjustify-content: space-between;\n\t\t\tgap: 18px;\n\t\t\tfont-size: 13px;\n\n\t\t\t.label { color: $muted; }\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------ residue callout\n\n\t.residue\n\t{\n\t\tposition: absolute;\n\t\ttop: 96px;\n\t\tleft: 50%;\n\t\ttransform: translateX( -50% );\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tfont-size: 22px;\n\t\tletter-spacing: 6px;\n\t\tcolor: $charge;\n\n\t\t.sub\n\t\t{\n\t\t\tfont-size: 12px;\n\t\t\tletter-spacing: 3px;\n\t\t\tcolor: $muted;\n\t\t\tmargin-top: 4px;\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------ crosshair\n\n\t.crosshair\n\t{\n\t\tposition: absolute;\n\t\tleft: 50%;\n\t\ttop: 50%;\n\t\twidth: 10px;\n\t\theight: 10px;\n\t\tmargin-left: -5px;\n\t\tmargin-top: -5px;\n\t\tborder: 2px solid rgba(255, 255, 255, 0.35);\n\t\tborder-radius: 50%;\n\t\ttransition: all 0.08s ease-out;\n\n\t\t\u0026.on-target\n\t\t{\n\t\t\tborder-color: $accent;\n\t\t\twidth: 18px;\n\t\t\theight: 18px;\n\t\t\tmargin-left: -9px;\n\t\t\tmargin-top: -9px;\n\t\t}\n\t}\n\n\t// The burn warning. A border that closes in from the screen edge, so it registers in\n\t// peripheral vision while you are aiming at something else entirely.\n\t.burn\n\t{\n\t\tposition: absolute;\n\t\ttop: 0px;\n\t\tleft: 0px;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tborder: 26px solid rgba( 255, 60, 20, 0.55 );\n\t}\n\n\t.burn-word\n\t{\n\t\tposition: absolute;\n\t\tbottom: 22%;\n\t\tleft: 50%;\n\t\ttransform: translateX( -50% );\n\t\tfont-size: 26px;\n\t\tletter-spacing: 12px;\n\t\tcolor: #ff5a2a;\n\t\ttext-stroke: 2px rgba( 0, 0, 0, 0.6 );\n\t}\n\n\t.hint\n\t{\n\t\tposition: absolute;\n\t\tbottom: 22px;\n\t\tleft: 50%;\n\t\ttransform: translateX( -50% );\n\t\tfont-size: 12px;\n\t\tcolor: $muted;\n\n\t\tb { color: $text; }\n\t}\n\n\t// ------------------------------------------------------------ upgrade panel\n\n\t// Catches clicks that miss the panel, so the play field dismisses the menu. Lower z-index\n\t// than .panel so the panel itself still gets its own clicks.\n\t.panel-backdrop\n\t{\n\t\tposition: absolute;\n\t\ttop: 0px;\n\t\tleft: 0px;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tz-index: 0;\n\t}\n\n\t.panel\n\t{\n\t\tposition: absolute;\n\t\ttop: 0px;\n\t\tright: 0px;\n\t\twidth: 468px;\n\t\theight: 100%;\n\t\tbackground-color: $panel-bg;\n\t\tborder-left: 2px solid $accent;\n\t\tflex-direction: column;\n\t\tpadding: 0px;\n\t\tz-index: 1;\n\n\t\t// A header BAND rather than a row of text floating in the same space as the content.\n\t\t// This is most of what separated the panel from the welcome sheets: the sheets have a\n\t\t// clear title zone and the panel had four words and a number sharing a margin.\n\t\t.panel-head\n\t\t{\n\t\t\talign-items: center;\n\t\t\tpadding: 20px 24px 0px 24px;\n\t\t\tmargin-bottom: 0px;\n\t\t\tgap: 18px;\n\t\t\tbackground-color: rgba( 0, 0, 0, 0.35 );\n\t\t\tborder-bottom: 1px solid rgba( 255, 122, 28, 0.22 );\n\n\t\t\t// The tabs used to wrap: at 17px with 4px of tracking, four labels plus the dust\n\t\t\t// readout overflowed the panel and \u0022UPGRADES\u0022 broke across two lines mid-word.\n\t\t\t// Nothing in this row may wrap or shrink.\n\t\t\tflex-wrap: nowrap;\n\n\t\t\t.title\n\t\t\t{\n\t\t\t\tfont-size: 13px;\n\t\t\t\tletter-spacing: 3px;\n\t\t\t\tcolor: $muted;\n\t\t\t\twhite-space: nowrap;\n\t\t\t\tflex-shrink: 0;\n\t\t\t\tpadding-bottom: 12px;\n\t\t\t\tborder-bottom: 2px solid rgba( 0, 0, 0, 0 );\n\t\t\t\ttransition: all 0.12s ease-out;\n\n\t\t\t\t\u0026:hover { color: $text; }\n\n\t\t\t\t\u0026.on\n\t\t\t\t{\n\t\t\t\t\tcolor: $accent;\n\t\t\t\t\tborder-bottom-color: $accent;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// The wallet, given the weight of a readout instead of the weight of a footnote.\n\t\t\t// It is the number every decision on this panel is measured against.\n\t\t\t.dust-inline\n\t\t\t{\n\t\t\t\tfont-size: 16px;\n\t\t\t\tcolor: $accent;\n\t\t\t\tmargin-left: auto;\n\t\t\t\tmargin-bottom: 12px;\n\t\t\t\tletter-spacing: 1px;\n\t\t\t\twhite-space: nowrap;\n\t\t\t\tflex-shrink: 0;\n\t\t\t}\n\t\t}\n\n\t\t.boards\n\t\t{\n\t\t\tflex-direction: column;\n\t\t\tflex-grow: 1;\n\t\t\toverflow-y: scroll;\n\t\t\tpadding: 18px 24px 24px 24px;\n\t\t\tgap: 18px;\n\t\t}\n\n\t\t.upgrades\n\t\t{\n\t\t\tflex-direction: column;\n\t\t\tflex-grow: 1;\n\t\t\toverflow-y: scroll;\n\t\t\tpadding: 16px 24px 24px 24px;\n\t\t\tgap: 10px;\n\t\t}\n\t}\n\n\t.board\n\t{\n\t\tflex-direction: column;\n\n\t\t.board-title\n\t\t{\n\t\t\tfont-size: 13px;\n\t\t\tletter-spacing: 4px;\n\t\t\tcolor: $accent-dim;\n\t\t\tpadding-bottom: 6px;\n\t\t\tborder-bottom: 1px solid rgba(255, 255, 255, 0.08);\n\t\t\tmargin-bottom: 6px;\n\t\t}\n\n\t\t.board-note\n\t\t{\n\t\t\tfont-size: 12px;\n\t\t\tcolor: $muted;\n\t\t\tpadding: 8px 0px;\n\n\t\t\t\u0026.error { color: $charge; }\n\t\t}\n\n\t\t.rank-row\n\t\t{\n\t\t\talign-items: baseline;\n\t\t\tpadding: 4px 6px;\n\t\t\tfont-size: 13px;\n\t\t\tgap: 10px;\n\n\t\t\t.rank { color: $muted; width: 44px; }\n\t\t\t.who { flex-grow: 1; }\n\t\t\t.score { color: $accent-dim; }\n\n\t\t\t\u0026.me\n\t\t\t{\n\t\t\t\tbackground-color: rgba(255, 122, 28, 0.14);\n\t\t\t\t.who { color: $accent; }\n\t\t\t\t.score { color: $accent; }\n\t\t\t}\n\t\t}\n\t}\n\n\t.refresh\n\t{\n\t\talign-self: center;\n\t\tpadding: 8px 20px;\n\t\tmargin-top: 4px;\n\t\tfont-size: 12px;\n\t\tletter-spacing: 4px;\n\t\tcolor: $muted;\n\t\tborder: 1px solid rgba(255, 255, 255, 0.12);\n\n\t\t\u0026:hover { color: $accent; border-color: $accent-dim; }\n\t}\n\n\t.charge\n\t{\n\t\tposition: absolute;\n\t\tbottom: 52px;\n\t\tleft: 50%;\n\t\ttransform: translateX( -50% );\n\t\tfont-size: 14px;\n\t\tletter-spacing: 3px;\n\t\tcolor: $muted;\n\n\t\tb { color: $text; }\n\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tgap: 7px;\n\n\t\t\u0026.ready { color: $accent; b { color: $accent; } }\n\t\t\u0026.armed { color: #ff5a3c; b { color: #ff5a3c; } }\n\t\t\u0026.window { color: $charge; b { color: $charge; } }\n\t\t\u0026.caught { color: #7dff9b; b { color: #7dff9b; } }\n\n\t\t.live { flex-direction: column; align-items: center; }\n\t\t.ready-line { color: $accent; }\n\n\t\t.depth\n\t\t{\n\t\t\twidth: 230px;\n\t\t\theight: 4px;\n\t\t\tmargin-top: 5px;\n\t\t\tbackground-color: rgba(255, 255, 255, 0.10);\n\n\t\t\t.fill { height: 100%; background-color: #ff5a3c; }\n\t\t}\n\n\t\t.reload\n\t\t{\n\t\t\tflex-direction: column;\n\t\t\talign-items: center;\n\n\t\t\t// The bar is the reload; the bright slice inside it is the target you aim for.\n\t\t\t.bar\n\t\t\t{\n\t\t\t\tposition: relative;\n\t\t\t\twidth: 290px;\n\t\t\t\theight: 11px;\n\t\t\t\tbackground-color: rgba(255, 255, 255, 0.08);\n\t\t\t\tborder: 1px solid rgba(255, 255, 255, 0.16);\n\n\t\t\t\t.window\n\t\t\t\t{\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: 0px;\n\t\t\t\t\theight: 100%;\n\t\t\t\t\tbackground-color: rgba(255, 211, 74, 0.55);\n\t\t\t\t\tborder-left: 1px solid $charge;\n\t\t\t\t\tborder-right: 1px solid $charge;\n\t\t\t\t}\n\n\t\t\t\t.playhead\n\t\t\t\t{\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: -3px;\n\t\t\t\t\twidth: 3px;\n\t\t\t\t\theight: 17px;\n\t\t\t\t\tbackground-color: #ffffff;\n\t\t\t\t}\n\n\t\t\t\t.fill\n\t\t\t\t{\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: 0px;\n\t\t\t\t\tleft: 0px;\n\t\t\t\t\theight: 100%;\n\t\t\t\t\tbackground-color: rgba(255, 122, 28, 0.75);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// The minigame is bigger and brighter than the passive cooldown bar: it wants\n\t\t\t// your attention for two seconds, then gets out of the way.\n\t\t\t\u0026.attempt .bar\n\t\t\t{\n\t\t\t\twidth: 360px;\n\t\t\t\theight: 15px;\n\t\t\t\tborder-color: $charge;\n\t\t\t}\n\t\t}\n\n\t\t.sub { font-size: 11px; letter-spacing: 2px; color: $muted; margin-top: 4px; }\n\t}\n\n\t// Projected position of the buried charge. Nothing in the world can show this, because the\n\t// charge is inside solid rock by design.\n\t.charge-marker\n\t{\n\t\tposition: absolute;\n\t\twidth: 0px;\n\t\theight: 0px;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\n\t\t.ring\n\t\t{\n\t\t\tposition: absolute;\n\t\t\tleft: -17px;\n\t\t\ttop: -17px;\n\t\t\twidth: 34px;\n\t\t\theight: 34px;\n\t\t\tborder: 2px solid #ff5a3c;\n\t\t\tborder-radius: 50%;\n\t\t}\n\n\t\t.label\n\t\t{\n\t\t\tposition: absolute;\n\t\t\ttop: 22px;\n\t\t\tleft: -60px;\n\t\t\twidth: 120px;\n\t\t\ttext-align: center;\n\t\t\tfont-size: 11px;\n\t\t\tletter-spacing: 2px;\n\t\t\tcolor: #ff5a3c;\n\t\t}\n\t}\n\n\t.charge-offscreen\n\t{\n\t\tposition: absolute;\n\t\ttop: 58%;\n\t\tleft: 50%;\n\t\ttransform: translateX( -50% );\n\t\tfont-size: 14px;\n\t\tletter-spacing: 4px;\n\t\tcolor: #ff5a3c;\n\t}\n\n\t// AN UPGRADE CARD.\n\t//\n\t// The note was that these read as a specification rather than as a game. Structurally the\n\t// problem was that name, level, description, effect and cost were five pieces of text at\n\t// four sizes stacked in a box, so there was no shape to scan and every row demanded to be\n\t// read in full. The fix is hierarchy, not more words: a bar down the left edge that lights\n\t// when you can afford it, the name given real size, the cost pulled out as its own chip on\n\t// the right, and the prose demoted to the quietest thing in the card.\n\t.upgrade\n\t{\n\t\tflex-direction: column;\n\t\tflex-shrink: 0;\n\t\tbackground-color: rgba(255, 255, 255, 0.028);\n\t\tborder: 1px solid rgba(255, 255, 255, 0.06);\n\t\tborder-left: 3px solid rgba(255, 255, 255, 0.10);\n\t\ttransition: all 0.1s ease-out;\n\n\t\t// The pad selection. A mouse player never sees this: a highlight that does not follow\n\t\t// the cursor would just be confusing next to a hover state.\n\t\t\u0026.selected\n\t\t{\n\t\t\tborder-color: $accent;\n\t\t\tborder-left-color: $charge;\n\t\t\tbackground-color: rgba( 255, 150, 40, 0.14 );\n\t\t}\n\n\t\t.body { flex-direction: column; padding: 13px 15px; }\n\n\t\t// Same rule as .primer: a wrapped line in a flex column needs an explicit width and\n\t\t// flex-shrink: 0 or it collapses and overdraws the row beneath it.\n\t\t.desc\n\t\t{\n\t\t\twidth: 100%;\n\t\t\tflex-shrink: 0;\n\t\t\tline-height: 16px;\n\t\t\tword-break: break-word;\n\t\t}\n\n\t\t.row { flex-shrink: 0; }\n\n\t\t.buymax\n\t\t{\n\t\t\tjustify-content: center;\n\t\t\tpadding: 5px 0px;\n\t\t\tfont-size: 11px;\n\t\t\tletter-spacing: 3px;\n\t\t\tcolor: $accent;\n\t\t\tbackground-color: rgba(255, 122, 28, 0.10);\n\t\t\tborder-top: 1px solid rgba(255, 122, 28, 0.28);\n\n\t\t\tb { color: $text; }\n\t\t\t\u0026:hover { background-color: rgba(255, 122, 28, 0.26); }\n\t\t}\n\n\t\t.row { justify-content: space-between; align-items: center; }\n\n\t\t// A title row with a mark on it. The glyph is what stops eight cards of dense text from\n\t\t// reading as one wall: you learn the shapes after a run or two and stop reading names.\n\t\t.head\n\t\t{\n\t\t\twidth: 100%;\n\t\t\tflex-shrink: 0;\n\t\t\talign-items: center;\n\t\t\tgap: 11px;\n\t\t}\n\n\t\t.glyph\n\t\t{\n\t\t\twidth: 22px;\n\t\t\theight: 22px;\n\t\t\tflex-shrink: 0;\n\t\t\tborder: 2px solid rgba( 255, 255, 255, 0.22 );\n\t\t\tbackground-color: rgba( 255, 255, 255, 0.05 );\n\t\t}\n\n\t\t// One per upgrade, drawn only from borders and background so nothing needs importing.\n\t\t// Deliberately not literal pictures: they are marks to recognise, not illustrations.\n\t\t.glyph.drillspeed { background-color: #ffc75c; border-color: #fff0c0; }\n\t\t.glyph.blastradius { background-color: #ff6a1a; border-color: #ffb27a; border-radius: 11px; }\n\t\t.glyph.chargechance { background-color: #ff2a10; border-color: #ff9a80; border-radius: 4px; }\n\t\t.glyph.chargeradius { background-color: transparent; border-color: #ff5a3c; border-radius: 11px; }\n\t\t.glyph.dustyield { background-color: #b8763a; border-color: #ffd9a8; }\n\t\t.glyph.drones { background-color: transparent; border-color: #ffd34a; border-bottom-width: 7px; }\n\t\t.glyph.demolition { background-color: #ff8a3a; border-color: #ffd0a8; border-radius: 4px; }\n\t\t.glyph.dronerate { background-color: #ffd34a; border-color: #fff4c8; border-radius: 11px; }\n\n\t\t.name\n\t\t{\n\t\t\tfont-size: 16px;\n\t\t\tletter-spacing: 1px;\n\t\t\tcolor: $text;\n\t\t\tflex-grow: 1;\n\t\t}\n\n\t\t// The level as a numbered badge rather than the words \u0022Lv 3\u0022. At a glance you want the\n\t\t// number, and the label was costing three characters of width on every single row.\n\t\t.level\n\t\t{\n\t\t\tflex-shrink: 0;\n\t\t\tmin-width: 26px;\n\t\t\tpadding: 2px 7px;\n\t\t\tfont-size: 12px;\n\t\t\ttext-align: center;\n\t\t\tcolor: $muted;\n\t\t\tbackground-color: rgba( 255, 255, 255, 0.06 );\n\t\t\tborder: 1px solid rgba( 255, 255, 255, 0.10 );\n\t\t}\n\n\t\t.desc\n\t\t{\n\t\t\tfont-size: 11px;\n\t\t\tline-height: 15px;\n\t\t\tcolor: $muted;\n\t\t\tmargin: 0px 0px 8px 0px;\n\t\t\topacity: 0.72;\n\t\t}\n\n\t\t.foot\n\t\t{\n\t\t\twidth: 100%;\n\t\t\tflex-shrink: 0;\n\t\t\tjustify-content: space-between;\n\t\t\talign-items: center;\n\t\t\tpadding-top: 8px;\n\t\t\tborder-top: 1px solid rgba( 255, 255, 255, 0.06 );\n\t\t}\n\n\t\t.current { font-size: 12px; letter-spacing: 1px; color: $accent-dim; }\n\n\t\t// The price, as a chip. It is the thing you are deciding about, so it gets an edge and\n\t\t// a background of its own instead of being the last words on a line.\n\t\t.cost\n\t\t{\n\t\t\tfont-size: 13px;\n\t\t\tletter-spacing: 1px;\n\t\t\tcolor: $muted;\n\t\t\tpadding: 4px 10px;\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.10);\n\t\t\tflex-shrink: 0;\n\t\t}\n\n\t\t\u0026.affordable\n\t\t{\n\t\t\tborder-color: rgba(255, 122, 28, 0.35);\n\t\t\tborder-left-color: $accent;\n\t\t\tbackground-color: rgba(255, 122, 28, 0.06);\n\n\t\t\t.cost\n\t\t\t{\n\t\t\t\tcolor: $accent;\n\t\t\t\tborder-color: rgba(255, 122, 28, 0.55);\n\t\t\t\tbackground-color: rgba(255, 122, 28, 0.12);\n\t\t\t}\n\n\t\t\t\u0026:hover\n\t\t\t{\n\t\t\t\tbackground-color: rgba(255, 122, 28, 0.15);\n\t\t\t\tborder-color: $accent;\n\n\t\t\t\t.cost { background-color: rgba(255, 122, 28, 0.26); }\n\t\t\t}\n\t\t}\n\n\t\t// Earned Marks recede: the list should read as \u0022what is left to do\u0022, not a trophy wall.\n\t\t\u0026.mark\n\t\t{\n\t\t\t.level { color: $muted; }\n\n\t\t\t\u0026.earned\n\t\t\t{\n\t\t\t\topacity: 0.55;\n\t\t\t\tborder-color: rgba(255, 211, 74, 0.30);\n\t\t\t\t.name { color: $charge; }\n\t\t\t\t.level { color: $charge; }\n\t\t\t}\n\t\t}\n\n\t\t// Core Tree nodes read as gold rather than ember, so the two currencies never blur.\n\t\t\u0026.core\n\t\t{\n\t\t\tborder-color: rgba(255, 211, 74, 0.20);\n\n\t\t\t.cost { color: $charge; }\n\n\t\t\t\u0026.affordable\n\t\t\t{\n\t\t\t\tborder-color: $charge;\n\t\t\t\tbackground-color: rgba(255, 211, 74, 0.10);\n\t\t\t\t\u0026:hover { background-color: rgba(255, 211, 74, 0.24); }\n\t\t\t}\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------ resonance\n\n\t.resonance\n\t{\n\t\tposition: absolute;\n\t\ttop: 50%;\n\t\tright: 40px;\n\t\ttransform: translateY( -50% );\n\t\tflex-direction: column;\n\t\talign-items: flex-end;\n\n\t\t.stacks\n\t\t{\n\t\t\tfont-size: 24px;\n\t\t\tletter-spacing: 4px;\n\t\t\tcolor: $charge;\n\t\t}\n\n\t\t.rbar\n\t\t{\n\t\t\twidth: 170px;\n\t\t\theight: 4px;\n\t\t\tmargin-top: 5px;\n\t\t\tbackground-color: rgba(255, 255, 255, 0.10);\n\n\t\t\t.fill { height: 100%; background-color: $charge; }\n\t\t}\n\n\t\t.count { font-size: 11px; letter-spacing: 2px; color: $muted; margin-top: 4px; }\n\t}\n\n\t.mark-toast\n\t{\n\t\tposition: absolute;\n\t\ttop: 22%;\n\t\tleft: 50%;\n\t\ttransform: translateX( -50% );\n\t\tflex-direction: column;\n\t\talign-items: center;\n\n\t\t.head { font-size: 11px; letter-spacing: 6px; color: $muted; }\n\t\t.name { font-size: 26px; letter-spacing: 4px; color: $charge; margin-top: 3px; }\n\t}\n\n\t.markbar\n\t{\n\t\twidth: 100%;\n\t\theight: 3px;\n\t\tmargin-top: 6px;\n\t\tbackground-color: rgba(255, 255, 255, 0.09);\n\n\t\t.fill { height: 100%; background-color: $accent-dim; }\n\t}\n\n\t.leeches\n\t{\n\t\tposition: absolute;\n\t\ttop: 132px;\n\t\tleft: 24px;\n\t\tflex-direction: column;\n\t\tfont-size: 13px;\n\t\tletter-spacing: 2px;\n\t\tcolor: #6fdc7a;\n\n\t\t.sub { font-size: 10px; letter-spacing: 1px; color: $muted; margin-top: 2px; }\n\t}\n\n\t.shield-banner\n\t{\n\t\tposition: absolute;\n\t\ttop: 84px;\n\t\tleft: 50%;\n\t\ttransform: translateX( -50% );\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tfont-size: 15px;\n\t\tletter-spacing: 4px;\n\t\tcolor: #6fd3ff;\n\n\t\t.sub { font-size: 11px; letter-spacing: 2px; color: $muted; margin-top: 2px; }\n\n\t\t// The break window is the reward for the skill shot, so it reads as a good thing.\n\t\t\u0026.broken { color: #9dff9a; }\n\t}\n\n\t// The one piece of text that announces a failure. Centred and large because by the time it\n\t// shows, the world has already rebuilt and there is nothing left in it to read.\n\t// Welcome pages and the start screen. A full-screen sheet over a black void.\n\t.overlay\n\t{\n\t\tposition: absolute;\n\t\ttop: 0px;\n\t\tleft: 0px;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tbackground-color: rgba( 0, 0, 0, 0.92 );\n\t}\n\n\t// The pause overlay is deliberately the only one you can see through. The welcome and start\n\t// sheets are a screen you are ON; pause is a screen you are BEHIND, and leaving the frozen\n\t// monolith dimly visible is what tells you the game is still there waiting.\n\t.overlay.transparent { background-color: rgba( 0, 0, 0, 0.72 ); }\n\n\t.sheet\n\t{\n\t\tflex-direction: column;\n\t\twidth: 780px;\n\n\t\t// Three buttons do not need the full reading width the tutorial pages were sized for.\n\t\t\u0026.pause { width: 460px; }\n\t\tmax-height: 88%;\n\t\tpadding: 32px 36px;\n\t\tborder: 2px solid $accent;\n\t\tbackground-color: rgba( 12, 8, 6, 0.96 );\n\t\toverflow-y: scroll;\n\n\t\t.sheet-eyebrow\n\t\t{\n\t\t\tfont-size: 11px;\n\t\t\tletter-spacing: 5px;\n\t\t\tcolor: $muted;\n\t\t\tmargin-bottom: 10px;\n\t\t\tflex-shrink: 0;\n\t\t}\n\n\t\t.sheet-title\n\t\t{\n\t\t\tfont-size: 38px;\n\t\t\tletter-spacing: 11px;\n\t\t\tcolor: $accent;\n\t\t\tflex-shrink: 0;\n\t\t\ttext-shadow: 0px 0px 28px rgba( 255, 150, 40, 0.55 );\n\t\t}\n\n\t\t.sheet-lead\n\t\t{\n\t\t\twidth: 100%;\n\t\t\tflex-shrink: 0;\n\t\t\tfont-size: 14px;\n\t\t\tline-height: 20px;\n\t\t\tletter-spacing: 1px;\n\t\t\tcolor: $text;\n\t\t\tmargin-top: 10px;\n\t\t\tmargin-bottom: 22px;\n\t\t\tword-break: break-word;\n\t\t}\n\n\t\t.sheet-rows\n\t\t{\n\t\t\tflex-direction: column;\n\t\t\twidth: 100%;\n\t\t\tflex-shrink: 0;\n\t\t\tgap: 16px;\n\t\t}\n\n\t\t// Each row is an icon and a paragraph. The icons are CSS shapes rather than art, so the\n\t\t// welcome screen needs no assets and cannot break on a missing texture.\n\t\t.srow\n\t\t{\n\t\t\twidth: 100%;\n\t\t\tflex-shrink: 0;\n\t\t\tgap: 16px;\n\t\t\talign-items: flex-start;\n\n\t\t\t.icon\n\t\t\t{\n\t\t\t\twidth: 44px;\n\t\t\t\theight: 44px;\n\t\t\t\tflex-shrink: 0;\n\t\t\t\tborder: 2px solid rgba( 255, 255, 255, 0.18 );\n\t\t\t}\n\n\t\t\t.icon.rock { background-color: #b8763a; border-color: #ffd9a8; }\n\t\t\t.icon.ladder\n\t\t\t{\n\t\t\t\tbackground-color: transparent;\n\t\t\t\tborder-color: $accent;\n\t\t\t\tborder-left-width: 10px;\n\t\t\t}\n\t\t\t.icon.bolt\n\t\t\t{\n\t\t\t\tbackground-color: #ffc75c;\n\t\t\t\tborder-color: #fff0c0;\n\t\t\t\theight: 12px;\n\t\t\t\tmargin-top: 16px;\n\t\t\t}\n\t\t\t// NOT \u0060.icon.charge\u0060. There is a \u0060.charge\u0060 readout at the bottom of the HUD with\n\t\t\t// \u0060position: absolute\u0060, and since \u0060.icon.charge\u0060 never redeclared \u0060position\u0060, the\n\t\t\t// tutorial icon inherited it and left its row entirely: the swatch simply vanished\n\t\t\t// and the text slid left. Two unrelated things wanted the same word.\n\t\t\t.icon.bore { background-color: #ff6a1a; border-color: #ffb27a; border-radius: 22px; }\n\t\t\t.icon.volatile { background-color: #ff2a10; border-color: #ff9a80; }\n\t\t\t.icon.spotter\n\t\t\t{\n\t\t\t\tbackground-color: #ffe14d;\n\t\t\t\tborder-color: #fff6bb;\n\t\t\t\tborder-radius: 22px;\n\t\t\t}\n\t\t\t.icon.sentinel { background-color: #ff5a2a; border-color: #ffb59a; border-radius: 8px; }\n\t\t\t.icon.anchor { background-color: #6fdd52; border-color: #c6ffb8; border-radius: 4px; }\n\t\t\t.icon.barrier\n\t\t\t{\n\t\t\t\tbackground-color: transparent;\n\t\t\t\tborder-color: #ff5f8f;\n\t\t\t\tborder-width: 4px;\n\t\t\t}\n\t\t\t.icon.crawler\n\t\t\t{\n\t\t\t\tbackground-color: #ff7a2a;\n\t\t\t\tborder-color: #ffd6a8;\n\t\t\t\theight: 24px;\n\t\t\t\tmargin-top: 12px;\n\t\t\t}\n\t\t\t// The floor page. These are swatches of the actual tile colours, so the page is\n\t\t\t// teaching you a lookup you will use rather than a set of arbitrary badges.\n\t\t\t.icon.rocket\n\t\t\t{\n\t\t\t\tbackground-color: #ff8a3a;\n\t\t\t\tborder-color: #ffd0a8;\n\t\t\t\tborder-bottom-width: 10px;\n\t\t\t\tborder-bottom-color: #fff0c0;\n\t\t\t}\n\n\t\t\t.icon.warn { background-color: #ff9e14; border-color: #ffd08a; }\n\t\t\t// Matches the raised slabs in the arena: brighter than anything else on the page,\n\t\t\t// and the only swatch in the whole tutorial that is not in the ember palette.\n\t\t\t.icon.dead\n\t\t\t{\n\t\t\t\tbackground-color: #c766ff;\n\t\t\t\tborder-color: #f0d0ff;\n\t\t\t\tborder-bottom-width: 8px;\n\t\t\t\tborder-bottom-color: #6a1090;\n\t\t\t}\n\t\t\t.icon.air\n\t\t\t{\n\t\t\t\tbackground-color: transparent;\n\t\t\t\tborder-color: #8fd8ff;\n\t\t\t\tborder-top-width: 10px;\n\t\t\t}\n\n\t\t\t.icon.leech { background-color: #7dff9b; border-color: #d8ffe4; border-radius: 22px; }\n\t\t\t.icon.move { background-color: transparent; border-color: #8fd8ff; border-bottom-width: 10px; }\n\n\t\t\t.stext\n\t\t\t{\n\t\t\t\tflex-direction: column;\n\t\t\t\tflex-grow: 1;\n\t\t\t\tflex-shrink: 1;\n\t\t\t\tfont-size: 12px;\n\t\t\t\tline-height: 18px;\n\t\t\t\tcolor: $muted;\n\t\t\t\tword-break: break-word;\n\n\t\t\t\tb\n\t\t\t\t{\n\t\t\t\t\tfont-size: 14px;\n\t\t\t\t\tletter-spacing: 2px;\n\t\t\t\t\tcolor: $text;\n\t\t\t\t\tmargin-bottom: 5px;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.c-y { color: #ffe14d; }\n\t\t\t.c-o { color: #ff9a2a; }\n\t\t\t.c-r { color: #ff3a2a; }\n\n\t\t\t// \u0060.stext\u0060 is a flex COLUMN, so an inline span inside a paragraph is not inline at\n\t\t\t// all: it becomes a full-width row of its own. The lock ramp was rendering as\n\t\t\t// \u0022yellow\u0022 / \u0022,\u0022 / \u0022orange\u0022 / \u0022, then\u0022 down six lines with the punctuation stranded.\n\t\t\t//\n\t\t\t// Rather than fight that, the three colours get an explicit row. It reads better as a\n\t\t\t// ramp than it ever did as a sentence.\n\t\t\t.ramp\n\t\t\t{\n\t\t\t\twidth: 100%;\n\t\t\t\tflex-shrink: 0;\n\t\t\t\tflex-direction: row;\n\t\t\t\talign-items: center;\n\t\t\t\tgap: 8px;\n\t\t\t\tmargin-top: 6px;\n\t\t\t\tmargin-bottom: 6px;\n\t\t\t\tfont-size: 11px;\n\t\t\t\tletter-spacing: 1px;\n\n\t\t\t\t.arrow { color: rgba( 255, 255, 255, 0.3 ); }\n\t\t\t}\n\t\t}\n\n\t\t.sheet-actions\n\t\t{\n\t\t\twidth: 100%;\n\t\t\tflex-shrink: 0;\n\t\t\tgap: 12px;\n\t\t\tmargin-top: 26px;\n\t\t\tjustify-content: flex-end;\n\n\t\t\t\u0026.column { flex-direction: column; justify-content: flex-start; }\n\t\t}\n\n\t\t// A settings row: name on the left, state on the right. A row rather than a button\n\t\t// because the label has to be readable at a glance without pressing anything, which is\n\t\t// the whole job of a mute switch.\n\t\t.toggle\n\t\t{\n\t\t\twidth: 100%;\n\t\t\tflex-direction: row;\n\t\t\talign-items: center;\n\t\t\tjustify-content: space-between;\n\t\t\tpadding: 11px 22px;\n\t\t\tfont-size: 13px;\n\t\t\tletter-spacing: 3px;\n\t\t\tcolor: $muted;\n\t\t\tborder: 1px solid rgba( 255, 255, 255, 0.12 );\n\t\t\tcursor: pointer;\n\t\t\ttransition: all 0.1s ease-out;\n\n\t\t\t\u0026:hover\n\t\t\t{\n\t\t\t\tborder-color: $accent;\n\t\t\t\tbackground-color: rgba( 255, 150, 40, 0.14 );\n\t\t\t}\n\n\t\t\t\u0026.selected\n\t\t\t{\n\t\t\t\tborder-color: $accent;\n\t\t\t\tbackground-color: rgba( 255, 150, 40, 0.16 );\n\t\t\t\t.name { color: $text; }\n\t\t\t}\n\n\t\t\t.state\n\t\t\t{\n\t\t\t\tfont-size: 12px;\n\t\t\t\tletter-spacing: 2px;\n\n\t\t\t\t\u0026.on { color: $accent; }\n\t\t\t\t\u0026.off { color: rgba( 255, 255, 255, 0.28 ); }\n\t\t\t}\n\t\t}\n\n\t\t.sheet-btn\n\t\t{\n\t\t\tflex-direction: column;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tpadding: 13px 26px;\n\t\t\tfont-size: 14px;\n\t\t\tletter-spacing: 4px;\n\t\t\tcolor: $text;\n\t\t\tborder: 1px solid rgba( 255, 255, 255, 0.24 );\n\t\t\tcursor: pointer;\n\t\t\ttransition: all 0.1s ease-out;\n\n\t\t\t\u0026:hover\n\t\t\t{\n\t\t\t\tborder-color: $accent;\n\t\t\t\tbackground-color: rgba( 255, 150, 40, 0.16 );\n\t\t\t}\n\n\t\t\t\u0026.ghost { color: $muted; border-color: rgba( 255, 255, 255, 0.12 ); }\n\n\t\t\t// Pad highlight. Written after .ghost so a highlighted ghost button still reads as\n\t\t\t// highlighted rather than keeping the dimmer border.\n\t\t\t\u0026.selected\n\t\t\t{\n\t\t\t\tcolor: $text;\n\t\t\t\tborder-color: $accent;\n\t\t\t\tbackground-color: rgba( 255, 150, 40, 0.16 );\n\t\t\t}\n\n\t\t\t\u0026.big { width: 100%; padding: 18px 22px; font-size: 17px; }\n\n\t\t\t// A fresh save cannot dent the Monolith. Shown, explained, and declined rather than\n\t\t\t// hidden: hiding it would make the shared half of the game invisible on day one.\n\t\t\t\u0026.locked\n\t\t\t{\n\t\t\t\tcolor: $muted;\n\t\t\t\tborder-color: rgba( 255, 255, 255, 0.08 );\n\t\t\t\t\u0026:hover { border-color: rgba( 255, 90, 60, 0.5 ); background-color: rgba( 255, 60, 40, 0.08 ); }\n\t\t\t}\n\n\t\t\t.sub\n\t\t\t{\n\t\t\t\twidth: 100%;\n\t\t\t\tflex-shrink: 0;\n\t\t\t\tfont-size: 10px;\n\t\t\t\tletter-spacing: 1px;\n\t\t\t\tline-height: 15px;\n\t\t\t\tcolor: $muted;\n\t\t\t\tmargin-top: 7px;\n\t\t\t\ttext-align: center;\n\t\t\t\tword-break: break-word;\n\t\t\t}\n\t\t}\n\t}\n\n\t// The end of a hundred stage climb. Deliberately the biggest thing in the game.\n\t.ladder-done\n\t{\n\t\tposition: absolute;\n\t\ttop: 18%;\n\t\tleft: 50%;\n\t\ttransform: translateX( -50% );\n\t\twidth: 620px;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tpadding: 34px 30px;\n\t\tbackground-color: rgba( 0, 0, 0, 0.86 );\n\t\tborder: 2px solid $accent;\n\n\t\t.done-title\n\t\t{\n\t\t\tfont-size: 40px;\n\t\t\tletter-spacing: 12px;\n\t\t\tcolor: $accent;\n\t\t\ttext-shadow: 0px 0px 30px rgba( 255, 150, 40, 0.6 );\n\t\t}\n\n\t\t.done-sub\n\t\t{\n\t\t\tfont-size: 12px;\n\t\t\tletter-spacing: 5px;\n\t\t\tcolor: $muted;\n\t\t\tmargin-top: 6px;\n\t\t}\n\n\t\t.done-time\n\t\t{\n\t\t\tfont-size: 54px;\n\t\t\tletter-spacing: 4px;\n\t\t\tcolor: #ffffff;\n\t\t\tmargin-top: 22px;\n\t\t}\n\n\t\t.done-rank\n\t\t{\n\t\t\tfont-size: 13px;\n\t\t\tletter-spacing: 3px;\n\t\t\tcolor: $charge;\n\t\t\tmargin-top: 6px;\n\t\t\tmargin-bottom: 26px;\n\t\t}\n\n\t\t.done-actions\n\t\t{\n\t\t\twidth: 100%;\n\t\t\tgap: 14px;\n\t\t\talign-items: stretch;\n\t\t}\n\n\t\t.done-btn\n\t\t{\n\t\t\tflex-grow: 1;\n\t\t\tflex-basis: 0px;\n\t\t\tflex-direction: column;\n\t\t\talign-items: center;\n\t\t\tpadding: 16px 12px;\n\t\t\tfont-size: 15px;\n\t\t\tletter-spacing: 4px;\n\t\t\tcolor: $text;\n\t\t\tborder: 1px solid rgba( 255, 255, 255, 0.22 );\n\t\t\tcursor: pointer;\n\t\t\ttransition: all 0.1s ease-out;\n\n\t\t\t\u0026:hover\n\t\t\t{\n\t\t\t\tborder-color: $accent;\n\t\t\t\tbackground-color: rgba( 255, 150, 40, 0.14 );\n\t\t\t}\n\n\t\t\t.sub\n\t\t\t{\n\t\t\t\twidth: 100%;\n\t\t\t\tflex-shrink: 0;\n\t\t\t\tfont-size: 10px;\n\t\t\t\tletter-spacing: 1px;\n\t\t\t\tline-height: 14px;\n\t\t\t\tcolor: $muted;\n\t\t\t\tmargin-top: 8px;\n\t\t\t\ttext-align: center;\n\t\t\t\tword-break: break-word;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Controller prompt row. Glyphs sit inline with their labels, so the bar reads the same as\n\t// the keyboard version rather than becoming a row of floating icons.\n\t// Control legend inside the upgrade panel. Sits under the tabs so it reads as belonging to\n\t// the panel rather than to the world behind it.\n\t.pad-hints\n\t{\n\t\talign-items: center;\n\t\tflex-wrap: nowrap;\n\t\tgap: 4px;\n\t\tpadding: 10px 24px 0px 24px;\n\t\tmargin-bottom: 0px;\n\t\tfont-size: 10px;\n\t\tletter-spacing: 1px;\n\t\tcolor: $muted;\n\n\t\timage\n\t\t{\n\t\t\twidth: 18px;\n\t\t\theight: 18px;\n\t\t\tflex-shrink: 0;\n\t\t}\n\n\t\tspan\n\t\t{\n\t\t\tmargin-right: 10px;\n\t\t\tflex-shrink: 0;\n\t\t\twhite-space: nowrap;\n\t\t}\n\t}\n\n\t.hint.pad\n\t{\n\t\talign-items: center;\n\t\tgap: 6px;\n\n\t\timage\n\t\t{\n\t\t\twidth: 22px;\n\t\t\theight: 22px;\n\t\t\tflex-shrink: 0;\n\t\t}\n\n\t\tspan\n\t\t{\n\t\t\tmargin-right: 12px;\n\t\t\tflex-shrink: 0;\n\t\t}\n\t}\n\n\t.stage-lost\n\t{\n\t\tposition: absolute;\n\t\ttop: 34%;\n\t\tleft: 0px;\n\t\twidth: 100%;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tpointer-events: none;\n\n\t\t.lost-title\n\t\t{\n\t\t\tfont-size: 46px;\n\t\t\tletter-spacing: 14px;\n\t\t\tcolor: #ff3a2a;\n\t\t\ttext-shadow: 0px 0px 26px rgba( 255, 40, 30, 0.75 );\n\t\t}\n\n\t\t.lost-sub\n\t\t{\n\t\t\tfont-size: 13px;\n\t\t\tletter-spacing: 3px;\n\t\t\tcolor: $muted;\n\t\t\tmargin-top: 10px;\n\t\t}\n\t}\n\n\t.hollow-banner\n\t{\n\t\tposition: absolute;\n\t\ttop: 62px;\n\t\tleft: 50%;\n\t\ttransform: translateX( -50% );\n\t\tfont-size: 12px;\n\t\tletter-spacing: 5px;\n\t\tcolor: #b98cff;\n\t}\n\n\t.hollow\n\t{\n\t\tflex-direction: column;\n\t\tpadding: 10px 12px;\n\t\tmargin-bottom: 10px;\n\t\tborder: 1px solid rgba(185, 140, 255, 0.24);\n\t\tbackground-color: rgba(185, 140, 255, 0.05);\n\n\t\tflex-shrink: 0;\n\n\t\t.row { justify-content: space-between; flex-shrink: 0; }\n\t\t.name { font-size: 14px; letter-spacing: 3px; color: #b98cff; }\n\t\t.level { font-size: 11px; color: $muted; }\n\n\t\t.desc\n\t\t{\n\t\t\twidth: 100%;\n\t\t\tflex-shrink: 0;\n\t\t\tfont-size: 11px;\n\t\t\tline-height: 16px;\n\t\t\tcolor: $muted;\n\t\t\tmargin: 4px 0px 8px 0px;\n\t\t\tword-break: break-word;\n\t\t}\n\n\t\t.hollow-btn\n\t\t{\n\t\t\tflex-direction: column;\n\t\t\tflex-shrink: 0;\n\t\t\tpadding: 6px 8px;\n\t\t\tmargin-bottom: 5px;\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.08);\n\n\t\t\t.hname { font-size: 12px; letter-spacing: 2px; color: $muted; flex-shrink: 0; }\n\n\t\t\t.sub\n\t\t\t{\n\t\t\t\twidth: 100%;\n\t\t\t\tflex-shrink: 0;\n\t\t\t\tfont-size: 10px;\n\t\t\t\tline-height: 14px;\n\t\t\t\tcolor: $muted;\n\t\t\t\tmargin-top: 3px;\n\t\t\t\tword-break: break-word;\n\t\t\t}\n\n\t\t\t\u0026.on\n\t\t\t{\n\t\t\t\tborder-color: rgba(185, 140, 255, 0.5);\n\t\t\t\t.hname { color: #b98cff; }\n\t\t\t\t\u0026:hover { background-color: rgba(185, 140, 255, 0.16); }\n\t\t\t}\n\n\t\t\t\u0026.cleared .hname { color: #7dff9b; }\n\t\t\t\u0026.active { border-color: #b98cff; background-color: rgba(185, 140, 255, 0.18); }\n\t\t}\n\t}\n\n\t.slag\n\t{\n\t\tflex-direction: column;\n\t\tpadding: 10px 12px;\n\t\tmargin-bottom: 10px;\n\t\tborder: 1px solid rgba(125, 255, 155, 0.22);\n\t\tbackground-color: rgba(125, 255, 155, 0.05);\n\n\t\tflex-shrink: 0;\n\n\t\t.row { justify-content: space-between; flex-shrink: 0; }\n\t\t.name { font-size: 14px; letter-spacing: 3px; color: #7dff9b; b { color: #ffffff; } }\n\t\t.level { font-size: 12px; color: $muted; }\n\n\t\t.desc\n\t\t{\n\t\t\twidth: 100%;\n\t\t\tflex-shrink: 0;\n\t\t\tfont-size: 11px;\n\t\t\tline-height: 16px;\n\t\t\tcolor: $muted;\n\t\t\tmargin-top: 4px;\n\t\t\tword-break: break-word;\n\t\t}\n\n\t\t.slag-actions { gap: 8px; margin-top: 9px; align-items: stretch; }\n\n\t\t.slag-btn\n\t\t{\n\t\t\tflex-grow: 1;\n\t\t\tflex-basis: 0px;\n\t\t\tflex-direction: column;\n\t\t\talign-items: center;\n\t\t\tpadding: 7px 6px;\n\t\t\tfont-size: 11px;\n\t\t\tletter-spacing: 2px;\n\t\t\tcolor: $muted;\n\n\t\t\t// The Fracture and Reroll blurbs are full sentences now. Without these they\n\t\t\t// collapsed onto one line and ran over the Hollow Runs block below.\n\t\t\t.sub\n\t\t\t{\n\t\t\t\twidth: 100%;\n\t\t\t\tflex-shrink: 0;\n\t\t\t\tline-height: 14px;\n\t\t\t\tword-break: break-word;\n\t\t\t}\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.10);\n\n\t\t\t.sub { font-size: 9px; letter-spacing: 1px; color: $muted; margin-top: 3px; }\n\n\t\t\t\u0026.on\n\t\t\t{\n\t\t\t\tcolor: #7dff9b;\n\t\t\t\tborder-color: rgba(125, 255, 155, 0.5);\n\t\t\t\t\u0026:hover { background-color: rgba(125, 255, 155, 0.16); }\n\t\t\t}\n\t\t}\n\t}\n\n\t// Explains what a currency IS before offering anything to spend it on. Cores, Slag and\n\t// Marks were three unlabelled systems sharing one panel, which is why they read as noise.\n\t// Explains what a currency IS before offering anything to spend it on.\n\t//\n\t// Every rule below about width and shrinking is load-bearing. Multi-line text in a flex\n\t// column collapses to a single line\u0027s worth of height unless it is told not to shrink, and\n\t// the overflow then draws straight over the next block: that is what made the Cores and\n\t// Marks tabs render as overlapping mush.\n\t.primer\n\t{\n\t\tflex-direction: column;\n\t\twidth: 100%;\n\t\tflex-shrink: 0;\n\t\tpadding: 12px 16px;\n\t\tmargin-bottom: 14px;\n\t\tbackground-color: rgba( 0, 0, 0, 0.25 );\n\t\tborder-left: 2px solid $accent-dim;\n\t\topacity: 0.9;\n\n\t\t.ptitle\n\t\t{\n\t\t\twidth: 100%;\n\t\t\tflex-shrink: 0;\n\t\t\tfont-size: 11px;\n\t\t\tletter-spacing: 3px;\n\t\t\tline-height: 18px;\n\t\t\tmargin-bottom: 6px;\n\t\t\tcolor: $accent;\n\t\t}\n\n\t\t.ptext\n\t\t{\n\t\t\twidth: 100%;\n\t\t\tflex-shrink: 0;\n\t\t\tfont-size: 12px;\n\t\t\tline-height: 18px;\n\t\t\tmargin-bottom: 8px;\n\t\t\tcolor: $muted;\n\t\t\tword-break: break-word;\n\t\t}\n\t}\n\n\t// The concrete before/after for the NEXT purchase. This is the line that makes a buy\n\t// decision possible without doing the arithmetic yourself.\n\t//\n\t// Promoted to the second most prominent thing in a card, after the name. It used to be\n\t// smaller than the description, which buried the only sentence that answers the question\n\t// the player is actually asking.\n\t.gain\n\t{\n\t\twidth: 100%;\n\t\tflex-shrink: 0;\n\t\tfont-size: 13px;\n\t\tline-height: 18px;\n\t\tcolor: $charge;\n\t\tmargin: 9px 0px 7px 0px;\n\t\tword-break: break-word;\n\t}\n\n\t.cores-head\n\t{\n\t\tjustify-content: center;\n\t\talign-items: baseline;\n\t\tpadding: 14px 0px 16px 0px;\n\t\tmargin-bottom: 12px;\n\t\tfont-size: 12px;\n\t\tletter-spacing: 3px;\n\t\tcolor: $muted;\n\t\tborder-bottom: 1px solid rgba( 255, 122, 28, 0.18 );\n\n\t\tb { color: $charge; font-size: 26px; letter-spacing: 0px; }\n\t}\n\n\t.travel\n\t{\n\t\tmargin-top: 14px;\n\t\tpadding: 12px;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\ttext-align: center;\n\t\tborder: 1px solid $accent-dim;\n\t\tbackground-color: rgba(255, 122, 28, 0.08);\n\n\t\t.title { font-size: 15px; letter-spacing: 4px; color: $accent; }\n\t\t.sub { font-size: 11px; color: $muted; margin-top: 4px; }\n\n\t\t\u0026:hover { background-color: rgba(255, 122, 28, 0.22); }\n\n\t\t\u0026.back\n\t\t{\n\t\t\tborder-color: rgba(255, 255, 255, 0.14);\n\t\t\tbackground-color: rgba(255, 255, 255, 0.04);\n\t\t\t.title { color: $text; }\n\t\t}\n\t}\n\n\t.collapse\n\t{\n\t\tmargin-top: 10px;\n\t\tpadding: 14px;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\t\topacity: 0.5;\n\n\t\t.title { font-size: 16px; letter-spacing: 5px; }\n\t\t.sub { font-size: 12px; color: $muted; margin-top: 4px; }\n\n\t\t\u0026.ready\n\t\t{\n\t\t\topacity: 1;\n\t\t\tborder-color: $charge;\n\t\t\tbackground-color: rgba(255, 177, 74, 0.1);\n\n\t\t\t.title { color: $charge; }\n\n\t\t\t\u0026:hover { background-color: rgba(255, 177, 74, 0.22); }\n\t\t}\n\t}\n}\n"},{"Ident":"idkman.monolith","Path":"Assembly.cs","FileName":"Assembly.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"global using Sandbox;\nglobal using System;\nglobal using System.Collections.Generic;\nglobal using System.Linq;\n"},{"Ident":"idkman.monolith","Path":"Game/AudioSettings.cs","FileName":"AudioSettings.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"namespace Monolith;\n\n/// \u003Csummary\u003E\n/// Sound and music volume, persisted separately from progress.\n///\n/// **Why its own file rather than a field on \u003Cc\u003ESaveData\u003C/c\u003E.** These are device settings, not\n/// progress. Someone who collapses and starts a fresh run should not get their audio turned back\n/// on, and more practically, the music component reads this during \u003Cc\u003EOnStart\u003C/c\u003E, which can run\n/// before \u003Csee cref=\u0022PlayerProgress\u0022/\u003E has loaded anything. A static with its own file has no\n/// ordering problem to solve.\n///\n/// **Why volumes and not booleans.** The request was for a mute, and mute is what the menu\n/// offers, but storing a float costs nothing extra and means a later \u002250%\u0022 option is a UI change\n/// rather than a save format change. Muted is simply zero.\n/// \u003C/summary\u003E\npublic static class AudioSettings\n{\n\tprivate const string SettingsPath = \u0022audio.json\u0022;\n\n\t/// \u003Csummary\u003EThe stored shape. Public because the JSON serialiser needs to see the fields.\u003C/summary\u003E\n\tpublic sealed class Data\n\t{\n\t\tpublic float Music { get; set; } = 1f;\n\t\tpublic float Effects { get; set; } = 1f;\n\t}\n\n\tprivate static Data current;\n\n\tprivate static Data Current\n\t{\n\t\tget\n\t\t{\n\t\t\tif ( current != null )\n\t\t\t\treturn current;\n\n\t\t\ttry\n\t\t\t{\n\t\t\t\tcurrent = FileSystem.Data.ReadJsonOrDefault\u003CData\u003E( SettingsPath, null ) ?? new Data();\n\t\t\t}\n\t\t\tcatch ( Exception e )\n\t\t\t{\n\t\t\t\tLog.Warning( $\u0022Could not load audio settings, using defaults: {e.Message}\u0022 );\n\t\t\t\tcurrent = new Data();\n\t\t\t}\n\n\t\t\treturn current;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EMusic volume, 0 to 1. Read every frame by \u003Csee cref=\u0022MonolithMusic\u0022/\u003E.\u003C/summary\u003E\n\tpublic static float MusicVolume =\u003E Current.Music;\n\n\t/// \u003Csummary\u003ESound effect volume, 0 to 1. Applied inside \u003Csee cref=\u0022Audio\u0022/\u003E.\u003C/summary\u003E\n\tpublic static float EffectsVolume =\u003E Current.Effects;\n\n\tpublic static bool MusicMuted =\u003E MusicVolume \u003C= 0.001f;\n\n\tpublic static bool EffectsMuted =\u003E EffectsVolume \u003C= 0.001f;\n\n\tpublic static void ToggleMusic()\n\t{\n\t\tCurrent.Music = MusicMuted ? 1f : 0f;\n\t\tSave();\n\t}\n\n\tpublic static void ToggleEffects()\n\t{\n\t\tCurrent.Effects = EffectsMuted ? 1f : 0f;\n\t\tSave();\n\t}\n\n\tprivate static void Save()\n\t{\n\t\ttry\n\t\t{\n\t\t\tFileSystem.Data.WriteJson( SettingsPath, Current );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\u0022Could not save audio settings: {e.Message}\u0022 );\n\t\t}\n\t}\n}\n"},{"Ident":"idkman.monolith","Path":"Game/CoreTree.cs","FileName":"CoreTree.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"namespace Monolith;\n\npublic enum CoreNodeKind\n{\n\tDeepening,\n\tCadence,\n\tAvarice,\n\tAnchored,\n\tForesight,\n\tSympathy,\n}\n\npublic sealed class CoreNodeDef\n{\n\tpublic CoreNodeKind Kind;\n\tpublic string Name;\n\tpublic string Description;\n\n\t/// \u003Csummary\u003ECores for the first level. Later levels scale by \u003Csee cref=\u0022CoreTree.Growth\u0022/\u003E.\u003C/summary\u003E\n\tpublic int BaseCost;\n\n\tpublic Func\u003Cint, string\u003E Format;\n\n\t/// \u003Csummary\u003EWhat the next level actually changes, in numbers. See UpgradeDef.Gain.\u003C/summary\u003E\n\tpublic Func\u003Cint, string\u003E Gain;\n}\n\n/// \u003Csummary\u003E\n/// Cores are SPENT here rather than being a flat multiplier.\n///\n/// A flat \u0022\u002B10% per Core\u0022 is the least interesting prestige a game can have: it changes the\n/// speed of a run and nothing else, so every run after the first is the same run slightly\n/// faster. Every node below has to change how a run is actually played, which is the standard\n/// Cookie Clicker\u0027s heavenly upgrades set and the reason its ascensions stay interesting\n/// hundreds of hours in.\n/// \u003C/summary\u003E\npublic static class CoreTree\n{\n\t/// \u003Csummary\u003ECost multiplier per level of the same node.\u003C/summary\u003E\n\tpublic const double Growth = 1.75;\n\n\tpublic static readonly CoreNodeDef[] All =\n\t{\n\t\tnew()\n\t\t{\n\t\t\tKind = CoreNodeKind.Deepening,\n\t\t\tName = \u0022Deepening\u0022,\n\t\t\tDescription = \u0022Every shot bites deeper. Blast radius scales harder, forever.\u0022,\n\t\t\tBaseCost = 1,\n\t\t\tFormat = lvl =\u003E $\u0022\u002B{lvl * Tuning.CoreDeepeningPerLevel * 100f:0}% blast radius\u0022,\n\t\t\tGain = lvl =\u003E $\u0022blast radius \u002B{lvl * Tuning.CoreDeepeningPerLevel * 100f:0}% to \u0022\n\t\t\t\t\u002B $\u0022\u002B{(lvl \u002B 1) * Tuning.CoreDeepeningPerLevel * 100f:0}%, permanently. \u0022\n\t\t\t\t\u002B \u0022Radius is cubed, so this is roughly \u0022\n\t\t\t\t\u002B $\u0022\u002B{(MathF.Pow( 1f \u002B (lvl \u002B 1) * Tuning.CoreDeepeningPerLevel, 3f ) / MathF.Pow( 1f \u002B lvl * Tuning.CoreDeepeningPerLevel, 3f ) - 1f) * 100f:0}% cubes per shot\u0022,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tKind = CoreNodeKind.Cadence,\n\t\t\tName = \u0022Cadence\u0022,\n\t\t\tDescription = \u0022You fire faster and your charges come back sooner.\u0022,\n\t\t\tBaseCost = 1,\n\t\t\tFormat = lvl =\u003E $\u0022\u002B{lvl * Tuning.CoreCadencePerLevel * 100f:0}% speed, \u0022 \u002B\n\t\t\t\t$\u0022-{lvl * Tuning.CoreCadenceCooldownPerLevel:0.0}s charge cooldown\u0022,\n\t\t\tGain = lvl =\u003E $\u0022fire rate \u002B{lvl * Tuning.CoreCadencePerLevel * 100f:0}% to \u0022\n\t\t\t\t\u002B $\u0022\u002B{(lvl \u002B 1) * Tuning.CoreCadencePerLevel * 100f:0}%, and charge cooldown \u0022\n\t\t\t\t\u002B $\u0022{MathF.Max( Tuning.DemolitionCooldownFloor, Tuning.DemolitionCooldown - lvl * Tuning.CoreCadenceCooldownPerLevel ):0.0}s to \u0022\n\t\t\t\t\u002B $\u0022{MathF.Max( Tuning.DemolitionCooldownFloor, Tuning.DemolitionCooldown - (lvl \u002B 1) * Tuning.CoreCadenceCooldownPerLevel ):0.0}s \u0022\n\t\t\t\t\u002B $\u0022(floor {Tuning.DemolitionCooldownFloor:0.0}s)\u0022,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tKind = CoreNodeKind.Avarice,\n\t\t\tName = \u0022Avarice\u0022,\n\t\t\tDescription = \u0022More dust per cube, so the next run arms itself faster.\u0022,\n\t\t\tBaseCost = 1,\n\t\t\tFormat = lvl =\u003E $\u0022\u002B{lvl * Tuning.CoreAvaricePerLevel * 100f:0}% dust\u0022,\n\t\t\tGain = lvl =\u003E $\u0022dust per cube \u002B{lvl * Tuning.CoreAvaricePerLevel * 100f:0}% to \u0022\n\t\t\t\t\u002B $\u0022\u002B{(lvl \u002B 1) * Tuning.CoreAvaricePerLevel * 100f:0}%, on every run from now on\u0022,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tKind = CoreNodeKind.Anchored,\n\t\t\tName = \u0022Anchored Upgrades\u0022,\n\t\t\tDescription = \u0022Keep some upgrade levels through a Collapse instead of losing all of them.\u0022,\n\t\t\tBaseCost = 3,\n\t\t\tFormat = lvl =\u003E $\u0022keep {lvl * Tuning.CoreAnchoredPerLevel} levels of each upgrade\u0022,\n\t\t\tGain = lvl =\u003E $\u0022keep {lvl * Tuning.CoreAnchoredPerLevel} to \u0022\n\t\t\t\t\u002B $\u0022{(lvl \u002B 1) * Tuning.CoreAnchoredPerLevel} levels of EACH of the \u0022\n\t\t\t\t\u002B $\u0022{Upgrades.All.Length} upgrades through a Collapse\u0022,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tKind = CoreNodeKind.Foresight,\n\t\t\tName = \u0022Foresight\u0022,\n\t\t\tDescription = \u0022Skip the opening stages entirely. Start each run further up the ladder.\u0022,\n\t\t\tBaseCost = 2,\n\t\t\tFormat = lvl =\u003E $\u0022start at stage {1 \u002B lvl * Tuning.CoreForesightPerLevel}\u0022,\n\t\t\tGain = lvl =\u003E $\u0022every run starts at stage {1 \u002B lvl * Tuning.CoreForesightPerLevel}, \u0022\n\t\t\t\t\u002B $\u0022now stage {1 \u002B (lvl \u002B 1) * Tuning.CoreForesightPerLevel}: \u0022\n\t\t\t\t\u002B $\u0022{Tuning.CoreForesightPerLevel} fewer stages to climb of the \u0022\n\t\t\t\t\u002B $\u0022{Tuning.PrestigeStageRequirement}\u0022,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tKind = CoreNodeKind.Sympathy,\n\t\t\tName = \u0022Sympathy\u0022,\n\t\t\tDescription = \u0022Charges become part of your baseline rather than something you buy.\u0022,\n\t\t\tBaseCost = 2,\n\t\t\tFormat = lvl =\u003E $\u0022\u002B{lvl * Tuning.CoreSympathyPerLevel * 100f:0.0}% charge chance\u0022,\n\t\t\tGain = lvl =\u003E $\u0022volatile blocks \u002B{lvl * Tuning.CoreSympathyPerLevel * 100f:0.0}% to \u0022\n\t\t\t\t\u002B $\u0022\u002B{(lvl \u002B 1) * Tuning.CoreSympathyPerLevel * 100f:0.0}% before you buy any \u0022\n\t\t\t\t\u002B \u0022Instability at all\u0022,\n\t\t},\n\t};\n\n\tpublic static CoreNodeDef Get( CoreNodeKind kind ) =\u003E All.First( x =\u003E x.Kind == kind );\n\n\tpublic static int CostAt( CoreNodeDef def, int level )\n\t\t=\u003E Math.Max( 1, (int)Math.Round( def.BaseCost * Math.Pow( Growth, level ) ) );\n}\n"},{"Ident":"idkman.monolith","Path":"Game/GameBootstrap.cs","FileName":"GameBootstrap.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"using Sandbox.Network;\n\nnamespace Monolith;\n\n/// \u003Csummary\u003E\n/// Starts (or joins) the session and builds this client\u0027s local rig. The rig is deliberately\n/// NOT a networked object in v1: the only thing that has to be shared is the monolith, and\n/// keeping players local removes the whole prefab and ownership surface. Avatars come later.\n/// \u003C/summary\u003E\npublic sealed class GameBootstrap : Component, Component.INetworkListener\n{\n\t[Property] public bool StartServer { get; set; } = true;\n\n\tprivate GameObject rig;\n\tprivate bool placed;\n\n\tprotected override void OnStart()\n\t{\n\t\tLogBuildStamp();\n\n\t\tif ( StartServer \u0026\u0026 !Networking.IsActive )\n\t\t{\n\t\t\tNetworking.CreateLobby( new LobbyConfig\n\t\t\t{\n\t\t\t\tMaxPlayers = 32,\n\t\t\t\tPrivacy = LobbyPrivacy.Public,\n\t\t\t\tName = \u0022MONOLITH\u0022,\n\t\t\t} );\n\t\t}\n\n\t\tCreateLocalRig();\n\t}\n\n\t// NAMES ARE NOT LOGGED.\n\t//\n\t// Streamer Mode gives a player a generic anonymous name precisely so it is not shown, and\n\t// neither \u0060Connection.Name\u0060 nor \u0060Connection.DisplayName\u0060 is documented as honouring it:\n\t// DisplayName is described only as the name \u0022with any potential nicknames or naughty words\n\t// filtered out\u0022, which is a profanity filter, not anonymity.\n\t//\n\t// Since the console gets screenshotted and shared, and the join line carries no debugging\n\t// value that the count does not, the safe reading is to print neither. Nothing downstream\n\t// needs a name: the leaderboard is the only place one is displayed, and it uses the\n\t// DisplayName the service itself returns.\n\n\tpublic void OnActive( Connection channel )\n\t{\n\t\tLog.Info( $\u0022A player joined. ({Connection.All.Count} connected)\u0022 );\n\t}\n\n\tpublic void OnDisconnected( Connection channel )\n\t{\n\t\tLog.Info( $\u0022A player left. ({Connection.All.Count} connected)\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Prints the tuning values the RUNNING build actually has.\n\t///\n\t/// A clean s\u0026amp;box compile is silent: the log records failures and nothing else. That makes\n\t/// \u0022no errors in the log\u0022 indistinguishable from \u0022nothing compiled\u0022, and this session has\n\t/// repeatedly reported the first while the second was true, including several times when the\n\t/// editor was not even running. There was no way to tell from the outside.\n\t///\n\t/// This is that way. Grep the log for \u0060[build]\u0060 and compare against the source: if the values\n\t/// match, the code you are looking at is the code that is running. If the line is missing or\n\t/// stale, nothing you just changed is live, whatever the absence of errors suggests.\n\t/// \u003C/summary\u003E\n\tprivate static void LogBuildStamp()\n\t{\n\t\tLog.Info( $\u0022[build] MONOLITH | spotter lock {Tuning.SpotterLockSeconds}s \u0022\n\t\t\t\u002B $\u0022cone {Tuning.SpotterViewCone} | sentinel arm {Tuning.SentinelArmSeconds}s \u0022\n\t\t\t\u002B $\u0022range {Tuning.SentinelRange} | maxproj {Tuning.MaxProjectileCount} \u0022\n\t\t\t\u002B $\u0022visible {Tuning.MaxVisibleProjectiles} | pixel {Tuning.RetroPixelScale} \u0022\n\t\t\t\u002B $\u0022| stage growth {Tuning.StageSizeGrowth}\u0022 );\n\n\t\t// The systems added most recently, for the same reason the line exists at all: a stamp\n\t\t// that does not mention the thing you just changed cannot tell you whether it is live.\n\t\tLog.Info( $\u0022[build] floor from stage {Tuning.FloorHazardFromStage} | \u0022\n\t\t\t\u002B $\u0022tile {Tuning.FloorTileSize}u | phase {Tuning.FloorPhaseSeconds}s \u0022\n\t\t\t\u002B $\u0022warn {Tuning.FloorWarnSeconds}s | dead {Tuning.FloorDeadChance:P0} \u0022\n\t\t\t\u002B $\u0022raised {Tuning.FloorDeadHeight}u | rocket {Tuning.RocketJumpForce} \u0022\n\t\t\t\u002B $\u0022blast {Tuning.RocketJumpBlastFraction:P0} | crawlers {Tuning.CrawlerMaxCount}\u0022 );\n\n\t\tLog.Info( $\u0022[build] look: ao {Tuning.VoxelAoStrength} | strata {Tuning.StrataBands}x\u0022\n\t\t\t\u002B $\u0022{Tuning.StrataThickness} contrast {Tuning.StrataContrast} | \u0022\n\t\t\t\u002B $\u0022shrapnel {Tuning.ShrapnelMaxPerBurst}/burst | \u0022\n\t\t\t\u002B $\u0022floor hot {Tuning.FloorHotFraction:P0} dead {Tuning.FloorDeadChance:P1}\u0022 );\n\t}\n\n\t// REAL clock, not the game clock. A frozen game clock is one of the things this exists to\n\t// detect, and a GameTimeSince here would never reach its interval in exactly that case: the\n\t// diagnostic would go silent precisely when it was needed.\n\tprivate TimeSince timeSinceReport = 99f;\n\tprivate int reportsLeft = 3;\n\n\t/// \u003Csummary\u003E\n\t/// Prints the gating state for the first few seconds of a session.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// \u0022Cannot shoot\u0022 and \u0022spawned inside the shape\u0022 both have several possible causes that look\n\t/// identical from the outside: a stuck pause, a stuck cursor, a missing snapshot, or a rig\n\t/// that was never repositioned. This prints all four at once so the next launch answers the\n\t/// question instead of narrowing it.\n\t///\n\t/// Uses the REAL clock, not the game clock, because one of the things it exists to diagnose\n\t/// is the game clock being frozen.\n\t/// \u003C/remarks\u003E\n\tprivate void ReportState()\n\t{\n\t\tif ( reportsLeft \u003C= 0 || timeSinceReport \u003C 1f )\n\t\t\treturn;\n\n\t\ttimeSinceReport = 0f;\n\t\treportsLeft--;\n\n\t\tvar manager = MonolithManager.Instance;\n\t\tvar movement = rig.IsValid() ? rig.Components.Get\u003CPlayerMovement\u003E() : null;\n\n\t\tstring where = \u0022no movement\u0022;\n\t\tstring shape = \u0022no world\u0022;\n\n\t\tif ( movement.IsValid() )\n\t\t\twhere = $\u0022{movement.WorldPosition} floorZ {movement.FloorZ:0}\u0022;\n\n\t\tif ( manager.IsValid() \u0026\u0026 manager.World != null )\n\t\t{\n\t\t\tvar b = manager.World.WorldBounds;\n\t\t\tshape = $\u0022centre {b.Center} mins {b.Mins} size {b.Size}\u0022;\n\n\t\t\tif ( movement.IsValid() )\n\t\t\t{\n\t\t\t\tfloat flat = (movement.WorldPosition - b.Center).WithZ( 0f ).Length;\n\t\t\t\tshape \u002B= $\u0022 | player {flat:0}u from centre\u0022;\n\t\t\t}\n\t\t}\n\n\t\tLog.Info( $\u0022[state] paused {GameTime.Paused} | cursor {Hud.CursorVisible} \u0022\n\t\t\t\u002B $\u0022| snapshot {(manager.IsValid() ? manager.SnapshotReady : false)} \u0022\n\t\t\t\u002B $\u0022| placed {placed} | miner {Scene.GetAllComponents\u003CMiner\u003E().Count()}\u0022 );\n\t\tLog.Info( $\u0022[state] player {where}\u0022 );\n\t\tLog.Info( $\u0022[state] shape  {shape}\u0022 );\n\t}\n\n\tprivate void CreateLocalRig()\n\t{\n\t\trig = new GameObject( true, \u0022Player Rig\u0022 );\n\n\t\t// Never networked: this exists only on the machine that made it.\n\t\trig.NetworkMode = NetworkMode.Never;\n\n\t\t// The camera lives on its own child object so it can sit behind the body in third\n\t\t// person. The rig itself stays at the EYE position, which is the space every bit of\n\t\t// movement, aiming and muzzle code already works in, so none of that has to change.\n\t\tvar cameraObject = new GameObject( true, \u0022Camera Boom\u0022 );\n\t\tcameraObject.Parent = rig;\n\t\tcameraObject.NetworkMode = NetworkMode.Never;\n\n\t\tvar camera = cameraObject.AddComponent\u003CCameraComponent\u003E();\n\t\tcamera.IsMainCamera = true;\n\t\tcamera.ClearFlags = ClearFlags.All;\n\t\tcamera.ZNear = 4f;\n\t\tcamera.ZFar = 100_000f;\n\t\tcamera.FieldOfView = 75f;\n\t\t// Pure black. Devil Daggers is mostly empty screen, and everything reads because it is\n\t\t// the only lit thing in frame.\n\t\tcamera.BackgroundColor = Color.Black;\n\n\t\t// A carved voxel mass is almost all coplanar faces, so direct lighting alone leaves it\n\t\t// reading as flat colour and you cannot tell what a click actually did. Ambient\n\t\t// occlusion is what makes craters, ledges and shafts legible.\n\t\t//\n\t\t// These are camera effects, so they belong on the camera object, not the rig.\n\t\tvar ao = cameraObject.AddComponent\u003CAmbientOcclusion\u003E();\n\t\t// Untyped literals: these engine properties differ in type between versions, and an\n\t\t// integer literal is valid whether the property is int or float.\n\t\t//\n\t\t// Dropped from 2 to 1: at full strength, stacked with the retro colour pass, AO was\n\t\t// filling every crevice with black and the carved surface became a dark smear. It is\n\t\t// here to READ the geometry, and past a point it hides it instead.\n\t\tao.Intensity = 1;\n\t\tao.Radius = 90;\n\n\t\t// YOU CARRY LIGHT. This is how the reference stays readable while being mostly black:\n\t\t// the action is lit and the distance falls to nothing, rather than everything being\n\t\t// uniformly dim. A lamp on the rig means the rock you are actually working is always\n\t\t// bright, and it solves the readability problem without lifting the void at all.\n\t\tvar lampObject = new GameObject( true, \u0022Player Lamp\u0022 );\n\t\tlampObject.Parent = rig;\n\t\tlampObject.NetworkMode = NetworkMode.Never;\n\t\tlampObject.LocalPosition = new Vector3( 40f, 0f, 30f );\n\n\t\tvar lamp = lampObject.AddComponent\u003CPointLight\u003E();\n\t\tlamp.LightColor = new Color( 1f, 0.72f, 0.42f ) * Tuning.PlayerLampBrightness;\n\t\tlamp.Radius = Tuning.PlayerLampRadius;\n\t\tlamp.Shadows = false;\n\n\t\tcameraObject.AddComponent\u003CBloom\u003E();\n\t\tcameraObject.AddComponent\u003CTonemapping\u003E();\n\n\t\tApplyRetroLook( cameraObject );\n\n\t\trig.AddComponent\u003CPlayerMovement\u003E();\n\t\trig.AddComponent\u003CPlayerProgress\u003E();\n\n\t\t// On the rig rather than on the world, because the score is per listener: it answers to\n\t\t// what is hunting YOU, and in a shared session two players should not hear one mix.\n\t\trig.AddComponent\u003CMonolithMusic\u003E();\n\n\t\tvar avatar = rig.AddComponent\u003CPlayerAvatar\u003E();\n\t\tavatar.CameraObject = cameraObject;\n\n\t\tvar miner = rig.AddComponent\u003CMiner\u003E();\n\t\tminer.Camera = camera;\n\n\t\tvar hudObject = new GameObject( true, \u0022HUD\u0022 );\n\t\thudObject.Parent = rig;\n\t\thudObject.NetworkMode = NetworkMode.Never;\n\t\thudObject.AddComponent\u003CScreenPanel\u003E();\n\t\thudObject.AddComponent\u003CHud\u003E();\n\n\t\t// Capture tools. Harmless in a shipped build: it does nothing until F9 or F10 is pressed.\n\t\trig.AddComponent\u003CPhotoMode\u003E();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The Devil Daggers look, as far as it can honestly be applied to what we render.\n\t///\n\t/// The research (GOALS 6d) gives four ingredients: a **320x240 render resolution**,\n\t/// **unfiltered textures with no anti-aliasing**, **low colour depth that is dithered**, and\n\t/// an **unshaded albedo shader with vertex colour faking the lighting**.\n\t///\n\t/// Three of those we can take. The fourth we deliberately cannot, and it is worth being\n\t/// precise about why rather than half-doing it:\n\t///\n\t/// **We keep our lighting and our ambient occlusion.** Devil Daggers can afford to be unlit\n\t/// because it has TEXTURES, and texture detail is what tells you the shape of a surface. Our\n\t/// monolith is untextured coplanar cubes. Strip the lighting and a crater, a ledge and a flat\n\t/// wall all become the same solid orange silhouette, and you can no longer see what your last\n\t/// shot did. AO is doing the job that texture does in the reference, so removing it to match\n\t/// the technique would lose the thing the technique was for.\n\t///\n\t/// Everything else is a post-process, which is exactly where a look like this belongs.\n\t/// \u003C/summary\u003E\n\tprivate static void ApplyRetroLook( GameObject cameraObject )\n\t{\n\t\t// 1. THE RESOLUTION. The single biggest contributor: chunky pixels, hard edges, no AA.\n\t\tvar pixelate = cameraObject.AddComponent\u003CPixelate\u003E();\n\t\tpixelate.Scale = Tuning.RetroPixelScale;\n\n\t\t// 2. THE PALETTE. Low colour depth reads as heavy contrast and reduced saturation, which\n\t\t// is what pushes everything toward the bone-and-ember range rather than full colour.\n\t\tvar colour = cameraObject.AddComponent\u003CColorAdjustments\u003E();\n\t\tcolour.Saturation = Tuning.RetroSaturation;\n\t\tcolour.Contrast = Tuning.RetroContrast;\n\t\tcolour.Brightness = Tuning.RetroBrightness;\n\n\t\t// 3. THE GRAIN. Standing in for dithering, which has no built-in component. Not the same\n\t\t// technique, but it does the same job: it breaks up flat areas so they stop reading as\n\t\t// clean modern gradients.\n\t\tvar grain = cameraObject.AddComponent\u003CFilmGrain\u003E();\n\t\tgrain.Intensity = Tuning.RetroGrain;\n\t\tgrain.Response = 0.5f;\n\n\t\t// 4. THE DARKNESS. The reference is mostly black with the action lit in the middle.\n\t\t// A vignette is the cheapest way to stop our arena grid from filling the corners.\n\t\tvar vignette = cameraObject.AddComponent\u003CVignette\u003E();\n\t\tvignette.Intensity = Tuning.RetroVignette;\n\t\tvignette.Roundness = 1f;\n\t\tvignette.Smoothness = 1f;\n\t\tvignette.Color = Color.Black;\n\n\t\t// Losing a stage had NO feedback at all: the manager recorded the reset and nothing read\n\t\t// it. Added last so it can find the vignette above and borrow it for the red wash.\n\t\tcameraObject.AddComponent\u003CStageLostEffect\u003E();\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\t// The pause clock is driven from here because this is the one component that is never\n\t\t// itself gated by the pause. Anything that freezes cannot be trusted to count the freeze.\n\t\tGameTime.Advance();\n\n\t\tReportState();\n\n\t\tif ( placed || !rig.IsValid() )\n\t\t\treturn;\n\n\t\tvar world = MonolithManager.Instance?.World;\n\t\tif ( world == null )\n\t\t\treturn;\n\n\t\t// Stand on the arena floor a little back from the shape, looking at it.\n\t\tvar bounds = world.WorldBounds;\n\t\tfloat floorZ = bounds.Mins.z;\n\t\tfloat distance = MathF.Max( 420f, bounds.Size.Length * 0.85f );\n\n\t\tvar position = bounds.Center.WithZ( floorZ )\n\t\t\t\u002B new Vector3( 0.35f, -1f, 0f ).Normal * distance;\n\n\t\tif ( rig.Components.TryGet\u003CPlayerMovement\u003E( out var movement ) )\n\t\t{\n\t\t\tmovement.FloorZ = floorZ;\n\t\t\tmovement.ArenaCentre = bounds.Center;\n\n\t\t\tvar eye = position.WithZ( floorZ \u002B movement.EyeHeight );\n\t\t\tmovement.PlaceOnFloor( position, Rotation.LookAt( (bounds.Center - eye).Normal ) );\n\t\t}\n\n\t\tplaced = true;\n\t}\n}\n"},{"Ident":"idkman.monolith","Path":"Game/PhotoMode.cs","FileName":"PhotoMode.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"namespace Monolith;\n\n/// \u003Csummary\u003E\n/// Capture tools for the store page. P hides the HUD, O takes a screenshot, and F6 video\n/// recording hides the HUD by itself.\n///\n/// **Why this exists.** The store screenshot page asks you to avoid text and logos and let the\n/// gameplay speak for itself, and this game draws a HUD every frame: a progress bar, a wallet, a\n/// charge readout, a crosshair. Every screenshot taken normally has all of it in shot. There was\n/// no way to get a clean frame without deleting components, so the useful screenshots were\n/// effectively impossible to take.\n///\n/// **The HUD is HIDDEN, not disabled.** Turning the component off would stop its \u0060OnUpdate\u0060,\n/// which is where the pause is decided, where the cursor is managed and where the charge marker\n/// is projected. Half the game would quietly change behaviour in exactly the frames being\n/// photographed. Setting a class that makes it invisible leaves every one of those systems\n/// running, so what you capture is the real game with the overlay taken off the glass.\n///\n/// **Deliberately not automated.** An earlier idea was a camera tour that flew to a few angles\n/// and captured on a timer. It would have produced pictures of an empty arena: what makes a\n/// screenshot worth looking at here is a Spotter mid-lock, a floor cycle caught on the amber\n/// frame, shrapnel rings on the ground with the chunks still in the air. A machine does not know\n/// when that is happening and a player does. This puts the shutter under your thumb instead.\n/// \u003C/summary\u003E\npublic sealed class PhotoMode : Component\n{\n\t/// \u003Csummary\u003E\n\t/// Native resolution capture. Passing the real window size means the shot matches what you\n\t/// framed rather than being re-rendered at some other aspect and cropped by the store.\n\t/// \u003C/summary\u003E\n\tprivate static readonly int Width = 1920;\n\tprivate static readonly int Height = 1080;\n\n\t// REAL clock. Photo mode deliberately works while the world is frozen, and a GameTimeSince\n\t// stops advancing there, so the shutter cooldown would never expire: you would get exactly\n\t// one screenshot per pause and no indication why the second did nothing. Same trap as the\n\t// state diagnostic in GameBootstrap.\n\tprivate TimeSince timeSinceShot = 99f;\n\n\tprotected override void OnStart()\n\t{\n\t\tLog.Info( \u0022[photo] ready. P hides the HUD, O captures 1920x1080, \u0022\n\t\t\t\u002B \u0022F6 records video and hides the HUD while it runs.\u0022 );\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\t// NOT gated on GameTime.Paused. Capturing with the world frozen is often the ONLY way to\n\t\t// get a clean frame of something fast, and a pause menu open behind a hidden HUD is\n\t\t// exactly the moment you want the shutter to work.\n\n\t\tWatchRecording();\n\n\t\tif ( Input.Pressed( \u0022PhotoHud\u0022 ) )\n\t\t{\n\t\t\tHud.PhotoHidden = !Hud.PhotoHidden;\n\n\t\t\tLog.Info( Hud.PhotoHidden\n\t\t\t\t? \u0022[photo] HUD hidden. O to capture, P to bring it back.\u0022\n\t\t\t\t: \u0022[photo] HUD restored.\u0022 );\n\t\t}\n\n\t\tif ( !Input.Pressed( \u0022PhotoShot\u0022 ) )\n\t\t\treturn;\n\n\t\t// A held key would otherwise fill the folder in a second.\n\t\tif ( timeSinceShot \u003C 0.4f )\n\t\t\treturn;\n\n\t\ttimeSinceShot = 0f;\n\t\tCapture();\n\t}\n\n\tprivate bool wasRecording;\n\tprivate bool hidForRecording;\n\n\t/// \u003Csummary\u003E\n\t/// Hides the HUD for the duration of an F6 video recording, and puts it back afterwards.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// **Recording cannot be started from code, so this hooks the state instead.** The engine\n\t/// exposes \u0060Game.IsRecordingVideo\u0060 as a read-only property and documents F6 and a \u0060video\u0060\n\t/// console command as the ways in, but there is no published API to trigger either.\n\t/// \u0060Sandbox.ConsoleSystem\u0060 exists as a type with no documented members, and guessing at a\n\t/// method signature there would be a compile error rather than something catchable.\n\t///\n\t/// Watching the flag gets the useful half anyway. The store wants gameplay without text or\n\t/// logos, and remembering to hide the HUD before every take is exactly the sort of thing you\n\t/// forget until you have recorded the good one with a progress bar across it.\n\t///\n\t/// Only restores the HUD if RECORDING hid it. Someone who pressed P first and then started\n\t/// recording wants it to stay hidden when they stop.\n\t/// \u003C/remarks\u003E\n\tprivate void WatchRecording()\n\t{\n\t\tbool recording;\n\n\t\ttry { recording = Game.IsRecordingVideo; }\n\t\tcatch ( Exception ) { return; }\n\n\t\tif ( recording == wasRecording )\n\t\t\treturn;\n\n\t\twasRecording = recording;\n\n\t\tif ( recording )\n\t\t{\n\t\t\thidForRecording = !Hud.PhotoHidden;\n\n\t\t\tif ( hidForRecording )\n\t\t\t\tHud.PhotoHidden = true;\n\n\t\t\tLog.Info( \u0022[photo] recording started, HUD hidden.\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tif ( hidForRecording )\n\t\t{\n\t\t\tHud.PhotoHidden = false;\n\t\t\thidForRecording = false;\n\t\t}\n\n\t\tLog.Info( \u0022[photo] recording stopped.\u0022 );\n\t}\n\n\tprivate static void Capture()\n\t{\n\t\ttry\n\t\t{\n\t\t\tGame.TakeHighResScreenshot( Width, Height );\n\n\t\t\tLog.Info( $\u0022[photo] captured {Width}x{Height} to \u0022\n\t\t\t\t\u002B \u0022D:/SteamLibrary/steamapps/common/sbox/screenshots\u0022 );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\t// Falls back to the plain capture, which goes to the Steam screenshot library. Worth\n\t\t\t// having: a failed screenshot at the moment you finally caught a good frame is a\n\t\t\t// genuinely annoying way to lose one.\n\t\t\tLog.Warning( $\u0022[photo] high-res capture failed ({e.Message}), trying plain.\u0022 );\n\n\t\t\ttry { Game.TakeScreenshot(); }\n\t\t\tcatch ( Exception inner ) { Log.Warning( $\u0022[photo] capture failed: {inner.Message}\u0022 ); }\n\t\t}\n\t}\n}\n"},{"Ident":"idkman.monolith","Path":"Game/PlayerAvatar.cs","FileName":"PlayerAvatar.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"namespace Monolith;\n\n/// \u003Csummary\u003E\n/// The player\u0027s body: a Terry wearing whatever the local user has actually configured, plus the\n/// third person camera that looks at him.\n///\n/// Two things are worth knowing about how this is wired.\n///\n/// **The rig stays at the eye position.** All the movement, aiming and muzzle code works in eye\n/// space and none of it changes here. The body is a child hung DOWN from the rig by\n/// \u003Csee cref=\u0022PlayerMovement.EyeHeight\u0022/\u003E, and the camera is a child pulled BACK from it. That\n/// keeps a third person view from touching a single line of the movement or firing code.\n///\n/// **The clothing comes from the user, not from us.** \u0060ClothingContainer.CreateFromLocalUser()\u0060\n/// reads the avatar the player set up in s\u0026box itself, so every skin, hat and colourway they\n/// own works with no per-item support on our side.\n/// \u003C/summary\u003E\npublic sealed class PlayerAvatar : Component\n{\n\t/// \u003Csummary\u003EThe object holding the camera. Created by GameBootstrap, positioned here.\u003C/summary\u003E\n\t[Property] public GameObject CameraObject { get; set; }\n\n\t/// \u003Csummary\u003EHow far behind the eye the camera sits.\u003C/summary\u003E\n\t[Property] public float CameraDistance { get; set; } = 165f;\n\n\t/// \u003Csummary\u003EHow far above the eye, so the body does not sit in the middle of the crosshair.\u003C/summary\u003E\n\t[Property] public float CameraHeight { get; set; } = 26f;\n\n\t/// \u003Csummary\u003ESideways offset. Over the shoulder keeps the crosshair clear of your own head.\u003C/summary\u003E\n\t[Property] public float CameraShoulder { get; set; } = 34f;\n\n\tprivate GameObject bodyObject;\n\tprivate SkinnedModelRenderer body;\n\n\tprivate GameObject gunObject;\n\tprivate static Model gunModel;\n\n\t/// \u003Csummary\u003E\n\t/// The player\u0027s own avatar, kept so the drone heads can wear it too. Built once: the call\n\t/// reads user data and is not something to do per drone per frame.\n\t/// \u003C/summary\u003E\n\tprivate ClothingContainer clothing;\n\n\tprivate PlayerMovement movement;\n\tprivate Miner miner;\n\n\t/// \u003Csummary\u003E\n\t/// Where bolts actually leave from: the tip of the barrel in Terry\u0027s right hand.\n\t///\n\t/// This exists because the muzzle used to be derived from the CAMERA\n\t/// (\u0060camera.WorldPosition \u002B forward*30 \u002B right*14 \u002B down*12\u0060). In first person that reads as\n\t/// firing from just off-screen. In third person the camera is behind and to the right of the\n\t/// body, so every bolt visibly spawned in the bottom right corner of the screen with nothing\n\t/// attached to it.\n\t/// \u003C/summary\u003E\n\tpublic Vector3 MuzzlePosition { get; private set; }\n\n\t/// \u003Csummary\u003EFalse until the gun exists, so the Miner knows to fall back to the camera.\u003C/summary\u003E\n\tpublic bool HasMuzzle { get; private set; }\n\n\n\t/// \u003Csummary\u003EYaw the body is currently facing. Turns to follow movement, not the camera.\u003C/summary\u003E\n\tprivate float bodyYaw;\n\n\tprotected override void OnStart()\n\t{\n\t\tmovement = Components.Get\u003CPlayerMovement\u003E();\n\t\tminer = Components.Get\u003CMiner\u003E();\n\n\t\tbodyObject = new GameObject( true, \u0022Terry\u0022 );\n\t\tbodyObject.NetworkMode = NetworkMode.Never;\n\t\tbodyObject.Parent = GameObject;\n\n\t\tbody = bodyObject.AddComponent\u003CSkinnedModelRenderer\u003E();\n\t\tbody.Model = Model.Load( \u0022models/citizen/citizen.vmdl\u0022 );\n\n\t\tDressFromLocalUser();\n\t\tCreateGun();\n\t}\n\n\tprivate void CreateGun()\n\t{\n\t\tgunObject = new GameObject( true, \u0022Drill Gun\u0022 );\n\t\tgunObject.NetworkMode = NetworkMode.Never;\n\n\t\tvar renderer = gunObject.AddComponent\u003CModelRenderer\u003E();\n\t\trenderer.Model = GetGunModel();\n\t\trenderer.Tint = new Color( 0.62f, 0.66f, 0.74f );\n\n\t\t// A little light at the business end, so the gun reads as the source of the bolts even\n\t\t// before one is fired.\n\t\tvar light = gunObject.AddComponent\u003CPointLight\u003E();\n\t\tlight.LightColor = new Color( 1f, 0.7f, 0.32f ) * 2.2f;\n\t\tlight.Radius = 190f;\n\t\tlight.Shadows = false;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Applies the player\u0027s own s\u0026box avatar. Wrapped because a user with no avatar set, or a\n\t/// build where the clothing service is unavailable, must not take the whole rig down with it:\n\t/// an undressed Terry is a far better outcome than no player at all.\n\t/// \u003C/summary\u003E\n\tprivate void DressFromLocalUser()\n\t{\n\t\ttry\n\t\t{\n\t\t\tclothing = ClothingContainer.CreateFromLocalUser();\n\t\t\tclothing?.Apply( body );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\u0022Could not load the local avatar, using a bare Terry: {e.Message}\u0022 );\n\t\t}\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\t// Frozen while a blocking screen or the pause menu is up. See GameTime.\n\t\tif ( GameTime.Paused )\n\t\t\treturn;\n\n\t\tif ( !body.IsValid() || !movement.IsValid() )\n\t\t\treturn;\n\n\t\t// Hang the body from the eye down to the floor.\n\t\tbodyObject.LocalPosition = Vector3.Down * movement.EyeHeight;\n\n\t\tvar velocity = movement.Velocity.WithZ( 0 );\n\n\t\t// The body turns to face where you are MOVING, not where you are looking, so a strafe\n\t\t// reads as a strafe. It only turns while there is real speed, otherwise standing still\n\t\t// and sweeping the mouse would spin him on the spot.\n\t\tif ( velocity.Length \u003E 40f )\n\t\t\tbodyYaw = velocity.Normal.EulerAngles.yaw;\n\t\telse\n\t\t\tbodyYaw = WorldRotation.Angles().yaw;\n\n\t\tbodyObject.WorldRotation = Rotation.FromYaw( bodyYaw );\n\n\t\tvar local = Rotation.FromYaw( bodyYaw ).Inverse * velocity;\n\n\t\tAnim( \u0022move_x\u0022, local.x );\n\t\tAnim( \u0022move_y\u0022, local.y );\n\t\tAnim( \u0022move_z\u0022, movement.Velocity.z );\n\t\tAnim( \u0022move_groundspeed\u0022, velocity.Length );\n\t\tAnim( \u0022b_grounded\u0022, movement.IsGrounded );\n\n\t\t// AIMING. This is what makes the gun look held rather than stuck to him.\n\t\t//\n\t\t// Setting holdtype alone leaves the arms hanging at his sides: the citizen graph only\n\t\t// raises them when it has something to aim AT, which means aim_body plus a non-zero\n\t\t// aim_body_weight. Without those the pose is idle and a gun pointed at the crosshair\n\t\t// reads as a floating prop, which is exactly how it looked.\n\t\t//\n\t\t// Holdtype 1 is the one-handed pistol pose. Rifle (2) is two-handed and leaves the left\n\t\t// hand gripping empty air next to a gun that is only in the right.\n\t\tAnim( \u0022holdtype\u0022, 1 );\n\t\tAnim( \u0022holdtype_handedness\u0022, 0 );\n\t\tAnim( \u0022aim_body_weight\u0022, 1f );\n\n\t\tvar look = WorldRotation.Forward;\n\n\t\tAnimLook( \u0022aim_body\u0022, look );\n\t\tAnimLook( \u0022aim_head\u0022, look );\n\t\tAnimLook( \u0022aim_eyes\u0022, look );\n\t}\n\n\t// Each parameter is set independently on purpose. They used to share one try block, so the\n\t// first name the graph did not recognise silently skipped every parameter after it, which is\n\t// how holdtype ended up never being applied at all.\n\n\tprivate void Anim( string name, float value )\n\t{\n\t\ttry { body.Set( name, value ); } catch ( Exception ) { }\n\t}\n\n\tprivate void Anim( string name, int value )\n\t{\n\t\ttry { body.Set( name, value ); } catch ( Exception ) { }\n\t}\n\n\tprivate void Anim( string name, bool value )\n\t{\n\t\ttry { body.Set( name, value ); } catch ( Exception ) { }\n\t}\n\n\tprivate void AnimLook( string name, Vector3 direction )\n\t{\n\t\ttry { body.SetLookDirection( name, direction ); } catch ( Exception ) { }\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Camera placement runs in OnPreRender so it is guaranteed to happen after movement has\n\t/// finished writing the rig position for the frame. Doing it in OnUpdate leaves it at the\n\t/// mercy of component ordering, which shows up as a camera that judders a frame behind.\n\t/// \u003C/summary\u003E\n\tprotected override void OnPreRender()\n\t{\n\t\tPlaceGun();\n\t\tUpdateDrones();\n\n\t\tif ( !CameraObject.IsValid() )\n\t\t\treturn;\n\n\t\t// Identity local rotation means the boom inherits the rig\u0027s full rotation, pitch\n\t\t// included, so looking up and down orbits the camera the way it should.\n\t\t// Source axes: \u002BX forward, \u002BY left, \u002BZ up. Negative Y is therefore the right shoulder.\n\t\tCameraObject.LocalRotation = Rotation.Identity;\n\n\t\t// The shake is ADDED here rather than written by the effect itself, because this line\n\t\t// runs every frame and would overwrite anything it wrote.\n\t\tCameraObject.LocalPosition = new Vector3( -CameraDistance, -CameraShoulder, CameraHeight )\n\t\t\t\u002B StageLostEffect.ShakeOffset;\n\n\t\tUpdateSpeedFov();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Widens the field of view with hop speed.\n\t///\n\t/// Devil Daggers gives a perfect hop an FOV increase as its reward (GOALS 6d), and it is the\n\t/// cheapest speed cue in games: you feel fast because the world stretches, with no HUD and no\n\t/// number. Eased rather than snapped, because a per-frame FOV jump reads as a glitch.\n\t/// \u003C/summary\u003E\n\tprivate void UpdateSpeedFov()\n\t{\n\t\tvar camera = CameraObject.Components.Get\u003CCameraComponent\u003E();\n\n\t\tif ( !camera.IsValid() || !movement.IsValid() )\n\t\t\treturn;\n\n\t\tfloat wanted = Tuning.BaseFieldOfView \u002B movement.HopCharge * Tuning.HopFovBoost;\n\n\t\tcamera.FieldOfView = MathX.Lerp( camera.FieldOfView, wanted,\n\t\t\tMathF.Min( 1f, Time.Delta * 7f ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Puts the gun in Terry\u0027s right hand and points it where you are aiming.\n\t///\n\t/// Position comes from the hand bone but rotation does NOT: the animgraph decides where the\n\t/// hand is, which is nowhere near the crosshair. Taking the position from the skeleton and\n\t/// the aim from the player is what makes the bolt leave the barrel and arrive under the\n\t/// crosshair at the same time.\n\t/// \u003C/summary\u003E\n\tprivate void PlaceGun()\n\t{\n\t\tif ( !gunObject.IsValid() || !body.IsValid() )\n\t\t\treturn;\n\n\t\tvar hand = WorldPosition;\n\t\tbool found = false;\n\n\t\ttry\n\t\t{\n\t\t\tif ( body.TryGetBoneTransform( \u0022hand_R\u0022, out var bone ) )\n\t\t\t{\n\t\t\t\thand = bone.Position;\n\t\t\t\tfound = true;\n\t\t\t}\n\t\t}\n\t\tcatch ( Exception )\n\t\t{\n\t\t\t// Bone names belong to the citizen addon. Falling back is better than failing.\n\t\t}\n\n\t\tif ( !found )\n\t\t{\n\t\t\t// Roughly where a held hand sits: forward, right, and a little below the eye.\n\t\t\tvar yaw = bodyObject.IsValid() ? bodyObject.WorldRotation : WorldRotation;\n\t\t\thand = WorldPosition \u002B yaw.Forward * 22f \u002B yaw.Right * 14f \u002B Vector3.Down * 16f;\n\t\t}\n\n\t\tvar aim = miner.IsValid() ? miner.AimPoint : WorldPosition \u002B WorldRotation.Forward * 1000f;\n\t\tvar direction = (aim - hand);\n\n\t\t// Sits slightly forward of the wrist rather than dead on the bone, which is the\n\t\t// difference between held and skewered.\n\t\tgunObject.WorldPosition = hand;\n\n\t\tif ( direction.Length \u003E 1f )\n\t\t{\n\t\t\tvar aimRotation = Rotation.LookAt( direction.Normal );\n\n\t\t\t// Blended toward the aim rather than snapped to it. With aim_body driving the arm the\n\t\t\t// hand already points roughly at the target, so a hard snap fights the animation and\n\t\t\t// makes the gun swim inside the fist; easing lets the two agree.\n\t\t\tgunObject.WorldRotation = HasMuzzle\n\t\t\t\t? Rotation.Lerp( gunObject.WorldRotation, aimRotation,\n\t\t\t\t\tMathF.Min( 1f, Time.Delta * 18f ) )\n\t\t\t\t: aimRotation;\n\n\t\t\tgunObject.WorldPosition = hand \u002B aimRotation.Forward * 3f;\n\t\t}\n\n\t\t// The barrel runs along local forward, so the muzzle is simply out along it. Scaled with\n\t\t// the model when it shrank.\n\t\tMuzzlePosition = gunObject.WorldPosition \u002B gunObject.WorldRotation.Forward * 17f;\n\t\tHasMuzzle = true;\n\t}\n\n\t/// \u003Csummary\u003EKeeps the arch of drones matching the upgrade and hanging above you.\u003C/summary\u003E\n\tprivate void UpdateDrones()\n\t{\n\t\tvar progress = PlayerProgress.Local;\n\t\tif ( !progress.IsValid() )\n\t\t\treturn;\n\n\t\tint wanted = progress.DronesDisabled ? 0 : progress.DroneCount;\n\n\t\tMiningDrone.MatchPopulation( Scene, wanted );\n\n\t\tif ( MiningDrone.All.Count == 0 )\n\t\t\treturn;\n\n\t\tvar aim = miner.IsValid() ? miner.AimPoint : WorldPosition \u002B WorldRotation.Forward * 1000f;\n\n\t\t// Yaw only. Passing the full rotation would tip the whole arch into the floor whenever\n\t\t// you looked down.\n\t\tfloat yaw = WorldRotation.Angles().yaw;\n\n\t\tfor ( int i = 0; i \u003C MiningDrone.All.Count; i\u002B\u002B )\n\t\t\tMiningDrone.All[i].Follow( WorldPosition, yaw, aim, i, MiningDrone.All.Count );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A blocky slab of a gun, built the same way as everything else here: a hand-rolled vertex\n\t/// buffer, so it matches the voxel look instead of importing a detailed weapon into a game\n\t/// made of cubes.\n\t/// \u003C/summary\u003E\n\tprivate static Model GetGunModel()\n\t{\n\t\tif ( gunModel != null )\n\t\t\treturn gunModel;\n\n\t\tvar vb = new VertexBuffer();\n\t\tvb.Init( true );\n\n\t\tint index = 0;\n\n\t\t// Body, then a barrel running out along \u002BX, then a grip hanging below.\n\t\t//\n\t\t// Roughly HALF the first version. Terry is 64 units tall and the original was a 44 unit\n\t\t// slab, which is a rifle the size of his torso. A hand-held thing has to be sized against\n\t\t// the hand, not against the shape you are shooting at.\n\t\tAddBox( vb, ref index, new Vector3( 2f, 0f, 0f ), new Vector3( 13f, 4.5f, 5.5f ) );\n\t\tAddBox( vb, ref index, new Vector3( 12f, 0f, 0.8f ), new Vector3( 9f, 2.5f, 2.5f ) );\n\t\tAddBox( vb, ref index, new Vector3( -1f, 0f, -4.5f ), new Vector3( 4f, 3f, 6f ) );\n\n\t\tvar mesh = new Mesh( Material.Load( \u0022materials/default.vmat\u0022 ) );\n\t\tmesh.CreateBuffers( vb );\n\n\t\tgunModel = new ModelBuilder().AddMesh( mesh ).Create();\n\t\treturn gunModel;\n\t}\n\n\tprivate static void AddBox( VertexBuffer vb, ref int index, Vector3 centre, Vector3 size )\n\t{\n\t\tVector3[] normals =\n\t\t{\n\t\t\tVector3.Forward, Vector3.Backward, Vector3.Left,\n\t\t\tVector3.Right, Vector3.Up, Vector3.Down,\n\t\t};\n\n\t\tforeach ( var n in normals )\n\t\t{\n\t\t\tvar reference = MathF.Abs( n.z ) \u003E 0.9f ? Vector3.Forward : Vector3.Up;\n\t\t\tvar u = Vector3.Cross( n, reference ).Normal;\n\t\t\tvar v = Vector3.Cross( n, u ).Normal;\n\n\t\t\tvar face = centre \u002B n * 0.5f * Project( size, n );\n\n\t\t\tvar du = u * 0.5f * Project( size, u );\n\t\t\tvar dv = v * 0.5f * Project( size, v );\n\n\t\t\tvar p0 = face - du - dv;\n\t\t\tvar p1 = face \u002B du - dv;\n\t\t\tvar p2 = face \u002B du \u002B dv;\n\t\t\tvar p3 = face - du \u002B dv;\n\n\t\t\tvb.Add( new Vertex( p0, n, u, new Vector4( 0, 0, 0, 0 ) ) );\n\t\t\tvb.Add( new Vertex( p1, n, u, new Vector4( 1, 0, 0, 0 ) ) );\n\t\t\tvb.Add( new Vertex( p2, n, u, new Vector4( 1, 1, 0, 0 ) ) );\n\t\t\tvb.Add( new Vertex( p3, n, u, new Vector4( 0, 1, 0, 0 ) ) );\n\n\t\t\tvb.AddRawIndex( index \u002B 0 ); vb.AddRawIndex( index \u002B 1 ); vb.AddRawIndex( index \u002B 2 );\n\t\t\tvb.AddRawIndex( index \u002B 0 ); vb.AddRawIndex( index \u002B 2 ); vb.AddRawIndex( index \u002B 3 );\n\n\t\t\tindex \u002B= 4;\n\t\t}\n\t}\n\n\tprivate static float Project( Vector3 size, Vector3 axis )\n\t\t=\u003E MathF.Abs( axis.x ) * size.x \u002B MathF.Abs( axis.y ) * size.y \u002B MathF.Abs( axis.z ) * size.z;\n\n\tprotected override void OnDestroy()\n\t{\n\t\tgunObject?.Destroy();\n\t\tgunObject = null;\n\n\t\tMiningDrone.MatchPopulation( Scene, 0 );\n\t}\n}\n"},{"Ident":"idkman.monolith","Path":"Game/Marks.cs","FileName":"Marks.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"namespace Monolith;\n\npublic sealed class MarkDef\n{\n\tpublic string Id;\n\tpublic string Name;\n\tpublic string Hint;\n\n\t/// \u003Csummary\u003EHas this been earned, given the player\u0027s current state?\u003C/summary\u003E\n\tpublic Func\u003CPlayerProgress, bool\u003E Earned;\n\n\t/// \u003Csummary\u003EProgress toward it, 0 to 1, for the UI. Optional.\u003C/summary\u003E\n\tpublic Func\u003CPlayerProgress, float\u003E Progress;\n}\n\n/// \u003Csummary\u003E\n/// Achievements, and the quiet global multiplier they carry.\n///\n/// The multiplier is the smaller half of what these are for. Their real job is to be a **quest\n/// log wearing a different hat**: a player who has run out of ideas reads the list and finds\n/// six things they had not thought to try. That is what Cookie Clicker\u0027s achievements actually\n/// do, and why several of these deliberately ask for something awkward rather than for a bigger\n/// number you were going to reach anyway.\n/// \u003C/summary\u003E\npublic static class Marks\n{\n\tpublic static readonly MarkDef[] All =\n\t{\n\t\tnew()\n\t\t{\n\t\t\tId = \u0022first-cube\u0022,\n\t\t\tName = \u0022First Cut\u0022,\n\t\t\tHint = \u0022Remove a cube.\u0022,\n\t\t\tEarned = p =\u003E p.Data.LifetimeCubes \u003E= 1,\n\t\t\tProgress = p =\u003E (float)Math.Clamp( p.Data.LifetimeCubes, 0, 1 ),\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tId = \u0022excavator\u0022,\n\t\t\tName = \u0022Excavator\u0022,\n\t\t\tHint = \u0022Remove 100,000 cubes.\u0022,\n\t\t\tEarned = p =\u003E p.Data.LifetimeCubes \u003E= 100_000,\n\t\t\tProgress = p =\u003E (float)(p.Data.LifetimeCubes / 100_000.0),\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tId = \u0022strip-miner\u0022,\n\t\t\tName = \u0022Strip Miner\u0022,\n\t\t\tHint = \u0022Remove 10 million cubes.\u0022,\n\t\t\tEarned = p =\u003E p.Data.LifetimeCubes \u003E= 10_000_000,\n\t\t\tProgress = p =\u003E (float)(p.Data.LifetimeCubes / 10_000_000.0),\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tId = \u0022half-way\u0022,\n\t\t\tName = \u0022Halfway Down\u0022,\n\t\t\tHint = \u0022Reach stage 50.\u0022,\n\t\t\tEarned = p =\u003E p.Data.BestStageEver \u003E= 50,\n\t\t\tProgress = p =\u003E p.Data.BestStageEver / 50f,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tId = \u0022centurion\u0022,\n\t\t\tName = \u0022Centurion\u0022,\n\t\t\tHint = \u0022Reach stage 100.\u0022,\n\t\t\tEarned = p =\u003E p.Data.BestStageEver \u003E= 100,\n\t\t\tProgress = p =\u003E p.Data.BestStageEver / 100f,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tId = \u0022first-collapse\u0022,\n\t\t\tName = \u0022Collapse\u0022,\n\t\t\tHint = \u0022Prestige once.\u0022,\n\t\t\tEarned = p =\u003E p.Data.Collapses \u003E= 1,\n\t\t\tProgress = p =\u003E p.Data.Collapses,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tId = \u0022ascendant\u0022,\n\t\t\tName = \u0022Ascendant\u0022,\n\t\t\tHint = \u0022Prestige five times.\u0022,\n\t\t\tEarned = p =\u003E p.Data.Collapses \u003E= 5,\n\t\t\tProgress = p =\u003E p.Data.Collapses / 5f,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tId = \u0022buried\u0022,\n\t\t\tName = \u0022Buried Deep\u0022,\n\t\t\tHint = \u0022Detonate 25 charges at full depth.\u0022,\n\t\t\tEarned = p =\u003E p.Data.DeepDetonations \u003E= 25,\n\t\t\tProgress = p =\u003E p.Data.DeepDetonations / 25f,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tId = \u0022perfect-hand\u0022,\n\t\t\tName = \u0022Perfect Hand\u0022,\n\t\t\tHint = \u0022Catch 20 active reloads.\u0022,\n\t\t\tEarned = p =\u003E p.Data.PerfectReloads \u003E= 20,\n\t\t\tProgress = p =\u003E p.Data.PerfectReloads / 20f,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tId = \u0022unstable\u0022,\n\t\t\tName = \u0022Unstable\u0022,\n\t\t\tHint = \u0022Set off 100 volatile cubes.\u0022,\n\t\t\tEarned = p =\u003E p.Data.VolatileDetonations \u003E= 100,\n\t\t\tProgress = p =\u003E p.Data.VolatileDetonations / 100f,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tId = \u0022air-superiority\u0022,\n\t\t\tName = \u0022Air Superiority\u0022,\n\t\t\tHint = \u0022Destroy 50 interceptors.\u0022,\n\t\t\tEarned = p =\u003E p.Data.InterceptorKills \u003E= 50,\n\t\t\tProgress = p =\u003E p.Data.InterceptorKills / 50f,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tId = \u0022unseen\u0022,\n\t\t\tName = \u0022Unseen\u0022,\n\t\t\tHint = \u0022Shoot down 10 Spotters.\u0022,\n\t\t\tEarned = p =\u003E p.Data.SpottersDowned \u003E= 10,\n\t\t\tProgress = p =\u003E p.Data.SpottersDowned / 10f,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tId = \u0022resonant\u0022,\n\t\t\tName = \u0022Resonant\u0022,\n\t\t\tHint = \u0022Reach 10 Resonance stacks at once.\u0022,\n\t\t\tEarned = p =\u003E p.Data.BestResonance \u003E= 10,\n\t\t\tProgress = p =\u003E p.Data.BestResonance / 10f,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tId = \u0022swift\u0022,\n\t\t\tName = \u0022Swift\u0022,\n\t\t\tHint = \u0022Finish the ladder in under 20 minutes.\u0022,\n\t\t\tEarned = p =\u003E p.Data.BestLadderSeconds \u003E 0f \u0026\u0026 p.Data.BestLadderSeconds \u003C 1200f,\n\t\t\tProgress = p =\u003E p.Data.BestLadderSeconds \u003C= 0f ? 0f : 1200f / p.Data.BestLadderSeconds,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tId = \u0022contributor\u0022,\n\t\t\tName = \u0022Contributor\u0022,\n\t\t\tHint = \u0022Earn prestige credit from felling the Monolith.\u0022,\n\t\t\tEarned = p =\u003E p.Data.MonolithCredits \u003E= 1,\n\t\t\tProgress = p =\u003E p.Data.MonolithCredits,\n\t\t},\n\t};\n\n\tpublic static MarkDef Get( string id ) =\u003E All.FirstOrDefault( m =\u003E m.Id == id );\n}\n"},{"Ident":"idkman.monolith","Path":"Game/MonolithMusic.cs","FileName":"MonolithMusic.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"namespace Monolith;\n\n/// \u003Csummary\u003E\n/// The score: a chiptune sequencer, synthesised in C# at startup. There is no audio file\n/// anywhere in this project.\n///\n/// **Why generate it.** s\u0026amp;box ships 63 addressable sound events and not one of them is music.\n/// The only music files on this machine live under \u0060download/assets\u0060, which is other creators\u0027\n/// packages cached by the client: not ours to use, and they would not resolve on anyone else\u0027s\n/// machine. Beyond that, the Play Fund makes a package ineligible if it contains copyrighted\n/// material, and music is the easiest way to fail that test. A score computed from arithmetic\n/// has no provenance to argue about. See GOALS section 8.\n///\n/// **What changed from the first attempt.** The first version was three sine drones with slow\n/// swells, and the verdict was fair: that is ambience, not music. Hotline Miami, Super Hexagon\n/// and the chiptune driver tracks they come from are not textures, they are SEQUENCES. They have\n/// a tempo you can nod to, a bassline that repeats, an arpeggio doing sixteenths, drums, and a\n/// melody. None of that can emerge from summed sine waves, no matter how they are modulated. So\n/// this is a step sequencer instead: notes on a grid, square and pulse waves, and a drum kit.\n///\n/// **The arrangement.** 168 BPM, sixteenth-note grid, eight bars, over a four chord loop in A\n/// minor: Am, F, C, G. Three layers play permanently and only their VOLUMES move, so the track\n/// builds with the run rather than cutting between tracks:\n///\n/// - **BED** is kick, a sixteenth-note bass, and the arpeggio. A complete track on its own.\n/// - **WORK** adds backbeat, hats and the arp doubled an octave up. Rises as the stage empties.\n/// - **THREAT** adds the vibrato lead, and rises with what is hunting you.\n///\n/// **Where the reference actually pointed.** The named tracks are Chipzel\u0027s, from Super Hexagon.\n/// The thing to take from them is not the palette, it is that **the arpeggio is the tune**: it\n/// runs continuously in sixteenths and the chords move underneath it. An earlier pass had the arp\n/// in a layer that faded up while mining, so the track had no melody at all for the first half of\n/// a stage. It is in the bed now, and the tempo went up to match.\n///\n/// **Why the loop is seamless.** 128 steps at 168 BPM is exactly 252000 samples at 22050 Hz, with\n/// nothing left over. Notes render with WRAPPING indices, so anything still ringing at the end\n/// continues into the top of the loop instead of being cut. There is no crossfade and no click.\n/// \u003C/summary\u003E\npublic sealed class MonolithMusic : Component\n{\n\t// ---------------------------------------------------------------- the grid\n\n\t/// \u003Csummary\u003E\n\t/// 22.05 kHz. Half of CD rate, chosen because square waves alias and a lower ceiling plus the\n\t/// filter below turns that aliasing into something that reads as a cheap synth rather than as\n\t/// harshness. It also halves generation time and memory.\n\t/// \u003C/summary\u003E\n\tprivate const int SampleRate = 22050;\n\n\t/// \u003Csummary\u003E\n\t/// 168, and the value is constrained rather than chosen freely. The loop must be a whole\n\t/// number of samples: \u0060LoopSamples\u0060 works out to 42336000 / BPM, so only tempos that divide\n\t/// that cleanly keep the seam silent. 150, 160, 168, 175 and 180 all do. 176 does not.\n\t/// \u003C/summary\u003E\n\tprivate const int Bpm = 168;\n\n\t/// \u003Csummary\u003ESixteenth notes. The arpeggio needs this resolution; nothing needs more.\u003C/summary\u003E\n\tprivate const int StepsPerBeat = 4;\n\n\t/// \u003Csummary\u003EEight bars of 4/4. Long enough to hold a chord progression and a melody.\u003C/summary\u003E\n\tprivate const int Steps = 128;\n\n\tprivate const int StepsPerBar = StepsPerBeat * 4;\n\n\tprivate const float StepSeconds = 60f / Bpm / StepsPerBeat;\n\n\t/// \u003Csummary\u003E12.8 seconds, and deliberately a whole number of samples.\u003C/summary\u003E\n\tprivate const int LoopSamples = (int)(SampleRate * Steps * StepSeconds);\n\n\t// ---------------------------------------------------------------- the music\n\t//\n\t// A minor: Am, F, C, G, two bars each. MIDI note numbers throughout, so the maths is the\n\t// standard one and the notes are readable to anyone who has seen a piano roll.\n\n\t/// \u003Csummary\u003ERoot of each chord, one per two bars.\u003C/summary\u003E\n\tprivate static readonly int[] ChordRoots = { 33, 29, 36, 31 };   // A1, F1, C2, G1\n\n\t/// \u003Csummary\u003EChord tones above each root, as semitone offsets. Minor, major, major, major.\u003C/summary\u003E\n\tprivate static readonly int[][] ChordTones =\n\t{\n\t\tnew[] { 0, 3, 7, 12 },   // Am\n\t\tnew[] { 0, 4, 7, 12 },   // F\n\t\tnew[] { 0, 4, 7, 12 },   // C\n\t\tnew[] { 0, 4, 7, 12 },   // G\n\t};\n\n\t/// \u003Csummary\u003E\n\t/// The lead, as (step, midi, length in steps). Written out rather than generated: a melody is\n\t/// the one part of this that an algorithm makes worse.\n\t/// \u003C/summary\u003E\n\tprivate static readonly int[][] Lead =\n\t{\n\t\t// Am\n\t\tnew[] { 0, 76, 3 }, new[] { 4, 74, 3 }, new[] { 8, 72, 6 }, new[] { 16, 74, 3 },\n\t\tnew[] { 20, 76, 3 }, new[] { 24, 69, 8 },\n\t\t// F\n\t\tnew[] { 32, 77, 3 }, new[] { 36, 76, 3 }, new[] { 40, 72, 6 }, new[] { 48, 69, 3 },\n\t\tnew[] { 52, 72, 3 }, new[] { 56, 76, 8 },\n\t\t// C\n\t\tnew[] { 64, 79, 3 }, new[] { 68, 76, 3 }, new[] { 72, 74, 6 }, new[] { 80, 72, 3 },\n\t\tnew[] { 84, 74, 3 }, new[] { 88, 76, 8 },\n\t\t// G\n\t\tnew[] { 96, 74, 3 }, new[] { 100, 76, 3 }, new[] { 104, 79, 6 }, new[] { 112, 78, 3 },\n\t\tnew[] { 116, 76, 3 }, new[] { 120, 74, 8 },\n\t};\n\n\t// ---------------------------------------------------------------- live state\n\n\tprivate SoundHandle bed, work, threat;\n\n\tprivate bool started;\n\n\tprivate float bedLevel, workLevel, threatLevel;\n\n\tprotected override void OnStart()\n\t{\n\t\tGenerate();\n\t}\n\n\tprivate void Generate()\n\t{\n\t\tif ( started )\n\t\t\treturn;\n\n\t\tstarted = true;\n\n\t\tbed = StartLayer( \u0022monolith_bed\u0022, RenderBed, Tuning.MusicBedVolume );\n\t\twork = StartLayer( \u0022monolith_work\u0022, RenderWork, 0f );\n\t\tthreat = StartLayer( \u0022monolith_threat\u0022, RenderThreat, 0f );\n\n\t\tif ( bed == null \u0026\u0026 work == null \u0026\u0026 threat == null )\n\t\t\tLog.Warning( \u0022[music] no layer started. The score is silent this session.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Renders one layer, wraps it in a looping sound, and starts it playing.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// The \u003Csee cref=\u0022SoundEvent\u0022/\u003E is built in code because the thing being played does not\n\t/// exist on disk. This is the only call in the file that is not arithmetic, so it is kept\n\t/// alone: if the engine changes how a runtime sound is played, this is all that changes.\n\t/// \u003C/remarks\u003E\n\tprivate static SoundHandle StartLayer( string name, Action\u003Cfloat[]\u003E compose, float volume )\n\t{\n\t\ttry\n\t\t{\n\t\t\tvar buffer = new float[LoopSamples];\n\n\t\t\tcompose( buffer );\n\t\t\tLowPass( buffer, 7400f );\n\t\t\tNormalise( buffer, 0.85f );\n\n\t\t\tvar file = SoundFile.FromPcm( name, Pack( buffer ), new SoundFile.PcmOptions\n\t\t\t{\n\t\t\t\tRate = SampleRate,\n\t\t\t\tBits = 16,\n\t\t\t\tChannels = 1,\n\t\t\t\tLoop = true,\n\t\t\t} );\n\n\t\t\t// UI, so it plays flat in both ears instead of being placed in the world. Music that\n\t\t\t// pans as you turn your head is a sound effect, not a score.\n\t\t\tvar evt = new SoundEvent\n\t\t\t{\n\t\t\t\tSounds = new List\u003CSoundFile\u003E { file },\n\t\t\t\tUI = true,\n\t\t\t\tVolume = 1f,\n\t\t\t\tDistanceAttenuation = false,\n\t\t\t\tOcclusionEnabled = false,\n\t\t\t\tReverbEnabled = false,\n\t\t\t};\n\n\t\t\tvar handle = Sound.Play( evt );\n\t\t\thandle.Volume = volume;\n\n\t\t\treturn handle;\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\u0022[music] layer \u0027{name}\u0027 failed: {e.Message}\u0022 );\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t// ---------------------------------------------------------------- the layers\n\n\t/// \u003Csummary\u003E\n\t/// BED. Kick, a driving sixteenth bass, and THE ARPEGGIO.\n\t///\n\t/// The arpeggio lives here rather than in a layer that fades up, and that is the single\n\t/// biggest correction from the previous version. In Chipzel\u0027s Super Hexagon writing the arp\n\t/// is not decoration over a groove, it IS the tune: it runs continuously in sixteenths from\n\t/// the first bar to the last and the chords change underneath it. Putting it in a layer that\n\t/// only rose while mining meant the track had no melodic content at all until you were\n\t/// halfway through a stage, which is most of why it read as noise.\n\t///\n\t/// So the bed is a complete track on its own. The other two layers add weight to it, they do\n\t/// not complete it.\n\t/// \u003C/summary\u003E\n\tprivate static void RenderBed( float[] buffer )\n\t{\n\t\tfor ( int bar = 0; bar \u003C Steps / StepsPerBar; bar\u002B\u002B )\n\t\t{\n\t\t\tint barStart = bar * StepsPerBar;\n\t\t\tint chord = ChordAt( barStart );\n\t\t\tint root = ChordRoots[chord];\n\t\t\tvar tones = ChordTones[chord];\n\n\t\t\t// Four on the floor. The thing you nod to.\n\t\t\tfor ( int beat = 0; beat \u003C 4; beat\u002B\u002B )\n\t\t\t\tKick( buffer, barStart \u002B beat * StepsPerBeat, 0.95f );\n\n\t\t\tfor ( int step = 0; step \u003C StepsPerBar; step\u002B\u002B )\n\t\t\t{\n\t\t\t\t// BASS. Straight sixteenths, staccato, with every fourth note an octave up. At\n\t\t\t\t// 168 BPM that is about eleven notes a second, which is what makes the track feel\n\t\t\t\t// like it is running rather than walking. The fast decay is what keeps it from\n\t\t\t\t// turning into a drone: each note has to clear out before the next arrives.\n\t\t\t\tint bassNote = (step % 4 == 3) ? root \u002B 12 : root;\n\t\t\t\tNote( buffer, barStart \u002B step, 0.9f, bassNote, 0.4f, Waveform.Triangle, 16f );\n\n\t\t\t\t// THE ARP. Six notes against a sixteen step bar, so the pattern lands in a\n\t\t\t\t// different place in each bar and the ear never quite catches the loop.\n\t\t\t\tint[] shape = { 0, 1, 2, 3, 2, 1 };\n\t\t\t\tint arpNote = root \u002B 36 \u002B tones[shape[step % shape.Length]];\n\n\t\t\t\t// A 25% pulse: thinner and more nasal than a square, which is the sound that cuts\n\t\t\t\t// through without adding weight.\n\t\t\t\tNote( buffer, barStart \u002B step, 1f, arpNote, 0.22f, Waveform.Pulse25, 13f );\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// WORK. Backbeat, hats, and the arpeggio doubled an octave up.\n\t///\n\t/// This layer is pure lift: it adds nothing new melodically, it makes what is already playing\n\t/// bigger and brighter. That is deliberate, because it fades in and out with how much of the\n\t/// stage is left, and a layer that carried its own melody would make the track sound like it\n\t/// was losing a part rather than relaxing.\n\t/// \u003C/summary\u003E\n\tprivate static void RenderWork( float[] buffer )\n\t{\n\t\tfor ( int bar = 0; bar \u003C Steps / StepsPerBar; bar\u002B\u002B )\n\t\t{\n\t\t\tint barStart = bar * StepsPerBar;\n\t\t\tint chord = ChordAt( barStart );\n\t\t\tvar tones = ChordTones[chord];\n\n\t\t\t// Snare on two and four.\n\t\t\tSnare( buffer, barStart \u002B StepsPerBeat, 0.7f );\n\t\t\tSnare( buffer, barStart \u002B StepsPerBeat * 3, 0.7f );\n\n\t\t\t// Closed hats on every off-beat sixteenth. Relentless, which is the point.\n\t\t\tfor ( int step = 1; step \u003C StepsPerBar; step \u002B= 2 )\n\t\t\t\tHat( buffer, barStart \u002B step, 0.14f, 0.03f );\n\n\t\t\tint[] shape = { 0, 1, 2, 3, 2, 1 };\n\n\t\t\tfor ( int step = 0; step \u003C StepsPerBar; step\u002B\u002B )\n\t\t\t{\n\t\t\t\t// The same arp an octave above the bed, quieter and thinner. Two octaves of the\n\t\t\t\t// same line is a chiptune staple and costs one more voice.\n\t\t\t\tint note = ChordRoots[chord] \u002B 48 \u002B tones[shape[step % shape.Length]];\n\t\t\t\tNote( buffer, barStart \u002B step, 0.8f, note, 0.12f, Waveform.Pulse25, 18f );\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// THREAT. The lead melody, with vibrato, rising with what is hunting you.\n\t///\n\t/// Two voices: a square lead and a copy detuned slightly under it. That detune is why these\n\t/// leads sound wide and slightly unstable rather than thin, because two close pitches beat\n\t/// against each other and the ear hears one large sound instead of two small ones. The\n\t/// vibrato on top is what stops a held square note reading as a test tone.\n\t/// \u003C/summary\u003E\n\tprivate static void RenderThreat( float[] buffer )\n\t{\n\t\tforeach ( var note in Lead )\n\t\t{\n\t\t\tint step = note[0];\n\t\t\tint midi = note[1];\n\t\t\tfloat length = note[2];\n\n\t\t\tNote( buffer, step, length, midi, 0.26f, Waveform.Square, 2.6f, vibrato: 0.010f );\n\t\t\tNote( buffer, step, length, midi, 0.19f, Waveform.Square, 2.6f,\n\t\t\t\tdetune: 0.07f, vibrato: 0.008f );\n\t\t}\n\n\t\t// An open hat on the last eighth of every bar: a push into the next one.\n\t\tfor ( int bar = 0; bar \u003C Steps / StepsPerBar; bar\u002B\u002B )\n\t\t\tHat( buffer, bar * StepsPerBar \u002B 14, 0.3f, 0.11f );\n\t}\n\n\t/// \u003Csummary\u003EWhich of the four chords is playing at a given step. Two bars each.\u003C/summary\u003E\n\tprivate static int ChordAt( int step )\n\t\t=\u003E (step / (StepsPerBar * 2)) % ChordRoots.Length;\n\n\t// ---------------------------------------------------------------- the synth\n\n\tprivate enum Waveform\n\t{\n\t\tSquare,\n\t\tPulse25,\n\t\tTriangle,\n\t\tSaw,\n\t}\n\n\t/// \u003Csummary\u003EEqual temperament, A4 = 440 Hz. MIDI note 69 is A4.\u003C/summary\u003E\n\tprivate static float Frequency( int midi ) =\u003E 440f * MathF.Pow( 2f, (midi - 69) / 12f );\n\n\tprivate static float Shape( Waveform wave, float phase )\n\t{\n\t\t// \u0060phase\u0060 is 0 to 1 across one cycle.\n\t\tswitch ( wave )\n\t\t{\n\t\t\tcase Waveform.Square: return phase \u003C 0.5f ? 1f : -1f;\n\t\t\tcase Waveform.Pulse25: return phase \u003C 0.25f ? 1f : -1f;\n\t\t\tcase Waveform.Saw: return 1f - 2f * phase;\n\n\t\t\tdefault:\n\t\t\t\treturn phase \u003C 0.5f ? (4f * phase - 1f) : (3f - 4f * phase);\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Renders one note into the buffer.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// Writes with a WRAPPING index. A note near the end of the loop keeps ringing into the\n\t/// beginning, which is what makes the seam inaudible: the alternative is either cutting the\n\t/// tail (a click) or leaving a gap (a stutter), and both are audible every 12.8 seconds\n\t/// forever.\n\t/// \u003C/remarks\u003E\n\tprivate static void Note( float[] buffer, int step, float lengthSteps, int midi,\n\t\tfloat gain, Waveform wave, float decay, float detune = 0f, float vibrato = 0f )\n\t{\n\t\tfloat frequency = Frequency( midi ) * (1f \u002B detune * 0.01f);\n\n\t\tint start = (int)(step * StepSeconds * SampleRate);\n\t\tint length = (int)(lengthSteps * StepSeconds * SampleRate);\n\n\t\tfloat phase = 0f;\n\t\tfloat advance = frequency / SampleRate;\n\n\t\tfor ( int i = 0; i \u003C length; i\u002B\u002B )\n\t\t{\n\t\t\t// Exponential decay, with a short attack so nothing starts on a click.\n\t\t\tfloat t = (float)i / SampleRate;\n\t\t\tfloat envelope = MathF.Exp( -decay * t );\n\n\t\t\tconst int attack = 48;\n\t\t\tif ( i \u003C attack )\n\t\t\t\tenvelope *= (float)i / attack;\n\n\t\t\t// VIBRATO. A held chiptune lead without it sounds like a test tone, because a square\n\t\t\t// wave has no natural movement of its own to fall back on. Delayed by an eighth of a\n\t\t\t// second so the note lands on pitch first and only then starts to sing, which is how\n\t\t\t// a player would actually phrase it.\n\t\t\tif ( vibrato \u003E 0f )\n\t\t\t{\n\t\t\t\tfloat depth = vibrato * MathF.Min( 1f, MathF.Max( 0f, (t - 0.12f) * 6f ) );\n\t\t\t\tadvance = frequency * (1f \u002B depth * MathF.Sin( t * 6.5f * MathF.Tau )) / SampleRate;\n\t\t\t}\n\n\t\t\tbuffer[(start \u002B i) % LoopSamples] \u002B= Shape( wave, phase ) * envelope * gain;\n\n\t\t\tphase \u002B= advance;\n\t\t\tif ( phase \u003E= 1f )\n\t\t\t\tphase -= 1f;\n\t\t}\n\t}\n\n\t// ---------------------------------------------------------------- the drums\n\t//\n\t// No samples, so each drum is a shape that behaves like the thing it is named after: a kick\n\t// is a pitch falling fast, a snare is noise plus a body tone, a hat is a very short noise.\n\n\tprivate static void Kick( float[] buffer, int step, float gain )\n\t{\n\t\tint start = (int)(step * StepSeconds * SampleRate);\n\t\tint length = (int)(0.28f * SampleRate);\n\n\t\tfloat phase = 0f;\n\n\t\tfor ( int i = 0; i \u003C length; i\u002B\u002B )\n\t\t{\n\t\t\tfloat t = (float)i / SampleRate;\n\n\t\t\t// The pitch drop is the kick. 130 Hz down to 45 Hz in about 40 milliseconds.\n\t\t\tfloat frequency = 45f \u002B 85f * MathF.Exp( -28f * t );\n\t\t\tfloat envelope = MathF.Exp( -14f * t );\n\n\t\t\tphase \u002B= frequency / SampleRate;\n\t\t\tif ( phase \u003E= 1f )\n\t\t\t\tphase -= 1f;\n\n\t\t\tbuffer[(start \u002B i) % LoopSamples] \u002B= MathF.Sin( phase * MathF.Tau ) * envelope * gain;\n\t\t}\n\t}\n\n\tprivate static void Snare( float[] buffer, int step, float gain )\n\t{\n\t\tint start = (int)(step * StepSeconds * SampleRate);\n\t\tint length = (int)(0.16f * SampleRate);\n\n\t\tuint seed = 0x5EED_1234;\n\t\tfloat phase = 0f;\n\n\t\tfor ( int i = 0; i \u003C length; i\u002B\u002B )\n\t\t{\n\t\t\tfloat t = (float)i / SampleRate;\n\t\t\tfloat envelope = MathF.Exp( -26f * t );\n\n\t\t\t// Noise for the rattle, plus a 190 Hz tone for the body. Noise alone reads as a\n\t\t\t// burst of static rather than as a drum being hit.\n\t\t\tfloat noise = NextNoise( ref seed );\n\n\t\t\tphase \u002B= 190f / SampleRate;\n\t\t\tif ( phase \u003E= 1f )\n\t\t\t\tphase -= 1f;\n\n\t\t\tfloat body = MathF.Sin( phase * MathF.Tau ) * 0.5f;\n\n\t\t\tbuffer[(start \u002B i) % LoopSamples] \u002B= (noise * 0.8f \u002B body) * envelope * gain;\n\t\t}\n\t}\n\n\tprivate static void Hat( float[] buffer, int step, float gain, float seconds )\n\t{\n\t\tint start = (int)(step * StepSeconds * SampleRate);\n\t\tint length = (int)(seconds * SampleRate);\n\n\t\tuint seed = 0x1A7_C0DE;\n\t\tfloat previous = 0f;\n\n\t\t// Decay scaled to the requested length, so a closed hat snaps and an open one rings.\n\t\tfloat decay = 4f / MathF.Max( 0.01f, seconds );\n\n\t\tfor ( int i = 0; i \u003C length; i\u002B\u002B )\n\t\t{\n\t\t\tfloat t = (float)i / SampleRate;\n\t\t\tfloat envelope = MathF.Exp( -decay * t );\n\n\t\t\tfloat noise = NextNoise( ref seed );\n\n\t\t\t// Crude high pass: the difference between consecutive samples. A hat is the bright\n\t\t\t// half of noise, and without this it sits on top of the kick instead of above it.\n\t\t\tfloat bright = noise - previous;\n\t\t\tprevious = noise;\n\n\t\t\tbuffer[(start \u002B i) % LoopSamples] \u002B= bright * envelope * gain;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// White noise from a deterministic generator, so the track is identical every run.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// Deliberately NOT \u003Cc\u003EGame.Random\u003C/c\u003E. That is seeded per tick and shared, so drawing from\n\t/// it would make the drums different on every launch and, worse, would consume from a stream\n\t/// that gameplay also uses.\n\t/// \u003C/remarks\u003E\n\tprivate static float NextNoise( ref uint state )\n\t{\n\t\t// xorshift32\n\t\tstate ^= state \u003C\u003C 13;\n\t\tstate ^= state \u003E\u003E 17;\n\t\tstate ^= state \u003C\u003C 5;\n\n\t\treturn (state / (float)uint.MaxValue) * 2f - 1f;\n\t}\n\n\t// ---------------------------------------------------------------- post\n\n\t/// \u003Csummary\u003E\n\t/// One pole low pass.\n\t///\n\t/// Square waves at 22 kHz alias badly, and the aliased partials are what make naive chiptune\n\t/// synthesis sound like grit rather than like a synth. Rolling the top off turns that into\n\t/// the filtered sound these tracks actually have, and costs one multiply per sample.\n\t/// \u003C/summary\u003E\n\tprivate static void LowPass( float[] buffer, float cutoff )\n\t{\n\t\tfloat rc = 1f / (MathF.Tau * cutoff);\n\t\tfloat dt = 1f / SampleRate;\n\t\tfloat alpha = dt / (rc \u002B dt);\n\n\t\t// Primed from the END of the buffer, because the buffer loops: starting from silence\n\t\t// would put a filter sweep at the top of every repetition.\n\t\tfloat value = buffer[^1];\n\n\t\tfor ( int i = 0; i \u003C buffer.Length; i\u002B\u002B )\n\t\t{\n\t\t\tvalue \u002B= alpha * (buffer[i] - value);\n\t\t\tbuffer[i] = value;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EScales the loudest peak to a target, so layers are balanced by design.\u003C/summary\u003E\n\tprivate static void Normalise( float[] buffer, float peak )\n\t{\n\t\tfloat loudest = 0f;\n\n\t\tforeach ( float sample in buffer )\n\t\t\tloudest = MathF.Max( loudest, MathF.Abs( sample ) );\n\n\t\tif ( loudest \u003C= 0.0001f )\n\t\t\treturn;\n\n\t\tfloat scale = peak / loudest;\n\n\t\tfor ( int i = 0; i \u003C buffer.Length; i\u002B\u002B )\n\t\t\tbuffer[i] *= scale;\n\t}\n\n\t/// \u003Csummary\u003EPacks to 16 bit signed little endian mono.\u003C/summary\u003E\n\tprivate static byte[] Pack( float[] buffer )\n\t{\n\t\tvar bytes = new byte[buffer.Length * 2];\n\n\t\tfor ( int i = 0; i \u003C buffer.Length; i\u002B\u002B )\n\t\t{\n\t\t\tshort sample = (short)(Math.Clamp( buffer[i], -1f, 1f ) * short.MaxValue);\n\n\t\t\tbytes[i * 2] = (byte)(sample \u0026 0xFF);\n\t\t\tbytes[i * 2 \u002B 1] = (byte)((sample \u003E\u003E 8) \u0026 0xFF);\n\t\t}\n\n\t\treturn bytes;\n\t}\n\n\t// ---------------------------------------------------------------- mixing\n\n\t/// \u003Csummary\u003E\n\t/// Rides the three volumes.\n\t///\n\t/// NOT gated on the pause the way the rest of the game is. Music continuing under a menu is\n\t/// the point of having it, and the duck below is what tells you the world stopped.\n\t/// \u003C/summary\u003E\n\tprotected override void OnUpdate()\n\t{\n\t\tfloat wantBed = Tuning.MusicBedVolume;\n\t\tfloat wantWork = 0f;\n\t\tfloat wantThreat = 0f;\n\n\t\tvar manager = MonolithManager.Instance;\n\n\t\tif ( manager != null )\n\t\t{\n\t\t\t// The arpeggio swells as the stage empties, so the last cubes of a stage feel like an\n\t\t\t// ending rather than like the middle.\n\t\t\tfloat cleared = manager.World?.Progress ?? 0f;\n\t\t\twantWork = Tuning.MusicWorkVolume * (0.45f \u002B 0.55f * cleared);\n\n\t\t\t// The lead follows what is hunting you, so the score answers to the same thing the\n\t\t\t// player is answering to.\n\t\t\twantThreat = Tuning.MusicThreatVolume\n\t\t\t\t* Math.Clamp( HazardPressure() / Tuning.MusicThreatFullAt, 0f, 1f );\n\t\t}\n\n\t\t// Ducked hard while paused. A menu over a full mix reads as the game still running, which\n\t\t// is exactly the impression the pause exists to dispel.\n\t\tif ( GameTime.Paused )\n\t\t{\n\t\t\twantWork *= 0.1f;\n\t\t\twantThreat *= 0f;\n\t\t\twantBed *= 0.4f;\n\t\t}\n\n\t\tfloat master = AudioSettings.MusicVolume;\n\n\t\twantBed *= master;\n\t\twantWork *= master;\n\t\twantThreat *= master;\n\n\t\t// Real delta, not game delta: these must keep moving while the world is frozen or the\n\t\t// duck would never arrive.\n\t\tfloat rate = Time.Delta * Tuning.MusicFadeRate;\n\n\t\tbedLevel = MathX.Lerp( bedLevel, wantBed, rate );\n\t\tworkLevel = MathX.Lerp( workLevel, wantWork, rate );\n\t\tthreatLevel = MathX.Lerp( threatLevel, wantThreat, rate );\n\n\t\tApply( bed, bedLevel );\n\t\tApply( work, workLevel );\n\t\tApply( threat, threatLevel );\n\t}\n\n\t/// \u003Csummary\u003EHow much is currently hunting the player, as a weighted count of live hazards.\u003C/summary\u003E\n\tprivate static float HazardPressure()\n\t{\n\t\tfloat pressure = 0f;\n\n\t\ttry\n\t\t{\n\t\t\tpressure \u002B= Spotter.All.Count * 1.5f;\n\t\t\tpressure \u002B= Sentinel.All.Count;\n\t\t\tpressure \u002B= Crawler.All.Count * 1.5f;\n\t\t\tpressure \u002B= Interceptor.All.Count * 0.5f;\n\t\t\tpressure \u002B= Leech.All.Count * 0.75f;\n\t\t\tpressure \u002B= Anchor.All.Count * 0.5f;\n\t\t}\n\t\tcatch ( Exception )\n\t\t{\n\t\t}\n\n\t\treturn pressure;\n\t}\n\n\tprivate static void Apply( SoundHandle handle, float volume )\n\t{\n\t\ttry\n\t\t{\n\t\t\tif ( handle != null )\n\t\t\t\thandle.Volume = volume;\n\t\t}\n\t\tcatch ( Exception )\n\t\t{\n\t\t}\n\t}\n}\n"},{"Ident":"idkman.monolith","Path":"Game/OrbitalBarrier.cs","FileName":"OrbitalBarrier.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"namespace Monolith;\n\n/// \u003Csummary\u003E\n/// A wide panel that orbits the shape and swallows anything fired through it.\n///\n/// The important difference from an interceptor: **you cannot shoot it away**. An interceptor\n/// is a target, so the answer is more damage. A barrier has no answer except to move, which is\n/// exactly why it exists. Standing still and holding fire stops being viable the moment one of\n/// these sweeps across your lane.\n///\n/// Blocking is tested in cylindrical coordinates about the shape centre rather than with a\n/// mesh, which makes it exact and costs a couple of trig calls per projectile step.\n/// \u003C/summary\u003E\npublic sealed class OrbitalBarrier : Component\n{\n\tprivate static readonly List\u003COrbitalBarrier\u003E all = new();\n\tpublic static IReadOnlyList\u003COrbitalBarrier\u003E All =\u003E all;\n\n\tprivate static Model panelModel;\n\n\t/// \u003Csummary\u003ECentre of the orbit, in world space.\u003C/summary\u003E\n\tpublic Vector3 Centre { get; set; }\n\n\t/// \u003Csummary\u003EDistance from the centre the panel sits at.\u003C/summary\u003E\n\tpublic float OrbitRadius { get; set; } = 700f;\n\n\t/// \u003Csummary\u003ECurrent angle around the centre, in degrees.\u003C/summary\u003E\n\tpublic float Angle { get; set; }\n\n\t/// \u003Csummary\u003EDegrees per second. Signed, so panels can counter-rotate.\u003C/summary\u003E\n\tpublic float AngularSpeed { get; set; } = 14f;\n\n\t/// \u003Csummary\u003EHalf-height of the panel above and below its centre height.\u003C/summary\u003E\n\tpublic float HalfHeight { get; set; } = 260f;\n\n\t/// \u003Csummary\u003EHeight of the panel\u0027s centre, relative to the orbit centre.\u003C/summary\u003E\n\tpublic float HeightOffset { get; set; }\n\n\tprivate ModelRenderer renderer;\n\n\tpublic static OrbitalBarrier Spawn( Scene scene, Vector3 centre, float radius,\n\t\tfloat angle, float speed, float heightOffset, float halfHeight )\n\t{\n\t\tif ( scene == null ) return null;\n\n\t\tvar obj = new GameObject( true, \u0022Orbital Barrier\u0022 );\n\t\tobj.NetworkMode = NetworkMode.Never;\n\n\t\tvar barrier = obj.AddComponent\u003COrbitalBarrier\u003E();\n\t\tbarrier.Centre = centre;\n\t\tbarrier.OrbitRadius = radius;\n\t\tbarrier.Angle = angle;\n\t\tbarrier.AngularSpeed = speed;\n\t\tbarrier.HeightOffset = heightOffset;\n\t\tbarrier.HalfHeight = halfHeight;\n\n\t\tvar mr = obj.AddComponent\u003CModelRenderer\u003E();\n\t\tmr.Model = GetPanelModel();\n\t\tmr.Tint = new Color( 0.55f, 0.14f, 0.30f );\n\n\t\tbarrier.renderer = mr;\n\t\tbarrier.Reposition();\n\n\t\treturn barrier;\n\t}\n\n\tprotected override void OnEnabled() =\u003E all.Add( this );\n\tprotected override void OnDisabled() =\u003E all.Remove( this );\n\n\tprotected override void OnUpdate()\n\t{\n\t\t// Frozen while a blocking screen or the pause menu is up. See GameTime.\n\t\tif ( GameTime.Paused )\n\t\t\treturn;\n\n\t\tAngle \u002B= AngularSpeed * Time.Delta;\n\t\tif ( Angle \u003E= 360f ) Angle -= 360f;\n\t\tif ( Angle \u003C 0f ) Angle \u002B= 360f;\n\n\t\tReposition();\n\n\t\tif ( !renderer.IsValid() )\n\t\t\treturn;\n\n\t\t// Emissive rather than dark. A dim maroon slab read as a painted wall; bars that glow\n\t\t// read as a field, which is what tells you at a glance that it is not part of the shape\n\t\t// and not something you can shoot away.\n\t\tfloat flash = MathF.Max( 0f, 1f - timeSinceAbsorb / AbsorbFlashSeconds );\n\t\tfloat hum = 0.85f \u002B 0.15f * MathF.Sin( Time.Now * 2.6f \u002B OrbitRadius );\n\n\t\t// Heat accumulates per shot eaten and bleeds off when you stop. A single flash is easy\n\t\t// to miss while firing ten times a second; a panel that visibly heats under sustained\n\t\t// fire tells you that you are wasting every one of those shots.\n\t\theat = Math.Clamp( heat \u002B absorbCount * HeatPerHit\n\t\t\t- HeatDecayPerSecond * Time.Delta, 0f, 1f );\n\n\t\tabsorbCount = 0;\n\n\t\t// Angry white through pink at full heat, so the message escalates rather than repeating.\n\t\tvar idle = new Color( 1.5f, 0.28f, 0.5f ) * hum;\n\t\tvar hot = Color.Lerp( idle, new Color( 1f, 0.55f, 0.75f ) * 5f, heat );\n\n\t\trenderer.Tint = Color.Lerp( hot, new Color( 1f, 0.85f, 0.95f ) * 11f, flash );\n\t}\n\n\tprivate void Reposition()\n\t{\n\t\tfloat radians = Angle * MathF.PI / 180f;\n\n\t\tvar position = Centre \u002B new Vector3(\n\t\t\tMathF.Cos( radians ) * OrbitRadius,\n\t\t\tMathF.Sin( radians ) * OrbitRadius,\n\t\t\tHeightOffset );\n\n\t\tWorldPosition = position;\n\n\t\t// Face the centre so the panel presents its width across your line of fire.\n\t\tWorldRotation = Rotation.LookAt( (Centre.WithZ( position.z ) - position).Normal );\n\n\t\t// Arc length at this radius gives the panel its apparent width.\n\t\tfloat arcWidth = Tuning.BarrierArcDegrees * MathF.PI / 180f * OrbitRadius;\n\t\tWorldScale = new Vector3( 24f, arcWidth, HalfHeight * 2f );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// True if the segment blocks this projectile step. Tested in cylindrical coordinates:\n\t/// a point is inside when its radius, height and bearing all fall within the panel.\n\t///\n\t/// Reports WHICH barrier and WHERE, because a lattice you can see through has to prove it\n\t/// is still solid. Without the absorb landing somewhere visible, a barrier that eats your\n\t/// shots while you can see the rock behind it just looks like the gun has stopped working.\n\t/// \u003C/summary\u003E\n\tpublic static bool Blocks( Vector3 start, Vector3 direction, float distance,\n\t\tout OrbitalBarrier hit, out Vector3 point )\n\t{\n\t\thit = null;\n\t\tpoint = start;\n\n\t\tif ( all.Count == 0 )\n\t\t\treturn false;\n\n\t\t// A barrier shields the MONOLITH. It does not shield the hazards.\n\t\t//\n\t\t// This was an unwinnable interaction. Barriers orbit at ~0.55 of the shape\u0027s diagonal\n\t\t// while Sentinels sit at 0.7 and Anchors at 0.9, so the hazards are ALWAYS outside the\n\t\t// barrier ring: every shot aimed at the things that force you to look away from the\n\t\t// shape was eaten by the ring on the way out. You could not cut a tether or kill a\n\t\t// sentinel at all, which is exactly what \u0022these do nothing\u0022 looks like.\n\t\t//\n\t\t// Only shots travelling INWARD are blocked now. Firing out at a hazard passes; firing in\n\t\t// at the rock is what the panel is there to stop. It also reads correctly: the thing is\n\t\t// a shield around the monolith, so it faces the monolith\u0027s attacker.\n\t\tif ( !IsHeadingInward( all[0].Centre, start, direction ) )\n\t\t\treturn false;\n\n\t\t// Sample along the step.\n\t\t//\n\t\t// Three samples used to be \u0022plenty\u0022, and it was not. A bolt travels 4200 units per\n\t\t// second, so at a good framerate one step is 30 to 70 units while the blocking slab is\n\t\t// 80 units thick radially. Approach it at a shallow angle and the sampled points could\n\t\t// straddle it entirely, which is why shots appeared to pass through from some directions\n\t\t// and not others. Sample density has to be set by the THINNEST dimension of the volume,\n\t\t// not by the step length.\n\t\tint samples = Math.Clamp( (int)(distance / 12f) \u002B 2, 4, 24 );\n\n\t\tfor ( int s = 0; s \u003C= samples; s\u002B\u002B )\n\t\t{\n\t\t\tvar sample = start \u002B direction * (distance * s / (float)samples);\n\n\t\t\tfor ( int i = 0; i \u003C all.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( !all[i].Contains( sample ) )\n\t\t\t\t\tcontinue;\n\n\t\t\t\thit = all[i];\n\t\t\t\tpoint = sample;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A shot just died on this panel. Flares the whole lattice for a moment so the absorb is\n\t/// attributed to the barrier and not to thin air.\n\t/// \u003C/summary\u003E\n\tpublic void Absorb()\n\t{\n\t\ttimeSinceAbsorb = 0f;\n\t\tabsorbCount\u002B\u002B;\n\t}\n\n\tprivate GameTimeSince timeSinceAbsorb = 99f;\n\n\t/// \u003Csummary\u003E\n\t/// Shots eaten. Drives a rising glow, so a barrier you are firing into gets visibly hotter\n\t/// the longer you waste ammunition on it. One flash is easy to miss when you are firing ten\n\t/// times a second; a panel that brightens under sustained fire is not.\n\t/// \u003C/summary\u003E\n\tprivate int absorbCount;\n\n\tprivate GameTimeSince timeSinceAbsorbDecay;\n\n\t/// \u003Csummary\u003ELength of the single-hit flare. Short: it is a punctuation mark, not a state.\u003C/summary\u003E\n\tprivate const float AbsorbFlashSeconds = 0.3f;\n\n\t/// \u003Csummary\u003EHits it takes to reach full heat, and how fast that heat bleeds off.\u003C/summary\u003E\n\tprivate const float HeatPerHit = 0.17f;\n\tprivate const float HeatDecayPerSecond = 0.8f;\n\n\tprivate float heat;\n\n\tprivate bool Contains( Vector3 point )\n\t{\n\t\tvar local = point - Centre;\n\n\t\tfloat height = local.z - HeightOffset;\n\t\tif ( MathF.Abs( height ) \u003E HalfHeight )\n\t\t\treturn false;\n\n\t\tfloat radius = new Vector2( local.x, local.y ).Length;\n\t\tif ( MathF.Abs( radius - OrbitRadius ) \u003E 40f )\n\t\t\treturn false;\n\n\t\tfloat bearing = MathF.Atan2( local.y, local.x ) * 180f / MathF.PI;\n\t\tif ( bearing \u003C 0f ) bearing \u002B= 360f;\n\n\t\tfloat delta = MathF.Abs( DeltaAngle( bearing, Angle ) );\n\t\treturn delta \u003C= Tuning.BarrierArcDegrees * 0.5f;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// True if the shot is closing on the shape centre in the horizontal plane.\n\t///\n\t/// Measured in XY only. Height is irrelevant to \u0022is this aimed at the monolith\u0022, and\n\t/// including it would mean a shot at a Spotter directly overhead counted as inward.\n\t/// \u003C/summary\u003E\n\tprivate static bool IsHeadingInward( Vector3 centre, Vector3 start, Vector3 direction )\n\t{\n\t\tvar outward = (start - centre).WithZ( 0f );\n\n\t\t// Standing dead centre: nothing sensible to compare against, so let it through rather\n\t\t// than blocking arbitrarily.\n\t\tif ( outward.Length \u003C 1f )\n\t\t\treturn false;\n\n\t\treturn Vector3.Dot( direction.WithZ( 0f ), outward.Normal ) \u003C 0f;\n\t}\n\n\t/// \u003Csummary\u003EShortest signed distance between two bearings, in degrees.\u003C/summary\u003E\n\tprivate static float DeltaAngle( float a, float b )\n\t{\n\t\tfloat d = (a - b \u002B 540f) % 360f - 180f;\n\t\treturn d;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A unit LATTICE, scaled per barrier into a wide panel of bars.\n\t///\n\t/// It used to be a solid slab, which meant a barrier drifting between you and the shape\n\t/// blanked out the thing you were trying to mine. The blocking test is unchanged and still\n\t/// covers the whole rectangle: only what you can see through it changed. That is the right\n\t/// trade, because the barrier\u0027s job is to move your feet, not to hide the level.\n\t///\n\t/// Local axes at draw time are (thickness, width, height), so the bars are laid out in the\n\t/// Y/Z plane and left full depth in X.\n\t/// \u003C/summary\u003E\n\tprivate static Model GetPanelModel()\n\t{\n\t\tif ( panelModel != null )\n\t\t\treturn panelModel;\n\n\t\tvar vb = new VertexBuffer();\n\t\tvb.Init( true );\n\n\t\tint index = 0;\n\n\t\t// JUST THE FRAME. The lattice version read as a window pane, which made a barrier look\n\t\t// like scenery you were behind rather than an obstacle in your way. An empty rectangle\n\t\t// says \u0022the opening is blocked\u0022 without pretending to be glass.\n\t\t//\n\t\t// \u0060depth\u0060 stays small: the bars used to span the panel\u0027s full 24 unit thickness, so the\n\t\t// moment you looked at one off-axis they occluded each other and it filled in solid.\n\t\t// A frame is only see-through when it is FLAT.\n\t\tconst float bar = 0.04f;\n\t\tconst float depth = 0.28f;\n\n\t\t// Top and bottom rails.\n\t\tAddBox( vb, ref index, new Vector3( 0f, 0f, -0.5f ), new Vector3( depth, 1f, bar ) );\n\t\tAddBox( vb, ref index, new Vector3( 0f, 0f, 0.5f ), new Vector3( depth, 1f, bar ) );\n\n\t\t// Left and right posts.\n\t\tAddBox( vb, ref index, new Vector3( 0f, -0.5f, 0f ), new Vector3( depth, bar, 1f ) );\n\t\tAddBox( vb, ref index, new Vector3( 0f, 0.5f, 0f ), new Vector3( depth, bar, 1f ) );\n\n\t\tvar mesh = new Mesh( Material.Load( \u0022materials/default.vmat\u0022 ) );\n\t\tmesh.CreateBuffers( vb );\n\n\t\tpanelModel = new ModelBuilder().AddMesh( mesh ).Create();\n\t\treturn panelModel;\n\t}\n\n\tprivate static void AddBox( VertexBuffer vb, ref int index, Vector3 centre, Vector3 size )\n\t{\n\t\tVector3[] normals =\n\t\t{\n\t\t\tVector3.Forward, Vector3.Backward, Vector3.Left,\n\t\t\tVector3.Right, Vector3.Up, Vector3.Down,\n\t\t};\n\n\t\tforeach ( var n in normals )\n\t\t{\n\t\t\tvar reference = MathF.Abs( n.z ) \u003E 0.9f ? Vector3.Forward : Vector3.Up;\n\t\t\tvar u = Vector3.Cross( n, reference ).Normal;\n\t\t\tvar v = Vector3.Cross( n, u ).Normal;\n\n\t\t\tvar face = centre \u002B n * 0.5f * Project( size, n );\n\n\t\t\tvar du = u * 0.5f * Project( size, u );\n\t\t\tvar dv = v * 0.5f * Project( size, v );\n\n\t\t\tvar p0 = face - du - dv;\n\t\t\tvar p1 = face \u002B du - dv;\n\t\t\tvar p2 = face \u002B du \u002B dv;\n\t\t\tvar p3 = face - du \u002B dv;\n\n\t\t\tvb.Add( new Vertex( p0, n, u, new Vector4( 0, 0, 0, 0 ) ) );\n\t\t\tvb.Add( new Vertex( p1, n, u, new Vector4( 1, 0, 0, 0 ) ) );\n\t\t\tvb.Add( new Vertex( p2, n, u, new Vector4( 1, 1, 0, 0 ) ) );\n\t\t\tvb.Add( new Vertex( p3, n, u, new Vector4( 0, 1, 0, 0 ) ) );\n\n\t\t\tvb.AddRawIndex( index \u002B 0 ); vb.AddRawIndex( index \u002B 1 ); vb.AddRawIndex( index \u002B 2 );\n\t\t\tvb.AddRawIndex( index \u002B 0 ); vb.AddRawIndex( index \u002B 2 ); vb.AddRawIndex( index \u002B 3 );\n\n\t\t\tindex \u002B= 4;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EMagnitude of a box extent along an axis direction.\u003C/summary\u003E\n\tprivate static float Project( Vector3 size, Vector3 axis )\n\t\t=\u003E MathF.Abs( axis.x ) * size.x \u002B MathF.Abs( axis.y ) * size.y \u002B MathF.Abs( axis.z ) * size.z;\n}\n"},{"Ident":"idkman.monolith","Path":"Game/Sentinel.cs","FileName":"Sentinel.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"namespace Monolith;\n\n/// \u003Csummary\u003E\n/// A hovering gun emplacement, and the first thing in the game that shoots back.\n///\n/// Every other hazard is passive. Barriers sit in the way, leeches sit on the rock, the Spotter\n/// watches. All of them can be ignored for a while, and mining is a thing you do *between*\n/// dealing with them. A sentinel\u0027s shot arrives whether or not you looked at it, so it is the\n/// first hazard that sets its own tempo rather than waiting for you to notice.\n///\n/// The orb is deliberately slow and destructible so the answer is a CHOICE, not a reflex:\n///\n///   - Move out of its path, which costs you your firing position.\n///   - Spend one shot killing the orb, which costs you a shot.\n///   - Spend six killing the sentinel, which costs a lot now and nothing later.\n///\n/// All three are correct in different situations, which is the point.\n/// \u003C/summary\u003E\npublic sealed class Sentinel : Component\n{\n\tprivate static readonly List\u003CSentinel\u003E all = new();\n\tpublic static IReadOnlyList\u003CSentinel\u003E All =\u003E all;\n\n\tprivate static Model bodyModel;\n\n\t[Property] public float Radius { get; set; } = 78f;\n\n\tpublic int Health { get; private set; } = Tuning.SentinelHealth;\n\n\tprivate ModelRenderer renderer;\n\tprivate PointLight glow;\n\n\tprivate Vector3 wanderTarget;\n\tprivate GameTimeSince timeSinceRetarget = 99f;\n\tprivate GameTimeSince timeSinceShot;\n\tprivate GameTimeSince timeSinceSpawn;\n\tprivate float spin;\n\n\tpublic static Sentinel Spawn( Scene scene, Vector3 position )\n\t{\n\t\tif ( scene == null ) return null;\n\n\t\tvar obj = new GameObject( true, \u0022Sentinel\u0022 );\n\t\tobj.NetworkMode = NetworkMode.Never;\n\t\tobj.WorldPosition = position;\n\n\t\tvar sentinel = obj.AddComponent\u003CSentinel\u003E();\n\t\tobj.WorldScale = sentinel.Radius;\n\n\t\tvar mr = obj.AddComponent\u003CModelRenderer\u003E();\n\t\tmr.Model = GetBodyModel();\n\t\tsentinel.renderer = mr;\n\n\t\tvar light = obj.AddComponent\u003CPointLight\u003E();\n\t\tlight.LightColor = new Color( 1f, 0.35f, 0.12f ) * 5f;\n\t\tlight.Radius = 700f;\n\t\tlight.Shadows = false;\n\t\tsentinel.glow = light;\n\n\t\tsentinel.wanderTarget = position;\n\n\t\t// Staggered so a group that arrives together does not fire in a single volley, which\n\t\t// would be one big dodge instead of a rhythm you have to keep track of.\n\t\t//\n\t\t// Capped at HALF the interval, not the whole of it. Rolling near the top meant a\n\t\t// sentinel could arrive with its reload already finished and shoot you before you had\n\t\t// seen it exist, which reads as being ambushed by the spawn rather than by the enemy.\n\t\tsentinel.timeSinceShot = Game.Random.Float( 0f, Tuning.SentinelFireInterval * 0.5f );\n\n\t\tLog.Info( \u0022A Sentinel has arrived.\u0022 );\n\t\treturn sentinel;\n\t}\n\n\tprotected override void OnEnabled() =\u003E all.Add( this );\n\tprotected override void OnDisabled() =\u003E all.Remove( this );\n\n\tprotected override void OnUpdate()\n\t{\n\t\t// Frozen while a blocking screen or the pause menu is up. See GameTime.\n\t\tif ( GameTime.Paused )\n\t\t\treturn;\n\n\t\tvar manager = MonolithManager.Instance;\n\t\tif ( !manager.IsValid() || manager.World == null )\n\t\t\treturn;\n\n\t\tHover( manager );\n\t\tFace( manager );\n\n\t\t// Charge-up glow, so the shot is telegraphed. A turret that fires without warning is\n\t\t// just damage; one that visibly winds up is something you can play around.\n\t\tfloat charge = Math.Clamp( timeSinceShot / Tuning.SentinelFireInterval, 0f, 1f );\n\n\t\tif ( glow.IsValid() )\n\t\t\tglow.LightColor = new Color( 1f, 0.35f, 0.12f ) * (2f \u002B charge * charge * 9f);\n\n\t\tif ( renderer.IsValid() )\n\t\t\trenderer.Tint = new Color( 1f, 0.42f, 0.18f ) * (1.2f \u002B charge * charge * 2.5f);\n\n\t\t// A beat after arriving before it can shoot at all. Even one second is the difference\n\t\t// between \u0022a turret turned up and I dealt with it\u0022 and \u0022I was hit by something that did\n\t\t// not exist a moment ago\u0022, and the second of those just feels like being cheated.\n\t\tif ( timeSinceSpawn \u003C Tuning.SentinelArmSeconds )\n\t\t\treturn;\n\n\t\tif ( timeSinceShot \u003C Tuning.SentinelFireInterval )\n\t\t\treturn;\n\n\t\tTryFire();\n\t}\n\n\tprivate void TryFire()\n\t{\n\t\tvar player = Scene.GetAllComponents\u003CPlayerMovement\u003E().FirstOrDefault();\n\t\tif ( !player.IsValid() )\n\t\t\treturn;\n\n\t\tvar manager = MonolithManager.Instance;\n\n\t\tvar toPlayer = player.WorldPosition - WorldPosition;\n\t\tfloat distance = toPlayer.Length;\n\n\t\tif ( distance \u003C 1f || distance \u003E Tuning.SentinelRange )\n\t\t\treturn;\n\n\t\t// Must actually be pointed at you. The charge glow plus the visible turn is the whole\n\t\t// warning, and firing without facing would throw both away.\n\t\tif ( !IsFacing( player.WorldPosition ) )\n\t\t\treturn;\n\n\t\t// Line of sight, so the shape is cover against sentinels too. Consistency matters more\n\t\t// than the individual rule: if the monolith blocks one threat it must block them all,\n\t\t// or players cannot reason about cover at all.\n\t\tif ( manager.World.TraceRay( WorldPosition, toPlayer.Normal, distance - 24f, out _ ) )\n\t\t\treturn;\n\n\t\ttimeSinceShot = 0f;\n\n\t\t// CALLS IT IN. A sentinel with eyes on you tells every Spotter in range where to look.\n\t\t//\n\t\t// This is what turns two hazards into a system. A sentinel is cheap, close-range and\n\t\t// survivable; a Spotter is the thing that actually costs you a stage but has to find you\n\t\t// first. Alerting only steers the search, it never grants a lock, so leaving a sentinel\n\t\t// alive does not kill you - it makes the Spotter that was going to miss you find you\n\t\t// instead. Killing the cheap thing is now how you stay hidden from the expensive one.\n\t\tSpotter.AlertAll( player.WorldPosition );\n\n\t\t// Leads its target, but DELIBERATELY UNDER-LEADS. It aims at where you would be if you\n\t\t// kept going, so holding a straight line gets you hit and changing direction beats it.\n\t\t// A perfect lead would teach the opposite lesson and make movement pointless.\n\t\tfloat flight = distance / Tuning.SentinelOrbSpeed;\n\t\tvar predicted = player.WorldPosition\n\t\t\t\u002B player.Velocity * flight * Tuning.SentinelLeadFactor;\n\n\t\tSentinelOrb.Spawn( Scene, WorldPosition, (predicted - WorldPosition).Normal );\n\t\tAudio.Play( Audio.MetalHit, WorldPosition, 0.5f, 0.6f );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Turns the barrel toward you at a capped rate, and idles by drifting when you are out of\n\t/// range.\n\t///\n\t/// This is the telegraph. Previously it just spun on the spot and fired at anything in line\n\t/// of sight, so a shot could arrive from a machine that had never appeared to be interested\n\t/// in you. Watching a turret swing round to face you is a warning you can act on, and\n\t/// because the turn is slow, walking around it genuinely spoils the shot.\n\t/// \u003C/summary\u003E\n\tprivate void Face( MonolithManager manager )\n\t{\n\t\tvar player = Scene.GetAllComponents\u003CPlayerMovement\u003E().FirstOrDefault();\n\n\t\t// DOES NOT TRACK DURING THE ARM WINDOW.\n\t\t//\n\t\t// The arm delay stopped it FIRING but not TURNING, so it swung onto you the instant it\n\t\t// appeared and then fired the moment the timer expired. From the player\u0027s side that is\n\t\t// indistinguishable from no delay at all: what you read as \u0022it locked on immediately\u0022 is\n\t\t// the barrel movement, not the shot.\n\t\t//\n\t\t// Drifting until armed means the delay is something you can SEE. It also stacks: it has\n\t\t// to spend the arm window idle and then still turn at SentinelTurnRate to find you.\n\t\tbool armed = timeSinceSpawn \u003E= Tuning.SentinelArmSeconds;\n\n\t\tfloat wanted;\n\n\t\tif ( armed \u0026\u0026 player.IsValid()\n\t\t\t\u0026\u0026 player.WorldPosition.Distance( WorldPosition ) \u003C= Tuning.SentinelRange )\n\t\t{\n\t\t\twanted = (player.WorldPosition - WorldPosition).WithZ( 0f ).Normal.EulerAngles.yaw;\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// Not armed, or nobody in reach: drift, so it still reads as alive rather than\n\t\t\t// switched off.\n\t\t\twanted = spin \u002B 40f * Time.Delta;\n\t\t}\n\n\t\t// Shortest way round, capped. Turning the long way past 180 degrees would look like the\n\t\t// turret had panicked.\n\t\tfloat step = Tuning.SentinelTurnRate * Time.Delta;\n\n\t\tspin \u002B= Math.Clamp( ShortestTurn( wanted, spin ), -step, step );\n\t\tWorldRotation = Rotation.FromYaw( spin );\n\t}\n\n\t/// \u003Csummary\u003ETrue when the barrel is pointed close enough at a target to fire.\u003C/summary\u003E\n\tprivate bool IsFacing( Vector3 target )\n\t{\n\t\tfloat wanted = (target - WorldPosition).WithZ( 0f ).Normal.EulerAngles.yaw;\n\t\treturn MathF.Abs( ShortestTurn( wanted, spin ) ) \u003C= Tuning.SentinelViewCone;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Signed degrees to turn FROM \u003Cparamref name=\u0022from\u0022/\u003E TO \u003Cparamref name=\u0022to\u0022/\u003E, in -180..180.\n\t///\n\t/// Written out rather than using MathX.DeltaDegrees because the docs do not say which way\n\t/// round its arguments subtract, and a sign error here would make the turret rotate away\n\t/// from its target forever. Cheap to spell out, impossible to get subtly wrong.\n\t/// \u003C/summary\u003E\n\tprivate static float ShortestTurn( float to, float from )\n\t\t=\u003E (to - from \u002B 540f) % 360f - 180f;\n\n\tprivate void Hover( MonolithManager manager )\n\t{\n\t\tvar bounds = manager.World.WorldBounds;\n\n\t\tif ( timeSinceRetarget \u003E 4.5f )\n\t\t{\n\t\t\ttimeSinceRetarget = 0;\n\n\t\t\t// Keeps its distance from the shape so it is never buried inside geometry, and so\n\t\t\t// turning to shoot it always means turning away from what you were mining.\n\t\t\tfloat spread = MathF.Max( 700f, bounds.Size.Length * 0.7f );\n\t\t\tfloat angle = Game.Random.Float( 0f, MathF.PI * 2f );\n\n\t\t\twanderTarget = bounds.Center \u002B new Vector3(\n\t\t\t\tMathF.Cos( angle ) * spread,\n\t\t\t\tMathF.Sin( angle ) * spread,\n\t\t\t\t0f );\n\n\t\t\twanderTarget.z = bounds.Center.z \u002B Tuning.SentinelHoverHeight;\n\t\t}\n\n\t\tvar toTarget = wanderTarget - WorldPosition;\n\n\t\tif ( toTarget.Length \u003E 10f )\n\t\t\tWorldPosition \u002B= toTarget.Normal * Tuning.SentinelSpeed * Time.Delta;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Re-arms for a new stage: pushes the reload back and restarts the spawn grace.\n\t///\n\t/// Sentinels carry over between stages so their reload is not reset by a fast player, and\n\t/// that is exactly what made them fire the instant a new stage condensed: a survivor arrives\n\t/// with \u0060timeSinceSpawn\u0060 long expired and its reload already full. The arm delay only ever\n\t/// covered a FRESH spawn, which is not the case that needed covering.\n\t///\n\t/// A new stage should always open with a moment to look around, whoever is still in the sky.\n\t/// \u003C/summary\u003E\n\tpublic void RearmForNewStage()\n\t{\n\t\ttimeSinceSpawn = 0f;\n\t\ttimeSinceShot = Game.Random.Float( 0f, Tuning.SentinelFireInterval * 0.4f );\n\t}\n\n\t/// \u003Csummary\u003ERe-arms every sentinel. Called when a new stage condenses.\u003C/summary\u003E\n\tpublic static void RearmAll()\n\t{\n\t\tfor ( int i = 0; i \u003C all.Count; i\u002B\u002B )\n\t\t{\n\t\t\tif ( all[i].IsValid() )\n\t\t\t\tall[i].RearmForNewStage();\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003ESphere test for one projectile step, same shape as every other hittable.\u003C/summary\u003E\n\tpublic static bool TryHit( Vector3 start, Vector3 direction, float distance, out Sentinel hit )\n\t{\n\t\thit = null;\n\t\tfloat best = float.MaxValue;\n\n\t\tfor ( int i = 0; i \u003C all.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar candidate = all[i];\n\t\t\tif ( !candidate.IsValid() ) continue;\n\n\t\t\tvar toCentre = candidate.WorldPosition - start;\n\t\t\tfloat along = Vector3.Dot( toCentre, direction );\n\n\t\t\tif ( along \u003C -candidate.Radius || along \u003E distance \u002B candidate.Radius )\n\t\t\t\tcontinue;\n\n\t\t\tfloat perpSq = toCentre.LengthSquared - along * along;\n\t\t\tif ( perpSq \u003E candidate.Radius * candidate.Radius )\n\t\t\t\tcontinue;\n\n\t\t\tif ( along \u003C best )\n\t\t\t{\n\t\t\t\tbest = along;\n\t\t\t\thit = candidate;\n\t\t\t}\n\t\t}\n\n\t\treturn hit != null;\n\t}\n\n\tpublic void TakeHit( int damage )\n\t{\n\t\tHealth -= damage;\n\n\t\tif ( Health \u003E 0 )\n\t\t{\n\t\t\tAudio.Play( Audio.MetalHit, WorldPosition, 0.5f, Audio.Vary( 1.3f ) );\n\t\t\treturn;\n\t\t}\n\n\t\tvar progress = PlayerProgress.Local;\n\n\t\tif ( progress.IsValid() )\n\t\t{\n\t\t\tprogress.AwardDust( Tuning.SentinelDustReward );\n\t\t\tprogress.AddResonance( 2 );\n\t\t\tprogress.Data.SentinelsDowned\u002B\u002B;\n\t\t}\n\n\t\tBlastEffect.Spawn( Scene, WorldPosition, 380f, true );\n\t\tDebris.Burst( Scene, WorldPosition, 200f, true );\n\t\tAudio.Play( Audio.Explosion, WorldPosition, 0.85f, Audio.Vary( 1.1f ) );\n\n\t\tLog.Info( \u0022Sentinel downed.\u0022 );\n\t\tGameObject.Destroy();\n\t}\n\n\t/// \u003Csummary\u003EA blunt turret: a wide drum with a barrel stub, unmistakably a gun.\u003C/summary\u003E\n\tprivate static Model GetBodyModel()\n\t{\n\t\tif ( bodyModel != null ) return bodyModel;\n\n\t\tvar vb = new VertexBuffer();\n\t\tvb.Init( true );\n\n\t\tint index = 0;\n\n\t\tVector3[] tips = { Vector3.Up * 0.5f, Vector3.Down * 0.5f };\n\t\tVector3[] ring =\n\t\t{\n\t\t\tnew( 1f, 0f, 0f ), new( 0.7f, 0.7f, 0f ), new( 0f, 1f, 0f ), new( -0.7f, 0.7f, 0f ),\n\t\t\tnew( -1f, 0f, 0f ), new( -0.7f, -0.7f, 0f ), new( 0f, -1f, 0f ), new( 0.7f, -0.7f, 0f ),\n\t\t};\n\n\t\tforeach ( var tip in tips )\n\t\t{\n\t\t\tfor ( int i = 0; i \u003C ring.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar a = ring[i];\n\t\t\t\tvar b = ring[(i \u002B 1) % ring.Length];\n\t\t\t\tvar (p1, p2) = tip.z \u003E 0 ? (a, b) : (b, a);\n\n\t\t\t\tvar normal = Vector3.Cross( p2 - tip, p1 - tip ).Normal;\n\t\t\t\tvar tangent = (p1 - tip).Normal;\n\n\t\t\t\tvb.Add( new Vertex( tip, normal, tangent, new Vector4( 0.5f, 0, 0, 0 ) ) );\n\t\t\t\tvb.Add( new Vertex( p1, normal, tangent, new Vector4( 0, 1, 0, 0 ) ) );\n\t\t\t\tvb.Add( new Vertex( p2, normal, tangent, new Vector4( 1, 1, 0, 0 ) ) );\n\n\t\t\t\tvb.AddRawIndex( index \u002B 0 );\n\t\t\t\tvb.AddRawIndex( index \u002B 1 );\n\t\t\t\tvb.AddRawIndex( index \u002B 2 );\n\t\t\t\tindex \u002B= 3;\n\t\t\t}\n\t\t}\n\n\t\tvar mesh = new Mesh( Material.Load( \u0022materials/default.vmat\u0022 ) );\n\t\tmesh.CreateBuffers( vb );\n\n\t\tbodyModel = new ModelBuilder().AddMesh( mesh ).Create();\n\t\treturn bodyModel;\n\t}\n}\n"},{"Ident":"idkman.monolith","Path":"Game/StageLostEffect.cs","FileName":"StageLostEffect.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"namespace Monolith;\n\n/// \u003Csummary\u003E\n/// What losing a stage looks and sounds like.\n///\n/// Until now, losing produced a line in the console and nothing else. \u0060StageResetAt\u0060 was being\n/// SET by the manager and read by absolutely nothing, so the stage silently rebuilt underneath\n/// you and the most likely reading was that the game had glitched. A failure state you cannot\n/// perceive is not a failure state, it is a bug report waiting to happen.\n///\n/// Devil Daggers gets away with brutal deaths because the death is unmistakable and the restart\n/// is instant (GOALS 6b rule 4). We take nothing from you but time, so the loss has to be\n/// *legible* rather than punishing: the screen goes red, the world thumps, and you are back in\n/// it within a second, knowing exactly what happened and who did it.\n/// \u003C/summary\u003E\npublic sealed class StageLostEffect : Component\n{\n\t/// \u003Csummary\u003ESeconds the red wash takes to fall away.\u003C/summary\u003E\n\t[Property] public float FlashSeconds { get; set; } = 1.4f;\n\n\t/// \u003Csummary\u003EHow far the camera is kicked at the moment of the hit.\u003C/summary\u003E\n\t[Property] public float ShakeStrength { get; set; } = 26f;\n\n\t[Property] public float ShakeSeconds { get; set; } = 0.65f;\n\n\tprivate Vignette vignette;\n\tprivate float lastHandledReset = -999f;\n\n\t/// \u003Csummary\u003E0 to 1 while the loss is still being announced. Read by the HUD for its banner.\u003C/summary\u003E\n\tpublic static float Intensity { get; private set; }\n\n\tprotected override void OnUpdate()\n\t{\n\t\t// Frozen while a blocking screen or the pause menu is up. See GameTime.\n\t\tif ( GameTime.Paused )\n\t\t\treturn;\n\n\t\t// Looked up lazily rather than in OnAwake. Component construction order is not something\n\t\t// worth depending on for a lookup this cheap, and getting it wrong would fail silently\n\t\t// as \u0022the red flash never happens\u0022, which is exactly the class of bug this file exists\n\t\t// to fix in the first place.\n\t\tvignette ??= Components.Get\u003CVignette\u003E();\n\n\t\tvar manager = MonolithManager.Instance;\n\t\tif ( !manager.IsValid() )\n\t\t\treturn;\n\n\t\t// Fires once per reset. Comparing against the stored timestamp rather than using an\n\t\t// event keeps this a pure reader: the manager does not need to know the effect exists.\n\t\tif ( manager.StageResetAt \u003E lastHandledReset )\n\t\t{\n\t\t\tlastHandledReset = manager.StageResetAt;\n\t\t\tAnnounce();\n\t\t}\n\n\t\tfloat since = GameTime.Now - lastHandledReset;\n\t\tIntensity = Math.Clamp( 1f - since / FlashSeconds, 0f, 1f );\n\n\t\tApplyWash();\n\t\tApplyShake( since );\n\t}\n\n\tprivate void Announce()\n\t{\n\t\tvar position = WorldPosition;\n\n\t\t// Two layers: a deep detonation for weight, and a sting on top so it cuts through\n\t\t// whatever else is playing. One sound is an event; two is an announcement.\n\t\tAudio.Play( Audio.Explosion, position, 1f, 0.45f );\n\t\tAudio.Play( Audio.Alert, position, 0.8f, 0.55f );\n\n\t\t// A third, flat layer that does not obey distance. The other two are placed in the world,\n\t\t// so losing a stage while standing far from the shape was oddly quiet for the worst thing\n\t\t// that can happen to you.\n\t\tAudio.StageLost();\n\n\t\tLog.Info( \u0022STAGE LOST.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Drives the existing Vignette red and then hands it back. Reusing the component the retro\n\t/// look already installed means the flash costs nothing extra and cannot fight it.\n\t/// \u003C/summary\u003E\n\tprivate void ApplyWash()\n\t{\n\t\tif ( !vignette.IsValid() )\n\t\t\treturn;\n\n\t\tif ( Intensity \u003C= 0f )\n\t\t{\n\t\t\tvignette.Intensity = Tuning.RetroVignette;\n\t\t\tvignette.Color = Color.Black;\n\t\t\treturn;\n\t\t}\n\n\t\t// Eased so the peak is a hard slam and the tail is a slow bleed, rather than a linear\n\t\t// fade that reads as a UI transition.\n\t\tfloat curve = Intensity * Intensity;\n\n\t\tvignette.Intensity = Tuning.RetroVignette \u002B curve * 0.75f;\n\t\tvignette.Color = Color.Lerp( Color.Black, new Color( 0.55f, 0.02f, 0.02f ), curve );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Current camera kick, ADDED by \u003Csee cref=\u0022PlayerAvatar\u0022/\u003E when it places the boom.\n\t///\n\t/// Published rather than applied directly: PlayerAvatar overwrites the camera\u0027s local\n\t/// position every single frame in OnPreRender, so anything written here would be silently\n\t/// discarded. Whoever owns a transform has to own all of it.\n\t/// \u003C/summary\u003E\n\tpublic static Vector3 ShakeOffset { get; private set; }\n\n\tprivate void ApplyShake( float since )\n\t{\n\t\tif ( since \u003E ShakeSeconds )\n\t\t{\n\t\t\tShakeOffset = Vector3.Zero;\n\t\t\treturn;\n\t\t}\n\n\t\t// Squared falloff and a fresh random direction each frame, so it reads as an impact\n\t\t// rather than a wobble.\n\t\tfloat falloff = 1f - (since / ShakeSeconds);\n\n\t\tShakeOffset = Vector3.Random.Normal * ShakeStrength * falloff * falloff;\n\t}\n}\n"},{"Ident":"idkman.monolith","Path":"Tuning.cs","FileName":"Tuning.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"namespace Monolith;\n\n/// \u003Csummary\u003E\n/// Every game-feel number lives here. The user iterates on feel constantly, so nothing\n/// tunable should be buried in gameplay code. See GOALS.md for the intent behind each value.\n/// \u003C/summary\u003E\npublic static class Tuning\n{\n\t// ---------------------------------------------------------------- world\n\n\t/// \u003Csummary\u003ESize of one voxel in world units. A citizen is roughly 72 units tall.\u003C/summary\u003E\n\tpublic const float VoxelSize = 16f;\n\n\t/// \u003Csummary\u003EVoxels per chunk axis. 32 gives 32768 voxels = a 4 KB bitset per chunk.\u003C/summary\u003E\n\tpublic const int ChunkSize = 32;\n\n\t// Stage sizes and shapes now live in Stages.cs, which is the ladder the player climbs.\n\n\t/// \u003Csummary\u003E\n\t/// Per-stage size multiplier, applied to each axis. The target is a **20 minute** run to\n\t/// stage 100 for a min-maxing player.\n\t///\n\t/// Derived by simulation, not by guessing: a greedy upgrade buyer starting from ~300 cubes\n\t/// at stage 1 (about 50 seconds) reaches stage 100 in about **18 minutes**. Change this and\n\t/// the whole pacing target moves with it. **[TUNE]**\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// **Was 1.11, which saturated.** At 1.11 a 7 unit axis reaches the 192 cap by stage 33, so\n\t/// every stage from there to 100 was the SAME maximum size: seventy identical slogs, and the\n\t/// worst possible case for both pacing and framerate.\n\t///\n\t/// 1.030 is derived, not guessed. Putting the old stage-26 size at stage 90 means\n\t/// growth^89 = 1.11^25, so growth = 13.585^(1/89) = 1.0298. The cap is now never reached\n\t/// inside 100 stages, so every stage really is a step up from the one before.\n\t/// \u003C/remarks\u003E\n\tpublic const float StageSizeGrowth = 1.030f;\n\n\t/// \u003Csummary\u003E\n\t/// Upper bound per axis. Lowered 192 to 160: 192^3 is 7.1M voxels and 216 chunks, which is a\n\t/// lot of meshing for a stage you are meant to clear in under a minute. With the growth rate\n\t/// above this is a safety net rather than something the ladder actually reaches.\n\t/// \u003C/summary\u003E\n\tpublic const int StageMaxAxis = 160;\n\n\t// ---------------------------------------------------------------- how the rock LOOKS\n\t//\n\t// See ChunkMesher. Greedy meshing merges a flat wall into a single quad, which is fast and\n\t// which is exactly why an untouched shape read as a featureless box. These are the two dials\n\t// that put the cubes back, and both work by making the merge more selective.\n\n\t/// \u003Csummary\u003E\n\t/// How dark a fully occluded voxel corner gets, as a multiplier. 1 disables AO entirely.\n\t///\n\t/// This is the single biggest contributor to something reading as VOXELS rather than as a\n\t/// shaded solid: it draws the boundary between one cube and the next, which is a scale that\n\t/// screen-space AO cannot work at. Costs nothing on flat runs, because uniform occlusion\n\t/// still merges into one quad. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float VoxelAoStrength = 0.42f;\n\n\t/// \u003Csummary\u003E\n\t/// Number of horizontal rock layers. **Set to 1 to switch strata off entirely**, which is\n\t/// also the first thing to try if meshing gets expensive.\n\t///\n\t/// AO only articulates surfaces that have been CARVED. A pristine box has no occlusion\n\t/// anywhere, so without this it stays a box until you shoot it. Banding gives the mass\n\t/// internal structure before you touch it. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const int StrataBands = 3;\n\n\t/// \u003Csummary\u003E\n\t/// Voxels per layer, and the performance dial for the whole feature. A band boundary breaks\n\t/// the greedy merge run, so thin layers mean many more quads on vertical faces. Horizontal\n\t/// faces are unaffected, since a whole face sits in one layer. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const int StrataThickness = 5;\n\n\t/// \u003Csummary\u003E\n\t/// Brightness spread between the lightest and darkest layer. Deliberately small: this should\n\t/// read as sedimentary rock, not as stripes. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float StrataContrast = 0.22f;\n\n\t// ---------------------------------------------------------------- the shared Monolith\n\n\t/// \u003Csummary\u003E\n\t/// The shared Monolith. 256^3 is 16.7M cubes: big enough to be a real destination and to\n\t/// dwarf a fresh player, small enough for the current DENSE chunk storage (512 chunks,\n\t/// ~2 MB). The \u0022weeks of collective effort\u0022 target in GOALS.md needs 2048^3, which needs\n\t/// sparse chunks first. This is the honest interim scale. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic static readonly Vector3Int MonolithSize = new( 256, 256, 256 );\n\n\t/// \u003Csummary\u003ESeconds the \u0022stage cleared\u0022 banner stays up before the next shape condenses.\u003C/summary\u003E\n\tpublic const float StageClearedPauseSeconds = 3.5f;\n\n\t/// \u003Csummary\u003E\n\t/// Below this many remaining cubes the leftovers light up and the HUD calls out the count.\n\t/// A single 16 unit cube in a 512 unit void is genuinely impossible to spot otherwise, which\n\t/// reads as the stage being stuck rather than nearly finished.\n\t/// \u003C/summary\u003E\n\tpublic const int ResidueRevealThreshold = 250;\n\n\t// ---------------------------------------------------------------- drill\n\t//\n\t// REAL TUNING. The playtest speedups are gone. Derived from a simulation of a greedy\n\t// upgrade buyer over all 100 stages, targeting ~18 minutes for a min-maxing player and a\n\t// stage 100 that fits inside StageMaxAxis.\n\t//\n\t// The critical lesson from that simulation: **blast radius scales value CUBICALLY** (a\n\t// sphere), so it needs a far steeper cost curve than the upgrades whose value is linear.\n\t// Giving every upgrade the same growth rate produced a runaway economy that cleared the\n\t// entire ladder in 72 seconds. Hence per-upgrade growth rates below.\n\n\t// NOTHING IS CAPPED. Upgrades must never show \u0022MAX\u0022: an incremental game dies the moment\n\t// a purchase stops being available. Every curve below grows without bound, and the ones\n\t// that are conceptually bounded (a probability) approach their limit asymptotically\n\t// instead of hitting a wall.\n\n\t/// \u003Csummary\u003E\n\t/// Shots per second. Level 0 is ~0.9 SECONDS BETWEEN SHOTS: slow and methodical on\n\t/// purpose, so the first upgrade you buy is the one that makes you faster and you feel it.\n\t/// The HUD presents this as a delay rather than a rate for the same reason.\n\t/// \u003C/summary\u003E\n\tpublic const float DrillSpeedBase = 1.1f;\n\tpublic const float DrillSpeedPerLevel = 0.30f;\n\n\t/// \u003Cremarks\u003E\n\t/// Per-level growth cut 0.16 to 0.11 alongside the stage-size change.\n\t///\n\t/// Blast radius is the single biggest driver of frame cost: it is cubic in voxels touched\n\t/// AND it is what decides how many chunks get dirtied per shot. Smaller stages need smaller\n\t/// blasts to feel the same, and the pair of changes together is what makes late stages\n\t/// cheap to render rather than merely shorter. **[TUNE]**\n\t/// \u003C/remarks\u003E\n\tpublic const float BlastRadiusBase = 0.9f;     // ~3 cubes per shot at level 0\n\tpublic const float BlastRadiusPerLevel = 0.11f;\n\n\t// Instability no longer rolls per shot. It seeds the SHAPE with volatile blocks, so the\n\t// upgrade buys targets to aim at rather than a random chance of a bigger explosion. The\n\t// player has to find and hit them.\n\n\t/// \u003Csummary\u003EFraction of blocks that are volatile at Instability level 0.\u003C/summary\u003E\n\tpublic const float VolatileChanceBase = 0.02f;\n\n\t/// \u003Csummary\u003EApproaches a ceiling asymptotically, so it never runs out of levels.\u003C/summary\u003E\n\tpublic const float VolatileChanceRate = 0.05f;\n\n\t/// \u003Csummary\u003EHard ceiling on volatile density. Past this the shape stops reading as rock.\u003C/summary\u003E\n\tpublic const float VolatileChanceMax = 0.22f;\n\n\t/// \u003Csummary\u003EBlast radius on a direct volatile hit, at Detonation level 0. Scaled down with\n\t/// the stage sizes: a detonation should still feel like a reward, not clear the stage.\u003C/summary\u003E\n\tpublic const float VolatileRadiusBase = 5f;\n\tpublic const float VolatileRadiusPerLevel = 0.45f;\n\n\tpublic const float DustYieldBase = 1.0f;       // dust per cube removed\n\tpublic const float DustYieldPerLevel = 0.22f;\n\n\t/// \u003Csummary\u003E\n\t/// Shots per second for ONE drone at Drone Cadence level 0. Deliberately slow: a fresh drone\n\t/// should read as a helper you then invest in, not as an instant doubling of your output.\n\t/// \u003C/summary\u003E\n\tpublic const float DroneFireRateBase = 0.32f;\n\n\t/// \u003Csummary\u003EAdded shots per second per drone, per Drone Cadence level.\u003C/summary\u003E\n\tpublic const float DroneFireRatePerLevel = 0.14f;\n\n\tpublic const float DroneRadiusScale = 0.6f;    // drones hit softer than the player\n\n\t// ---------------------------------------------------------------- projectiles\n\n\t/// \u003Csummary\u003E\n\t/// Units per second. Fast enough that a shot feels immediate at mining range, slow enough\n\t/// that you can watch it travel and that something can get in its way.\n\t/// \u003C/summary\u003E\n\tpublic const float ProjectileSpeed = 4200f;\n\n\t/// \u003Csummary\u003E\n\t/// Charges fly considerably slower than shots. You watch the whole flight and the bore that\n\t/// follows it, and time the detonation off that path, so the travel IS the mechanic.\n\t/// \u003C/summary\u003E\n\tpublic const float ChargeProjectileSpeed = 625f;\n\n\t/// \u003Csummary\u003E\n\t/// Seconds before a projectile that hit nothing gives up.\n\t///\n\t/// Cut 3.0 to 1.2. At \u003Csee cref=\u0022ProjectileSpeed\u0022/\u003E that is still 5,000 units, further than\n\t/// the width of any arena, so nothing that could have hit something is cut short. Three\n\t/// seconds meant a bolt fired at open sky loitered for 12,600 units of empty space while\n\t/// holding a slot in the live-projectile cap.\n\t/// \u003C/summary\u003E\n\tpublic const float ProjectileLifetime = 1.2f;\n\n\t/// \u003Csummary\u003E\n\t/// Spread applied to extra projectiles from multi-shot, in degrees. Enough that they land\n\t/// on different cubes (which is the point of multi-shot) without feeling inaccurate.\n\t/// \u003C/summary\u003E\n\tpublic const float MultiShotSpreadDegrees = 2.4f;\n\n\t/// \u003Csummary\u003E\n\t/// Most bolts a single trigger pull will ever DRAW, however many prestiges you have.\n\t///\n\t/// \u0060ProjectileCount\u0060 grows by one per prestige and is unbounded. At 21 it is already a wall\n\t/// of objects leaving the barrel at once, reported twice as looking like a shotgun blast,\n\t/// and at 60 it would be worse in every way: more allocation, more overdraw, and no clearer\n\t/// as feedback. Nobody can read twenty-one simultaneous bolts as twenty-one.\n\t///\n\t/// Past this cap the reward is PRESERVED but converted: the extras fold into blast radius\n\t/// instead of spawning, so the shot does the same work and stays legible. See\n\t/// \u003Csee cref=\u0022Upgrades.MultiShotRadiusScale\u0022/\u003E.\n\t/// \u003C/summary\u003E\n\tpublic const int MaxVisibleProjectiles = 6;\n\n\t/// \u003Csummary\u003E\n\t/// Ceiling on \u0060ProjectileCount\u0060, which is a PRESTIGE reward and was unbounded.\n\t///\n\t/// It read \u00601 \u002B Collapses\u0060 on the assumption that Collapses rises by one per prestige. Once\n\t/// felling a Monolith started awarding twenty, that assumption broke and the value reached\n\t/// 205: two hundred and five bolts a shot, or after the visible cap, a blast radius scaled\n\t/// 3.25x and removing millions of voxels per trigger pull. The game stalled.\n\t///\n\t/// 24 keeps prestige meaningfully rewarding (radius scaled up to 1.6x once folded) while\n\t/// bounding the worst case. Growth past here has to come from the Core Tree, which is\n\t/// designed for it, rather than from a value that multiplies work per frame. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const int MaxProjectileCount = 24;\n\n\t// ---------------------------------------------------------------- interceptors\n\n\tpublic const int InterceptorHealth = 3;\n\tpublic const int InterceptorChargeDamage = 3;\n\tpublic const float InterceptorSpeed = 190f;\n\tpublic const float InterceptorRetargetSeconds = 2.4f;\n\tpublic const float InterceptorSpawnInterval = 2.2f;\n\tpublic const int InterceptorMaxPopulation = 12;\n\n\t/// \u003Csummary\u003EStage at which interceptors first appear. The opening stays uncontested.\u003C/summary\u003E\n\tpublic const int InterceptorFirstStage = 3;\n\n\t/// \u003Csummary\u003EStages between each additional simultaneous interceptor.\u003C/summary\u003E\n\tpublic const int InterceptorStagesPerExtra = 3;\n\n\t/// \u003Csummary\u003EDust for destroying one. Scaled by yield upgrades like anything else.\u003C/summary\u003E\n\tpublic const double InterceptorDustReward = 40;\n\n\t// ---------------------------------------------------------------- volatile cubes\n\n\n\n\n\n\t// ---------------------------------------------------------------- orbital barriers\n\n\t/// \u003Csummary\u003E\n\t/// Wide panels that orbit the shape and block shots. Unlike interceptors you cannot kill\n\t/// them: the only answer is to move, which is the entire reason they exist.\n\t/// \u003C/summary\u003E\n\tpublic const int BarrierFirstStage = 5;\n\tpublic const int BarrierStagesPerExtra = 9;\n\tpublic const int BarrierMaxCount = 4;\n\n\t/// \u003Csummary\u003EAngular width of one panel, in degrees.\u003C/summary\u003E\n\tpublic const float BarrierArcDegrees = 52f;\n\n\t/// \u003Csummary\u003EDegrees per second. Slow enough to read, fast enough to force repositioning.\u003C/summary\u003E\n\tpublic const float BarrierSpeedMin = 9f;\n\tpublic const float BarrierSpeedMax = 22f;\n\n\t// ---------------------------------------------------------------- spotter\n\n\t/// \u003Csummary\u003EChance a stage spawns Spotters at some point.\u003C/summary\u003E\n\tpublic const float SpotterChance = 0.5f;\n\n\t/// \u003Csummary\u003E\n\t/// How many arrive together. Cut from 2-4 to 1-2.\n\t///\n\t/// A pack was the wrong shape of threat. Four of them meant no angle was ever safe, which\n\t/// sounds threatening and actually reads as noise: you cannot learn to beat a specific\n\t/// Spotter when there are four, so you stop trying. One or two, each of which can genuinely\n\t/// catch you, is more frightening than four you cannot reason about. They also carry over\n\t/// between stages now, so the population builds anyway.\n\t/// \u003C/summary\u003E\n\tpublic const int SpotterCountMin = 1;\n\tpublic const int SpotterCountMax = 2;\n\n\t/// \u003Csummary\u003E\n\t/// Stages during which at most ONE Spotter may exist, and it arrives less often.\n\t///\n\t/// The opening is where a player is still learning that the beam is a countdown and that the\n\t/// shape is cover. Two of them converging before either lesson has landed is not difficulty,\n\t/// it is a player who never finds out what happened. They still carry over between stages,\n\t/// so the population climbs the moment this grace ends.\n\t/// \u003C/summary\u003E\n\tpublic const int SpotterGraceStages = 5;\n\n\t/// \u003Csummary\u003EChance of a Spotter at all during the grace. Roughly half the usual.\u003C/summary\u003E\n\tpublic const float SpotterGraceChance = 0.25f;\n\n\t/// \u003Csummary\u003E\n\t/// Minimum bearing between two Spotters arriving together, in degrees. Placed on opposite\n\t/// sides rather than clustered, so a pair covers the arena instead of stacking one threat.\n\t/// \u003C/summary\u003E\n\tpublic const float SpotterSpacingDegrees = 130f;\n\n\t/// \u003Csummary\u003E\n\t/// A Spotter on standby ignores you entirely until you are this close. They hang around\n\t/// the shape as scenery you can pick off at leisure, and only become a threat once you\n\t/// come to work near them.\n\t/// \u003C/summary\u003E\n\tpublic const float SpotterWakeRange = 1500f;\n\n\t/// \u003Csummary\u003E\n\t/// Fraction of the stage that must be cleared before a Spotter arrives.\n\t///\n\t/// Deliberately expressed as PROGRESS, not seconds. A wall-clock delay meant a fast player\n\t/// cleared the stage before the Spotter ever showed up, so it effectively did not exist\n\t/// once you had a couple of upgrades. Progress scales with the player automatically.\n\t/// \u003C/summary\u003E\n\tpublic const float SpotterArriveMinProgress = 0.15f;\n\tpublic const float SpotterArriveMaxProgress = 0.55f;\n\n\t/// \u003Csummary\u003ELow: it should die to a couple of ordinary shots, like a stubborn block.\u003C/summary\u003E\n\tpublic const int SpotterHealth = 3;\n\n\t/// \u003Csummary\u003E\n\t/// Body radius, which is also the sphere a projectile has to cross to hit one. Deliberately\n\t/// large: it hangs a long way off against an empty sky, where a small silhouette is both hard\n\t/// to read as a threat and hard to lead.\n\t/// \u003C/summary\u003E\n\tpublic const float SpotterRadius = 145f;\n\n\tpublic const float SpotterHoverHeight = 620f;\n\tpublic const float SpotterSpeed = 240f;\n\tpublic const float SpotterRetargetSeconds = 3.0f;\n\n\t/// \u003Csummary\u003E\n\t/// How fast the beam can turn, in degrees per second.\n\t///\n\t/// **This is the whole encounter.** The beam is always trying to point at you, so the only\n\t/// question that matters is whether you can move across its arc faster than it can follow.\n\t/// Raise it and the Spotter becomes unavoidable; lower it and it becomes decorative.\n\t///\n\t/// At the standoff distance a sprint is worth roughly 35 degrees per second and a strafe\n\t/// double jump momentarily far more, so running perpendicular to the beam beats it and\n\t/// running straight at or away from it does not. That is the lesson it should teach. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float SpotterTrackDegreesPerSecond = 62f;\n\n\t/// \u003Csummary\u003E\n\t/// Lateral speed, in units per second, the beam can follow REGARDLESS of how close you are.\n\t///\n\t/// A pure angular rate has a nasty flaw: the closer you stand, the more degrees per second\n\t/// your movement is worth, so standing directly under a Spotter made it physically unable to\n\t/// track you. Being right beneath the searchlight was the safest place in the arena, which is\n\t/// exactly backwards. The slew budget is now the LARGER of the angular rate and whatever\n\t/// angle this linear speed works out to at your current distance.\n\t///\n\t/// Set just under sprint speed (609) on purpose: walking never escapes, sprinting sideways\n\t/// slowly slips the gaze, and a dash breaks it outright. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float SpotterTrackLinearSpeed = 520f;\n\n\t/// \u003Csummary\u003E\n\t/// Half-angle of the wide SEARCH cone: how far off its gaze it can still notice you.\n\t///\n\t/// Two narrower versions of this failed in playtest (7 then 15 degrees). The mistake was\n\t/// treating spotting and tracking as the same number. They are not:\n\t///\n\t///   SPOTTING is a wide cone. A searchlight sees anything in the general direction it faces,\n\t///   which is why walking through its area gets you caught and why the answer is cover.\n\t///   TRACKING is the tight line that snaps to you once it has you, and its job is only to\n\t///   make being caught unmistakable.\n\t///\n\t/// Collapsing the two into one tight angle produced a Spotter that could stare straight at\n\t/// you and never notice, which is what \u0022spotters are still not working\u0022 meant.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// 50 down to 28. Fifty was chosen when the gaze tracked you unconditionally and the cone was\n\t/// the only thing standing between you and a permanent lock, so it had to be wide enough to\n\t/// make acquisition possible at all. Now that it patrols while unaware, the cone is doing its\n\t/// real job: deciding whether a sweep passing near you actually finds you. A narrow one makes\n\t/// that a near miss rather than a formality.\n\t/// \u003C/remarks\u003E\n\tpublic const float SpotterViewCone = 28f;\n\n\n\t/// \u003Csummary\u003EHorizontal distance an awake Spotter tries to hold from you. **[TUNE]**\u003C/summary\u003E\n\tpublic const float SpotterStandoff = 560f;\n\n\t/// \u003Csummary\u003EMove speed while hunting. Faster than the standby drift.\u003C/summary\u003E\n\tpublic const float SpotterChaseSpeed = 330f;\n\n\n\t/// \u003Csummary\u003E\n\t/// Thickness of the tracking line, in world units. Was 7, which multiplied out to a beam\n\t/// wide enough to blank out the middle of the screen once the search states drew at 3.5x\n\t/// and 5.5x. It is a line, not a wall: the COLOUR carries the threat, not the area.\n\t/// \u003C/summary\u003E\n\tpublic const float SpotterBeamThickness = 0.85f;\n\n\t/// \u003Csummary\u003EHow far the beam reaches.\u003C/summary\u003E\n\tpublic const float SpotterBeamRange = 3200f;\n\n\t/// \u003Csummary\u003E\n\t/// Seconds in the beam WITH line of sight before your stage progress is wiped.\n\t///\n\t/// The beam runs yellow, then orange, then red, and red occupies the FINAL SECOND. So the\n\t/// colour is the countdown: by the time it is red you have exactly one second left. Three\n\t/// seconds total gives two seconds of escalating warning before that.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// Cut 3.0 to 2.0. Three seconds was tuned when a stage lasted a minute; against stages that\n\t/// fall in seconds it was long enough that simply continuing to mine usually outran it.\n\t/// Two seconds means being seen is a thing you have to answer NOW.\n\t/// \u003C/remarks\u003E\n\tpublic const float SpotterLockSeconds = 1.0f;\n\n\t/// \u003Csummary\u003E\n\t/// Lock fraction at which the beam turns orange, then red. RedAt moved 0.667 to 0.5 so that\n\t/// red still means ONE SECOND LEFT against the shorter lock. The colour has to keep meaning\n\t/// the same thing in wall-clock terms or the warning stops being learnable.\n\t/// \u003C/summary\u003E\n\tpublic const float SpotterOrangeAt = 0.25f;\n\tpublic const float SpotterRedAt = 0.5f;\n\n\t/// \u003Csummary\u003E\n\t/// Seconds the beam must hold you before the lock begins to count. **Zero on purpose.**\n\t///\n\t/// It used to be 0.45s of grace, which existed because the beam SNAPPED onto you the instant\n\t/// line of sight opened and something had to absorb that unfairness. Now the beam has to\n\t/// physically turn onto you at \u003Csee cref=\u0022SpotterTrackDegreesPerSecond\u0022/\u003E, so the grace is\n\t/// built into the movement and a second one on top would only make the countdown lie about\n\t/// when it started. On you means counting.\n\t/// \u003C/summary\u003E\n\tpublic const float SpotterAcquireSeconds = 0f;\n\n\t/// \u003Csummary\u003EHow fast the lock unwinds once you are clear. Faster than it fills, on purpose.\u003C/summary\u003E\n\tpublic const float SpotterDecayMultiplier = 2.2f;\n\n\t/// \u003Csummary\u003E\n\t/// Seconds after a wipe during which NO Spotter can begin a lock.\n\t///\n\t/// A Spotter no longer removes itself when it catches you: the threat that beat you is still\n\t/// there when the stage rebuilds, which is the whole reason it is a threat. That only works\n\t/// with a grace period, otherwise it simply re-locks the moment the new shape appears and you\n\t/// never get a first move.\n\t/// \u003C/summary\u003E\n\tpublic const float SpotterWipeCooldown = 4.5f;\n\n\n\t/// \u003Csummary\u003EDust for shooting one down.\u003C/summary\u003E\n\tpublic const double SpotterDustReward = 250;\n\n\t// ---------------------------------------------------------------- sentinels\n\t//\n\t// The first thing in the game that SHOOTS BACK. Every other hazard is passive: barriers sit\n\t// in the way, leeches sit on the rock, the Spotter watches. All of them can be ignored for a\n\t// while. A sentinel cannot, because its shot arrives whether or not you looked at it.\n\t//\n\t// Its orb is deliberately slow and destructible, so the answer is a choice rather than a\n\t// reflex: dodge it, or spend a shot killing it, or spend several killing the sentinel and\n\t// stop the problem at the source.\n\n\t/// \u003Csummary\u003E\n\t/// How close you have to be before a sentinel will fire.\n\t///\n\t/// Shorter than the Spotter\u0027s reach on purpose. A sentinel should be a local problem you\n\t/// walk into and can walk out of, not something shooting at you from across the arena: at\n\t/// unlimited range four of them meant permanent incoming fire wherever you stood, with no\n\t/// positional decision to make about it.\n\t/// \u003C/summary\u003E\n\tpublic const float SentinelRange = 1500f;\n\n\t/// \u003Csummary\u003E\n\t/// Half-angle a sentinel must have you within to fire, measured off its facing.\n\t///\n\t/// It had NO view at all: anything in range with line of sight was shot at, from any\n\t/// direction, which meant its shots arrived with no warning and no way to play around it.\n\t/// Now it must physically turn to face you first, and turning is visible.\n\t/// \u003C/summary\u003E\n\t/// \u003Csummary\u003E\n\t/// Seconds after arriving, or after a new stage condenses, before a sentinel may aim or fire.\n\t///\n\t/// Spawning is not an attack. The real delay is longer than this number: it spends this\n\t/// window drifting and only THEN starts turning toward you at\n\t/// \u003Csee cref=\u0022SentinelTurnRate\u0022/\u003E, so a sentinel that appears behind you has to travel most\n\t/// of a half turn before it can shoot.\n\t/// \u003C/summary\u003E\n\tpublic const float SentinelArmSeconds = 1.0f;\n\n\n\tpublic const float SentinelViewCone = 26f;\n\n\t/// \u003Csummary\u003EDegrees per second it can turn. Slow enough that walking around it works.\u003C/summary\u003E\n\tpublic const float SentinelTurnRate = 55f;\n\n\tpublic const int SentinelHealth = 6;\n\tpublic const float SentinelHoverHeight = 300f;\n\tpublic const float SentinelSpeed = 120f;\n\n\t/// \u003Csummary\u003ESeconds between shots. Slow: this is a metronome you plan around.\u003C/summary\u003E\n\tpublic const float SentinelFireInterval = 3.4f;\n\n\t/// \u003Csummary\u003EStage at which sentinels first appear.\u003C/summary\u003E\n\tpublic const int SentinelFirstStage = 6;\n\n\t/// \u003Csummary\u003EStages between each additional simultaneous sentinel.\u003C/summary\u003E\n\tpublic const int SentinelStagesPerExtra = 12;\n\n\tpublic const int SentinelMaxCount = 4;\n\tpublic const double SentinelDustReward = 400;\n\n\t/// \u003Csummary\u003EOrb speed. Slow enough to read, fast enough that ignoring it is a decision.\u003C/summary\u003E\n\tpublic const float SentinelOrbSpeed = 430f;\n\n\t/// \u003Csummary\u003EHow close an orb has to get to count as a hit.\u003C/summary\u003E\n\tpublic const float SentinelOrbRadius = 42f;\n\n\t/// \u003Csummary\u003ESeconds an orb survives before fizzling.\u003C/summary\u003E\n\tpublic const float SentinelOrbLifetime = 7f;\n\n\t/// \u003Csummary\u003E\n\t/// How far ahead of you a sentinel leads its shot. Below 1 on purpose: it aims at where you\n\t/// WOULD be, so holding a straight line gets you hit and changing direction beats it. That\n\t/// is the whole lesson, and a perfect lead would teach the opposite one.\n\t/// \u003C/summary\u003E\n\tpublic const float SentinelLeadFactor = 0.75f;\n\n\t// ---------------------------------------------------------------- anchors\n\n\t/// \u003Csummary\u003E\n\t/// A cable fired at the player from across the arena. While attached it halves your speed and\n\t/// kills hop boosts, and the ONLY way off is to shoot the far end.\n\t///\n\t/// This is the hazard that most directly forces the thing being asked for: it cannot be\n\t/// answered by moving, it cannot be answered by ignoring it, and the target is nowhere near\n\t/// the shape you are mining. You have to turn your back on your work to deal with it.\n\t/// \u003C/summary\u003E\n\tpublic const int AnchorHealth = 3;\n\tpublic const float AnchorHoverHeight = 140f;\n\tpublic const float AnchorRange = 2400f;\n\n\t/// \u003Csummary\u003EMovement multiplier while tethered.\u003C/summary\u003E\n\tpublic const float AnchorSlowFactor = 0.5f;\n\n\t/// \u003Csummary\u003E\n\t/// How fast a hold drags your existing speed down to its ceiling, as a lerp rate.\n\t///\n\t/// Needed because capping the wish speed alone does nothing to a player who is already\n\t/// moving faster: Accelerate only adds. Eased rather than clamped so it feels like being\n\t/// pulled back rather than hitting a wall.\n\t/// \u003C/summary\u003E\n\tpublic const float HoldDragRate = 3.2f;\n\n\n\t/// \u003Csummary\u003ESeconds a tether holds before it releases on its own.\u003C/summary\u003E\n\tpublic const float AnchorHoldSeconds = 9f;\n\n\t/// \u003Csummary\u003ESeconds between one releasing and it being able to fire again.\u003C/summary\u003E\n\tpublic const float AnchorCooldownSeconds = 7f;\n\n\tpublic const int AnchorFirstStage = 11;\n\tpublic const int AnchorStagesPerExtra = 16;\n\tpublic const int AnchorMaxCount = 3;\n\tpublic const double AnchorDustReward = 320;\n\n\t// ---------------------------------------------------------------- crawlers\n\t//\n\t// The first thing that comes for you on the FLOOR. Every other hazard sits in the sky and\n\t// asks you to look up; this one turns the ground you are standing on into a problem, which\n\t// is the half of the arena nothing was using.\n\t//\n\t// It is the payoff for the Anchor. A tether halves your speed, and on its own that is only\n\t// annoying: something that closes at a fixed rate turns the same tether into a countdown.\n\t// Neither is very interesting alone and the pair is the most dangerous thing in the game.\n\n\tpublic const int CrawlerHealth = 5;\n\n\t/// \u003Csummary\u003E\n\t/// Speed it starts a chase at. **Below walking pace on purpose.** You should always be able\n\t/// to walk away from a fresh crawler, so being caught is the result of standing still or\n\t/// being held, never of it simply being faster than you.\n\t/// \u003C/summary\u003E\n\tpublic const float CrawlerBaseSpeed = 190f;\n\n\t/// \u003Csummary\u003E\n\t/// Speed it reaches after \u003Csee cref=\u0022CrawlerRampSeconds\u0022/\u003E of unbroken pursuit.\n\t///\n\t/// Sits between walking (420) and sprinting (609): a fully wound-up crawler outruns a walk\n\t/// and loses to a sprint, so the answer is always to commit to moving rather than to have\n\t/// out-levelled it. Against the Anchor\u0027s halved 210 it wins comfortably, which is the point.\n\t/// \u003C/summary\u003E\n\tpublic const float CrawlerMaxSpeed = 520f;\n\n\t/// \u003Csummary\u003ESeconds of pursuit to reach full speed. Long enough to see it happening.\u003C/summary\u003E\n\tpublic const float CrawlerRampSeconds = 14f;\n\n\t/// \u003Csummary\u003EHow close it has to get to take the stage.\u003C/summary\u003E\n\tpublic const float CrawlerGrabRadius = 62f;\n\n\t/// \u003Csummary\u003ESeconds after a grab before any crawler can grab again.\u003C/summary\u003E\n\tpublic const float CrawlerGrabCooldown = 4.5f;\n\n\t/// \u003Csummary\u003EDistance from the shape it walks in from.\u003C/summary\u003E\n\tpublic const float CrawlerSpawnDistance = 1900f;\n\n\tpublic const int CrawlerFirstStage = 5;\n\tpublic const int CrawlerStagesPerExtra = 9;\n\t/// \u003Cremarks\u003E\n\t/// Raised 3 to 6, and moved earlier, alongside the lethal floor. The two systems are the same\n\t/// idea from opposite directions: the floor says you cannot stand still, and a crawler says\n\t/// you cannot stand still HERE. On an empty floor a pack of six would just be noise, but once\n\t/// half the ground is a countdown, deciding which safe tile to run to while something is\n\t/// closing on the obvious one is the whole game. **[TUNE]**\n\t/// \u003C/remarks\u003E\n\tpublic const int CrawlerMaxCount = 6;\n\tpublic const double CrawlerDustReward = 500;\n\n\t// ---------------------------------------------------------------- being hit\n\n\t/// \u003Csummary\u003E\n\t/// Seconds you cannot fire after taking a hit.\n\t///\n\t/// Consequences cost TIME, never earned progress (GOALS 6b). A stagger breaks your hop chain,\n\t/// interrupts your mining and drops your Resonance, which is expensive in an incremental game\n\t/// without being punitive. Nothing takes dust, cubes or levels away.\n\t/// \u003C/summary\u003E\n\tpublic const float StaggerSeconds = 0.7f;\n\n\t/// \u003Csummary\u003EFraction of horizontal speed kept when staggered. Low: the hop chain is the cost.\u003C/summary\u003E\n\tpublic const float StaggerSpeedKept = 0.25f;\n\n\t// ---------------------------------------------------------------- shields\n\n\t/// \u003Csummary\u003E\n\t/// Chance a stage begins shielded. A shield jams DRONES only: you can still mine by hand,\n\t/// so the answer to it is to play, which is precisely the point of having it.\n\t/// \u003C/summary\u003E\n\tpublic const float ShieldChance = 0.14f;\n\n\tpublic const float ShieldMinSeconds = 25f;\n\tpublic const float ShieldMaxSeconds = 60f;\n\n\t/// \u003Csummary\u003E\n\t/// Chance is high enough to matter, but a shield used to be INVISIBLE: its only effects\n\t/// were jamming drones you may not own yet and a line of HUD text. It now renders as a\n\t/// shell around the shape so it is obviously a thing that is happening to you.\n\t/// \u003C/summary\u003E\n\tpublic const float ShieldShellPadding = 90f;\n\n\t/// \u003Csummary\u003E\n\t/// The shield\u0027s weak point: a node hanging above the shape. Shooting it drops the shield for\n\t/// \u003Csee cref=\u0022ShieldBreakSeconds\u0022/\u003E, then it comes back.\n\t///\n\t/// **A drone cannot break it.** That is the whole design: the shield exists to stop idling\n\t/// through a stage, so if the drones could clear their own jam it would undo itself. Only\n\t/// your shots count, which turns a shielded stage from a wait into a repeating skill beat.\n\t/// \u003C/summary\u003E\n\tpublic const float ShieldBreakSeconds = 2f;\n\n\t/// \u003Csummary\u003EHow far above the top of the shape the node hangs.\u003C/summary\u003E\n\tpublic const float ShieldNodeHeight = 300f;\n\n\t/// \u003Csummary\u003EBody radius, and the sphere a shot must cross to strike it.\u003C/summary\u003E\n\tpublic const float ShieldNodeRadius = 115f;\n\n\n\t// ---------------------------------------------------------------- bunny hopping\n\t//\n\t// Devil Daggers does NOT gain speed from strafing (see GOALS 6d):\n\t//\n\t//   \u0022In Devil Daggers, the speed comes solely from bhopping. Hitting jump at the correct\n\t//    intervals is what makes you speed up, and strafing does not affect your speed in any way.\u0022\n\t//\n\t// Our movement was a faithful Quake implementation, which is a different game. Quake rewards\n\t// STEERING; the reference rewards TIMING. Timing is the better fit here because it is a skill\n\t// you can visibly get better at without having to learn air-strafe theory first.\n\n\t/// \u003Csummary\u003E\n\t/// Seconds before landing in which a jump press counts as a PERFECT hop. Generous on\n\t/// purpose: this should be learnable in a minute, not a fortnight.\n\t/// \u003C/summary\u003E\n\tpublic const float PerfectHopWindow = 0.14f;\n\n\t/// \u003Csummary\u003ESpeed added by a perfect hop, in units per second.\u003C/summary\u003E\n\tpublic const float PerfectHopBoost = 105f;\n\n\t/// \u003Csummary\u003ESpeed added by a mistimed hop. Nonzero so hopping always beats not hopping.\u003C/summary\u003E\n\tpublic const float SloppyHopBoost = 22f;\n\n\t/// \u003Csummary\u003E\n\t/// Ceiling on hop-chain speed as a multiple of ground speed. The reference is effectively\n\t/// uncapped, but our arena has walls and our hazards are tuned against a known top speed, so\n\t/// an unbounded chain would break the Spotter\u0027s tracking budget. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float HopSpeedMax = 2.6f;\n\n\t/// \u003Csummary\u003EExtra field of view at full hop speed. The cheapest speed cue in games.\u003C/summary\u003E\n\tpublic const float HopFovBoost = 16f;\n\n\t/// \u003Csummary\u003EBase field of view. Kept here so the hop boost has something to add to.\u003C/summary\u003E\n\tpublic const float BaseFieldOfView = 75f;\n\n\t// ---------------------------------------------------------------- arena\n\n\t/// \u003Csummary\u003E\n\t/// Minimum half-width of the walled arena, measured from the shape centre.\n\t///\n\t/// This used to be the ONLY value, fixed at 2600. The shared Monolith is 4096 units per axis,\n\t/// so its barriers orbited at ~3900 and sat entirely outside the walls: the player could\n\t/// never reach them and they blocked nothing. The arena now scales with the shape (see\n\t/// \u003Csee cref=\u0022ArenaShapeMultiple\u0022/\u003E) and this is just the floor for small stages.\n\t/// \u003C/summary\u003E\n\tpublic const float ArenaMinHalfExtent = 2600f;\n\n\t/// \u003Csummary\u003E\n\t/// Arena half-width as a multiple of the shape\u0027s bounding diagonal. Has to leave room for the\n\t/// barrier orbit (0.55 of the diagonal) plus somewhere to stand outside it.\n\t/// \u003C/summary\u003E\n\tpublic const float ArenaShapeMultiple = 0.85f;\n\n\t/// \u003Csummary\u003EFraction of the arena a barrier orbit may use. Keeps them inside the walls.\u003C/summary\u003E\n\tpublic const float BarrierMaxArenaFraction = 0.8f;\n\n\t/// \u003Csummary\u003EHeight of the containing walls.\u003C/summary\u003E\n\tpublic const float ArenaWallHeight = 900f;\n\n\t// ---------------------------------------------------------------- demolition charges\n\n\t/// \u003Csummary\u003E\n\t/// Right click plants a charge, left click sets it off. The cooldown is deliberately\n\t/// FIXED regardless of level: upgrades buy blast size, never frequency, so the rhythm of\n\t/// the ability stays the same from level 1 to level 500.\n\t/// \u003C/summary\u003E\n\tpublic const float DemolitionCooldown = 10f;\n\n\t/// \u003Csummary\u003E\n\t/// The Demolition upgrade buys blast SIZE and nothing else. Level 0 is deliberately modest\n\t/// now: at radius 9 the very first charge removed ~3,000 cubes, which is most of an early\n\t/// stage in one click and made the whole opening curve irrelevant.\n\t/// \u003C/summary\u003E\n\t// THE CHARGE IS SIZED AS A FRACTION OF THE STAGE, NOT AS A FIXED RADIUS.\n\t//\n\t// A fixed radius cannot stay impactful across a ladder whose shapes run from 500 cubes to\n\t// two million: the same sphere is the whole of stage 1 and a pinprick at stage 60, so the\n\t// ability decays into irrelevance exactly as the stages get long enough to need it. Sizing\n\t// it against the stage means one charge always means the same THING.\n\t//\n\t// The curve is the standard asymptotic one used for VolatileChance, for the same reason:\n\t// increments shrink forever and the ceiling is never reached, so the upgrade can never show\n\t// \u0022MAX\u0022 (GOALS rule 3b). Steps land close to the requested shape: 10% at level 0, about 20%\n\t// by level 10, about 33% by level 30, and creeping toward 80% without arriving.\n\n\t/// \u003Csummary\u003EFraction of the stage a charge removes at Demolition level 0.\u003C/summary\u003E\n\tpublic const float DemolitionFractionBase = 0.10f;\n\n\t/// \u003Csummary\u003E\n\t/// Ceiling the fraction approaches and never reaches.\n\t///\n\t/// **25%, not 80%.** The charge is a REPEATABLE ability on a ten second cooldown, so the\n\t/// per-shot figure has to be read as a rate rather than a one-off: at 80% two clicks end a\n\t/// stage, and with the Cadence node driving the cooldown toward its three second floor the\n\t/// ladder becomes a sequence of right clicks with mining as decoration. At 25% it takes\n\t/// four perfect charges, which is a strong tool that still needs the rest of the game.\n\t/// \u003C/summary\u003E\n\tpublic const float DemolitionFractionMax = 0.25f;\n\n\t/// \u003Csummary\u003E\n\t/// Curve rate. Chosen so level 10 lands near 18% and the first upgrade is worth about 1.5\n\t/// points: solving \u00601 - 1/(1 \u002B 10r) = (0.18 - 0.10) / (0.25 - 0.10)\u0060 gives r = 0.114.\n\t/// Increments shrink from there and never quite arrive. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float DemolitionFractionRate = 0.114f;\n\n\t/// \u003Csummary\u003E\n\t/// Hard ceiling on the fraction when mining the SHARED MONOLITH.\n\t///\n\t/// The fraction model is right for the ladder and wrong for the Monolith. A stage is\n\t/// something you clear in a minute, so \u0022a tenth of it per charge\u0022 is a good tool. The\n\t/// Monolith is 16.7M cubes and is supposed to take a lobby days: the same tenth would be\n\t/// 1.67M cubes per click and would reduce the shared destination to about ten button\n\t/// presses.\n\t///\n\t/// One percent is 167k cubes, which still reads as an enormous crater and still matters,\n\t/// while leaving the Monolith something you chip down together rather than delete. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float DemolitionMonolithFractionMax = 0.01f;\n\n\t/// \u003Csummary\u003E\n\t/// Fraction of the shape\u0027s SHORTEST axis that counts as fully buried, capped by\n\t/// \u003Csee cref=\u0022DemolitionIdealDepth\u0022/\u003E.\n\t///\n\t/// A flat depth cannot work across a ladder whose shapes run from 8 voxels to 160: at 26 the\n\t/// early stages could never reach it at all. See \u0060Miner.EffectiveIdealDepth\u0060.\n\t/// \u003C/summary\u003E\n\tpublic const float DemolitionDepthFraction = 0.4f;\n\n\t/// \u003Csummary\u003E\n\t/// How far a charge flies before it stops and hangs, if it never meets rock.\n\t///\n\t/// It used to fizzle at \u0060MineRayLength\u0060 (20,000 units), which punished a near miss twice:\n\t/// once for missing and again with the full cooldown for nothing. It now waits to be set off\n\t/// wherever it stopped.\n\t/// \u003C/summary\u003E\n\tpublic const float ChargeMaxFlight = 2200f;\n\n\t/// \u003Csummary\u003EVoxels per second the charge bores into the shape once fired.\u003C/summary\u003E\n\tpublic const float DemolitionDrillSpeed = 22f;\n\n\t/// \u003Csummary\u003ESeconds the charge keeps boring before it fizzles and is wasted.\u003C/summary\u003E\n\tpublic const float DemolitionMaxDrillTime = 2.2f;\n\n\t/// \u003Csummary\u003ESeconds between the tunnel-carving steps that make the bore visible.\u003C/summary\u003E\n\tpublic const float DemolitionCarveInterval = 0.12f;\n\n\t/// \u003Csummary\u003ERadius of the pilot hole the charge bores on its way in.\u003C/summary\u003E\n\tpublic const float DemolitionBoreRadius = 1.4f;\n\n\t/// \u003Csummary\u003E\n\t/// Depth, in voxels, at which a detonation is fully buried. Blowing up on the surface\n\t/// wastes most of the sphere on empty air; burying it first is the skill in the ability.\n\t/// \u003C/summary\u003E\n\tpublic const float DemolitionIdealDepth = 26f;\n\n\t/// \u003Csummary\u003EExtra blast radius multiplier at ideal depth, tapering to 1.0 at the surface.\u003C/summary\u003E\n\tpublic const float DemolitionDepthBonus = 0.6f;\n\n\t// ---------------------------------------------------------------- active reload\n\n\t/// \u003Csummary\u003E\n\t/// The reload attempt is a SHORT bar that runs immediately after firing, not the tail of\n\t/// the long cooldown. Two seconds, with a randomly placed target: catching it hands the\n\t/// charge straight back, missing it drops you onto the full \u003Csee cref=\u0022DemolitionCooldown\u0022/\u003E.\n\t///\n\t/// Short and random is the whole point. It has to demand attention at exactly the moment\n\t/// you are also watching a charge bore into the rock, so the two compete.\n\t/// \u003C/summary\u003E\n\tpublic const float ActiveReloadDuration = 2.0f;\n\n\t/// \u003Csummary\u003EWidth of the target as a fraction of the bar.\u003C/summary\u003E\n\tpublic const float ActiveReloadWindowWidth = 0.13f;\n\n\t/// \u003Csummary\u003EEarliest and latest the target can be placed, as fractions of the bar.\u003C/summary\u003E\n\tpublic const float ActiveReloadMinStart = 0.18f;\n\tpublic const float ActiveReloadMaxStart = 0.80f;\n\n\t// ---------------------------------------------------------------- economy\n\n\t/// \u003Csummary\u003E\n\t/// Fallback growth. Each upgrade overrides this via \u003Csee cref=\u0022Upgrades\u0022/\u003E, because an\n\t/// upgrade whose value scales cubically cannot share a cost curve with one that scales\n\t/// linearly without the economy running away.\n\t/// \u003C/summary\u003E\n\tpublic const double UpgradeCostGrowth = 1.30;\n\n\t/// \u003Csummary\u003ECost growth per upgrade. Radius upgrades are cubic in value, hence 1.85.\u003C/summary\u003E\n\tpublic const double GrowthDrillSpeed = 1.36;\n\tpublic const double GrowthBlastRadius = 1.85;\n\tpublic const double GrowthChargeChance = 1.28;\n\tpublic const double GrowthChargeRadius = 1.85;\n\tpublic const double GrowthDustYield = 1.32;\n\tpublic const double GrowthDemolition = 1.55;\n\n\t/// \u003Csummary\u003E\n\t/// Drones and Drone Cadence are deliberately a TRADE, not a ladder.\n\t///\n\t/// Total drone output is count x rate, so both scale output linearly and the interesting\n\t/// question is which to buy next. Drones cost more per level and grow faster, because each\n\t/// one is another object in the arch and another thing on screen; Cadence is the cheap way\n\t/// to get more out of what you already have. The result is that a wide arch of slow drones\n\t/// and a narrow arch of fast ones both work, and the crossover moves as you buy. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const double GrowthDrones = 1.46;\n\tpublic const double GrowthDroneRate = 1.30;\n\n\tpublic const double CostDrillSpeed = 30;\n\tpublic const double CostBlastRadius = 60;\n\tpublic const double CostChargeChance = 500;\n\tpublic const double CostChargeRadius = 900;\n\tpublic const double CostDustYield = 250;\n\tpublic const double CostDrones = 1600;\n\tpublic const double CostDroneRate = 700;\n\tpublic const double CostDemolition = 800;\n\n\t// ---------------------------------------------------------------- prestige\n\n\t/// \u003Csummary\u003E\n\t/// The solo ladder runs to this stage. Reaching it unlocks Collapse, which resets you to\n\t/// stage 1 with permanent multipliers so the numbers can keep climbing.\n\t/// \u003C/summary\u003E\n\tpublic const int PrestigeStageRequirement = 100;\n\n\t/// \u003Csummary\u003EStages cleared per Core awarded on Collapse. 100 stages = 10 Cores = \u002B100%.\u003C/summary\u003E\n\tpublic const int StagesPerCore = 10;\n\n\t/// \u003Csummary\u003E\n\t/// Baseline value of a Core you have EARNED, whether or not you have spent it. Kept small\n\t/// on purpose: the interesting power is in the Core Tree, and a large passive multiplier\n\t/// here would make spending Cores feel like a downgrade.\n\t/// \u003C/summary\u003E\n\tpublic const float CorePowerPerCore = 0.04f;\n\n\t// ---------------------------------------------------------------- marks\n\n\t/// \u003Csummary\u003E\n\t/// Global multiplier per Mark earned. Small individually: the list is long, and the point\n\t/// is a steady background hum of progress, not a cliff.\n\t/// \u003C/summary\u003E\n\tpublic const float MarkPowerEach = 0.03f;\n\n\t// ---------------------------------------------------------------- resonance\n\n\t/// \u003Csummary\u003E\n\t/// Seconds a Resonance stack survives without a new trigger. Short: this is the mechanic\n\t/// that distinguishes someone actively playing from someone letting drones tick over, so\n\t/// it has to lapse the moment attention does.\n\t/// \u003C/summary\u003E\n\tpublic const float ResonanceWindow = 3.2f;\n\n\t/// \u003Csummary\u003EDust multiplier added per stack. At 20 stacks that is roughly 5x.\u003C/summary\u003E\n\tpublic const float ResonancePerStack = 0.2f;\n\n\t/// \u003Csummary\u003EStacks granted by catching an active reload. The skill shot is worth more.\u003C/summary\u003E\n\tpublic const int ResonanceReloadBonus = 3;\n\n\t/// \u003Csummary\u003ENo ceiling would let a single lucky chain break the economy.\u003C/summary\u003E\n\tpublic const int ResonanceMaxStacks = 40;\n\n\t// ---------------------------------------------------------------- slag\n\n\t/// \u003Csummary\u003E\n\t/// Real-world minutes per Slag. This is the only currency that accrues on a wall clock,\n\t/// including while the game is shut, which is what makes opening it on a day you do not\n\t/// intend to grind still worth doing.\n\t/// \u003C/summary\u003E\n\tpublic const double SlagMinutesEach = 90;\n\n\t/// \u003Csummary\u003E\n\t/// Cap on stored Slag. Low enough that it is worth spending rather than hoarding, high\n\t/// enough that a couple of days away is not punished.\n\t/// \u003C/summary\u003E\n\tpublic const int SlagMax = 8;\n\n\t/// \u003Csummary\u003ESlag cost to shatter half of what is left of the current shape.\u003C/summary\u003E\n\tpublic const int SlagCostFracture = 2;\n\n\t/// \u003Csummary\u003ESlag cost to reroll the current stage into a different shape.\u003C/summary\u003E\n\tpublic const int SlagCostReroll = 1;\n\n\t/// \u003Csummary\u003EFraction of remaining cubes a Fracture destroys.\u003C/summary\u003E\n\tpublic const float FractureFraction = 0.5f;\n\n\t// ---------------------------------------------------------------- leeches\n\n\t/// \u003Csummary\u003E\n\t/// Fraction of your dust income a single leech siphons away while attached.\n\t///\n\t/// Note this costs you INCOME, never cubes. Cookie Clicker\u0027s wrinklers eat your cookies,\n\t/// but \u0022the block count drops when I am not clicking\u0022 is a rule the user set explicitly,\n\t/// and a leech that ate the shape would break it. Siphoning income preserves the actual\n\t/// mechanic (a parasite you deliberately fatten before popping) without touching the count.\n\t/// \u003C/summary\u003E\n\tpublic const float LeechSiphonEach = 0.09f;\n\n\t/// \u003Csummary\u003ECeiling on total siphon, so a swarm can never zero your income.\u003C/summary\u003E\n\tpublic const float LeechSiphonMax = 0.55f;\n\n\t/// \u003Csummary\u003EMultiplier on everything a leech siphoned, paid out when you pop it.\u003C/summary\u003E\n\tpublic const float LeechPayoutBonus = 2.4f;\n\n\tpublic const int LeechHealth = 4;\n\tpublic const float LeechSpawnInterval = 22f;\n\tpublic const int LeechMaxPopulation = 4;\n\n\t/// \u003Csummary\u003EStage at which leeches begin appearing.\u003C/summary\u003E\n\tpublic const int LeechFirstStage = 8;\n\n\t// ---------------------------------------------------------------- hollow runs\n\n\t/// \u003Csummary\u003EBonus Cores for completing the ladder under a restriction.\u003C/summary\u003E\n\tpublic const int HollowRunCoreReward = 5;\n\n\t// ---------------------------------------------------------------- core tree\n\n\tpublic const float CoreDeepeningPerLevel = 0.15f;\n\tpublic const float CoreCadencePerLevel = 0.12f;\n\tpublic const float CoreCadenceCooldownPerLevel = 0.6f;\n\tpublic const float CoreAvaricePerLevel = 0.25f;\n\tpublic const int CoreAnchoredPerLevel = 3;\n\tpublic const int CoreForesightPerLevel = 4;\n\tpublic const float CoreSympathyPerLevel = 0.02f;\n\n\t/// \u003Csummary\u003ECharges never become certain, so the cooldown floor keeps Cadence bounded.\u003C/summary\u003E\n\tpublic const float DemolitionCooldownFloor = 3.0f;\n\n\t/// \u003Csummary\u003E\n\t/// Minimum share of a shared Monolith you must personally remove to earn prestige credit\n\t/// when it falls. Stops anyone idling in the lobby from collecting the same reward as the\n\t/// people who actually did the work.\n\t/// \u003C/summary\u003E\n\tpublic const double MonolithCreditShare = 0.01;\n\n\t/// \u003Csummary\u003E\n\t/// Prestige levels awarded for felling a shared Monolith at the minimum 1% share.\n\t///\n\t/// A Monolith is 16.7M cubes and takes a lobby days or weeks. Paying the same single level\n\t/// as one twenty minute solo ladder made it strictly the worst way to earn prestige, which\n\t/// is the opposite of what a shared destination is for.\n\t/// \u003C/summary\u003E\n\tpublic const int MonolithPrestigeBase = 5;\n\n\t/// \u003Csummary\u003E\n\t/// Extra prestige levels at a 100% share, scaled linearly by your actual contribution.\n\t/// Someone who personally removed a third of it gets meaningfully more than someone who\n\t/// scraped the 1% floor, without the floor player feeling cheated. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const int MonolithPrestigeShareBonus = 15;\n\n\t// ---------------------------------------------------------------- the retro look\n\t//\n\t// Devil Daggers renders at 320x240 with unfiltered textures, no anti-aliasing, and low\n\t// colour depth that is dithered (GOALS 6d). These approximate that with the post-process\n\t// components s\u0026box already ships, which is the only honest way to get there without writing\n\t// and debugging HLSL against a compile loop that freezes every twenty minutes.\n\n\t// THE CORRECTION THAT MATTERS: Devil Daggers is HIGH CONTRAST, not LOW BRIGHTNESS.\n\t//\n\t// The first attempt at these values conflated the two and the result was unplayable: the\n\t// shape was a brown smear, hazards in the periphery vanished, and the whole frame read as\n\t// mud. The reference\u0027s black is genuinely black, but everything IN it is bright, and the\n\t// readability comes from that gap. Darkening the lit half destroys the very thing being\n\t// copied. Every value below moved in the direction of \u0022lift the lit parts, keep the void\n\t// black\u0022 rather than \u0022turn the lights down\u0022.\n\n\t/// \u003Csummary\u003E\n\t/// \u003Cc\u003EPixelate.Scale\u003C/c\u003E.\n\t///\n\t/// **I had this backwards.** I read it as a resolution fraction, where higher means sharper,\n\t/// and \u0022raised\u0022 it 0.45 to 0.62 to 0.78 across three passes trying to add fidelity. It is\n\t/// the pixelation AMOUNT: higher means chunkier. Every \u0022fix\u0022 made it worse, which is exactly\n\t/// what the last screenshot showed.\n\t///\n\t/// 0.15 is a light dusting: enough that edges stair-step and the image is not clean modern\n\t/// 3D, nowhere near enough to hide a 16 unit cube. **[TUNE, and note the direction: DOWN is\n\t/// sharper.]**\n\t/// \u003C/summary\u003E\n\tpublic const float RetroPixelScale = 0.15f;\n\n\t/// \u003Csummary\u003E\n\t/// Low colour depth reads as slightly reduced saturation. Contrast is kept only just above\n\t/// neutral: pushing it crushes an already dark scene into pure black, which is what happened\n\t/// at 1.16. Brightness is now ABOVE 1, lifting the lit half without touching the void.\n\t/// \u003C/summary\u003E\n\tpublic const float RetroSaturation = 0.90f;\n\tpublic const float RetroContrast = 1.04f;\n\tpublic const float RetroBrightness = 1.12f;\n\n\t/// \u003Csummary\u003E\n\t/// Standing in for dithering. Low: grain over dark areas is just mud, and at 0.16 it was\n\t/// actively hiding the shape rather than texturing it.\n\t/// \u003C/summary\u003E\n\tpublic const float RetroGrain = 0.06f;\n\n\t/// \u003Csummary\u003E\n\t/// Barely there. At 0.55 this was the single worst offender: it darkened the PERIPHERY,\n\t/// which is exactly where the Spotters, Sentinels and Anchors live. A vignette that hides\n\t/// the threats is a vignette working against the game.\n\t/// \u003C/summary\u003E\n\tpublic const float RetroVignette = 0.14f;\n\n\t/// \u003Csummary\u003E\n\t/// The lamp on the player rig. This is the readability fix, not the colour pass: it lights\n\t/// what you are working on and lets the distance fall to black on its own, which is the\n\t/// reference\u0027s actual model. Radius is generous so a whole stage is legible from the floor.\n\t/// **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float PlayerLampBrightness = 9f;\n\tpublic const float PlayerLampRadius = 2600f;\n\n\t// ---------------------------------------------------------------- rendering\n\n\t/// \u003Csummary\u003E\n\t/// Milliseconds per frame spent remeshing dirty chunks.\n\t///\n\t/// This replaced a flat \u00222 chunks per frame\u0022, which was the cause of large stages loading in\n\t/// visibly chunky slabs and of black holes appearing in the shape mid-fight. A big blast\n\t/// dirties dozens of chunks at once, so a fixed count of two could never catch up and the\n\t/// backlog just grew: what you saw was not slow loading, it was a queue that never drained.\n\t///\n\t/// A time budget adapts instead. Small chunks rebuild many per frame, expensive ones fewer,\n\t/// and the frame cost stays roughly constant either way. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float ChunkRebuildMillisecondsPerFrame = 6f;\n\n\t/// \u003Csummary\u003E\n\t/// Extra time allowed while a large backlog is outstanding, which is what an initial load is.\n\t/// Briefly spending a third of the frame budget to get the shape on screen is worth it.\n\t/// \u003C/summary\u003E\n\tpublic const float ChunkRebuildCatchUpMilliseconds = 13f;\n\n\n\t/// \u003Csummary\u003EBacklog above which catch-up applies.\u003C/summary\u003E\n\tpublic const int ChunkRebuildCatchUpThreshold = 24;\n\n\t/// \u003Csummary\u003EHard ceiling per frame, so one cheap frame cannot run away.\u003C/summary\u003E\n\tpublic const int ChunkRebuildMaxPerFrame = 96;\n\n\t/// \u003Csummary\u003EHow far the mining ray will travel, in world units.\u003C/summary\u003E\n\tpublic const float MineRayLength = 20_000f;\n\n\t// ---------------------------------------------------------------- the rocket jump\n\n\t/// \u003Csummary\u003E\n\t/// Impulse applied by a charge detonating at your feet, before falloff.\n\t///\n\t/// Against a \u003Cc\u003EJumpPower\u003C/c\u003E of 480 this is roughly a double-height launch at point blank,\n\t/// which is what makes it worth a whole charge. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float RocketJumpForce = 900f;\n\n\t/// \u003Csummary\u003E\n\t/// Distance at which the launch has fallen to nothing. Choosing where to put the charge is\n\t/// how you choose your power, so this is the width of that dial. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float RocketJumpRadius = 700f;\n\n\t/// \u003Csummary\u003E\n\t/// Share of a full demolition blast that a floor hit still removes.\n\t///\n\t/// The rocket jump has to keep real damage or it becomes a tax you pay to travel, and it must\n\t/// not keep ALL of it or the timed detonation has no reason to exist. At 0.55 a floor shot\n\t/// beside the shape is a genuine excavation plus a launch, while a patient buried charge is\n\t/// still comfortably the better way to move rock.\n\t///\n\t/// Applied to the CUBE COUNT, not the radius. Radius is the cube root of volume, so 0.55 of\n\t/// the cubes is about 0.82 of the radius: the sphere looks nearly as big and removes a bit\n\t/// over half as much, which is the right way round for something that should still feel\n\t/// powerful. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float RocketJumpBlastFraction = 0.55f;\n\n\t/// \u003Csummary\u003E\n\t/// How much upward bias is mixed into the blast direction.\n\t///\n\t/// Without it, a charge landing ahead of you pushes almost horizontally and slides you along\n\t/// the ground rather than launching you. The bias is what makes every rocket jump a jump\n\t/// regardless of the geometry you happened to fire at. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float RocketJumpUpBias = 0.85f;\n\n\t// ---------------------------------------------------------------- shrapnel\n\t//\n\t// See Shrapnel. A volatile detonation throws chunks of the monolith outward, and they take the\n\t// stage if one lands on you. Everything below exists to keep it DODGEABLE: slow, arcing,\n\t// bright, and never numerous.\n\n\t/// \u003Csummary\u003E\n\t/// One piece per this many world units of blast radius. Higher means fewer pieces. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float ShrapnelPerRadius = 18f;\n\n\t/// \u003Csummary\u003ECeiling per detonation, so a huge Instability radius cannot blanket the arena.\u003C/summary\u003E\n\tpublic const int ShrapnelMaxPerBurst = 12;\n\n\t/// \u003Csummary\u003ECeiling across the whole scene.\u003C/summary\u003E\n\tpublic const int ShrapnelMaxLive = 60;\n\n\t/// \u003Csummary\u003E\n\t/// Half-angle of the cone the landing points are thrown into, around the outward direction.\n\t///\n\t/// Wide enough that a burst covers an arc of the arena rather than a single lane you sidestep\n\t/// once and forget, narrow enough that the pattern still reads as coming FROM the detonation\n\t/// rather than raining everywhere. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float ShrapnelSpreadDegrees = 55f;\n\n\t/// \u003Csummary\u003E\n\t/// Seconds from launch to impact.\n\t///\n\t/// The flight is a DURATION rather than a speed, which is what lets the arc be solved exactly\n\t/// so a piece lands where its ring says it will. It is also the whole reaction budget: long\n\t/// enough to see the ring, decide, and walk out, short enough that ignoring it is not an\n\t/// option. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float ShrapnelFlightTime = 1.7f;\n\n\t/// \u003Csummary\u003E\n\t/// How far around the player the landing points are scattered.\n\t///\n\t/// Wide enough that a burst covers ground rather than stacking on one spot, tight enough that\n\t/// the whole pattern is a place you have to leave. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float ShrapnelScatter = 380f;\n\n\t/// \u003Csummary\u003E\n\t/// How fast a landing point walks toward the player, in units per second.\n\t///\n\t/// **Deliberately far below a walk.** The player moves at 420, so stepping out of a ring\n\t/// always works and standing in one never does. That asymmetry is the mechanic: it is not\n\t/// trying to hit you, it is trying to make you leave, and what makes leaving interesting is\n\t/// the floor tile and the crawler you have to leave TOWARD. Raise it and shrapnel becomes\n\t/// unfair; drop it to 0 and it stops being a threat at all. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float ShrapnelDriftSpeed = 120f;\n\n\t/// \u003Csummary\u003ELethal radius of the landing blast, and the size of the ring drawn for it.\u003C/summary\u003E\n\tpublic const float ShrapnelImpactRadius = 110f;\n\n\t/// \u003Csummary\u003E\n\t/// How far above the floor the landing blast still catches you.\n\t///\n\t/// A full jump peaks about 82 units up, so this deliberately exceeds it: **hopping over an\n\t/// impact must not work.** The lethal floor already pays you for being airborne, and if the\n\t/// air were safe from this too there would be one answer to everything. Being in the air\n\t/// should dodge the FLOOR and expose you to the SKY. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float ShrapnelImpactHeight = 140f;\n\n\t/// \u003Csummary\u003E\n\t/// Gravity on a piece. Only shapes how high the arc goes now that arrival time is fixed, so\n\t/// this is a readability dial rather than a range one: higher means a flatter, faster-looking\n\t/// throw. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float ShrapnelGravity = 900f;\n\n\t/// \u003Csummary\u003E\n\t/// Delay before a piece can hurt you. A detonation triggered at close range would otherwise\n\t/// kill you on the frame it spawned, punishing you for landing your best shot. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float ShrapnelArmSeconds = 0.35f;\n\n\t/// \u003Csummary\u003EHow close a piece has to get. Generous to the player rather than to the rock.\u003C/summary\u003E\n\tpublic const float ShrapnelHitRadius = 44f;\n\n\t/// \u003Csummary\u003E\n\t/// Shared cooldown across every piece, exactly like the Crawler grab. One detonation throws\n\t/// ten chunks and a single bad moment must be charged once, not ten times.\n\t/// \u003C/summary\u003E\n\tpublic const float ShrapnelHitCooldown = 3f;\n\n\t// ---------------------------------------------------------------- the lethal floor\n\t//\n\t// See FloorHazard. Half the arena floor turns deadly on a cycle, which is what turns the\n\t// movement techniques from optional speed into the thing keeping you alive.\n\n\t/// \u003Csummary\u003E\n\t/// Tile edge length, and the most load-bearing number in the system. Derived from how far a\n\t/// jump actually carries you rather than picked for looks.\n\t///\n\t/// A full jump hangs for \u003Cc\u003E2 * JumpPower / Gravity\u003C/c\u003E, which is 2 * 480 / 1400 = **0.69\n\t/// seconds**. Multiply by the speed you carry into it:\n\t///\n\t/// - walking, 420 units/s: **288 units**, so a jump clears one tile and very little more\n\t/// - sprinting, 609: **418 units**, comfortably one, sometimes two\n\t/// - a maintained hop chain, up to 1092: **749 units**, nearly three\n\t///\n\t/// At 260 the floor is therefore always escapable with the most basic jump in the game, and\n\t/// the speed techniques are what let you choose WHERE you land rather than merely surviving.\n\t/// Raise this and hops become mandatory; lower it and the floor stops mattering. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float FloorTileSize = 260f;\n\n\t/// \u003Csummary\u003E\n\t/// How long the floor holds every tile safe after a stage is built or reset.\n\t///\n\t/// The player does not choose where a stage puts them, so the moment of arrival cannot be\n\t/// lethal. Long enough to land, read the arena and pick a direction. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float FloorStageGraceSeconds = 3f;\n\n\t/// \u003Csummary\u003E\n\t/// How far the permanently lethal tiles stand proud of the floor.\n\t///\n\t/// **Colour alone was not carrying this.** The retro pass renders at 15% resolution through\n\t/// bloom and film grain, under an orange player lamp, so a tile is a handful of muddy pixels\n\t/// and violet against ember reads as \u0022another dark warm square\u0022. Raising them into low slabs\n\t/// gives them a silhouette, visible sides and a different response to the lamp, and a\n\t/// silhouette survives pixelation in a way that a hue never will. Purely visual: nothing\n\t/// collides with them. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float FloorDeadHeight = 22f;\n\n\t/// \u003Csummary\u003EHow often a fresh half of the floor is rolled. **[TUNE]**\u003C/summary\u003E\n\tpublic const float FloorPhaseSeconds = 3.5f;\n\n\t/// \u003Csummary\u003E\n\t/// Warning before the hot tiles kill. This is reaction budget, and the difference between a\n\t/// movement puzzle and a reflex test.\n\t///\n\t/// **Raised 1.0 to 1.75 after the first playtest.** One second is enough time to move if you\n\t/// were already watching the floor, and not enough if you were doing the thing the game is\n\t/// actually about, which is aiming at rock. The lethal window shrinks by the same amount\n\t/// (phase 3.5 minus warn 1.75 leaves 1.75 seconds of danger), so the floor is no less deadly,\n\t/// it just stops punishing you for looking at the monolith. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float FloorWarnSeconds = 1.75f;\n\n\t/// \u003Csummary\u003E\n\t/// How long you can stand on a live tile before it takes the stage.\n\t///\n\t/// **Contact used to be instant death**, which made the floor read as arbitrary rather than\n\t/// dangerous: a tile turning red under a foot that was already mid-stride killed you for a\n\t/// decision made half a second earlier. A short dwell means brushing one is a mistake you can\n\t/// correct and camping one is not. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float FloorBurnSeconds = 0.75f;\n\n\t/// \u003Csummary\u003E\n\t/// How fast the burn meter empties once you are clear, as a multiple of how fast it fills.\n\t///\n\t/// Below 1 on purpose. If it emptied instantly, hopping on the spot on a red tile would be\n\t/// survivable forever, because airborne is already safe. Recovering slower than you burn\n\t/// makes the answer actually going somewhere. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float FloorBurnRecoverRate = 0.6f;\n\n\t/// \u003Csummary\u003E\n\t/// Fraction of the floor that goes hot each phase.\n\t///\n\t/// **Cut 0.5 to 0.32.** Half the floor going lethal sounds like a coin flip and does not play\n\t/// like one: with a random split, a 50% floor leaves safe tiles isolated, so almost every\n\t/// phase became a forced jump rather than a route you chose. At about a third the safe tiles\n\t/// connect into paths, which turns the question from \u0022can I survive this\u0022 into \u0022which way do\n\t/// I go\u0022, and that second question is the one that is actually fun. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float FloorHotFraction = 0.32f;\n\n\t/// \u003Csummary\u003E\n\t/// Fraction of tiles that are permanently lethal.\n\t///\n\t/// **Cut 0.07 to 0.03.** At 7% a twenty by twenty grid carried about 25 instant-death squares,\n\t/// which is not a hazard scattered through the arena, it is a minefield: enough that they\n\t/// stopped reading as landmarks to route around and started reading as random. Three percent\n\t/// is roughly ten, which is few enough to remember where they are. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float FloorDeadChance = 0.012f;\n\n\t/// \u003Csummary\u003E\n\t/// Radius around the arena centre kept clear of permanent tiles, so a stage cannot drop you\n\t/// onto one before you have taken a step.\n\t/// \u003C/summary\u003E\n\tpublic const float FloorDeadSafeRadius = 900f;\n\n\t/// \u003Csummary\u003E\n\t/// Stage at which the floor arms, counting the way the player counts: 1 is the first stage.\n\t///\n\t/// **This was compared against the 0-based \u0060Tier\u0060 and so armed a stage late.** Three whole\n\t/// stages went by with no floor, which from the outside is indistinguishable from the whole\n\t/// system being broken. The conversion now happens in MonolithManager and this really does\n\t/// mean what it says. Set to 1 to have it live immediately. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const int FloorHazardFromStage = 2;\n\n\t// ---------------------------------------------------------------- music\n\t//\n\t// See MonolithMusic. Three layers play permanently and only their volumes move, so these are\n\t// ceilings rather than switches: the bed is what you always hear, and the other two are how\n\t// far the score is allowed to rise when you are working or being hunted. **[TUNE]**\n\n\t/// \u003Csummary\u003E\n\t/// Bass, kick and hat: the groove that is always playing.\n\t///\n\t/// The three ceilings deliberately sum to 1.0. Each layer is normalised to a 0.85 peak when\n\t/// it is generated, so a full mix lands at 0.85 and cannot clip no matter what the run is\n\t/// doing. Raise one of these and lower another. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float MusicBedVolume = 0.40f;\n\n\t/// \u003Csummary\u003EThe arpeggio and backbeat, at a fully cleared stage. **[TUNE]**\u003C/summary\u003E\n\tpublic const float MusicWorkVolume = 0.30f;\n\n\t/// \u003Csummary\u003EThe lead melody, at full hazard pressure. **[TUNE]**\u003C/summary\u003E\n\tpublic const float MusicThreatVolume = 0.30f;\n\n\t/// \u003Csummary\u003E\n\t/// Weighted hazard count at which the threat layer is at full volume. Roughly two spotters\n\t/// and a sentinel. Set so an ordinary stage sits well under it and a bad one does not. **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float MusicThreatFullAt = 4f;\n\n\t/// \u003Csummary\u003E\n\t/// How quickly layers chase their target volume, per second. Slow enough that killing one\n\t/// hazard does not audibly drop the music, fast enough that the pause duck feels immediate.\n\t/// **[TUNE]**\n\t/// \u003C/summary\u003E\n\tpublic const float MusicFadeRate = 1.6f;\n}\n"},{"Ident":"idkman.monolith","Path":"UI/LeaderboardData.cs","FileName":"LeaderboardData.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"using System.Threading.Tasks;\n\nnamespace Monolith;\n\n/// \u003Csummary\u003E\n/// Fetches one global leaderboard and flattens it into plain rows for the HUD.\n///\n/// All the s\u0026amp;box-typed leaderboard code is deliberately confined to \u003Csee cref=\u0022Load\u0022/\u003E and\n/// uses \u003Cc\u003Evar\u003C/c\u003E, so if the API surface shifts there is exactly one method to correct rather\n/// than a UI full of engine types.\n/// \u003C/summary\u003E\npublic sealed class LeaderboardData\n{\n\tpublic struct Row\n\t{\n\t\tpublic int Rank;\n\t\tpublic string Name;\n\n\t\t/// \u003Csummary\u003EFormatted for display.\u003C/summary\u003E\n\t\tpublic string Value;\n\n\t\t/// \u003Csummary\u003E\n\t\t/// The unformatted number. Kept alongside the string so callers can COMPARE against a\n\t\t/// board without reparsing \u00222:05.3\u0022, which is the kind of round trip that quietly breaks\n\t\t/// the first time a format changes.\n\t\t/// \u003C/summary\u003E\n\t\tpublic double Raw;\n\n\t\tpublic bool IsMe;\n\t}\n\n\tpublic string Title { get; }\n\tpublic string StatName { get; }\n\n\t/// \u003Csummary\u003ESpeedrun boards rank by lowest time, and format seconds rather than counts.\u003C/summary\u003E\n\tpublic bool LowestWins { get; init; }\n\n\tpublic List\u003CRow\u003E Rows { get; } = new();\n\n\tpublic bool Loading { get; private set; }\n\tpublic string Error { get; private set; }\n\tpublic bool HasLoaded { get; private set; }\n\n\t/// \u003Csummary\u003EBumped on every state change so the HUD\u0027s BuildHash notices.\u003C/summary\u003E\n\tpublic int Revision { get; private set; }\n\n\tpublic LeaderboardData( string title, string statName )\n\t{\n\t\tTitle = title;\n\t\tStatName = statName;\n\t}\n\n\tpublic async Task Load( int maxEntries = 15 )\n\t{\n\t\tif ( Loading ) return;\n\n\t\tLoading = true;\n\t\tError = null;\n\t\tRevision\u002B\u002B;\n\n\t\ttry\n\t\t{\n\t\t\tvar board = Sandbox.Services.Leaderboards.GetFromStat( MonolithStats.PackageIdent, StatName );\n\t\t\tboard.MaxEntries = maxEntries;\n\n\t\t\tif ( LowestWins )\n\t\t\t{\n\t\t\t\t// A speedrun board wants the smallest submitted time, ordered ascending.\n\t\t\t\tboard.SetAggregationMin();\n\t\t\t\tboard.SetSortAscending();\n\t\t\t}\n\n\t\t\tawait board.Refresh();\n\n\t\t\tRows.Clear();\n\n\t\t\t// Compared as text so it does not matter whether the engine types these as\n\t\t\t// long or ulong, which differ between Connection and the leaderboard entry.\n\t\t\tvar mySteamId = Connection.Local?.SteamId.ToString();\n\n\t\t\tforeach ( var entry in board.Entries )\n\t\t\t{\n\t\t\t\tRows.Add( new Row\n\t\t\t\t{\n\t\t\t\t\tRank = (int)entry.Rank,\n\t\t\t\t\tName = string.IsNullOrWhiteSpace( entry.DisplayName ) ? \u0022unknown\u0022 : entry.DisplayName,\n\t\t\t\t\tValue = LowestWins\n\t\t\t\t\t\t? Num.Duration( (double)entry.Value )\n\t\t\t\t\t\t: Num.Short( (double)entry.Value ),\n\t\t\t\t\tRaw = (double)entry.Value,\n\t\t\t\t\tIsMe = mySteamId != null \u0026\u0026 entry.SteamId.ToString() == mySteamId,\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\tHasLoaded = true;\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tError = e.Message;\n\t\t\tLog.Warning( $\u0022Leaderboard \u0027{StatName}\u0027 failed to load: {e.Message}\u0022 );\n\t\t}\n\t\tfinally\n\t\t{\n\t\t\tLoading = false;\n\t\t\tRevision\u002B\u002B;\n\t\t}\n\t}\n}\n"},{"Ident":"idkman.monolith","Path":"styles/base/_splitcontainer.scss","FileName":"_splitcontainer.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"\r\n.splitcontainer\r\n{\r\n\tflex-grow: 1;\r\n\r\n\t\u003E .split-left, \u003E .split-right\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t}\r\n\r\n\t\u003E .split-left\r\n\t{\r\n\t\theight: 100%;\r\n\t}\r\n\r\n\t\u003E .splitter\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t\twidth: 8px;\r\n\t\tcursor: ew-resize;\r\n\r\n\t\t\u0026:hover\r\n\t\t{\r\n\t\t}\r\n\t}\r\n\r\n\t\u0026.dragging \u003E .splitter\r\n\t{\r\n\t}\r\n\r\n\t\u0026.vertical\r\n\t{\r\n\t\tflex-direction: column;\r\n\r\n\t\t\u003E .splitter\r\n\t\t{\r\n\t\t\theight: 8px;\r\n\t\t\twidth: 100%;\r\n\t\t\tcursor: ns-resize;\r\n\t\t}\r\n\t}\r\n}"},{"Ident":"idkman.monolith","Path":"styles/base/_navigator.scss","FileName":"_navigator.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":".navigator-body\r\n{\r\n\t\u0026.hidden\r\n\t{\r\n\t\tdisplay: none;\r\n\t}\r\n}\r\n"},{"Ident":"idkman.monolith","Path":"Game/FloorHazard.cs","FileName":"FloorHazard.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"namespace Monolith;\n\n/// \u003Csummary\u003E\n/// The arena floor, divided into tiles that turn lethal on a three second cycle.\n///\n/// **What it is for.** Every movement tool in this game was optional. Hops, the double jump dash\n/// and sprinting all make you faster, and faster was worth something only because a stage is\n/// timed. Nothing ever required you to move at a particular moment, so a player who never learned\n/// any of it simply cleared stages more slowly. This is the system that makes the movement\n/// mandatory: half the floor becomes deadly, you get one second of warning, and standing still is\n/// the one thing that is always wrong.\n///\n/// **The cycle.** Every \u003Csee cref=\u0022Tuning.FloorPhaseSeconds\u0022/\u003E a fresh 50/50 split is rolled. The\n/// tiles that are about to become lethal light amber for \u003Csee cref=\u0022Tuning.FloorWarnSeconds\u0022/\u003E,\n/// then go red and kill. So you are never asked to react instantly: you are asked to be somewhere\n/// else by the time the second runs out, which is a movement problem rather than a reflex test.\n///\n/// **The violet tiles are different.** A small number are lethal permanently and never cycle.\n/// They are why the safe half is not simply a place to stand: the route between safe tiles has\n/// holes in it, and those holes do not move. Touching one ends the stage immediately with no\n/// warning at all, so they never spawn near where a stage drops you.\n///\n/// **Only while grounded.** Airborne is always safe. That is the whole design: the answer to a\n/// lethal floor is to not be on the floor, and every technique the game already has is a way of\n/// doing that for longer.\n/// \u003C/summary\u003E\npublic sealed class FloorHazard : Component\n{\n\t/// \u003Csummary\u003ETile states, stored per tile in a flat array indexed y * width \u002B x.\u003C/summary\u003E\n\tprivate enum Tile : byte\n\t{\n\t\tSafe,\n\t\tHot,\n\t\tDead,\n\t}\n\n\tprivate Tile[] tiles;\n\tprivate int width;\n\tprivate float tileSize;\n\n\t/// \u003Csummary\u003EWorld position of the minimum corner of tile zero.\u003C/summary\u003E\n\tprivate Vector3 origin;\n\n\tprivate float floorZ;\n\n\tprivate GameObject hotObject, deadObject;\n\tprivate ModelRenderer hotRenderer, deadRenderer;\n\n\tprivate GameTimeSince timeSincePhase = 999f;\n\n\t/// \u003Csummary\u003ESince the stage was built or reset. Drives the arrival grace.\u003C/summary\u003E\n\tprivate GameTimeSince timeSinceArrival = 999f;\n\n\tprivate bool armed;\n\tprivate bool announcedFirstPhase;\n\n\t/// \u003Csummary\u003E\n\t/// True while the floor is holding still because a stage has just been built.\n\t///\n\t/// **Found in a real log, not by reasoning.** The first playtest showed a stage condensing at\n\t/// 16:04:26.57 and the floor taking it at 16:04:27.57, one second later to the millisecond.\n\t/// The player never had a chance: \u0060FitTo\u0060 rolled a phase the instant the shape appeared, the\n\t/// warning ran while the game was still repositioning them, and they were killed standing\n\t/// exactly where they had been put. A hazard you are placed inside is not difficulty.\n\t/// \u003C/summary\u003E\n\tprivate bool Settling =\u003E timeSinceArrival \u003C Tuning.FloorStageGraceSeconds;\n\n\t/// \u003Csummary\u003ETrue once the warning has elapsed and the hot tiles actually kill.\u003C/summary\u003E\n\tprivate bool Live =\u003E timeSincePhase \u003E= Tuning.FloorWarnSeconds;\n\n\t/// \u003Csummary\u003E\n\t/// Lays the grid out under a stage. Called whenever a shape is placed.\n\t/// \u003C/summary\u003E\n\t/// \u003Cparam name=\u0022stage\u0022\u003E\n\t/// The stage number the PLAYER sees, counting from 1. The caller converts from the 0-based\n\t/// \u0060Tier\u0060, and that conversion is the entire reason this parameter is documented: the first\n\t/// version compared \u0060Tier\u0060 directly against a constant named \u0022from stage\u0022, so a threshold of\n\t/// 3 actually armed on stage 4 and three full stages went by with no floor at all.\n\t/// \u003C/param\u003E\n\tpublic void FitTo( BBox bounds, float halfExtent, int stage, bool inMonolith )\n\t{\n\t\tClear();\n\t\tannouncedFirstPhase = false;\n\n\t\t// A grace period, for the same reason the Spotter has one. A player who has just read the\n\t\t// tutorial should get to learn what mining feels like before the floor starts trying to\n\t\t// kill them. Set FloorHazardFromStage to 1 to have it on from the very first stage.\n\t\tarmed = inMonolith || stage \u003E= Tuning.FloorHazardFromStage;\n\n\t\tif ( !armed )\n\t\t{\n\t\t\tLog.Info( $\u0022[floor] stage {stage}: not armed \u0022\n\t\t\t\t\u002B $\u0022(arms at stage {Tuning.FloorHazardFromStage}).\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tfloorZ = bounds.Mins.z;\n\t\ttileSize = Tuning.FloorTileSize;\n\n\t\twidth = Math.Max( 4, (int)MathF.Ceiling( halfExtent * 2f / tileSize ) );\n\n\t\t// Centred on the arena, so the grid lines up with the walls rather than drifting.\n\t\torigin = new Vector3(\n\t\t\tbounds.Center.x - width * tileSize * 0.5f,\n\t\t\tbounds.Center.y - width * tileSize * 0.5f,\n\t\t\tfloorZ );\n\n\t\ttiles = new Tile[width * width];\n\n\t\tSeedDeadTiles();\n\t\tBuildDeadMesh();\n\n\t\t// Deliberately NOT NewPhase(). Every tile starts safe and the first roll waits out the\n\t\t// grace, so arriving in a stage is never itself lethal.\n\t\tBuildHotMesh();\n\t\ttimeSinceArrival = 0f;\n\t\ttimeSincePhase = 0f;\n\n\t\tint deadCount = 0;\n\t\tforeach ( var t in tiles )\n\t\t\tif ( t == Tile.Dead ) deadCount\u002B\u002B;\n\n\t\t// Instrumentation, because \u0022I did not see any tiles\u0022 and \u0022the tiles are broken\u0022 look\n\t\t// identical from the outside. This says which one it was.\n\t\tLog.Info( $\u0022[floor] stage {stage}: armed. {width}x{width} tiles of {tileSize:0}u, \u0022\n\t\t\t\u002B $\u0022{deadCount} permanent, first roll in {Tuning.FloorStageGraceSeconds}s.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Scatters the permanently lethal tiles, once per stage.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// Excluded from a radius around the arena centre and from the outermost ring. The centre\n\t/// because that is roughly where a stage drops you, and an unavoidable death on arrival is not\n\t/// difficulty, it is a bug. The outer ring because being shoved into a wall by a rocket jump\n\t/// should not be fatal for reasons you could not have seen coming.\n\t/// \u003C/remarks\u003E\n\tprivate void SeedDeadTiles()\n\t{\n\t\tfloat centre = (width - 1) * 0.5f;\n\t\tfloat safeRadius = Tuning.FloorDeadSafeRadius / tileSize;\n\n\t\tfor ( int y = 0; y \u003C width; y\u002B\u002B )\n\t\t{\n\t\t\tfor ( int x = 0; x \u003C width; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( x == 0 || y == 0 || x == width - 1 || y == width - 1 )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tfloat dx = x - centre;\n\t\t\t\tfloat dy = y - centre;\n\n\t\t\t\tif ( MathF.Sqrt( dx * dx \u002B dy * dy ) \u003C safeRadius )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tif ( Game.Random.Float() \u003C Tuning.FloorDeadChance )\n\t\t\t\t\ttiles[y * width \u002B x] = Tile.Dead;\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003ERolls a fresh half of the floor as hot, leaving the permanent tiles alone.\u003C/summary\u003E\n\tprivate void NewPhase()\n\t{\n\t\tif ( tiles == null )\n\t\t\treturn;\n\n\t\ttimeSincePhase = 0f;\n\n\t\tint hot = 0;\n\n\t\tfor ( int i = 0; i \u003C tiles.Length; i\u002B\u002B )\n\t\t{\n\t\t\tif ( tiles[i] == Tile.Dead )\n\t\t\t\tcontinue;\n\n\t\t\ttiles[i] = Game.Random.Float() \u003C Tuning.FloorHotFraction ? Tile.Hot : Tile.Safe;\n\n\t\t\tif ( tiles[i] == Tile.Hot ) hot\u002B\u002B;\n\t\t}\n\n\t\tBuildHotMesh();\n\n\t\tif ( !announcedFirstPhase )\n\t\t{\n\t\t\tannouncedFirstPhase = true;\n\t\t\tLog.Info( $\u0022[floor] first phase live: {hot} tiles hot, \u0022\n\t\t\t\t\u002B $\u0022{Tuning.FloorWarnSeconds}s warning.\u0022 );\n\t\t}\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\t// Frozen while a blocking screen or the pause menu is up. See GameTime.\n\t\tif ( GameTime.Paused )\n\t\t\treturn;\n\n\t\tif ( !armed || tiles == null )\n\t\t\treturn;\n\n\t\tif ( Settling )\n\t\t\treturn;\n\n\t\tif ( timeSincePhase \u003E= Tuning.FloorPhaseSeconds )\n\t\t\tNewPhase();\n\n\t\tRecolour();\n\t\tCheckPlayers();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The warning, told entirely by colour: amber while it is a threat, red once it is real.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// Eased rather than switched, and the tint keeps moving after it goes live. A hard colour\n\t/// swap at exactly one second gives no sense of how long is left, and the entire job of the\n\t/// warning is to be readable as a countdown.\n\t/// \u003C/remarks\u003E\n\tprivate void Recolour()\n\t{\n\t\tif ( !hotRenderer.IsValid() )\n\t\t\treturn;\n\n\t\tfloat warn = Math.Clamp( timeSincePhase / Tuning.FloorWarnSeconds, 0f, 1f );\n\n\t\t// Amber to red, and DIMMER as it arms rather than brighter. Counterintuitive, but the\n\t\t// dead slabs below are the brightest thing on the floor, so the cycling tiles have to\n\t\t// live in the lower half of the value range or the two compete.\n\t\tvar colour = Color.Lerp(\n\t\t\tnew Color( 1f, 0.58f, 0.06f ),\n\t\t\tnew Color( 0.85f, 0.08f, 0.03f ),\n\t\t\twarn );\n\n\t\t// Pulses once it is live, so a lethal tile never sits still and cannot be mistaken at a\n\t\t// glance for one that is merely warning.\n\t\tif ( Live )\n\t\t\tcolour *= 0.7f \u002B 0.3f * MathF.Sin( Time.Now * 14f );\n\n\t\thotRenderer.Tint = colour;\n\n\t\t// The permanent tiles, deliberately the one thing on screen that is NOT in the palette.\n\t\t// Everything else in this game lives in the bone and ember range, so a cold bright violet\n\t\t// is the only colour left that cannot be confused with rock, fire, warning or blood. It\n\t\t// is also kept far brighter than anything else so it survives the retro downsample, and\n\t\t// it breathes slowly rather than flashing: it is a wall, not an alarm.\n\t\tif ( deadRenderer.IsValid() )\n\t\t\tdeadRenderer.Tint = new Color( 0.85f, 0.35f, 1f )\n\t\t\t\t* (1.5f \u002B 0.35f * MathF.Sin( Time.Now * 2.2f ));\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// How close the local player is to burning, 0 to 1. Read by the HUD.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// Static because there is exactly one local player and the HUD has no reference to this\n\t/// component. Reset by \u003Csee cref=\u0022Clear\u0022/\u003E so a rebuilt stage never inherits a hot meter.\n\t/// \u003C/remarks\u003E\n\tpublic static float BurnFraction { get; private set; }\n\n\t/// \u003Csummary\u003EAccumulated contact with a live tile, per player, in seconds.\u003C/summary\u003E\n\tprivate readonly Dictionary\u003CPlayerMovement, float\u003E burn = new();\n\n\tprivate GameTimeSince timeSinceBurnTick;\n\n\t/// \u003Csummary\u003E\n\t/// Takes the stage from anyone standing somewhere they should not be.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// **Touching a live tile is not instant death.** It used to be, and it made the floor feel\n\t/// arbitrary rather than dangerous: a tile going red under a foot that was already mid-stride\n\t/// killed you for a decision you had made half a second earlier. Contact now fills a meter,\n\t/// and leaving empties it.\n\t///\n\t/// It empties SLOWER than it fills, which is the part that matters. If it reset the instant\n\t/// you stepped off, hopping on the spot on a red tile would be survivable forever, since\n\t/// airborne is already safe. Recovering at a fraction of the burn rate means the answer has to\n\t/// be actually going somewhere, which is what the whole system is for.\n\t///\n\t/// The permanent violet slabs are deliberately still instant. They never cycle, they are\n\t/// raised so you can see them from anywhere, and being the one thing in the game that offers\n\t/// no second chance is their entire identity.\n\t/// \u003C/remarks\u003E\n\tprivate void CheckPlayers()\n\t{\n\t\tvar manager = MonolithManager.Instance;\n\t\tif ( !manager.IsValid() )\n\t\t\treturn;\n\n\t\tBurnFraction = 0f;\n\n\t\tforeach ( var player in Scene.GetAllComponents\u003CPlayerMovement\u003E() )\n\t\t{\n\t\t\tburn.TryGetValue( player, out float held );\n\n\t\t\tbool onFire = false;\n\n\t\t\t// Airborne is always safe. This is the entire mechanic.\n\t\t\tif ( player.IsGrounded )\n\t\t\t{\n\t\t\t\tvar tile = TileAt( player.WorldPosition );\n\n\t\t\t\tif ( tile == Tile.Dead )\n\t\t\t\t{\n\t\t\t\t\tburn.Remove( player );\n\t\t\t\t\tKill( manager, player, \u0022stepped on a dead tile\u0022 );\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tonFire = tile == Tile.Hot \u0026\u0026 Live;\n\t\t\t}\n\n\t\t\theld \u002B= onFire\n\t\t\t\t? GameTime.Delta\n\t\t\t\t: -GameTime.Delta * Tuning.FloorBurnRecoverRate;\n\n\t\t\theld = Math.Clamp( held, 0f, Tuning.FloorBurnSeconds );\n\t\t\tburn[player] = held;\n\n\t\t\tfloat fraction = held / Tuning.FloorBurnSeconds;\n\t\t\tBurnFraction = MathF.Max( BurnFraction, fraction );\n\n\t\t\tif ( onFire )\n\t\t\t\tTickBurnAudio( player, fraction );\n\n\t\t\tif ( held \u003E= Tuning.FloorBurnSeconds )\n\t\t\t{\n\t\t\t\tburn.Remove( player );\n\t\t\t\tKill( manager, player, \u0022burned by the floor\u0022 );\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A tick that quickens as you burn.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// The same trick the Crawler uses for footsteps, for the same reason: a meter that only\n\t/// exists on the HUD is a meter you will not look at while something is chasing you. An\n\t/// accelerating sound tells you how long you have without asking you to look anywhere.\n\t/// \u003C/remarks\u003E\n\tprivate void TickBurnAudio( PlayerMovement player, float fraction )\n\t{\n\t\tfloat interval = MathX.Lerp( 0.22f, 0.06f, fraction );\n\n\t\tif ( timeSinceBurnTick \u003C interval )\n\t\t\treturn;\n\n\t\ttimeSinceBurnTick = 0f;\n\t\tAudio.Play( Audio.Crumble, player.WorldPosition,\n\t\t\t0.3f \u002B 0.5f * fraction, 0.8f \u002B 0.8f * fraction );\n\t}\n\n\tprivate void Kill( MonolithManager manager, PlayerMovement player, string reason )\n\t{\n\t\tBurnFraction = 0f;\n\n\t\tAudio.Play( Audio.Explosion, player.WorldPosition, 1f, 0.5f );\n\n\t\t// Rebuilding the stage calls FitTo, which lays a fresh all-safe grid and restarts the\n\t\t// grace. So a loss cannot cascade: you are never dropped back onto the tile that just\n\t\t// killed you with the clock already run down.\n\t\tmanager.ResetStageProgress( reason );\n\t}\n\n\tprivate Tile TileAt( Vector3 world )\n\t{\n\t\tif ( tiles == null )\n\t\t\treturn Tile.Safe;\n\n\t\tint x = (int)MathF.Floor( (world.x - origin.x) / tileSize );\n\t\tint y = (int)MathF.Floor( (world.y - origin.y) / tileSize );\n\n\t\tif ( x \u003C 0 || y \u003C 0 || x \u003E= width || y \u003E= width )\n\t\t\treturn Tile.Safe;\n\n\t\treturn tiles[y * width \u002B x];\n\t}\n\n\t// ---------------------------------------------------------------- meshes\n\n\tprivate void BuildHotMesh()\n\t{\n\t\thotObject ??= MakeChild( \u0022Floor Hot\u0022, out hotRenderer );\n\n\t\tif ( hotRenderer.IsValid() )\n\t\t\thotRenderer.Model = BuildTiles( Tile.Hot );\n\t}\n\n\tprivate void BuildDeadMesh()\n\t{\n\t\tdeadObject ??= MakeChild( \u0022Floor Dead\u0022, out deadRenderer );\n\n\t\tif ( deadRenderer.IsValid() )\n\t\t\tdeadRenderer.Model = BuildTiles( Tile.Dead );\n\t}\n\n\tprivate GameObject MakeChild( string name, out ModelRenderer renderer )\n\t{\n\t\tvar obj = new GameObject( true, name );\n\t\tobj.NetworkMode = NetworkMode.Never;\n\t\tobj.Parent = GameObject;\n\t\tobj.WorldPosition = Vector3.Zero;\n\n\t\trenderer = obj.AddComponent\u003CModelRenderer\u003E();\n\t\treturn obj;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Builds one flat quad per tile of the given state.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// Rebuilt every phase rather than kept as fixed models that get shown and hidden. At a few\n\t/// hundred quads a rebuild costs microseconds and happens once every three seconds, so the\n\t/// simpler code wins easily over anything cleverer.\n\t///\n\t/// Lifted slightly off the floor so it does not fight the VoidGrid for the same pixels, and\n\t/// inset so neighbouring tiles read as separate rather than as one field of colour.\n\t/// \u003C/remarks\u003E\n\tprivate Model BuildTiles( Tile want )\n\t{\n\t\tvar vb = new VertexBuffer();\n\t\tvb.Init( true );\n\n\t\tint index = 0;\n\t\tbool dead = want == Tile.Dead;\n\n\t\t// The permanent tiles get a bigger gap as well as height, so they read as separate blocks\n\t\t// standing in the floor rather than as a painted region of it.\n\t\tfloat inset = tileSize * (dead ? 0.14f : 0.06f);\n\t\tfloat z = floorZ \u002B 2f;\n\t\tfloat height = dead ? Tuning.FloorDeadHeight : 0f;\n\n\t\tfor ( int y = 0; y \u003C width; y\u002B\u002B )\n\t\t{\n\t\t\tfor ( int x = 0; x \u003C width; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( tiles[y * width \u002B x] != want )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tfloat x0 = origin.x \u002B x * tileSize \u002B inset;\n\t\t\t\tfloat y0 = origin.y \u002B y * tileSize \u002B inset;\n\t\t\t\tfloat x1 = x0 \u002B tileSize - inset * 2f;\n\t\t\t\tfloat y1 = y0 \u002B tileSize - inset * 2f;\n\n\t\t\t\tif ( dead )\n\t\t\t\t\tAddSlab( vb, ref index, x0, y0, x1, y1, z, height );\n\t\t\t\telse\n\t\t\t\t\tAddQuad( vb, ref index,\n\t\t\t\t\t\tnew Vector3( x0, y0, z ),\n\t\t\t\t\t\tnew Vector3( x1, y0, z ),\n\t\t\t\t\t\tnew Vector3( x1, y1, z ),\n\t\t\t\t\t\tnew Vector3( x0, y1, z ) );\n\t\t\t}\n\t\t}\n\n\t\t// An empty vertex buffer is not a valid model, and a phase where nothing is hot is\n\t\t// entirely possible. Returning null clears the renderer instead of throwing.\n\t\tif ( index == 0 )\n\t\t\treturn null;\n\n\t\tvar mesh = new Mesh( Material.Load( \u0022materials/default.vmat\u0022 ) );\n\t\tmesh.CreateBuffers( vb );\n\n\t\treturn new ModelBuilder().AddMesh( mesh ).Create();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A low box: a top face plus four sides.\n\t///\n\t/// The sides are the point. A flat quad and a raised one look identical from directly above,\n\t/// and the whole reason for the height is that the four vertical faces catch the player lamp\n\t/// at a completely different angle from the floor. That is what makes a dead tile readable at\n\t/// a glance and from across the arena, where its colour is only a few pixels wide.\n\t/// \u003C/summary\u003E\n\tprivate static void AddSlab( VertexBuffer vb, ref int index,\n\t\tfloat x0, float y0, float x1, float y1, float z, float height )\n\t{\n\t\tfloat top = z \u002B height;\n\n\t\tAddQuad( vb, ref index,\n\t\t\tnew Vector3( x0, y0, top ), new Vector3( x1, y0, top ),\n\t\t\tnew Vector3( x1, y1, top ), new Vector3( x0, y1, top ) );\n\n\t\tAddQuad( vb, ref index,\n\t\t\tnew Vector3( x0, y0, z ), new Vector3( x1, y0, z ),\n\t\t\tnew Vector3( x1, y0, top ), new Vector3( x0, y0, top ) );\n\n\t\tAddQuad( vb, ref index,\n\t\t\tnew Vector3( x1, y1, z ), new Vector3( x0, y1, z ),\n\t\t\tnew Vector3( x0, y1, top ), new Vector3( x1, y1, top ) );\n\n\t\tAddQuad( vb, ref index,\n\t\t\tnew Vector3( x1, y0, z ), new Vector3( x1, y1, z ),\n\t\t\tnew Vector3( x1, y1, top ), new Vector3( x1, y0, top ) );\n\n\t\tAddQuad( vb, ref index,\n\t\t\tnew Vector3( x0, y1, z ), new Vector3( x0, y0, z ),\n\t\t\tnew Vector3( x0, y0, top ), new Vector3( x0, y1, top ) );\n\t}\n\n\tprivate static void AddQuad( VertexBuffer vb, ref int index,\n\t\tVector3 p0, Vector3 p1, Vector3 p2, Vector3 p3 )\n\t{\n\t\tvar up = Vector3.Up;\n\t\tvar tangent = (p1 - p0).Normal;\n\n\t\tvb.Add( new Vertex( p0, up, tangent, new Vector4( 0, 0, 0, 0 ) ) );\n\t\tvb.Add( new Vertex( p1, up, tangent, new Vector4( 1, 0, 0, 0 ) ) );\n\t\tvb.Add( new Vertex( p2, up, tangent, new Vector4( 1, 1, 0, 0 ) ) );\n\t\tvb.Add( new Vertex( p3, up, tangent, new Vector4( 0, 1, 0, 0 ) ) );\n\n\t\tvb.AddRawIndex( index \u002B 0 ); vb.AddRawIndex( index \u002B 1 ); vb.AddRawIndex( index \u002B 2 );\n\t\tvb.AddRawIndex( index \u002B 0 ); vb.AddRawIndex( index \u002B 2 ); vb.AddRawIndex( index \u002B 3 );\n\n\t\tindex \u002B= 4;\n\t}\n\n\tprivate void Clear()\n\t{\n\t\tBurnFraction = 0f;\n\t\tburn.Clear();\n\n\t\thotObject?.Destroy();\n\t\tdeadObject?.Destroy();\n\n\t\thotObject = null;\n\t\tdeadObject = null;\n\t\thotRenderer = null;\n\t\tdeadRenderer = null;\n\n\t\ttiles = null;\n\t}\n\n\tprotected override void OnDestroy() =\u003E Clear();\n}\n"},{"Ident":"idkman.monolith","Path":"Game/GameTime.cs","FileName":"GameTime.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":355854,"Code":"namespace Monolith;\n\n/// \u003Csummary\u003E\n/// A clock that stops when the game is paused, and the pause state itself.\n///\n/// **Why this exists rather than a bare bool.** Gating every \u0060OnUpdate\u0060 behind a flag stops\n/// things MOVING, but it does not stop time PASSING, and most of this game is written against\n/// absolute deadlines: \u0060shieldEndsAt = Time.Now \u002B 40f\u0060, \u0060runStartedAt = Time.Now\u0060,\n/// \u0060clearedBannerUntil = Time.Now \u002B 3.5f\u0060. Read the tutorial for two minutes with a naive pause\n/// and every one of those deadlines is in the past when you resume. The shield expires\n/// instantly, the banner never shows, and the run timer that feeds the leaderboard has two\n/// minutes of reading time baked into it.\n///\n/// So the pause has to move the clock, not just the components. \u003Csee cref=\u0022Now\u0022/\u003E is real time\n/// minus every second ever spent paused, which means a deadline written against it keeps\n/// exactly the remaining time it had.\n///\n/// **What deliberately still uses the real clock.** Three things must keep running while\n/// paused, and they are the reason this is opt-in per call site rather than a global override:\n/// the frame-hitch watchdog and the mesher backlog report (both diagnostics measuring wall time,\n/// which is the whole point of them), and the controller navigation repeat in the HUD, which has\n/// to work while the pause menu is open. Cosmetic \u0060MathF.Sin( Time.Now )\u0060 pulses also stay on\n/// the real clock; they are inside gated updates anyway, so they freeze with everything else.\n///\n/// **Multiplayer.** Pausing is refused outright when anyone else is connected. The monolith is\n/// host-authoritative and shared, so one player opening a menu cannot be allowed to stop the\n/// world for everyone, and stopping it only locally would desync them from a world that kept\n/// going. Solo, which is how this is played, gets a real pause. Otherwise the menu opens over a\n/// live game, the way it does in any online game.\n/// \u003C/summary\u003E\npublic static class GameTime\n{\n\t/// \u003Csummary\u003ETotal real seconds spent paused since launch. The offset between the clocks.\u003C/summary\u003E\n\tprivate static float pausedTotal;\n\n\t/// \u003Csummary\u003ESet for the frames on which the world is frozen.\u003C/summary\u003E\n\tpublic static bool Paused { get; private set; }\n\n\t/// \u003Csummary\u003E\n\t/// True when the pause menu is open but the world is NOT frozen, which is the multiplayer\n\t/// case. The menu still needs to know it is showing so it can draw and take input.\n\t/// \u003C/summary\u003E\n\tpublic static bool MenuOpenLive { get; private set; }\n\n\t/// \u003Csummary\u003E\n\t/// Game time, in seconds. Advances with real time, except while paused. Use this for anything\n\t/// that measures gameplay: run timers, shield deadlines, cooldowns.\n\t/// \u003C/summary\u003E\n\tpublic static float Now =\u003E Time.Now - pausedTotal;\n\n\t/// \u003Csummary\u003E\n\t/// Frame delta, forced to zero while paused. Mostly a safety net: gated updates never read\n\t/// this while paused, but anything that slips through integrates nothing rather than jumping.\n\t/// \u003C/summary\u003E\n\tpublic static float Delta =\u003E Paused ? 0f : Time.Delta;\n\n\t/// \u003Csummary\u003E\n\t/// Requests a pause. Returns whether the world actually froze, which is false in multiplayer.\n\t/// \u003C/summary\u003E\n\tpublic static bool SetPaused( bool wanted )\n\t{\n\t\tif ( !wanted )\n\t\t{\n\t\t\tPaused = false;\n\t\t\tMenuOpenLive = false;\n\t\t\treturn false;\n\t\t}\n\n\t\tif ( CanFreeze() )\n\t\t{\n\t\t\tPaused = true;\n\t\t\tMenuOpenLive = false;\n\t\t\treturn true;\n\t\t}\n\n\t\t// Someone else is playing. Show the menu, leave the world running.\n\t\tPaused = false;\n\t\tMenuOpenLive = true;\n\t\treturn false;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Solo means \u0022nobody else is connected\u0022. Wrapped because \u003Cc\u003EConnection.All\u003C/c\u003E throws if the\n\t/// networking system is not up yet, which is true for the first frames of a scene, and the\n\t/// honest answer in that window is that we are alone.\n\t/// \u003C/summary\u003E\n\tprivate static bool CanFreeze()\n\t{\n\t\ttry { return Connection.All.Count \u003C= 1; }\n\t\tcatch ( Exception ) { return true; }\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Accumulates paused time. Must be called once per frame from something that is NOT itself\n\t/// gated by the pause, or the clock would never catch up and the offset would never grow.\n\t/// \u003C/summary\u003E\n\tpublic static void Advance()\n\t{\n\t\tif ( Paused )\n\t\t\tpausedTotal \u002B= Time.Delta;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Drops the pause without crediting the time. Used when the scene is torn down and restarted,\n\t/// where carrying an offset forward would be meaningless.\n\t/// \u003C/summary\u003E\n\tpublic static void Reset()\n\t{\n\t\tPaused = false;\n\t\tMenuOpenLive = false;\n\t}\n}\n\n/// \u003Csummary\u003E\n/// \u003Csee cref=\u0022TimeSince\u0022/\u003E, but on the pausable clock.\n///\n/// The engine struct reads \u003Cc\u003ETime.Now\u003C/c\u003E internally and there is no way to redirect it, so\n/// pausing means having our own. Deliberately minimal: this codebase only ever assigns a float\n/// to these and reads them back as a float, so an implicit conversion each way is the entire\n/// surface that is needed. \u0060Relative\u0060, \u0060Absolute\u0060, \u0060Fraction\u0060 and \u0060Passed\u0060 are not reproduced\n/// because nothing uses them, and guessing at semantics we do not need is how bugs get in.\n/// \u003C/summary\u003E\n/// \u003Cremarks\u003E\n/// **There are deliberately no comparison operators here, and that is load bearing.** Every use\n/// in this codebase is of the form \u003Cc\u003EtimeSinceFired \u0026gt; 0.5f\u003C/c\u003E. With both implicit\n/// conversions present, defining \u003Cc\u003Eoperator \u0026gt;(GameTimeSince, GameTimeSince)\u003C/c\u003E as well would\n/// give that expression two equally valid readings: convert the left side down to float, or\n/// convert the literal up to a struct. That is an ambiguity error at every call site. Leaving the\n/// operators out means the float conversion is the only path and the comparison just works. The\n/// engine\u0027s own \u003Csee cref=\u0022TimeSince\u0022/\u003E resolves it the same way.\n/// \u003C/remarks\u003E\npublic struct GameTimeSince\n{\n\tprivate float absolute;\n\n\t/// \u003Csummary\u003ESeconds elapsed on the game clock since this was last assigned.\u003C/summary\u003E\n\tpublic float Relative =\u003E GameTime.Now - absolute;\n\n\tpublic static implicit operator float( GameTimeSince ts ) =\u003E ts.Relative;\n\n\tpublic static implicit operator GameTimeSince( float seconds )\n\t\t=\u003E new() { absolute = GameTime.Now - seconds };\n\n\tpublic override string ToString() =\u003E Relative.ToString();\n}\n"}]}