# Enemy scaling

> How enemy HP, damage, speed, numbers, elites and bosses scale over a tibo.dog run.

Source page: https://tibodog-wiki.vercel.app/enemy-scaling · Wiki updated 5 Nov 2026, 21:46 UTC · Game version 75 (`main-CSuyb4iv.js`, data synced 5 Nov 2026, 21:45 UTC)

Enemies get tougher as the run goes on. **Verified**: these values come from `scaling` in the game config and the scaling function in `sim-BkfBhQfw.js`.

## Formula

These formulas describe Standard with late-game content enabled. Let *a* = min(seconds/60, 20). From 20:00, five-minute phases cycle through HP factors **1, 1.4, 2, 2.8** and damage factors **1, 1.6, 2.56, 4.1**. Every full four-phase cycle multiplies HP by 4 and damage by 6.55. Within a phase, HP additionally multiplies by `1 + 0.05 × minutes into phase`, damage by `1 + 0.04 × minutes into phase`. Before 20:00 both post factors are 1. Easy's old overtime hold does not delay this new curve. [sim-BkfBhQfw.js: `gr=[{hp:1,damage:1},{hp:1.4,damage:1.6},{hp:2,damage:2.56},{hp:2.8,damage:4.1}],_r=1200`] [sim-BkfBhQfw.js: `Sr(e)*(1+.05*xr(e,t))`] [sim-BkfBhQfw.js: `yr(t).damage*Re(6.55,br(e))*(1+.04*xr(e,t))`]

- **Pest HP** ×= `(1 + 0.03·a + 0.009·a²) × post HP × difficulty pestHp`
- **Enemy damage** ×= `(1 + 0.0025·a²) × post damage × early × late × difficulty damage`, where early rises linearly from 0.4 at 0 s to 1 at 900 s, and late = `1 + 0.2 × max(0, a − 15)`
- **Speed**, **count** and **spawn-rate** scaling factors stay at 1. This removes their former overtime growth, not the ordinary spawn-wave schedule. [sim-BkfBhQfw.js: `e.speed=Math.min(r.maxSpeedMult,1),e.count=1,e.rate=1`]

These early and late values apply to Easy, Normal and Hard. Nightmare reaches early ×1 at 405 s, and uses late = `1 + 0.064 × max(0, a − 5)`. [sim-BkfBhQfw.js: `lateDamageFromMin:t.scaling.lateDamageFromMin*n.lateRampTime,lateDamagePerMin:t.scaling.lateDamagePerMin*n.lateRamp`]

 [sim-BkfBhQfw.js: `e.hp=(1+r.hpLinearPerMin*a+r.hpQuadraticPerMin*a*a)*s*t.level.pestHp`] Powers use the game's deterministic power function (`x^y`) imported from `boot-CPW8VVmR.js`.

Charging pests get a 0.6 s windup from 20:00 instead of 0.5 s; their dash itself is unchanged. [sim-BkfBhQfw.js: `return!Cr(e)||e.runClock<_r?t:Math.max(t,.6)`]

### Computed multipliers (Normal)

| Time | Pest HP × | Enemy damage × | Speed × | Count × |
|---|---|---|---|---|
| 0:00 | 0.65 | 0.22 | 1 | 1 |
| 1:00 | 0.68 | 0.24 | 1 | 1 |
| 5:00 | 0.89 | 0.35 | 1 | 1 |
| 10:00 | 1.43 | 0.55 | 1 | 1 |
| 15:00 | 2.26 | 0.86 | 1 | 1 |
| 20:00 | 3.38 | 2.2 | 1 | 1 |
| 21:00 | 3.55 | 2.29 | 1 | 1 |
| 22:00 | 3.72 | 2.38 | 1 | 1 |
| 23:00 | 3.89 | 2.46 | 1 | 1 |
| 25:00 | 4.73 | 3.52 | 1 | 1 |

Computed with the formula above, including Normal's HP and damage multipliers (see [Modes](/modes)).

### Damage scaling by level

These values include each level's damage multiplier and early/late ramp, before curse and armor.

