TRXTD/src/game/config.ts
Tronax aa68343ac2
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".
2026-08-17 20:29:47 +02:00

671 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { DifficultyDef, DifficultyId, EnemyDef, EnemyKind, MapDef, MapId, TowerDef, TowerKind, WaveGroup } from './types'
export const TILE = 48
export const COLS = 20
export const ROWS = 11
export const W = COLS * TILE // 960
export const H = ROWS * TILE // 528
export const TOTAL_WAVES = 30
export const INTERMISSION = 20 // seconds between waves
export const SELL_RATIO = 0.7
/** cost to remove blocking obstacles */
export const OBSTACLE_COST: Record<'tree' | 'rock', number> = {
tree: 60,
rock: 40,
}
/** multiplayer player identity colors (blue = player 1, orange = player 2) */
export const PLAYER_COLORS: [string, string] = ['#4da3ff', '#ffb14d']
export function playerColor(playerId: number | null): string {
return playerId === null || playerId === 0 ? PLAYER_COLORS[0] : PLAYER_COLORS[1]
}
export const MAPS: Record<MapId, MapDef> = {
meadow: {
id: 'meadow',
name: 'Grüne Lichtung',
subtitle: 'Klassisches Tal',
desc: 'Ausgewogener S-Kurven-Pfad durch saftige Wiesen mit vielen strategischen Bauplätzen.',
icon: '🌿',
theme: {
biome: 'grassland',
bgGradient: ['#3d6b39', '#48783f'],
checkerAlpha: 0.022,
gridColor: 'rgba(0,0,0,0.06)',
speckleDark: 'rgba(0,0,0,0.05)',
speckleLight: 'rgba(255,255,255,0.04)',
pathBorder: '#5f4a33',
pathEdge: '#7a6142',
pathMain: '#c2a276',
pathDash: 'rgba(216,195,154,0.55)',
pathPebbles: ['rgba(95,74,51,0.5)', 'rgba(230,210,175,0.5)'],
portalColor: '#2a1e3d',
portalRing: 'rgba(178,120,255,0.7)',
portalCore: 'rgba(200,160,255,0.9)',
baseColor: '#8d8577',
baseAccent: '#a49b8b',
treeFoliage: ['#2f6b33', '#3b7c3c'],
treeTrunk: '#6b4526',
rockColor: '#8b9298',
rockHighlight: 'rgba(255,255,255,0.22)',
bushColor: ['#3e7a3a', '#4f8f45'],
flowers: ['#e8657f', '#f5d55b', '#ffffff', '#c17ee0'],
obstacleTypes: { primary: 'tree', secondary: 'rock' },
},
waypoints: [
[-1, 5],
[2, 5],
[2, 1],
[6, 1],
[6, 9],
[10, 9],
[10, 1],
[14, 1],
[14, 9],
[17, 9],
[17, 5],
[20, 5],
],
flyWaypoints: [
[-1, 5],
[20, 5],
],
decorCounts: {
primaryObstacle: 7,
secondaryObstacle: 5,
bush: 10,
detail: 22,
},
decorDistLimits: {
primaryDist: [2.1, 99],
secondaryDist: [1.6, 99],
},
},
desert: {
id: 'desert',
name: 'Sonnendünen',
subtitle: 'Glühende Wüste',
desc: 'Langer, gewundener Sanddünenpfad mit Palmen, Felsblöcken und Kakteen.',
icon: '🏜️',
theme: {
biome: 'desert',
bgGradient: ['#c99a4e', '#d8a95d'],
checkerAlpha: 0.035,
gridColor: 'rgba(110,65,15,0.08)',
speckleDark: 'rgba(90,50,10,0.08)',
speckleLight: 'rgba(255,240,190,0.08)',
pathBorder: '#7d4a1b',
pathEdge: '#9a602a',
pathMain: '#e6c88b',
pathDash: 'rgba(255,245,210,0.6)',
pathPebbles: ['rgba(125,74,27,0.5)', 'rgba(255,230,170,0.5)'],
portalColor: '#3a2010',
portalRing: 'rgba(255,170,60,0.8)',
portalCore: 'rgba(255,220,130,0.95)',
baseColor: '#b58a5c',
baseAccent: '#d4aa7c',
treeFoliage: ['#3e803a', '#5ba040'], // Palm fronds
treeTrunk: '#8a623a',
rockColor: '#ba7a4e', // Sandstone rocks
rockHighlight: 'rgba(255,235,190,0.35)',
bushColor: ['#7a8f35', '#9ab540'], // Desert scrub
flowers: ['#ff8a43', '#ffc83b', '#e05a47', '#ffdf80'],
obstacleTypes: { primary: 'tree', secondary: 'rock' },
},
waypoints: [
[-1, 2],
[4, 2],
[4, 8],
[8, 8],
[8, 2],
[12, 2],
[12, 8],
[16, 8],
[16, 3],
[20, 3],
],
flyWaypoints: [
[-1, 2],
[20, 3],
],
decorCounts: {
primaryObstacle: 6,
secondaryObstacle: 6,
bush: 8,
detail: 16,
},
decorDistLimits: {
primaryDist: [2.0, 99],
secondaryDist: [1.5, 99],
},
},
frostland: {
id: 'frostland',
name: 'Frostgipfel',
subtitle: 'Eisige Einöde',
desc: 'Ein eisiger Serpentinenweg durch Schnee und gefrorene Tannenwälder.',
icon: '❄️',
theme: {
biome: 'snow',
bgGradient: ['#7d9bb8', '#90abc7'],
checkerAlpha: 0.03,
gridColor: 'rgba(40,70,110,0.08)',
speckleDark: 'rgba(30,60,95,0.07)',
speckleLight: 'rgba(255,255,255,0.18)',
pathBorder: '#3b556b',
pathEdge: '#526f87',
pathMain: '#c8dceb',
pathDash: 'rgba(235,245,255,0.7)',
pathPebbles: ['rgba(60,90,115,0.5)', 'rgba(240,250,255,0.7)'],
portalColor: '#122538',
portalRing: 'rgba(120,210,255,0.85)',
portalCore: 'rgba(200,240,255,0.95)',
baseColor: '#607991',
baseAccent: '#819db8',
treeFoliage: ['#1e4a4d', '#2c6663'], // Pine trees covered in snow
treeTrunk: '#423b38',
rockColor: '#5c7891', // Frosty ice crystals/rocks
rockHighlight: 'rgba(215,245,255,0.45)',
bushColor: ['#3a6369', '#507f82'],
flowers: ['#bfe8ff', '#ffffff', '#90d4ff', '#e0f4ff'],
obstacleTypes: { primary: 'tree', secondary: 'rock' },
},
waypoints: [
[-1, 8],
[3, 8],
[3, 2],
[7, 2],
[7, 9],
[13, 9],
[13, 3],
[17, 3],
[17, 7],
[20, 7],
],
flyWaypoints: [
[-1, 8],
[20, 7],
],
decorCounts: {
primaryObstacle: 8,
secondaryObstacle: 5,
bush: 9,
detail: 20,
},
decorDistLimits: {
primaryDist: [2.0, 99],
secondaryDist: [1.6, 99],
},
},
volcano: {
id: 'volcano',
name: 'Lavabruch',
subtitle: 'Feurige Schlucht',
desc: 'Aggressiver, enger Pfad durch erkaltetes Lavagestein mit glühenden Magmaadern.',
icon: '🌋',
theme: {
biome: 'magma',
bgGradient: ['#282024', '#332629'],
checkerAlpha: 0.04,
gridColor: 'rgba(255,80,20,0.06)',
speckleDark: 'rgba(0,0,0,0.12)',
speckleLight: 'rgba(255,110,40,0.08)',
pathBorder: '#521f18',
pathEdge: '#782d20',
pathMain: '#e86a38',
pathDash: 'rgba(255,200,100,0.65)',
pathPebbles: ['rgba(80,25,20,0.6)', 'rgba(255,160,60,0.6)'],
portalColor: '#2b0d09',
portalRing: 'rgba(255,80,30,0.9)',
portalCore: 'rgba(255,200,80,0.95)',
baseColor: '#4d3b3b',
baseAccent: '#6b4f4f',
treeFoliage: ['#382b28', '#473430'], // Charred dead trees
treeTrunk: '#1a1414',
rockColor: '#3d3434', // Basalt obsidian rocks
rockHighlight: 'rgba(255,130,50,0.4)',
bushColor: ['#472c26', '#5e3730'],
flowers: ['#ff4d29', '#ff9436', '#ffd24d', '#ff2a2a'], // Glowing embers
obstacleTypes: { primary: 'tree', secondary: 'rock' },
},
waypoints: [
[-1, 1],
[4, 1],
[4, 6],
[1, 6],
[1, 9],
[8, 9],
[8, 4],
[14, 4],
[14, 8],
[18, 8],
[18, 2],
[20, 2],
],
flyWaypoints: [
[-1, 1],
[20, 2],
],
decorCounts: {
primaryObstacle: 7,
secondaryObstacle: 6,
bush: 8,
detail: 24,
},
decorDistLimits: {
primaryDist: [1.9, 99],
secondaryDist: [1.5, 99],
},
},
}
export const MAP_ORDER: MapId[] = ['meadow', 'desert', 'frostland', 'volcano']
/** Default fallback waypoints for backward compatibility */
export const MAP_WAYPOINTS: [number, number][] = MAPS.meadow.waypoints
export const FLY_WAYPOINTS: [number, number][] = MAPS.meadow.flyWaypoints
export const TOWER_ORDER: TowerKind[] = ['arrow', 'cannon', 'frost', 'tesla', 'laser']
export const TOWERS: Record<TowerKind, TowerDef> = {
arrow: {
kind: 'arrow',
name: 'Bogenturm',
icon: '🏹',
desc: 'Günstiger Standardturm mit schnellen Einzelschüssen.',
cost: 50,
upgradeCost: [60, 110, 260, 420, 700, 1200, 2000, 3400],
levels: [
{ damage: 12, range: 110, rate: 1.7 },
{ damage: 22, range: 120, rate: 2.0 },
{ damage: 38, range: 132, rate: 2.4 },
{ damage: 70, range: 140, rate: 2.6 },
{ damage: 95, range: 148, rate: 2.8 },
{ damage: 125, range: 155, rate: 3.0 },
{ damage: 180, range: 165, rate: 3.2 },
{ damage: 240, range: 175, rate: 3.4 },
{ damage: 320, range: 185, rate: 3.6 },
],
hitsFlying: true,
projectileSpeed: 460,
multi: [1, 1, 1, 2, 2, 3, 3, 3, 4],
dotDps: [0, 0, 0, 0, 0, 0, 12, 18, 28],
dotDuration: [0, 0, 0, 0, 0, 0, 2, 2.5, 3],
dotColor: '#7ddc4f',
color: '#9a6a33',
accent: '#e8c27a',
hotkey: '1',
},
cannon: {
kind: 'cannon',
name: 'Kanone',
icon: '🧨',
desc: 'Flächenschaden durch Explosion trifft aber keine Flieger.',
cost: 110,
upgradeCost: [100, 180, 380, 600, 950, 1600, 2600, 4200],
levels: [
{ damage: 28, range: 118, rate: 0.55 },
{ damage: 50, range: 128, rate: 0.62 },
{ damage: 92, range: 140, rate: 0.7 },
{ damage: 150, range: 150, rate: 0.75 },
{ damage: 200, range: 158, rate: 0.8 },
{ damage: 260, range: 165, rate: 0.85 },
{ damage: 380, range: 175, rate: 0.9 },
{ damage: 500, range: 185, rate: 0.95 },
{ damage: 680, range: 195, rate: 1.0 },
],
hitsFlying: false,
projectileSpeed: 300,
splash: [42, 48, 56, 64, 70, 76, 84, 92, 100],
dotDps: [0, 0, 0, 10, 14, 20, 26, 34, 45],
dotDuration: [0, 0, 0, 2, 2, 2.5, 2.5, 3, 3],
dotColor: '#ff9c40',
flak: [false, false, false, false, false, false, true, true, true],
color: '#57616e',
accent: '#ff9c40',
hotkey: '2',
},
frost: {
kind: 'frost',
name: 'Eisturm',
icon: '❄️',
desc: 'Frostpuls verlangsamt ALLE Gegner in Reichweite (auch Flieger).',
cost: 80,
upgradeCost: [70, 130, 260, 420, 700, 1100, 1800, 3000],
levels: [
{ damage: 5, range: 95, rate: 0.9 },
{ damage: 9, range: 105, rate: 1.0 },
{ damage: 15, range: 118, rate: 1.1 },
{ damage: 22, range: 128, rate: 1.15 },
{ damage: 30, range: 138, rate: 1.2 },
{ damage: 40, range: 148, rate: 1.25 },
{ damage: 60, range: 158, rate: 1.3 },
{ damage: 80, range: 170, rate: 1.35 },
{ damage: 105, range: 182, rate: 1.4 },
],
hitsFlying: true,
slowFactor: [0.55, 0.45, 0.35, 0.32, 0.3, 0.28, 0.25, 0.22, 0.2],
slowDuration: [1.5, 1.9, 2.3, 2.5, 2.7, 2.9, 3.1, 3.3, 3.5],
freezeDuration: [0, 0, 0, 0.7, 0.9, 1.1, 1.3, 1.5, 1.8],
color: '#4d8fb5',
accent: '#aef0ff',
hotkey: '3',
},
tesla: {
kind: 'tesla',
name: 'Teslaturm',
icon: '⚡',
desc: 'Kettenblitz springt auf mehrere Gegner über.',
cost: 130,
upgradeCost: [120, 200, 400, 650, 1000, 1700, 2800, 4500],
levels: [
{ damage: 22, range: 105, rate: 1.0 },
{ damage: 38, range: 115, rate: 1.1 },
{ damage: 66, range: 128, rate: 1.2 },
{ damage: 95, range: 138, rate: 1.3 },
{ damage: 125, range: 145, rate: 1.4 },
{ damage: 160, range: 152, rate: 1.5 },
{ damage: 230, range: 160, rate: 1.6 },
{ damage: 300, range: 168, rate: 1.7 },
{ damage: 400, range: 175, rate: 1.8 },
],
hitsFlying: true,
chain: [3, 4, 5, 6, 6, 7, 7, 8, 9],
chainRange: [100, 100, 100, 110, 110, 115, 120, 125, 130],
stunDuration: [0, 0, 0, 0.2, 0.25, 0.3, 0.35, 0.4, 0.5],
stormEvery: [0, 0, 0, 0, 0, 0, 5, 5, 4],
color: '#6a5fb0',
accent: '#c8b4ff',
hotkey: '4',
},
laser: {
kind: 'laser',
name: 'Laserturm',
icon: '💫',
desc: 'Soforttreffer mit hoher Reichweite und hohem Schaden.',
cost: 160,
upgradeCost: [150, 260, 500, 800, 1300, 2100, 3400, 5500],
levels: [
{ damage: 60, range: 170, rate: 0.5 },
{ damage: 105, range: 185, rate: 0.55 },
{ damage: 185, range: 205, rate: 0.6 },
{ damage: 170, range: 215, rate: 0.6 },
{ damage: 230, range: 225, rate: 0.65 },
{ damage: 300, range: 235, rate: 0.7 },
{ damage: 420, range: 250, rate: 0.75 },
{ damage: 560, range: 265, rate: 0.8 },
{ damage: 750, range: 280, rate: 0.85 },
],
hitsFlying: true,
prism: [0, 0, 0, 2, 2, 3, 0, 0, 0],
pierce: [false, false, false, false, false, false, true, true, true],
color: '#b0483f',
accent: '#ff8a7a',
hotkey: '5',
},
}
/** max tower level */
export const MAX_TOWER_LEVEL = 9
/** tier of a tower level: 0 = yellow (1-3), 1 = orange (4-6), 2 = blue (7-9) */
export function towerTier(level: number): number {
return Math.min(2, Math.floor((level - 1) / 3))
}
/** tier colors for level pips, stars and auras */
export const TIER_COLORS = ['#ffd23e', '#ff9c40', '#4da3ff']
export const ENEMIES: Record<EnemyKind, EnemyDef> = {
normal: {
kind: 'normal',
name: 'Kriecher',
hp: 55,
speed: 55,
reward: 8,
radius: 11,
dmg: 1,
flying: false,
color: '#58c14b',
},
fast: {
kind: 'fast',
name: 'Läufer',
hp: 34,
speed: 105,
reward: 9,
radius: 9,
dmg: 1,
flying: false,
color: '#ffd23e',
},
tank: {
kind: 'tank',
name: 'Panzer',
hp: 210,
speed: 34,
reward: 20,
radius: 14,
dmg: 2,
flying: false,
color: '#7d8894',
},
flyer: {
kind: 'flyer',
name: 'Flieger',
hp: 48,
speed: 72,
reward: 10,
radius: 10,
dmg: 1,
flying: true,
color: '#b06ee8',
},
boss: {
kind: 'boss',
name: 'Boss',
hp: 1500,
speed: 42,
reward: 130,
radius: 20,
dmg: 5,
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 (steeper curve: late waves get noticeably tougher) */
export function waveHpScale(n: number): number {
const m = n - 1
return 1 + 0.16 * m + 0.025 * m * m
}
/** speed multiplier for wave n */
export function waveSpeedScale(n: number): number {
return Math.min(1.32, 1 + 0.008 * (n - 1))
}
/** reward multiplier for wave n */
export function waveRewardScale(n: number): number {
return 1 + 0.04 * (n - 1)
}
export function waveClearBonus(n: number): number {
return 35 + 8 * n
}
export function earlyCallBonus(remainingSeconds: number): number {
return Math.round(remainingSeconds * 2.5)
}
const G = (kind: EnemyKind, count: number, interval: number): WaveGroup => ({ kind, count, interval })
/**
* 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)],
/* 3 */ [G('normal', 8, 0.9), G('fast', 5, 0.7)],
/* 4 */ [G('fast', 10, 0.55), G('normal', 8, 0.8)],
/* 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), 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('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 30 */
export function endlessWave(n: number): WaveGroup[] {
const m = n - 30
const groups: WaveGroup[] = []
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
}
export function getWave(n: number): WaveGroup[] {
if (n <= WAVES.length) return WAVES[n - 1]
return endlessWave(n)
}
export const DIFFICULTIES: Record<DifficultyId, DifficultyDef> = {
easy: {
id: 'easy',
name: 'Leicht',
desc: '30 Leben, mehr Startgold, schwächere Gegner',
lives: 30,
money: 320,
hpMul: 0.85,
},
normal: {
id: 'normal',
name: 'Normal',
desc: '20 Leben, ausgewogenes Balancing',
lives: 20,
money: 260,
hpMul: 1,
},
hard: {
id: 'hard',
name: 'Schwer',
desc: '12 Leben, weniger Gold, zähere Gegner',
lives: 12,
money: 220,
hpMul: 1.15,
},
}
export const ENEMY_COLORS: Record<EnemyKind, string> = {
normal: '#58c14b',
fast: '#ffd23e',
tank: '#7d8894',
flyer: '#b06ee8',
boss: '#d84b3f',
brute: '#b0763a',
phantom: '#a9c6de',
scorpion: '#c2503d',
golem: '#8a8f7a',
dragon: '#9b3a8f',
}
export const ENEMY_ICONS: Record<EnemyKind, string> = {
normal: '🟢',
fast: '🟡',
tank: '⬜',
flyer: '🟣',
boss: '🔴',
brute: '🟤',
phantom: '⚪',
scorpion: '🦂',
golem: '🗿',
dragon: '🐲',
}