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