| Time | Easy × | Normal × | Hard × | Nightmare × |
|---|---|---|---|---|
| 0:00 | 0.16 | 0.22 | 0.24 | 0.38 |
| 5:00 | 0.255 | 0.351 | 0.383 | 0.852 |
| 6:45 | 0.299 | 0.41 | 0.448 | 1.177 |
| 10:00 | 0.4 | 0.55 | 0.6 | 1.568 |
| 15:00 | 0.625 | 0.859 | 0.938 | 2.434 |
| 20:00 | 1.6 | 2.2 | 2.4 | 3.724 |

## Enemy bullets

| Setting | Value |
|---|---|
| Bullet count +/min | 0.04 |
| Max bullet count × | 2.5 |
| Fire rate +/min | 0.03 |
| Max fire rate × | 2.2 |
| Bullet speed +/min | 0.015 |
| Max bullet speed × | 1.6 |
| Bullet cap | 1024 |
| Bullet life (s) | 6 |

## Elites

Elites follow a fixed curve (`elites.curve`). HP is the elite's HP at that minute; damage and speed are multipliers.

| Minute | Tier | HP | Damage × | Speed × |
|---|---|---|---|---|
| 1 | 2 | 300 | 1.6 | 0.95 |
| 3 | 2 | 390 | 1.6 | 0.95 |
| 5 | 3 | 750 | 1.6 | 0.95 |
| 8 | 3 | 1300 | 1.6 | 1.05 |
| 10 | 4 | 2400 | 1.6 | 1.1 |
| 15 | 5 | 10000 | 1.6 | 1.2 |
| 17 | 5 | 18000 | 1.6 | 1.25 |
| 20 | 5 | 32000 | 1.6 | 1.3 |

**Verified**: elite pests have pixel-art death animations with a gold rim that breaks away. [changelog-panel-DtW-mCDY.js: `Elite pests fall apart in crisp pixels when they die, and their gold rim breaks off as they go.`]

## Charging pest effects

**Verified**: charging pests kick up dust and bouncing grit when they launch and stop, with a dust trail during the charge. The renderer hides these effects with reduced motion or zero effects opacity. These are visual effects; charge timing, speed and damage are unchanged. [scene-B2rwbRKj.js: `` var eZ=Uint8Array.from(w.enemies,e=>+(e.behavior.kind===`charger`)) ``] [scene-B2rwbRKj.js: `e.still||n.opacity<=0`]

## Bosses

