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:
parent
f7ab56e475
commit
aa68343ac2
13 changed files with 402 additions and 38 deletions
|
|
@ -5,7 +5,7 @@ export const COLS = 20
|
|||
export const ROWS = 11
|
||||
export const W = COLS * TILE // 960
|
||||
export const H = ROWS * TILE // 528
|
||||
export const TOTAL_WAVES = 20
|
||||
export const TOTAL_WAVES = 30
|
||||
export const INTERMISSION = 20 // seconds between waves
|
||||
export const SELL_RATIO = 0.7
|
||||
|
||||
|
|
@ -474,12 +474,68 @@ export const ENEMIES: Record<EnemyKind, EnemyDef> = {
|
|||
flying: false,
|
||||
color: '#d84b3f',
|
||||
},
|
||||
// --- spätere Wellen: stärkere Gegnertypen ---
|
||||
brute: {
|
||||
kind: 'brute',
|
||||
name: 'Brute',
|
||||
hp: 130,
|
||||
speed: 50,
|
||||
reward: 14,
|
||||
radius: 13,
|
||||
dmg: 2,
|
||||
flying: false,
|
||||
color: '#b0763a',
|
||||
},
|
||||
phantom: {
|
||||
kind: 'phantom',
|
||||
name: 'Phantom',
|
||||
hp: 75,
|
||||
speed: 92,
|
||||
reward: 13,
|
||||
radius: 10,
|
||||
dmg: 1,
|
||||
flying: true,
|
||||
color: '#a9c6de',
|
||||
},
|
||||
scorpion: {
|
||||
kind: 'scorpion',
|
||||
name: 'Skorpion',
|
||||
hp: 90,
|
||||
speed: 122,
|
||||
reward: 16,
|
||||
radius: 11,
|
||||
dmg: 2,
|
||||
flying: false,
|
||||
color: '#c2503d',
|
||||
},
|
||||
golem: {
|
||||
kind: 'golem',
|
||||
name: 'Golem',
|
||||
hp: 560,
|
||||
speed: 27,
|
||||
reward: 42,
|
||||
radius: 17,
|
||||
dmg: 3,
|
||||
flying: false,
|
||||
color: '#8a8f7a',
|
||||
},
|
||||
dragon: {
|
||||
kind: 'dragon',
|
||||
name: 'Drache',
|
||||
hp: 950,
|
||||
speed: 56,
|
||||
reward: 85,
|
||||
radius: 18,
|
||||
dmg: 3,
|
||||
flying: true,
|
||||
color: '#9b3a8f',
|
||||
},
|
||||
}
|
||||
|
||||
/** hp multiplier for wave n */
|
||||
/** hp multiplier for wave n (steeper curve: late waves get noticeably tougher) */
|
||||
export function waveHpScale(n: number): number {
|
||||
const m = n - 1
|
||||
return 1 + 0.18 * m + 0.02 * m * m
|
||||
return 1 + 0.16 * m + 0.025 * m * m
|
||||
}
|
||||
|
||||
/** speed multiplier for wave n */
|
||||
|
|
@ -502,7 +558,11 @@ export function earlyCallBonus(remainingSeconds: number): number {
|
|||
|
||||
const G = (kind: EnemyKind, count: number, interval: number): WaveGroup => ({ kind, count, interval })
|
||||
|
||||
/** hand-tuned waves 1..20 */
|
||||
/**
|
||||
* Hand-tuned campaign waves 1..30.
|
||||
* New enemy types debut progressively: Brute (8), Phantom (12), Skorpion (14),
|
||||
* Golem (18), Drache (22). Wave 30 is the final wall before endless mode.
|
||||
*/
|
||||
export const WAVES: WaveGroup[][] = [
|
||||
/* 1 */ [G('normal', 8, 1.1)],
|
||||
/* 2 */ [G('normal', 12, 0.9)],
|
||||
|
|
@ -511,30 +571,44 @@ export const WAVES: WaveGroup[][] = [
|
|||
/* 5 */ [G('normal', 10, 0.75), G('tank', 3, 2.2)],
|
||||
/* 6 */ [G('fast', 12, 0.5), G('tank', 4, 2.0)],
|
||||
/* 7 */ [G('flyer', 8, 0.9), G('normal', 10, 0.7)],
|
||||
/* 8 */ [G('fast', 10, 0.5), G('flyer', 10, 0.7)],
|
||||
/* 8 */ [G('fast', 10, 0.5), G('flyer', 10, 0.7), G('brute', 2, 2.4)],
|
||||
/* 9 */ [G('tank', 6, 1.6), G('fast', 12, 0.45), G('normal', 10, 0.6)],
|
||||
/* 10 */ [G('boss', 1, 1), G('normal', 12, 0.8)],
|
||||
/* 11 */ [G('flyer', 14, 0.55), G('fast', 12, 0.45)],
|
||||
/* 12 */ [G('tank', 8, 1.3), G('normal', 16, 0.5)],
|
||||
/* 13 */ [G('fast', 20, 0.35), G('flyer', 12, 0.5)],
|
||||
/* 14 */ [G('tank', 10, 1.1), G('fast', 16, 0.4)],
|
||||
/* 15 */ [G('boss', 2, 8), G('flyer', 10, 0.5)],
|
||||
/* 16 */ [G('normal', 24, 0.35), G('tank', 8, 1.0)],
|
||||
/* 17 */ [G('fast', 24, 0.3), G('flyer', 16, 0.45)],
|
||||
/* 18 */ [G('tank', 12, 0.9), G('normal', 20, 0.4)],
|
||||
/* 19 */ [G('fast', 20, 0.3), G('flyer', 20, 0.4), G('tank', 8, 0.9)],
|
||||
/* 20 */ [G('tank', 8, 1.0), G('boss', 2, 8), G('fast', 16, 0.35)],
|
||||
/* 12 */ [G('phantom', 6, 0.8), G('flyer', 10, 0.6), G('brute', 4, 1.8)],
|
||||
/* 13 */ [G('fast', 20, 0.35), G('tank', 6, 1.2), G('phantom', 6, 0.7)],
|
||||
/* 14 */ [G('scorpion', 8, 0.7), G('fast', 16, 0.4), G('tank', 6, 1.2)],
|
||||
/* 15 */ [G('boss', 2, 8), G('flyer', 10, 0.5), G('phantom', 6, 0.7)],
|
||||
/* 16 */ [G('normal', 20, 0.35), G('tank', 6, 1.0), G('brute', 5, 1.4)],
|
||||
/* 17 */ [G('fast', 24, 0.3), G('flyer', 16, 0.45), G('scorpion', 8, 0.65)],
|
||||
/* 18 */ [G('golem', 2, 5), G('tank', 9, 0.9), G('phantom', 7, 0.6)],
|
||||
/* 19 */ [G('fast', 20, 0.3), G('flyer', 20, 0.4), G('tank', 8, 0.9), G('scorpion', 6, 0.6)],
|
||||
/* 20 */ [G('boss', 2, 8), G('tank', 8, 1.0), G('brute', 8, 1.2), G('fast', 16, 0.35)],
|
||||
/* 21 */ [G('golem', 3, 4), G('scorpion', 10, 0.55), G('phantom', 8, 0.55)],
|
||||
/* 22 */ [G('dragon', 1, 1), G('flyer', 16, 0.45), G('fast', 20, 0.3)],
|
||||
/* 23 */ [G('tank', 14, 0.8), G('golem', 3, 4), G('normal', 24, 0.3)],
|
||||
/* 24 */ [G('scorpion', 14, 0.5), G('phantom', 12, 0.5), G('fast', 24, 0.28)],
|
||||
/* 25 */ [G('boss', 3, 7), G('golem', 3, 4), G('brute', 10, 1.0)],
|
||||
/* 26 */ [G('dragon', 2, 9), G('flyer', 20, 0.4), G('phantom', 10, 0.5)],
|
||||
/* 27 */ [G('tank', 16, 0.7), G('golem', 4, 3.5), G('scorpion', 12, 0.5)],
|
||||
/* 28 */ [G('fast', 30, 0.24), G('phantom', 14, 0.45), G('scorpion', 12, 0.45), G('flyer', 18, 0.35)],
|
||||
/* 29 */ [G('golem', 4, 3), G('brute', 10, 0.9), G('tank', 10, 0.7), G('dragon', 2, 9)],
|
||||
/* 30 */ [G('boss', 3, 6), G('dragon', 2, 8), G('golem', 4, 3), G('scorpion', 12, 0.4)],
|
||||
]
|
||||
|
||||
/** endless mode waves after 20 */
|
||||
/** endless mode waves after 30 */
|
||||
export function endlessWave(n: number): WaveGroup[] {
|
||||
const m = n - 20
|
||||
const m = n - 30
|
||||
const groups: WaveGroup[] = []
|
||||
if (n % 5 === 0) groups.push(G('boss', 1 + Math.floor(m / 5), 8))
|
||||
groups.push(G('tank', 8 + m * 2, Math.max(0.5, 1.1 - m * 0.02)))
|
||||
groups.push(G('fast', 18 + m * 3, Math.max(0.15, 0.32 - m * 0.01)))
|
||||
groups.push(G('flyer', 14 + m * 2, Math.max(0.25, 0.45 - m * 0.01)))
|
||||
groups.push(G('normal', 20 + m * 4, Math.max(0.18, 0.35 - m * 0.01)))
|
||||
if (n % 5 === 0) groups.push(G('boss', 1 + Math.floor(m / 4), 7))
|
||||
if (m >= 2) groups.push(G('dragon', 1 + Math.floor(m / 6), 9))
|
||||
groups.push(G('golem', 2 + Math.floor(m / 2), Math.max(2.2, 3.5 - m * 0.04)))
|
||||
groups.push(G('tank', 10 + m * 2, Math.max(0.5, 1.0 - m * 0.015)))
|
||||
groups.push(G('scorpion', 10 + m * 2, Math.max(0.3, 0.55 - m * 0.01)))
|
||||
groups.push(G('fast', 20 + m * 3, Math.max(0.15, 0.3 - m * 0.008)))
|
||||
groups.push(G('flyer', 16 + m * 2, Math.max(0.25, 0.4 - m * 0.008)))
|
||||
groups.push(G('phantom', 8 + m * 2, Math.max(0.3, 0.5 - m * 0.01)))
|
||||
groups.push(G('normal', 24 + m * 4, Math.max(0.18, 0.32 - m * 0.008)))
|
||||
return groups
|
||||
}
|
||||
|
||||
|
|
@ -576,6 +650,11 @@ export const ENEMY_COLORS: Record<EnemyKind, string> = {
|
|||
tank: '#7d8894',
|
||||
flyer: '#b06ee8',
|
||||
boss: '#d84b3f',
|
||||
brute: '#b0763a',
|
||||
phantom: '#a9c6de',
|
||||
scorpion: '#c2503d',
|
||||
golem: '#8a8f7a',
|
||||
dragon: '#9b3a8f',
|
||||
}
|
||||
|
||||
export const ENEMY_ICONS: Record<EnemyKind, string> = {
|
||||
|
|
@ -584,4 +663,9 @@ export const ENEMY_ICONS: Record<EnemyKind, string> = {
|
|||
tank: '⬜',
|
||||
flyer: '🟣',
|
||||
boss: '🔴',
|
||||
brute: '🟤',
|
||||
phantom: '⚪',
|
||||
scorpion: '🦂',
|
||||
golem: '🗿',
|
||||
dragon: '🐲',
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue