Tuning.cs
namespace Monolith;

/// <summary>
/// Every game-feel number lives here. The user iterates on feel constantly, so nothing
/// tunable should be buried in gameplay code. See GOALS.md for the intent behind each value.
/// </summary>
public static class Tuning
{
	// ---------------------------------------------------------------- world

	/// <summary>Size of one voxel in world units. A citizen is roughly 72 units tall.</summary>
	public const float VoxelSize = 16f;

	/// <summary>Voxels per chunk axis. 32 gives 32768 voxels = a 4 KB bitset per chunk.</summary>
	public const int ChunkSize = 32;

	// Stage sizes and shapes now live in Stages.cs, which is the ladder the player climbs.

	/// <summary>
	/// Per-stage size multiplier, applied to each axis. The target is a **20 minute** run to
	/// stage 100 for a min-maxing player.
	///
	/// Derived by simulation, not by guessing: a greedy upgrade buyer starting from ~300 cubes
	/// at stage 1 (about 50 seconds) reaches stage 100 in about **18 minutes**. Change this and
	/// the whole pacing target moves with it. **[TUNE]**
	/// </summary>
	/// <remarks>
	/// **Was 1.11, which saturated.** At 1.11 a 7 unit axis reaches the 192 cap by stage 33, so
	/// every stage from there to 100 was the SAME maximum size: seventy identical slogs, and the
	/// worst possible case for both pacing and framerate.
	///
	/// 1.030 is derived, not guessed. Putting the old stage-26 size at stage 90 means
	/// growth^89 = 1.11^25, so growth = 13.585^(1/89) = 1.0298. The cap is now never reached
	/// inside 100 stages, so every stage really is a step up from the one before.
	/// </remarks>
	public const float StageSizeGrowth = 1.030f;

	/// <summary>
	/// Upper bound per axis. Lowered 192 to 160: 192^3 is 7.1M voxels and 216 chunks, which is a
	/// lot of meshing for a stage you are meant to clear in under a minute. With the growth rate
	/// above this is a safety net rather than something the ladder actually reaches.
	/// </summary>
	public const int StageMaxAxis = 160;

	// ---------------------------------------------------------------- how the rock LOOKS
	//
	// See ChunkMesher. Greedy meshing merges a flat wall into a single quad, which is fast and
	// which is exactly why an untouched shape read as a featureless box. These are the two dials
	// that put the cubes back, and both work by making the merge more selective.

	/// <summary>
	/// How dark a fully occluded voxel corner gets, as a multiplier. 1 disables AO entirely.
	///
	/// This is the single biggest contributor to something reading as VOXELS rather than as a
	/// shaded solid: it draws the boundary between one cube and the next, which is a scale that
	/// screen-space AO cannot work at. Costs nothing on flat runs, because uniform occlusion
	/// still merges into one quad. **[TUNE]**
	/// </summary>
	public const float VoxelAoStrength = 0.42f;

	/// <summary>
	/// Number of horizontal rock layers. **Set to 1 to switch strata off entirely**, which is
	/// also the first thing to try if meshing gets expensive.
	///
	/// AO only articulates surfaces that have been CARVED. A pristine box has no occlusion
	/// anywhere, so without this it stays a box until you shoot it. Banding gives the mass
	/// internal structure before you touch it. **[TUNE]**
	/// </summary>
	public const int StrataBands = 3;

	/// <summary>
	/// Voxels per layer, and the performance dial for the whole feature. A band boundary breaks
	/// the greedy merge run, so thin layers mean many more quads on vertical faces. Horizontal
	/// faces are unaffected, since a whole face sits in one layer. **[TUNE]**
	/// </summary>
	public const int StrataThickness = 5;

	/// <summary>
	/// Brightness spread between the lightest and darkest layer. Deliberately small: this should
	/// read as sedimentary rock, not as stripes. **[TUNE]**
	/// </summary>
	public const float StrataContrast = 0.22f;

	// ---------------------------------------------------------------- the shared Monolith

	/// <summary>
	/// The shared Monolith. 256^3 is 16.7M cubes: big enough to be a real destination and to
	/// dwarf a fresh player, small enough for the current DENSE chunk storage (512 chunks,
	/// ~2 MB). The "weeks of collective effort" target in GOALS.md needs 2048^3, which needs
	/// sparse chunks first. This is the honest interim scale. **[TUNE]**
	/// </summary>
	public static readonly Vector3Int MonolithSize = new( 256, 256, 256 );

	/// <summary>Seconds the "stage cleared" banner stays up before the next shape condenses.</summary>
	public const float StageClearedPauseSeconds = 3.5f;

	/// <summary>
	/// Below this many remaining cubes the leftovers light up and the HUD calls out the count.
	/// A single 16 unit cube in a 512 unit void is genuinely impossible to spot otherwise, which
	/// reads as the stage being stuck rather than nearly finished.
	/// </summary>
	public const int ResidueRevealThreshold = 250;

	// ---------------------------------------------------------------- drill
	//
	// REAL TUNING. The playtest speedups are gone. Derived from a simulation of a greedy
	// upgrade buyer over all 100 stages, targeting ~18 minutes for a min-maxing player and a
	// stage 100 that fits inside StageMaxAxis.
	//
	// The critical lesson from that simulation: **blast radius scales value CUBICALLY** (a
	// sphere), so it needs a far steeper cost curve than the upgrades whose value is linear.
	// Giving every upgrade the same growth rate produced a runaway economy that cleared the
	// entire ladder in 72 seconds. Hence per-upgrade growth rates below.

	// NOTHING IS CAPPED. Upgrades must never show "MAX": an incremental game dies the moment
	// a purchase stops being available. Every curve below grows without bound, and the ones
	// that are conceptually bounded (a probability) approach their limit asymptotically
	// instead of hitting a wall.

	/// <summary>
	/// Shots per second. Level 0 is ~0.9 SECONDS BETWEEN SHOTS: slow and methodical on
	/// purpose, so the first upgrade you buy is the one that makes you faster and you feel it.
	/// The HUD presents this as a delay rather than a rate for the same reason.
	/// </summary>
	public const float DrillSpeedBase = 1.1f;
	public const float DrillSpeedPerLevel = 0.30f;

	/// <remarks>
	/// Per-level growth cut 0.16 to 0.11 alongside the stage-size change.
	///
	/// Blast radius is the single biggest driver of frame cost: it is cubic in voxels touched
	/// AND it is what decides how many chunks get dirtied per shot. Smaller stages need smaller
	/// blasts to feel the same, and the pair of changes together is what makes late stages
	/// cheap to render rather than merely shorter. **[TUNE]**
	/// </remarks>
	public const float BlastRadiusBase = 0.9f;     // ~3 cubes per shot at level 0
	public const float BlastRadiusPerLevel = 0.11f;

	// Instability no longer rolls per shot. It seeds the SHAPE with volatile blocks, so the
	// upgrade buys targets to aim at rather than a random chance of a bigger explosion. The
	// player has to find and hit them.

	/// <summary>Fraction of blocks that are volatile at Instability level 0.</summary>
	public const float VolatileChanceBase = 0.02f;

	/// <summary>Approaches a ceiling asymptotically, so it never runs out of levels.</summary>
	public const float VolatileChanceRate = 0.05f;

	/// <summary>Hard ceiling on volatile density. Past this the shape stops reading as rock.</summary>
	public const float VolatileChanceMax = 0.22f;

	/// <summary>Blast radius on a direct volatile hit, at Detonation level 0. Scaled down with
	/// the stage sizes: a detonation should still feel like a reward, not clear the stage.</summary>
	public const float VolatileRadiusBase = 5f;
	public const float VolatileRadiusPerLevel = 0.45f;

	public const float DustYieldBase = 1.0f;       // dust per cube removed
	public const float DustYieldPerLevel = 0.22f;

