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):

Levelraw = 2.0^(lvl-1)
11.0×
22.0×
34.0×
516.0×
10512.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.

DimensionperLevelcapValuecapSteepness
turretFireRate1.253.00.30
moveSpeed1.102.00.25
limbSweepSpeed1.152.50.20
spawnerFrequency1.203.00.25
effectRadius1.102.50.20
enrageTimer0.880.250.30

Example (turretFireRate, perLevel 1.25, cap 3.0, k 0.30):

LevelrawrawGrowthapproachresult
11.0000.0000.0001.000×
21.2500.2500.0721.144×
31.5630.5630.1551.310×
52.4411.4410.3511.702×
107.4516.4510.8562.713×
2086.7485.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):

Levelraw = 1.12^(lvl-1)result
11.0001.000×
31.2541.254×
51.5741.574×
71.9741.974×
82.2112.000× (clamped)
154.8872.000× (clamped)

Picking the right cap type

QuestionAnswerCap type
Can this stat literally double forever without breaking anything?yesuncapped
Does the stat have a soft “feels bad past X” threshold but no technical ceiling?yessoft_cap
Is there a hard technical ceiling (collision, rendering, perf) above which the game breaks?yeshard_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 * rawGrowthapproachresult (fraction of capRange used)
0.5~0.3939% of the way to the cap
1.0~0.6363%
2.0~0.8686%
3.0~0.9595%
5.0~0.99399.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 full ResolvedBossScaling bundle (one multiplier per dimension) for a given level.