Boss HP is multiplied by ×1 from minute 0, ×3 from minute 16, ×4 from minute 18, ×6 from minute 20 (`bosses.hpSteps`). See the [boss schedule](/bosses#wave-schedule) for which boss arrives when.

## Enemy level and armor

**Verified**: every enemy has a level used against Tibo's armor: `10 + floor(minutes × 1)`, +5 for elites and +10 for bosses. [sim-BkfBhQfw.js: `function Xa(e,t){let n=e.armor;return n.firstLevel+Math.floor(t/60*n.levelsPerMinute)}`] See [Stats](/stats#armor).

## Crowd limits

| Setting | Value |
|---|---|
| Base max enemies (Easy/Normal) | 500 |
| Base hard cap (Easy/Normal) | 550 |
| Hard max / hard cap | 650 / 700 |
| Nightmare max / hard cap | 800 / 850 |
| Max shooters | 30 |
| Opening (s) | 120 |
| Opening density | 0.6 |
| Early ceiling (Normal) | 35 alive at 90 s, 150 alive at 255 s |
| Max XP gems on the ground | 350 |

See [late crowds](/modes#late-crowds) for the 16:00–17:00 Hard/Nightmare ceiling ramp and measured old/new caps.

## Late-run XP drops

**Verified**: in Standard with late-game content enabled, a pest's XP drop is multiplied by the **HP phase step**, without HP's within-phase 5%-per-minute growth. It stays ×1 through just before 25:00, becomes ×1.4 at 25:00, ×2 at 30:00 and ×2.8 at 35:00; the next cycle begins at ×4 at 40:00. The factor uses the time of death, not spawn time, and the biome XP multiplier still applies. Story and a disabled late-game flag do not apply it. [sim-BkfBhQfw.js: `n.xp[t]=n.xp[t]*Sr(e.runClock)`] [sim-BkfBhQfw.js: `return t===0?1:yr(t).hp*Re(4,br(e))`] [sim-BkfBhQfw.js: `n.xp[t]*c.xpMult`]

Measured old/new original death and pickup code, seed `wiki`, a forced pest carrying 7 XP at (137, 41): old drops stayed 7; new drops were 7 at 24:59, 9.8 at 25:00 and 26:13, 14 at 30:00, 19.6 at 35:00 and 28 at 40:00 in the ×1 biome. Story and disabled-feature controls stayed 7. These are isolated drop probes, not evidence of natural level-up pace.

## Busy-fight readability

**Verified**: boss ground warnings now use a separate raised floor layer, above ordinary pools, hazards and shadows. Busy-fight rendering also suppresses more small death puffs, widens damage-number merging, moves merged totals towards the contributing hits, and moves numbers away from Tibo. This changes presentation, not damage amounts. [scene-B2rwbRKj.js: `renderOrder:2.9,depthTest:!0,hairline:!1,lift:.8,arcCull:!0`] [scene-B2rwbRKj.js: `e.x[t]=e.x[t]+(n.x[r]-e.x[t])*s,e.y[t]=e.y[t]+(n.y[r]-e.y[t])*s`] [scene-B2rwbRKj.js: `Math.round(48-40*SY())`]

Isolated original-renderer probes checked quiet, half-pressure and full-pressure states, small/large deaths, number placement at Tibo's bounds and screen edges, and pressure decay/reset. For two merged hits of 30 and 70 at (10, 20) and (30, 70), the displayed total stayed 100; its position changed from (10, 20) to (24, 55) at full pressure. These checks do not measure frame rate, device temperature or battery use.

### Damage numbers

**Verified**: ordinary hit numbers have a 1 s visible-age window; crits get 1.25 s. They fade in over 0.15 s, pop, rise with easing and then fade out. Merged-hit settling can delay that window by 0.3 s. This is display timing, not a change to damage. [boot-CPW8VVmR.js: `hitSec:1,critSec:1.25,fadeInSec:.15,fadeOutSec:.35`] [scene-B2rwbRKj.js: `return n-e.first[t]-(e.settled?.3:0)`]

The base font size is 4% of the shorter viewport dimension, with a 10 CSS-pixel minimum and no former 30-pixel cap. The display budget stays between 5 and 12 totals, depending on viewport size; crowd pressure can reduce it further. Larger totals and crits take priority rather than showing every hit at once. [boot-CPW8VVmR.js: `sizeShare:.04,sizeMinCssPx:10`] [boot-CPW8VVmR.js: `budgetMin:5,budgetMax:12,budgetCssPx:40`] [scene-B2rwbRKj.js: `return Math.max(n,Math.min(e.cssW,e.cssH)*t)*e.dpr`] [scene-B2rwbRKj.js: `return e.amount[t]*(e.kind[t]===1?4:1)`] Reduced motion removes sideways drift and the late-game pop; it does not disable damage numbers. [scene-B2rwbRKj.js: `return e.still?r:e.overkill?r*function(e)`]

Original old/new renderer probes checked lifetime boundaries, fading, viewport sizing, merging, reduced motion and priority ties. At a 390-pixel shorter viewport dimension the base size changed from 24.375 to 15.6 CSS pixels; two merged hits of 30 and 70 still display 100. These are isolated rendering checks, not frame-rate or physical-device measurements.

**Verified**: new ordinary pest spawns are limited to **2 shooters before 3:00**, **4 from 3:00 to before 6:00**, then the normal cap of 30. This limits shooter selection when spawning pests, not the number of bullets, bosses or elites already present. The early caps use the difficulty's `shooterCap` multiplier, currently 1 on every level; Story uses these defaults too. [sim-BkfBhQfw.js: `shooterSteps:[{untilSec:180,max:2},{untilSec:360,max:4}]`] [sim-BkfBhQfw.js: `if(e.runClock<i.untilSec)return Math.min(t.maxShooters,i.max)`] [sim-BkfBhQfw.js: `if(Hl[i]===0||Kl(e)<Gl(e))return i`]

## Sources

Game version 75: `main-CSuyb4iv.js` / `boot-CPW8VVmR.js` / `sim-BkfBhQfw.js` from https://tibo.dog/, researched 2026-11-05. See [Versions & updates](/versions).
- Game config and formulas in `sim-BkfBhQfw.js`