	/// <summary>
	/// Shots per second for ONE drone at Drone Cadence level 0. Deliberately slow: a fresh drone
	/// should read as a helper you then invest in, not as an instant doubling of your output.
	/// </summary>
	public const float DroneFireRateBase = 0.32f;

	/// <summary>Added shots per second per drone, per Drone Cadence level.</summary>
	public const float DroneFireRatePerLevel = 0.14f;

	public const float DroneRadiusScale = 0.6f;    // drones hit softer than the player

	// ---------------------------------------------------------------- projectiles

	/// <summary>
	/// Units per second. Fast enough that a shot feels immediate at mining range, slow enough
	/// that you can watch it travel and that something can get in its way.
	/// </summary>
	public const float ProjectileSpeed = 4200f;

	/// <summary>
	/// Charges fly considerably slower than shots. You watch the whole flight and the bore that
	/// follows it, and time the detonation off that path, so the travel IS the mechanic.
	/// </summary>
	public const float ChargeProjectileSpeed = 625f;

	/// <summary>
	/// Seconds before a projectile that hit nothing gives up.
	///
	/// Cut 3.0 to 1.2. At <see cref="ProjectileSpeed"/> that is still 5,000 units, further than
	/// the width of any arena, so nothing that could have hit something is cut short. Three
	/// seconds meant a bolt fired at open sky loitered for 12,600 units of empty space while
	/// holding a slot in the live-projectile cap.
	/// </summary>
	public const float ProjectileLifetime = 1.2f;

	/// <summary>
	/// Spread applied to extra projectiles from multi-shot, in degrees. Enough that they land
	/// on different cubes (which is the point of multi-shot) without feeling inaccurate.
	/// </summary>
	public const float MultiShotSpreadDegrees = 2.4f;

	/// <summary>
	/// Most bolts a single trigger pull will ever DRAW, however many prestiges you have.
	///
	/// `ProjectileCount` grows by one per prestige and is unbounded. At 21 it is already a wall
	/// of objects leaving the barrel at once, reported twice as looking like a shotgun blast,
	/// and at 60 it would be worse in every way: more allocation, more overdraw, and no clearer
	/// as feedback. Nobody can read twenty-one simultaneous bolts as twenty-one.
	///
	/// Past this cap the reward is PRESERVED but converted: the extras fold into blast radius
	/// instead of spawning, so the shot does the same work and stays legible. See
	/// <see cref="Upgrades.MultiShotRadiusScale"/>.
	/// </summary>
	public const int MaxVisibleProjectiles = 6;

	/// <summary>
	/// Ceiling on `ProjectileCount`, which is a PRESTIGE reward and was unbounded.
	///
	/// It read `1 + Collapses` on the assumption that Collapses rises by one per prestige. Once
	/// felling a Monolith started awarding twenty, that assumption broke and the value reached
	/// 205: two hundred and five bolts a shot, or after the visible cap, a blast radius scaled
	/// 3.25x and removing millions of voxels per trigger pull. The game stalled.
	///
	/// 24 keeps prestige meaningfully rewarding (radius scaled up to 1.6x once folded) while
	/// bounding the worst case. Growth past here has to come from the Core Tree, which is
	/// designed for it, rather than from a value that multiplies work per frame. **[TUNE]**
	/// </summary>
	public const int MaxProjectileCount = 24;

	// ---------------------------------------------------------------- interceptors

	public const int InterceptorHealth = 3;
	public const int InterceptorChargeDamage = 3;
	public const float InterceptorSpeed = 190f;
	public const float InterceptorRetargetSeconds = 2.4f;
	public const float InterceptorSpawnInterval = 2.2f;
	public const int InterceptorMaxPopulation = 12;

	/// <summary>Stage at which interceptors first appear. The opening stays uncontested.</summary>
	public const int InterceptorFirstStage = 3;

	/// <summary>Stages between each additional simultaneous interceptor.</summary>
	public const int InterceptorStagesPerExtra = 3;

	/// <summary>Dust for destroying one. Scaled by yield upgrades like anything else.</summary>
	public const double InterceptorDustReward = 40;

	// ---------------------------------------------------------------- volatile cubes





	// ---------------------------------------------------------------- orbital barriers

	/// <summary>
	/// Wide panels that orbit the shape and block shots. Unlike interceptors you cannot kill
	/// them: the only answer is to move, which is the entire reason they exist.
	/// </summary>
	public const int BarrierFirstStage = 5;
	public const int BarrierStagesPerExtra = 9;
	public const int BarrierMaxCount = 4;

	/// <summary>Angular width of one panel, in degrees.</summary>
	public const float BarrierArcDegrees = 52f;

	/// <summary>Degrees per second. Slow enough to read, fast enough to force repositioning.</summary>
	public const float BarrierSpeedMin = 9f;
	public const float BarrierSpeedMax = 22f;

	// ---------------------------------------------------------------- spotter

	/// <summary>Chance a stage spawns Spotters at some point.</summary>
	public const float SpotterChance = 0.5f;

	/// <summary>
	/// How many arrive together. Cut from 2-4 to 1-2.
	///
	/// A pack was the wrong shape of threat. Four of them meant no angle was ever safe, which
	/// sounds threatening and actually reads as noise: you cannot learn to beat a specific
	/// Spotter when there are four, so you stop trying. One or two, each of which can genuinely
	/// catch you, is more frightening than four you cannot reason about. They also carry over
	/// between stages now, so the population builds anyway.
	/// </summary>
	public const int SpotterCountMin = 1;
	public const int SpotterCountMax = 2;

	/// <summary>
	/// Stages during which at most ONE Spotter may exist, and it arrives less often.
	///
	/// The opening is where a player is still learning that the beam is a countdown and that the
	/// shape is cover. Two of them converging before either lesson has landed is not difficulty,
	/// it is a player who never finds out what happened. They still carry over between stages,
	/// so the population climbs the moment this grace ends.
	/// </summary>
	public const int SpotterGraceStages = 5;

	/// <summary>Chance of a Spotter at all during the grace. Roughly half the usual.</summary>
	public const float SpotterGraceChance = 0.25f;

	/// <summary>
	/// Minimum bearing between two Spotters arriving together, in degrees. Placed on opposite
	/// sides rather than clustered, so a pair covers the arena instead of stacking one threat.
	/// </summary>
	public const float SpotterSpacingDegrees = 130f;

	/// <summary>
	/// A Spotter on standby ignores you entirely until you are this close. They hang around
	/// the shape as scenery you can pick off at leisure, and only become a threat once you
	/// come to work near them.
	/// </summary>
	public const float SpotterWakeRange = 1500f;

	/// <summary>
	/// Fraction of the stage that must be cleared before a Spotter arrives.
	///
	/// Deliberately expressed as PROGRESS, not seconds. A wall-clock delay meant a fast player
	/// cleared the stage before the Spotter ever showed up, so it effectively did not exist
	/// once you had a couple of upgrades. Progress scales with the player automatically.
	/// </summary>
	public const float SpotterArriveMinProgress = 0.15f;
	public const float SpotterArriveMaxProgress = 0.55f;

	/// <summary>Low: it should die to a couple of ordinary shots, like a stubborn block.</summary>
	public const int SpotterHealth = 3;

	/// <summary>
	/// Body radius, which is also the sphere a projectile has to cross to hit one. Deliberately
	/// large: it hangs a long way off against an empty sky, where a small silhouette is both hard
	/// to read as a threat and hard to lead.
	/// </summary>
	public const float SpotterRadius = 145f;

	public const float SpotterHoverHeight = 620f;
	public const float SpotterSpeed = 240f;
	public const float SpotterRetargetSeconds = 3.0f;

