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 = { 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 = { 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 = { 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 = { 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 = { 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 = { normal: '🟢', fast: '🟡', tank: '⬜', flyer: '🟣', boss: '🔴', brute: '🟤', phantom: '⚪', scorpion: '🦂', golem: '🗿', dragon: '🐲', }