feat: initialize TRXTD browser tower defense

This commit is contained in:
Tronax 2026-08-15 16:07:09 +02:00
commit c4347f8420
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
34 changed files with 8302 additions and 0 deletions

303
src/game/config.ts Normal file
View file

@ -0,0 +1,303 @@
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: '🔴',
}