	/// <summary>
	/// How fast the beam can turn, in degrees per second.
	///
	/// **This is the whole encounter.** The beam is always trying to point at you, so the only
	/// question that matters is whether you can move across its arc faster than it can follow.
	/// Raise it and the Spotter becomes unavoidable; lower it and it becomes decorative.
	///
	/// At the standoff distance a sprint is worth roughly 35 degrees per second and a strafe
	/// double jump momentarily far more, so running perpendicular to the beam beats it and
	/// running straight at or away from it does not. That is the lesson it should teach. **[TUNE]**
	/// </summary>
	public const float SpotterTrackDegreesPerSecond = 62f;

	/// <summary>
	/// Lateral speed, in units per second, the beam can follow REGARDLESS of how close you are.
	///
	/// A pure angular rate has a nasty flaw: the closer you stand, the more degrees per second
	/// your movement is worth, so standing directly under a Spotter made it physically unable to
	/// track you. Being right beneath the searchlight was the safest place in the arena, which is
	/// exactly backwards. The slew budget is now the LARGER of the angular rate and whatever
	/// angle this linear speed works out to at your current distance.
	///
	/// Set just under sprint speed (609) on purpose: walking never escapes, sprinting sideways
	/// slowly slips the gaze, and a dash breaks it outright. **[TUNE]**
	/// </summary>
	public const float SpotterTrackLinearSpeed = 520f;

	/// <summary>
	/// Half-angle of the wide SEARCH cone: how far off its gaze it can still notice you.
	///
	/// Two narrower versions of this failed in playtest (7 then 15 degrees). The mistake was
	/// treating spotting and tracking as the same number. They are not:
	///
	///   SPOTTING is a wide cone. A searchlight sees anything in the general direction it faces,
	///   which is why walking through its area gets you caught and why the answer is cover.
	///   TRACKING is the tight line that snaps to you once it has you, and its job is only to
	///   make being caught unmistakable.
	///
	/// Collapsing the two into one tight angle produced a Spotter that could stare straight at
	/// you and never notice, which is what "spotters are still not working" meant.
	/// </summary>
	/// <remarks>
	/// 50 down to 28. Fifty was chosen when the gaze tracked you unconditionally and the cone was
	/// the only thing standing between you and a permanent lock, so it had to be wide enough to
	/// make acquisition possible at all. Now that it patrols while unaware, the cone is doing its
	/// real job: deciding whether a sweep passing near you actually finds you. A narrow one makes
	/// that a near miss rather than a formality.
	/// </remarks>
	public const float SpotterViewCone = 28f;


	/// <summary>Horizontal distance an awake Spotter tries to hold from you. **[TUNE]**</summary>
	public const float SpotterStandoff = 560f;

	/// <summary>Move speed while hunting. Faster than the standby drift.</summary>
	public const float SpotterChaseSpeed = 330f;


	/// <summary>
	/// Thickness of the tracking line, in world units. Was 7, which multiplied out to a beam
	/// wide enough to blank out the middle of the screen once the search states drew at 3.5x
	/// and 5.5x. It is a line, not a wall: the COLOUR carries the threat, not the area.
	/// </summary>
	public const float SpotterBeamThickness = 0.85f;

	/// <summary>How far the beam reaches.</summary>
	public const float SpotterBeamRange = 3200f;

	/// <summary>
	/// Seconds in the beam WITH line of sight before your stage progress is wiped.
	///
	/// The beam runs yellow, then orange, then red, and red occupies the FINAL SECOND. So the
	/// colour is the countdown: by the time it is red you have exactly one second left. Three
	/// seconds total gives two seconds of escalating warning before that.
	/// </summary>
	/// <remarks>
	/// Cut 3.0 to 2.0. Three seconds was tuned when a stage lasted a minute; against stages that
	/// fall in seconds it was long enough that simply continuing to mine usually outran it.
	/// Two seconds means being seen is a thing you have to answer NOW.
	/// </remarks>
	public const float SpotterLockSeconds = 1.0f;

	/// <summary>
	/// Lock fraction at which the beam turns orange, then red. RedAt moved 0.667 to 0.5 so that
	/// red still means ONE SECOND LEFT against the shorter lock. The colour has to keep meaning
	/// the same thing in wall-clock terms or the warning stops being learnable.
	/// </summary>
	public const float SpotterOrangeAt = 0.25f;
	public const float SpotterRedAt = 0.5f;

	/// <summary>
	/// Seconds the beam must hold you before the lock begins to count. **Zero on purpose.**
	///
	/// It used to be 0.45s of grace, which existed because the beam SNAPPED onto you the instant
	/// line of sight opened and something had to absorb that unfairness. Now the beam has to
	/// physically turn onto you at <see cref="SpotterTrackDegreesPerSecond"/>, so the grace is
	/// built into the movement and a second one on top would only make the countdown lie about
	/// when it started. On you means counting.
	/// </summary>
	public const float SpotterAcquireSeconds = 0f;

	/// <summary>How fast the lock unwinds once you are clear. Faster than it fills, on purpose.</summary>
	public const float SpotterDecayMultiplier = 2.2f;

	/// <summary>
	/// Seconds after a wipe during which NO Spotter can begin a lock.
	///
	/// A Spotter no longer removes itself when it catches you: the threat that beat you is still
	/// there when the stage rebuilds, which is the whole reason it is a threat. That only works
	/// with a grace period, otherwise it simply re-locks the moment the new shape appears and you
	/// never get a first move.
	/// </summary>
	public const float SpotterWipeCooldown = 4.5f;


	/// <summary>Dust for shooting one down.</summary>
	public const double SpotterDustReward = 250;

	// ---------------------------------------------------------------- sentinels
	//
	// The first thing in the game that SHOOTS BACK. Every other hazard is passive: barriers sit
	// in the way, leeches sit on the rock, the Spotter watches. All of them can be ignored for a
	// while. A sentinel cannot, because its shot arrives whether or not you looked at it.
	//
	// Its orb is deliberately slow and destructible, so the answer is a choice rather than a
	// reflex: dodge it, or spend a shot killing it, or spend several killing the sentinel and
	// stop the problem at the source.

	/// <summary>
	/// How close you have to be before a sentinel will fire.
	///
	/// Shorter than the Spotter's reach on purpose. A sentinel should be a local problem you
	/// walk into and can walk out of, not something shooting at you from across the arena: at
	/// unlimited range four of them meant permanent incoming fire wherever you stood, with no
	/// positional decision to make about it.
	/// </summary>
	public const float SentinelRange = 1500f;

	/// <summary>
	/// Half-angle a sentinel must have you within to fire, measured off its facing.
	///
	/// It had NO view at all: anything in range with line of sight was shot at, from any
	/// direction, which meant its shots arrived with no warning and no way to play around it.
	/// Now it must physically turn to face you first, and turning is visible.
	/// </summary>
	/// <summary>
	/// Seconds after arriving, or after a new stage condenses, before a sentinel may aim or fire.
	///
	/// Spawning is not an attack. The real delay is longer than this number: it spends this
	/// window drifting and only THEN starts turning toward you at
	/// <see cref="SentinelTurnRate"/>, so a sentinel that appears behind you has to travel most
	/// of a half turn before it can shoot.
	/// </summary>
	public const float SentinelArmSeconds = 1.0f;


	public const float SentinelViewCone = 26f;

	/// <summary>Degrees per second it can turn. Slow enough that walking around it works.</summary>
	public const float SentinelTurnRate = 55f;

	public const int SentinelHealth = 6;
	public const float SentinelHoverHeight = 300f;
	public const float SentinelSpeed = 120f;

	/// <summary>Seconds between shots. Slow: this is a metronome you plan around.</summary>
	public const float SentinelFireInterval = 3.4f;

	/// <summary>Stage at which sentinels first appear.</summary>
	public const int SentinelFirstStage = 6;

	/// <summary>Stages between each additional simultaneous sentinel.</summary>
	public const int SentinelStagesPerExtra = 12;

	public const int SentinelMaxCount = 4;
	public const double SentinelDustReward = 400;

	/// <summary>Orb speed. Slow enough to read, fast enough that ignoring it is a decision.</summary>
	public const float SentinelOrbSpeed = 430f;

