303 lines
7.4 KiB
TypeScript
303 lines
7.4 KiB
TypeScript
import type { DifficultyDef, DifficultyId, EnemyDef, EnemyKind, 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 = 20
|
||
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,
|
||
}
|
||
|
||
/** ground path in tile coords; -1/20 are outside the grid (spawn/exit) */
|
||
export const MAP_WAYPOINTS: [number, number][] = [
|
||
[-1, 5],
|
||
[2, 5],
|
||
[2, 1],
|
||
[6, 1],
|
||
[6, 9],
|
||
[10, 9],
|
||
[10, 1],
|
||
[14, 1],
|
||
[14, 9],
|
||
[17, 9],
|
||
[17, 5],
|
||
[20, 5],
|
||
]
|
||
|
||
/** flying enemies fly straight across the middle */
|
||
export const FLY_WAYPOINTS: [number, number][] = [
|
||
[-1, 5],
|
||
[20, 5],
|
||
]
|
||
|
||
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],
|
||
levels: [
|
||
{ damage: 12, range: 110, rate: 1.7 },
|
||
{ damage: 22, range: 120, rate: 2.0 },
|
||
{ damage: 38, range: 132, rate: 2.4 },
|
||
],
|
||
hitsFlying: true,
|
||
projectileSpeed: 460,
|
||
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],
|
||
levels: [
|
||
{ damage: 28, range: 118, rate: 0.55 },
|
||
{ damage: 50, range: 128, rate: 0.62 },
|
||
{ damage: 92, range: 140, rate: 0.7 },
|
||
],
|
||
hitsFlying: false,
|
||
projectileSpeed: 300,
|
||
splash: [42, 48, 56],
|
||
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],
|
||
levels: [
|
||
{ damage: 5, range: 95, rate: 0.9 },
|
||
{ damage: 9, range: 105, rate: 1.0 },
|
||
{ damage: 15, range: 118, rate: 1.1 },
|
||
],
|
||
hitsFlying: true,
|
||
slowFactor: [0.55, 0.45, 0.35],
|
||
slowDuration: [1.5, 1.9, 2.3],
|
||
color: '#4d8fb5',
|
||
accent: '#aef0ff',
|
||
hotkey: '3',
|
||
},
|
||
tesla: {
|
||
kind: 'tesla',
|
||
name: 'Teslaturm',
|
||
icon: '⚡',
|
||
desc: 'Kettenblitz springt auf mehrere Gegner über.',
|
||
cost: 130,
|
||
upgradeCost: [120, 200],
|
||
levels: [
|
||
{ damage: 22, range: 105, rate: 1.0 },
|
||
{ damage: 38, range: 115, rate: 1.1 },
|
||
{ damage: 66, range: 128, rate: 1.2 },
|
||
],
|
||
hitsFlying: true,
|
||
chain: [3, 4, 5],
|
||
chainRange: 100,
|
||
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],
|
||
levels: [
|
||
{ damage: 60, range: 170, rate: 0.5 },
|
||
{ damage: 105, range: 185, rate: 0.55 },
|
||
{ damage: 185, range: 205, rate: 0.6 },
|
||
],
|
||
hitsFlying: true,
|
||
color: '#b0483f',
|
||
accent: '#ff8a7a',
|
||
hotkey: '5',
|
||
},
|
||
}
|
||
|
||
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',
|
||
},
|
||
}
|
||
|
||
/** hp multiplier for wave n */
|
||
export function waveHpScale(n: number): number {
|
||
const m = n - 1
|
||
return 1 + 0.18 * m + 0.02 * 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 waves 1..20 */
|
||
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)],
|
||
/* 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)],
|
||
]
|
||
|
||
/** endless mode waves after 20 */
|
||
export function endlessWave(n: number): WaveGroup[] {
|
||
const m = n - 20
|
||
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)))
|
||
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',
|
||
}
|
||
|
||
export const ENEMY_ICONS: Record<EnemyKind, string> = {
|
||
normal: '🟢',
|
||
fast: '🟡',
|
||
tank: '⬜',
|
||
flyer: '🟣',
|
||
boss: '🔴',
|
||
}
|