feat(balance): 30-wave campaign, 5 new enemy types, steeper per-wave scaling

The campaign was too easy: a handful of un-upgraded towers stopped
everything. This makes the game substantially harder across three axes.

Campaign extended from 20 to 30 hand-tuned waves (endless now starts at
31). Victory messages, HUD label (via store.totalWaves synced from
TOTAL_WAVES), start screen, lobby and README updated.

Five new enemy types debut in later waves, each with a hand-drawn
canvas look:
- Brute (wave 8): fast tank hybrid, 130 HP, 2 lives
- Phantom (wave 12): fast flyer with real HP, 2 lives
- Scorpion (wave 14): very fast ground, 2 lives
- Golem (wave 18): walking bunker, 560 HP, 3 lives
- Dragon (wave 22): flying boss, 950 HP, 3 lives, gets its own top
  boss health bar like the boss

Per-wave HP scaling steepened (1 + 0.16m + 0.025m², was 0.18m + 0.02m²):
wave 10 now ~4.6x (was 4.2x), wave 20 ~13.2x (was 11.6x), wave 30 ~29x.
Late waves mix the new types into heavy compositions; wave 30 is the
final wall (3 bosses, 2 dragons, 4 golems, 12 scorpions). Endless waves
scale all ten types with bosses every 5 and dragons from endless+2.

Balance validated with the headless greedy-bot simulation: the bot
(capped around tower level 5 on fixed spots) previously trivially won
the campaign and now dies at wave 29 — clearing wave 30 requires
evolved towers (level 6+), research bonuses and good placement. The
sim docs were updated to describe this new tuning philosophy.

New render smoke test (npm test): draws all ten enemy kinds through
the real renderer with a stub 2D context, two frames each with slow/
DoT/flash effects active, guarding every drawEnemy code path. 55 checks
green, build clean. Browser-verified: campaign starts and HUD shows
"Welle x/30".
This commit is contained in:
Tronax 2026-08-17 20:29:47 +02:00
parent f7ab56e475
commit aa68343ac2
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
13 changed files with 402 additions and 38 deletions

View file

@ -1,6 +1,12 @@
/**
* Headless balance simulation: a greedy bot plays the game on 'normal'
* to verify the campaign is winnable (and not trivially easy).
* Headless balance simulation: a greedy bot plays the campaign on 'normal'
* as a difficulty baseline. The bot uses predetermined build spots and a
* naive greedy upgrade policy (it plateaus around tower level 5), so where
* it dies marks the difficulty wall for un-evolved towers. The 30-wave
* campaign is tuned so that clearing it requires evolved towers (level 6+),
* research bonuses and good placement the bot dying late (wave ~28+) is
* expected and healthy; dying before ~wave 15 would mean the mid-game is
* overtuned.
*
* Run: npm run sim
*/
@ -8,8 +14,8 @@ import { GameEngine } from '../src/game/engine.ts'
import { TOWERS } from '../src/game/config.ts'
import type { TowerKind } from '../src/game/types.ts'
/** campaign balance stays tuned for 3 levels evolutions are endless-mode luxury */
const SIM_MAX_LEVEL = 3
/** upper tower level bound for the greedy bot (it rarely reaches it) */
const SIM_MAX_LEVEL = 9
const eng = new GameEngine()
eng.startGame('normal')
@ -153,7 +159,7 @@ while (eng.phase !== 'gameover' && eng.phase !== 'victory' && guard < 60 * 60 *
console.log('---')
console.log(`Ergebnis: ${eng.phase === 'victory' ? 'SIEG' : 'NIEDERLAGE'} in Welle ${eng.waveNo}, Leben übrig: ${eng.lives}, Punkte: ${eng.score}`)
if (eng.phase !== 'victory') {
console.log(' → Balancing zu schwer! Gegner-Werte abschwächen.')
console.log(' → Greedy-Baseline gescheitert. Tod vor ~Welle 15 wäre zu schwer, spät (~28+) ist Ziel.')
} else if (eng.lives > 14) {
console.log(' → Balancing zu leicht (zu viele Leben übrig).')
}