	/// <summary>How close an orb has to get to count as a hit.</summary>
	public const float SentinelOrbRadius = 42f;

	/// <summary>Seconds an orb survives before fizzling.</summary>
	public const float SentinelOrbLifetime = 7f;

	/// <summary>
	/// How far ahead of you a sentinel leads its shot. Below 1 on purpose: it aims at where you
	/// WOULD be, so holding a straight line gets you hit and changing direction beats it. That
	/// is the whole lesson, and a perfect lead would teach the opposite one.
	/// </summary>
	public const float SentinelLeadFactor = 0.75f;

	// ---------------------------------------------------------------- anchors

	/// <summary>
	/// A cable fired at the player from across the arena. While attached it halves your speed and
	/// kills hop boosts, and the ONLY way off is to shoot the far end.
	///
	/// This is the hazard that most directly forces the thing being asked for: it cannot be
	/// answered by moving, it cannot be answered by ignoring it, and the target is nowhere near
	/// the shape you are mining. You have to turn your back on your work to deal with it.
	/// </summary>
	public const int AnchorHealth = 3;
	public const float AnchorHoverHeight = 140f;
	public const float AnchorRange = 2400f;

	/// <summary>Movement multiplier while tethered.</summary>
	public const float AnchorSlowFactor = 0.5f;

	/// <summary>
	/// How fast a hold drags your existing speed down to its ceiling, as a lerp rate.
	///
	/// Needed because capping the wish speed alone does nothing to a player who is already
	/// moving faster: Accelerate only adds. Eased rather than clamped so it feels like being
	/// pulled back rather than hitting a wall.
	/// </summary>
	public const float HoldDragRate = 3.2f;


	/// <summary>Seconds a tether holds before it releases on its own.</summary>
	public const float AnchorHoldSeconds = 9f;

	/// <summary>Seconds between one releasing and it being able to fire again.</summary>
	public const float AnchorCooldownSeconds = 7f;

	public const int AnchorFirstStage = 11;
	public const int AnchorStagesPerExtra = 16;
	public const int AnchorMaxCount = 3;
	public const double AnchorDustReward = 320;

	// ---------------------------------------------------------------- crawlers
	//
	// The first thing that comes for you on the FLOOR. Every other hazard sits in the sky and
	// asks you to look up; this one turns the ground you are standing on into a problem, which
	// is the half of the arena nothing was using.
	//
	// It is the payoff for the Anchor. A tether halves your speed, and on its own that is only
	// annoying: something that closes at a fixed rate turns the same tether into a countdown.
	// Neither is very interesting alone and the pair is the most dangerous thing in the game.

	public const int CrawlerHealth = 5;

	/// <summary>
	/// Speed it starts a chase at. **Below walking pace on purpose.** You should always be able
	/// to walk away from a fresh crawler, so being caught is the result of standing still or
	/// being held, never of it simply being faster than you.
	/// </summary>
	public const float CrawlerBaseSpeed = 190f;

	/// <summary>
	/// Speed it reaches after <see cref="CrawlerRampSeconds"/> of unbroken pursuit.
	///
	/// Sits between walking (420) and sprinting (609): a fully wound-up crawler outruns a walk
	/// and loses to a sprint, so the answer is always to commit to moving rather than to have
	/// out-levelled it. Against the Anchor's halved 210 it wins comfortably, which is the point.
	/// </summary>
	public const float CrawlerMaxSpeed = 520f;

	/// <summary>Seconds of pursuit to reach full speed. Long enough to see it happening.</summary>
	public const float CrawlerRampSeconds = 14f;

	/// <summary>How close it has to get to take the stage.</summary>
	public const float CrawlerGrabRadius = 62f;

	/// <summary>Seconds after a grab before any crawler can grab again.</summary>
	public const float CrawlerGrabCooldown = 4.5f;

	/// <summary>Distance from the shape it walks in from.</summary>
	public const float CrawlerSpawnDistance = 1900f;

	public const int CrawlerFirstStage = 5;
	public const int CrawlerStagesPerExtra = 9;
	/// <remarks>
	/// Raised 3 to 6, and moved earlier, alongside the lethal floor. The two systems are the same
	/// idea from opposite directions: the floor says you cannot stand still, and a crawler says
	/// you cannot stand still HERE. On an empty floor a pack of six would just be noise, but once
	/// half the ground is a countdown, deciding which safe tile to run to while something is
	/// closing on the obvious one is the whole game. **[TUNE]**
	/// </remarks>
	public const int CrawlerMaxCount = 6;
	public const double CrawlerDustReward = 500;

	// ---------------------------------------------------------------- being hit

	/// <summary>
	/// Seconds you cannot fire after taking a hit.
	///
	/// Consequences cost TIME, never earned progress (GOALS 6b). A stagger breaks your hop chain,
	/// interrupts your mining and drops your Resonance, which is expensive in an incremental game
	/// without being punitive. Nothing takes dust, cubes or levels away.
	/// </summary>
	public const float StaggerSeconds = 0.7f;

	/// <summary>Fraction of horizontal speed kept when staggered. Low: the hop chain is the cost.</summary>
	public const float StaggerSpeedKept = 0.25f;

	// ---------------------------------------------------------------- shields

	/// <summary>
	/// Chance a stage begins shielded. A shield jams DRONES only: you can still mine by hand,
	/// so the answer to it is to play, which is precisely the point of having it.
	/// </summary>
	public const float ShieldChance = 0.14f;

	public const float ShieldMinSeconds = 25f;
	public const float ShieldMaxSeconds = 60f;

	/// <summary>
	/// Chance is high enough to matter, but a shield used to be INVISIBLE: its only effects
	/// were jamming drones you may not own yet and a line of HUD text. It now renders as a
	/// shell around the shape so it is obviously a thing that is happening to you.
	/// </summary>
	public const float ShieldShellPadding = 90f;

	/// <summary>
	/// The shield's weak point: a node hanging above the shape. Shooting it drops the shield for
	/// <see cref="ShieldBreakSeconds"/>, then it comes back.
	///
	/// **A drone cannot break it.** That is the whole design: the shield exists to stop idling
	/// through a stage, so if the drones could clear their own jam it would undo itself. Only
	/// your shots count, which turns a shielded stage from a wait into a repeating skill beat.
	/// </summary>
	public const float ShieldBreakSeconds = 2f;

	/// <summary>How far above the top of the shape the node hangs.</summary>
	public const float ShieldNodeHeight = 300f;

	/// <summary>Body radius, and the sphere a shot must cross to strike it.</summary>
	public const float ShieldNodeRadius = 115f;


	// ---------------------------------------------------------------- bunny hopping
	//
	// Devil Daggers does NOT gain speed from strafing (see GOALS 6d):
	//
	//   "In Devil Daggers, the speed comes solely from bhopping. Hitting jump at the correct
	//    intervals is what makes you speed up, and strafing does not affect your speed in any way."
	//
	// Our movement was a faithful Quake implementation, which is a different game. Quake rewards
	// STEERING; the reference rewards TIMING. Timing is the better fit here because it is a skill
	// you can visibly get better at without having to learn air-strafe theory first.

	/// <summary>
	/// Seconds before landing in which a jump press counts as a PERFECT hop. Generous on
	/// purpose: this should be learnable in a minute, not a fortnight.
	/// </summary>
	public const float PerfectHopWindow = 0.14f;

	/// <summary>Speed added by a perfect hop, in units per second.</summary>
	public const float PerfectHopBoost = 105f;

	/// <summary>Speed added by a mistimed hop. Nonzero so hopping always beats not hopping.</summary>
	public const float SloppyHopBoost = 22f;

	/// <summary>
	/// Ceiling on hop-chain speed as a multiple of ground speed. The reference is effectively
	/// uncapped, but our arena has walls and our hazards are tuned against a known top speed, so
	/// an unbounded chain would break the Spotter's tracking budget. **[TUNE]**
	/// </summary>
	public const float HopSpeedMax = 2.6f;

