Soft-Cap Formula
Boss scaling is multi-dimensional: each stat (HP, damage, fire rate, move speed, etc.) gets its own per-level growth curve and its own cap behavior. The cap type decides what happens once the raw exponential growth starts producing values that would be unfun — either it keeps going, asymptotes toward a ceiling, or slams into a wall. All three behaviors are computed by a single function (computeDimension in src/starship-survivors/data/boss-scaling.ts).
The shared starting point
Every dimension starts from the same raw multiplier:
raw = perLevel ^ (level - 1)
level is the 1-based current game level; level 1 always returns 1 (no scaling). perLevel is the multiplicative growth per level — 1.5 means +50% per level, 0.88 means ×0.88 per level (used by the enrage timer, which gets shorter over time).
After computing raw, the cap type decides how to project it onto the final multiplier.
Cap type 1: uncapped
result = raw
Pure exponential — no ceiling, no smoothing. Used when you want the stat to genuinely run away forever, because the rest of the game (player damage scaling, build power) is expected to keep pace.
Used for: hp (perLevel 2.0), damage (perLevel 2.15).
Example (hp, perLevel 2.0):
| Level | raw = 2.0^(lvl-1) |
|---|---|
| 1 | 1.0× |
| 2 | 2.0× |
| 3 | 4.0× |
| 5 | 16.0× |
| 10 | 512.0× |
Cap type 2: soft_cap
rawGrowth = raw - 1
capRange = capValue - 1
approach = 1 - exp(-capSteepness * rawGrowth)
result = 1 + capRange * approach
The raw multiplier is converted into “growth above 1,” then run through an exponential-decay approach toward the asymptote. The result starts at 1 (when raw = 1), climbs steeply at first, and asymptotically approaches capValue without ever reaching it. If capValue <= 1 the function short-circuits to 1 — a degenerate config that disables the dimension.
capSteepness (called k in the formula) controls how fast the curve approaches the asymptote. Typical range is 0.15–0.5. Higher k means the soft cap bites harder and sooner; lower k means the curve hugs the raw exponential for more levels before bending over.
Used for: stats where uncapped exponential growth would break the game feel (a turret that fires 60×/sec, a boss that out-runs the player) but a brick-wall cap would feel artificial.
| Dimension | perLevel | capValue | capSteepness |
|---|---|---|---|
turretFireRate | 1.25 | 3.0 | 0.30 |
moveSpeed | 1.10 | 2.0 | 0.25 |
limbSweepSpeed | 1.15 | 2.5 | 0.20 |
spawnerFrequency | 1.20 | 3.0 | 0.25 |
effectRadius | 1.10 | 2.5 | 0.20 |
enrageTimer | 0.88 | 0.25 | 0.30 |
Example (turretFireRate, perLevel 1.25, cap 3.0, k 0.30):
| Level | raw | rawGrowth | approach | result |
|---|---|---|---|---|
| 1 | 1.000 | 0.000 | 0.000 | 1.000× |
| 2 | 1.250 | 0.250 | 0.072 | 1.144× |
| 3 | 1.563 | 0.563 | 0.155 | 1.310× |
| 5 | 2.441 | 1.441 | 0.351 | 1.702× |
| 10 | 7.451 | 6.451 | 0.856 | 2.713× |
| 20 | 86.74 | 85.74 | ≈1.000 | ≈3.000× |
Notice how raw blows up to 86× by level 20 but result is held just under the 3.0 ceiling. That’s the soft cap doing its job.
Note on enrageTimer: perLevel is below 1, so raw shrinks with level (0.88, 0.774, 0.681, …). Because rawGrowth = raw - 1 becomes negative, the soft-cap math still works — approach goes negative, and result is pulled toward capValue = 0.25 from above (i.e., the enrage timer asymptotes down to 25% of base, which on a 120s base is 30 seconds).
Cap type 3: hard_cap
result = min(raw, capValue)
A brick wall. The raw exponential is computed and used until it crosses capValue, at which point the result clamps. There’s no smoothing on the approach — the curve goes from “still growing” to “completely flat” in one frame.
Used for: stats where the physics or rendering would break above a hard ceiling. Today: projectileSpeed (perLevel 1.12, capValue 2.0). Projectiles faster than 2× base would tunnel through colliders.
Example (projectileSpeed, perLevel 1.12, cap 2.0):
| Level | raw = 1.12^(lvl-1) | result |
|---|---|---|
| 1 | 1.000 | 1.000× |
| 3 | 1.254 | 1.254× |
| 5 | 1.574 | 1.574× |
| 7 | 1.974 | 1.974× |
| 8 | 2.211 | 2.000× (clamped) |
| 15 | 4.887 | 2.000× (clamped) |
Picking the right cap type
| Question | Answer | Cap type |
|---|---|---|
| Can this stat literally double forever without breaking anything? | yes | uncapped |
| Does the stat have a soft “feels bad past X” threshold but no technical ceiling? | yes | soft_cap |
| Is there a hard technical ceiling (collision, rendering, perf) above which the game breaks? | yes | hard_cap |
If in doubt, prefer soft_cap with a generous capValue and a moderate capSteepness (~0.25). It’s the most forgiving option — the curve still grows monotonically forever, just slower as it approaches the asymptote, so high-level play stays meaningful without producing absurd values.
The k (capSteepness) intuition
For a soft cap, the curve passes through approximately these milestones:
k * rawGrowth | approach | result (fraction of capRange used) |
|---|---|---|
| 0.5 | ~0.39 | 39% of the way to the cap |
| 1.0 | ~0.63 | 63% |
| 2.0 | ~0.86 | 86% |
| 3.0 | ~0.95 | 95% |
| 5.0 | ~0.993 | 99.3% |
So with k = 0.3, the curve is ~63% of the way to its asymptote when rawGrowth ≈ 3.33 (i.e., raw ≈ 4.33). Tuning a soft cap means picking k so that the “63% mark” lands at the level you want the curve to start visibly bending.
Source
src/starship-survivors/data/boss-scaling.ts— formulas (computeDimension), types (CapType,ScalingDimension,BossScalingConfig), default config (DEFAULT_BOSS_SCALING), and base enrage timer (BASE_ENRAGE_TIMER_SEC = 120).resolveBossScaling(config, level)returns the fullResolvedBossScalingbundle (one multiplier per dimension) for a given level.