	/// <summary>Extra field of view at full hop speed. The cheapest speed cue in games.</summary>
	public const float HopFovBoost = 16f;

	/// <summary>Base field of view. Kept here so the hop boost has something to add to.</summary>
	public const float BaseFieldOfView = 75f;

	// ---------------------------------------------------------------- arena

	/// <summary>
	/// Minimum half-width of the walled arena, measured from the shape centre.
	///
	/// This used to be the ONLY value, fixed at 2600. The shared Monolith is 4096 units per axis,
	/// so its barriers orbited at ~3900 and sat entirely outside the walls: the player could
	/// never reach them and they blocked nothing. The arena now scales with the shape (see
	/// <see cref="ArenaShapeMultiple"/>) and this is just the floor for small stages.
	/// </summary>
	public const float ArenaMinHalfExtent = 2600f;

	/// <summary>
	/// Arena half-width as a multiple of the shape's bounding diagonal. Has to leave room for the
	/// barrier orbit (0.55 of the diagonal) plus somewhere to stand outside it.
	/// </summary>
	public const float ArenaShapeMultiple = 0.85f;

	/// <summary>Fraction of the arena a barrier orbit may use. Keeps them inside the walls.</summary>
	public const float BarrierMaxArenaFraction = 0.8f;

	/// <summary>Height of the containing walls.</summary>
	public const float ArenaWallHeight = 900f;

	// ---------------------------------------------------------------- demolition charges

	/// <summary>
	/// Right click plants a charge, left click sets it off. The cooldown is deliberately
	/// FIXED regardless of level: upgrades buy blast size, never frequency, so the rhythm of
	/// the ability stays the same from level 1 to level 500.
	/// </summary>
	public const float DemolitionCooldown = 10f;

	/// <summary>
	/// The Demolition upgrade buys blast SIZE and nothing else. Level 0 is deliberately modest
	/// now: at radius 9 the very first charge removed ~3,000 cubes, which is most of an early
	/// stage in one click and made the whole opening curve irrelevant.
	/// </summary>
	// THE CHARGE IS SIZED AS A FRACTION OF THE STAGE, NOT AS A FIXED RADIUS.
	//
	// A fixed radius cannot stay impactful across a ladder whose shapes run from 500 cubes to
	// two million: the same sphere is the whole of stage 1 and a pinprick at stage 60, so the
	// ability decays into irrelevance exactly as the stages get long enough to need it. Sizing
	// it against the stage means one charge always means the same THING.
	//
	// The curve is the standard asymptotic one used for VolatileChance, for the same reason:
	// increments shrink forever and the ceiling is never reached, so the upgrade can never show
	// "MAX" (GOALS rule 3b). Steps land close to the requested shape: 10% at level 0, about 20%
	// by level 10, about 33% by level 30, and creeping toward 80% without arriving.

	/// <summary>Fraction of the stage a charge removes at Demolition level 0.</summary>
	public const float DemolitionFractionBase = 0.10f;

	/// <summary>
	/// Ceiling the fraction approaches and never reaches.
	///
	/// **25%, not 80%.** The charge is a REPEATABLE ability on a ten second cooldown, so the
	/// per-shot figure has to be read as a rate rather than a one-off: at 80% two clicks end a
	/// stage, and with the Cadence node driving the cooldown toward its three second floor the
	/// ladder becomes a sequence of right clicks with mining as decoration. At 25% it takes
	/// four perfect charges, which is a strong tool that still needs the rest of the game.
	/// </summary>
	public const float DemolitionFractionMax = 0.25f;

	/// <summary>
	/// Curve rate. Chosen so level 10 lands near 18% and the first upgrade is worth about 1.5
	/// points: solving `1 - 1/(1 + 10r) = (0.18 - 0.10) / (0.25 - 0.10)` gives r = 0.114.
	/// Increments shrink from there and never quite arrive. **[TUNE]**
	/// </summary>
	public const float DemolitionFractionRate = 0.114f;

	/// <summary>
	/// Hard ceiling on the fraction when mining the SHARED MONOLITH.
	///
	/// The fraction model is right for the ladder and wrong for the Monolith. A stage is
	/// something you clear in a minute, so "a tenth of it per charge" is a good tool. The
	/// Monolith is 16.7M cubes and is supposed to take a lobby days: the same tenth would be
	/// 1.67M cubes per click and would reduce the shared destination to about ten button
	/// presses.
	///
	/// One percent is 167k cubes, which still reads as an enormous crater and still matters,
	/// while leaving the Monolith something you chip down together rather than delete. **[TUNE]**
	/// </summary>
	public const float DemolitionMonolithFractionMax = 0.01f;

	/// <summary>
	/// Fraction of the shape's SHORTEST axis that counts as fully buried, capped by
	/// <see cref="DemolitionIdealDepth"/>.
	///
	/// A flat depth cannot work across a ladder whose shapes run from 8 voxels to 160: at 26 the
	/// early stages could never reach it at all. See `Miner.EffectiveIdealDepth`.
	/// </summary>
	public const float DemolitionDepthFraction = 0.4f;

	/// <summary>
	/// How far a charge flies before it stops and hangs, if it never meets rock.
	///
	/// It used to fizzle at `MineRayLength` (20,000 units), which punished a near miss twice:
	/// once for missing and again with the full cooldown for nothing. It now waits to be set off
	/// wherever it stopped.
	/// </summary>
	public const float ChargeMaxFlight = 2200f;

	/// <summary>Voxels per second the charge bores into the shape once fired.</summary>
	public const float DemolitionDrillSpeed = 22f;

	/// <summary>Seconds the charge keeps boring before it fizzles and is wasted.</summary>
	public const float DemolitionMaxDrillTime = 2.2f;

	/// <summary>Seconds between the tunnel-carving steps that make the bore visible.</summary>
	public const float DemolitionCarveInterval = 0.12f;

	/// <summary>Radius of the pilot hole the charge bores on its way in.</summary>
	public const float DemolitionBoreRadius = 1.4f;

	/// <summary>
	/// Depth, in voxels, at which a detonation is fully buried. Blowing up on the surface
	/// wastes most of the sphere on empty air; burying it first is the skill in the ability.
	/// </summary>
	public const float DemolitionIdealDepth = 26f;

	/// <summary>Extra blast radius multiplier at ideal depth, tapering to 1.0 at the surface.</summary>
	public const float DemolitionDepthBonus = 0.6f;

	// ---------------------------------------------------------------- active reload

	/// <summary>
	/// The reload attempt is a SHORT bar that runs immediately after firing, not the tail of
	/// the long cooldown. Two seconds, with a randomly placed target: catching it hands the
	/// charge straight back, missing it drops you onto the full <see cref="DemolitionCooldown"/>.
	///
	/// Short and random is the whole point. It has to demand attention at exactly the moment
	/// you are also watching a charge bore into the rock, so the two compete.
	/// </summary>
	public const float ActiveReloadDuration = 2.0f;

	/// <summary>Width of the target as a fraction of the bar.</summary>
	public const float ActiveReloadWindowWidth = 0.13f;

	/// <summary>Earliest and latest the target can be placed, as fractions of the bar.</summary>
	public const float ActiveReloadMinStart = 0.18f;
	public const float ActiveReloadMaxStart = 0.80f;

	// ---------------------------------------------------------------- economy

	/// <summary>
	/// Fallback growth. Each upgrade overrides this via <see cref="Upgrades"/>, because an
	/// upgrade whose value scales cubically cannot share a cost curve with one that scales
	/// linearly without the economy running away.
	/// </summary>
	public const double UpgradeCostGrowth = 1.30;

	/// <summary>Cost growth per upgrade. Radius upgrades are cubic in value, hence 1.85.</summary>
	public const double GrowthDrillSpeed = 1.36;
	public const double GrowthBlastRadius = 1.85;
	public const double GrowthChargeChance = 1.28;
	public const double GrowthChargeRadius = 1.85;
	public const double GrowthDustYield = 1.32;
	public const double GrowthDemolition = 1.55;

	/// <summary>
	/// Drones and Drone Cadence are deliberately a TRADE, not a ladder.
	///
	/// Total drone output is count x rate, so both scale output linearly and the interesting
	/// question is which to buy next. Drones cost more per level and grow faster, because each
	/// one is another object in the arch and another thing on screen; Cadence is the cheap way
	/// to get more out of what you already have. The result is that a wide arch of slow drones
	/// and a narrow arch of fast ones both work, and the crossover moves as you buy. **[TUNE]**
	/// </summary>
	public const double GrowthDrones = 1.46;
	public const double GrowthDroneRate = 1.30;

	public const double CostDrillSpeed = 30;
	public const double CostBlastRadius = 60;
	public const double CostChargeChance = 500;
	public const double CostChargeRadius = 900;
	public const double CostDustYield = 250;
	public const double CostDrones = 1600;
	public const double CostDroneRate = 700;
	public const double CostDemolition = 800;

	// ---------------------------------------------------------------- prestige

	/// <summary>
	/// The solo ladder runs to this stage. Reaching it unlocks Collapse, which resets you to
	/// stage 1 with permanent multipliers so the numbers can keep climbing.
	/// </summary>
	public const int PrestigeStageRequirement = 100;

	/// <summary>Stages cleared per Core awarded on Collapse. 100 stages = 10 Cores = +100%.</summary>
	public const int StagesPerCore = 10;

	/// <summary>
	/// Baseline value of a Core you have EARNED, whether or not you have spent it. Kept small
	/// on purpose: the interesting power is in the Core Tree, and a large passive multiplier
	/// here would make spending Cores feel like a downgrade.
	/// </summary>
	public const float CorePowerPerCore = 0.04f;

	// ---------------------------------------------------------------- marks

	/// <summary>
	/// Global multiplier per Mark earned. Small individually: the list is long, and the point
	/// is a steady background hum of progress, not a cliff.
	/// </summary>
	public const float MarkPowerEach = 0.03f;

	// ---------------------------------------------------------------- resonance

	/// <summary>
	/// Seconds a Resonance stack survives without a new trigger. Short: this is the mechanic
	/// that distinguishes someone actively playing from someone letting drones tick over, so
	/// it has to lapse the moment attention does.
	/// </summary>
	public const float ResonanceWindow = 3.2f;

	/// <summary>Dust multiplier added per stack. At 20 stacks that is roughly 5x.</summary>
	public const float ResonancePerStack = 0.2f;

	/// <summary>Stacks granted by catching an active reload. The skill shot is worth more.</summary>
	public const int ResonanceReloadBonus = 3;

	/// <summary>No ceiling would let a single lucky chain break the economy.</summary>
	public const int ResonanceMaxStacks = 40;

	// ---------------------------------------------------------------- slag

	/// <summary>
	/// Real-world minutes per Slag. This is the only currency that accrues on a wall clock,
	/// including while the game is shut, which is what makes opening it on a day you do not
	/// intend to grind still worth doing.
	/// </summary>
	public const double SlagMinutesEach = 90;

	/// <summary>
	/// Cap on stored Slag. Low enough that it is worth spending rather than hoarding, high
	/// enough that a couple of days away is not punished.
	/// </summary>
	public const int SlagMax = 8;

	/// <summary>Slag cost to shatter half of what is left of the current shape.</summary>
	public const int SlagCostFracture = 2;

	/// <summary>Slag cost to reroll the current stage into a different shape.</summary>
	public const int SlagCostReroll = 1;

	/// <summary>Fraction of remaining cubes a Fracture destroys.</summary>
	public const float FractureFraction = 0.5f;

	// ---------------------------------------------------------------- leeches

	/// <summary>
	/// Fraction of your dust income a single leech siphons away while attached.
	///
	/// Note this costs you INCOME, never cubes. Cookie Clicker's wrinklers eat your cookies,
	/// but "the block count drops when I am not clicking" is a rule the user set explicitly,
	/// and a leech that ate the shape would break it. Siphoning income preserves the actual
	/// mechanic (a parasite you deliberately fatten before popping) without touching the count.
	/// </summary>
	public const float LeechSiphonEach = 0.09f;

	/// <summary>Ceiling on total siphon, so a swarm can never zero your income.</summary>
	public const float LeechSiphonMax = 0.55f;

	/// <summary>Multiplier on everything a leech siphoned, paid out when you pop it.</summary>
	public const float LeechPayoutBonus = 2.4f;

	public const int LeechHealth = 4;
	public const float LeechSpawnInterval = 22f;
	public const int LeechMaxPopulation = 4;

	/// <summary>Stage at which leeches begin appearing.</summary>
	public const int LeechFirstStage = 8;

	// ---------------------------------------------------------------- hollow runs

	/// <summary>Bonus Cores for completing the ladder under a restriction.</summary>
	public const int HollowRunCoreReward = 5;

	// ---------------------------------------------------------------- core tree

	public const float CoreDeepeningPerLevel = 0.15f;
	public const float CoreCadencePerLevel = 0.12f;
	public const float CoreCadenceCooldownPerLevel = 0.6f;
	public const float CoreAvaricePerLevel = 0.25f;
	public const int CoreAnchoredPerLevel = 3;
	public const int CoreForesightPerLevel = 4;
	public const float CoreSympathyPerLevel = 0.02f;

	/// <summary>Charges never become certain, so the cooldown floor keeps Cadence bounded.</summary>
	public const float DemolitionCooldownFloor = 3.0f;

	/// <summary>
	/// Minimum share of a shared Monolith you must personally remove to earn prestige credit
	/// when it falls. Stops anyone idling in the lobby from collecting the same reward as the
	/// people who actually did the work.
	/// </summary>
	public const double MonolithCreditShare = 0.01;

	/// <summary>
	/// Prestige levels awarded for felling a shared Monolith at the minimum 1% share.
	///
	/// A Monolith is 16.7M cubes and takes a lobby days or weeks. Paying the same single level
	/// as one twenty minute solo ladder made it strictly the worst way to earn prestige, which
	/// is the opposite of what a shared destination is for.
	/// </summary>
	public const int MonolithPrestigeBase = 5;

	/// <summary>
	/// Extra prestige levels at a 100% share, scaled linearly by your actual contribution.
	/// Someone who personally removed a third of it gets meaningfully more than someone who
	/// scraped the 1% floor, without the floor player feeling cheated. **[TUNE]**
	/// </summary>
	public const int MonolithPrestigeShareBonus = 15;

	// ---------------------------------------------------------------- the retro look
	//
	// Devil Daggers renders at 320x240 with unfiltered textures, no anti-aliasing, and low
	// colour depth that is dithered (GOALS 6d). These approximate that with the post-process
	// components s&box already ships, which is the only honest way to get there without writing
	// and debugging HLSL against a compile loop that freezes every twenty minutes.

	// THE CORRECTION THAT MATTERS: Devil Daggers is HIGH CONTRAST, not LOW BRIGHTNESS.
	//
	// The first attempt at these values conflated the two and the result was unplayable: the
	// shape was a brown smear, hazards in the periphery vanished, and the whole frame read as
	// mud. The reference's black is genuinely black, but everything IN it is bright, and the
	// readability comes from that gap. Darkening the lit half destroys the very thing being
	// copied. Every value below moved in the direction of "lift the lit parts, keep the void
	// black" rather than "turn the lights down".

	/// <summary>
	/// <c>Pixelate.Scale</c>.
	///
	/// **I had this backwards.** I read it as a resolution fraction, where higher means sharper,
	/// and "raised" it 0.45 to 0.62 to 0.78 across three passes trying to add fidelity. It is
	/// the pixelation AMOUNT: higher means chunkier. Every "fix" made it worse, which is exactly
	/// what the last screenshot showed.
	///
	/// 0.15 is a light dusting: enough that edges stair-step and the image is not clean modern
	/// 3D, nowhere near enough to hide a 16 unit cube. **[TUNE, and note the direction: DOWN is
	/// sharper.]**
	/// </summary>
	public const float RetroPixelScale = 0.15f;

	/// <summary>
	/// Low colour depth reads as slightly reduced saturation. Contrast is kept only just above
	/// neutral: pushing it crushes an already dark scene into pure black, which is what happened
	/// at 1.16. Brightness is now ABOVE 1, lifting the lit half without touching the void.
	/// </summary>
	public const float RetroSaturation = 0.90f;
	public const float RetroContrast = 1.04f;
	public const float RetroBrightness = 1.12f;

	/// <summary>
	/// Standing in for dithering. Low: grain over dark areas is just mud, and at 0.16 it was
	/// actively hiding the shape rather than texturing it.
	/// </summary>
	public const float RetroGrain = 0.06f;

	/// <summary>
	/// Barely there. At 0.55 this was the single worst offender: it darkened the PERIPHERY,
	/// which is exactly where the Spotters, Sentinels and Anchors live. A vignette that hides
	/// the threats is a vignette working against the game.
	/// </summary>
	public const float RetroVignette = 0.14f;

	/// <summary>
	/// The lamp on the player rig. This is the readability fix, not the colour pass: it lights
	/// what you are working on and lets the distance fall to black on its own, which is the
	/// reference's actual model. Radius is generous so a whole stage is legible from the floor.
	/// **[TUNE]**
	/// </summary>
	public const float PlayerLampBrightness = 9f;
	public const float PlayerLampRadius = 2600f;

	// ---------------------------------------------------------------- rendering

	/// <summary>
	/// Milliseconds per frame spent remeshing dirty chunks.
	///
	/// This replaced a flat "2 chunks per frame", which was the cause of large stages loading in
	/// visibly chunky slabs and of black holes appearing in the shape mid-fight. A big blast
	/// dirties dozens of chunks at once, so a fixed count of two could never catch up and the
	/// backlog just grew: what you saw was not slow loading, it was a queue that never drained.
	///
	/// A time budget adapts instead. Small chunks rebuild many per frame, expensive ones fewer,
	/// and the frame cost stays roughly constant either way. **[TUNE]**
	/// </summary>
	public const float ChunkRebuildMillisecondsPerFrame = 6f;

	/// <summary>
	/// Extra time allowed while a large backlog is outstanding, which is what an initial load is.
	/// Briefly spending a third of the frame budget to get the shape on screen is worth it.
	/// </summary>
	public const float ChunkRebuildCatchUpMilliseconds = 13f;


	/// <summary>Backlog above which catch-up applies.</summary>
	public const int ChunkRebuildCatchUpThreshold = 24;

	/// <summary>Hard ceiling per frame, so one cheap frame cannot run away.</summary>
	public const int ChunkRebuildMaxPerFrame = 96;

	/// <summary>How far the mining ray will travel, in world units.</summary>
	public const float MineRayLength = 20_000f;

	// ---------------------------------------------------------------- the rocket jump

	/// <summary>
	/// Impulse applied by a charge detonating at your feet, before falloff.
	///
	/// Against a <c>JumpPower</c> of 480 this is roughly a double-height launch at point blank,
	/// which is what makes it worth a whole charge. **[TUNE]**
	/// </summary>
	public const float RocketJumpForce = 900f;

	/// <summary>
	/// Distance at which the launch has fallen to nothing. Choosing where to put the charge is
	/// how you choose your power, so this is the width of that dial. **[TUNE]**
	/// </summary>
	public const float RocketJumpRadius = 700f;

	/// <summary>
	/// Share of a full demolition blast that a floor hit still removes.
	///
	/// The rocket jump has to keep real damage or it becomes a tax you pay to travel, and it must
	/// not keep ALL of it or the timed detonation has no reason to exist. At 0.55 a floor shot
	/// beside the shape is a genuine excavation plus a launch, while a patient buried charge is
	/// still comfortably the better way to move rock.
	///
	/// Applied to the CUBE COUNT, not the radius. Radius is the cube root of volume, so 0.55 of
	/// the cubes is about 0.82 of the radius: the sphere looks nearly as big and removes a bit
	/// over half as much, which is the right way round for something that should still feel
	/// powerful. **[TUNE]**
	/// </summary>
	public const float RocketJumpBlastFraction = 0.55f;

	/// <summary>
	/// How much upward bias is mixed into the blast direction.
	///
	/// Without it, a charge landing ahead of you pushes almost horizontally and slides you along
	/// the ground rather than launching you. The bias is what makes every rocket jump a jump
	/// regardless of the geometry you happened to fire at. **[TUNE]**
	/// </summary>
	public const float RocketJumpUpBias = 0.85f;

	// ---------------------------------------------------------------- shrapnel
	//
	// See Shrapnel. A volatile detonation throws chunks of the monolith outward, and they take the
	// stage if one lands on you. Everything below exists to keep it DODGEABLE: slow, arcing,
	// bright, and never numerous.

	/// <summary>
	/// One piece per this many world units of blast radius. Higher means fewer pieces. **[TUNE]**
	/// </summary>
	public const float ShrapnelPerRadius = 18f;

	/// <summary>Ceiling per detonation, so a huge Instability radius cannot blanket the arena.</summary>
	public const int ShrapnelMaxPerBurst = 12;

	/// <summary>Ceiling across the whole scene.</summary>
	public const int ShrapnelMaxLive = 60;

	/// <summary>
	/// Half-angle of the cone the landing points are thrown into, around the outward direction.
	///
	/// Wide enough that a burst covers an arc of the arena rather than a single lane you sidestep
	/// once and forget, narrow enough that the pattern still reads as coming FROM the detonation
	/// rather than raining everywhere. **[TUNE]**
	/// </summary>
	public const float ShrapnelSpreadDegrees = 55f;

	/// <summary>
	/// Seconds from launch to impact.
	///
	/// The flight is a DURATION rather than a speed, which is what lets the arc be solved exactly
	/// so a piece lands where its ring says it will. It is also the whole reaction budget: long
	/// enough to see the ring, decide, and walk out, short enough that ignoring it is not an
	/// option. **[TUNE]**
	/// </summary>
	public const float ShrapnelFlightTime = 1.7f;

	/// <summary>
	/// How far around the player the landing points are scattered.
	///
	/// Wide enough that a burst covers ground rather than stacking on one spot, tight enough that
	/// the whole pattern is a place you have to leave. **[TUNE]**
	/// </summary>
	public const float ShrapnelScatter = 380f;

	/// <summary>
	/// How fast a landing point walks toward the player, in units per second.
	///
	/// **Deliberately far below a walk.** The player moves at 420, so stepping out of a ring
	/// always works and standing in one never does. That asymmetry is the mechanic: it is not
	/// trying to hit you, it is trying to make you leave, and what makes leaving interesting is
	/// the floor tile and the crawler you have to leave TOWARD. Raise it and shrapnel becomes
	/// unfair; drop it to 0 and it stops being a threat at all. **[TUNE]**
	/// </summary>
	public const float ShrapnelDriftSpeed = 120f;

	/// <summary>Lethal radius of the landing blast, and the size of the ring drawn for it.</summary>
	public const float ShrapnelImpactRadius = 110f;

	/// <summary>
	/// How far above the floor the landing blast still catches you.
	///
	/// A full jump peaks about 82 units up, so this deliberately exceeds it: **hopping over an
	/// impact must not work.** The lethal floor already pays you for being airborne, and if the
	/// air were safe from this too there would be one answer to everything. Being in the air
	/// should dodge the FLOOR and expose you to the SKY. **[TUNE]**
	/// </summary>
	public const float ShrapnelImpactHeight = 140f;

	/// <summary>
	/// Gravity on a piece. Only shapes how high the arc goes now that arrival time is fixed, so
	/// this is a readability dial rather than a range one: higher means a flatter, faster-looking
	/// throw. **[TUNE]**
	/// </summary>
	public const float ShrapnelGravity = 900f;

	/// <summary>
	/// Delay before a piece can hurt you. A detonation triggered at close range would otherwise
	/// kill you on the frame it spawned, punishing you for landing your best shot. **[TUNE]**
	/// </summary>
	public const float ShrapnelArmSeconds = 0.35f;

	/// <summary>How close a piece has to get. Generous to the player rather than to the rock.</summary>
	public const float ShrapnelHitRadius = 44f;

	/// <summary>
	/// Shared cooldown across every piece, exactly like the Crawler grab. One detonation throws
	/// ten chunks and a single bad moment must be charged once, not ten times.
	/// </summary>
	public const float ShrapnelHitCooldown = 3f;

	// ---------------------------------------------------------------- the lethal floor
	//
	// See FloorHazard. Half the arena floor turns deadly on a cycle, which is what turns the
	// movement techniques from optional speed into the thing keeping you alive.

	/// <summary>
	/// Tile edge length, and the most load-bearing number in the system. Derived from how far a
	/// jump actually carries you rather than picked for looks.
	///
	/// A full jump hangs for <c>2 * JumpPower / Gravity</c>, which is 2 * 480 / 1400 = **0.69
	/// seconds**. Multiply by the speed you carry into it:
	///
	/// - walking, 420 units/s: **288 units**, so a jump clears one tile and very little more
	/// - sprinting, 609: **418 units**, comfortably one, sometimes two
	/// - a maintained hop chain, up to 1092: **749 units**, nearly three
	///
	/// At 260 the floor is therefore always escapable with the most basic jump in the game, and
	/// the speed techniques are what let you choose WHERE you land rather than merely surviving.
	/// Raise this and hops become mandatory; lower it and the floor stops mattering. **[TUNE]**
	/// </summary>
	public const float FloorTileSize = 260f;

	/// <summary>
	/// How long the floor holds every tile safe after a stage is built or reset.
	///
	/// The player does not choose where a stage puts them, so the moment of arrival cannot be
	/// lethal. Long enough to land, read the arena and pick a direction. **[TUNE]**
	/// </summary>
	public const float FloorStageGraceSeconds = 3f;

	/// <summary>
	/// How far the permanently lethal tiles stand proud of the floor.
	///
	/// **Colour alone was not carrying this.** The retro pass renders at 15% resolution through
	/// bloom and film grain, under an orange player lamp, so a tile is a handful of muddy pixels
	/// and violet against ember reads as "another dark warm square". Raising them into low slabs
	/// gives them a silhouette, visible sides and a different response to the lamp, and a
	/// silhouette survives pixelation in a way that a hue never will. Purely visual: nothing
	/// collides with them. **[TUNE]**
	/// </summary>
	public const float FloorDeadHeight = 22f;

	/// <summary>How often a fresh half of the floor is rolled. **[TUNE]**</summary>
	public const float FloorPhaseSeconds = 3.5f;

	/// <summary>
	/// Warning before the hot tiles kill. This is reaction budget, and the difference between a
	/// movement puzzle and a reflex test.
	///
	/// **Raised 1.0 to 1.75 after the first playtest.** One second is enough time to move if you
	/// were already watching the floor, and not enough if you were doing the thing the game is
	/// actually about, which is aiming at rock. The lethal window shrinks by the same amount
	/// (phase 3.5 minus warn 1.75 leaves 1.75 seconds of danger), so the floor is no less deadly,
	/// it just stops punishing you for looking at the monolith. **[TUNE]**
	/// </summary>
	public const float FloorWarnSeconds = 1.75f;

	/// <summary>
	/// How long you can stand on a live tile before it takes the stage.
	///
	/// **Contact used to be instant death**, which made the floor read as arbitrary rather than
	/// dangerous: a tile turning red under a foot that was already mid-stride killed you for a
	/// decision made half a second earlier. A short dwell means brushing one is a mistake you can
	/// correct and camping one is not. **[TUNE]**
	/// </summary>
	public const float FloorBurnSeconds = 0.75f;

	/// <summary>
	/// How fast the burn meter empties once you are clear, as a multiple of how fast it fills.
	///
	/// Below 1 on purpose. If it emptied instantly, hopping on the spot on a red tile would be
	/// survivable forever, because airborne is already safe. Recovering slower than you burn
	/// makes the answer actually going somewhere. **[TUNE]**
	/// </summary>
	public const float FloorBurnRecoverRate = 0.6f;

	/// <summary>
	/// Fraction of the floor that goes hot each phase.
	///
	/// **Cut 0.5 to 0.32.** Half the floor going lethal sounds like a coin flip and does not play
	/// like one: with a random split, a 50% floor leaves safe tiles isolated, so almost every
	/// phase became a forced jump rather than a route you chose. At about a third the safe tiles
	/// connect into paths, which turns the question from "can I survive this" into "which way do
	/// I go", and that second question is the one that is actually fun. **[TUNE]**
	/// </summary>
	public const float FloorHotFraction = 0.32f;

	/// <summary>
	/// Fraction of tiles that are permanently lethal.
	///
	/// **Cut 0.07 to 0.03.** At 7% a twenty by twenty grid carried about 25 instant-death squares,
	/// which is not a hazard scattered through the arena, it is a minefield: enough that they
	/// stopped reading as landmarks to route around and started reading as random. Three percent
	/// is roughly ten, which is few enough to remember where they are. **[TUNE]**
	/// </summary>
	public const float FloorDeadChance = 0.012f;

	/// <summary>
	/// Radius around the arena centre kept clear of permanent tiles, so a stage cannot drop you
	/// onto one before you have taken a step.
	/// </summary>
	public const float FloorDeadSafeRadius = 900f;

	/// <summary>
	/// Stage at which the floor arms, counting the way the player counts: 1 is the first stage.
	///
	/// **This was compared against the 0-based `Tier` and so armed a stage late.** Three whole
	/// stages went by with no floor, which from the outside is indistinguishable from the whole
	/// system being broken. The conversion now happens in MonolithManager and this really does
	/// mean what it says. Set to 1 to have it live immediately. **[TUNE]**
	/// </summary>
	public const int FloorHazardFromStage = 2;

	// ---------------------------------------------------------------- music
	//
	// See MonolithMusic. Three layers play permanently and only their volumes move, so these are
	// ceilings rather than switches: the bed is what you always hear, and the other two are how
	// far the score is allowed to rise when you are working or being hunted. **[TUNE]**

	/// <summary>
	/// Bass, kick and hat: the groove that is always playing.
	///
	/// The three ceilings deliberately sum to 1.0. Each layer is normalised to a 0.85 peak when
	/// it is generated, so a full mix lands at 0.85 and cannot clip no matter what the run is
	/// doing. Raise one of these and lower another. **[TUNE]**
	/// </summary>
	public const float MusicBedVolume = 0.40f;

	/// <summary>The arpeggio and backbeat, at a fully cleared stage. **[TUNE]**</summary>
	public const float MusicWorkVolume = 0.30f;

	/// <summary>The lead melody, at full hazard pressure. **[TUNE]**</summary>
	public const float MusicThreatVolume = 0.30f;

	/// <summary>
	/// Weighted hazard count at which the threat layer is at full volume. Roughly two spotters
	/// and a sentinel. Set so an ordinary stage sits well under it and a bad one does not. **[TUNE]**
	/// </summary>
	public const float MusicThreatFullAt = 4f;

	/// <summary>
	/// How quickly layers chase their target volume, per second. Slow enough that killing one
	/// hazard does not audibly drop the music, fast enough that the pause duck feels immediate.
	/// **[TUNE]**
	/// </summary>
	public const float MusicFadeRate = 1.6f;
}