feat: add multi-map system with 4 distinct biomes, layouts, and multiplayer sync

- Introduce 4 playable map environments:
  * Meadow (Grüne Lichtung): Classic balanced S-curve path with lush grasslands, flowers, and standard obstacles
  * Desert (Sonnendünen): Winding dune trail with sandstone rocks, oasis palms, and scrub
  * Frostland (Frostgipfel): Icy serpentine path through snowfields with frost-covered pines and crystal rocks
  * Volcano (Lavabruch): Tight aggressive layout through dark obsidian/basalt with glowing magma streams and floating ember sparks
- Add comprehensive MapDef & MapTheme configuration for distinct waypoint routes, flying trajectories, biome color palettes, portal/keep styles, and obstacle densities
- Extend Canvas 2D renderer to dynamically adapt backgrounds, path texturing, portal effects, and procedurally drawn biome foliage/decorations per map
- Upgrade GameEngine with dynamic map loading, obstacle clearing logic, and per-map highscore persistence
- Implement multiplayer map synchronization in WebSocket server (set-map event), room state, and lobby UI allowing host map selection
- Update StartScreen and LobbyScreen components with interactive map selection cards and per-map highscore tracking
- Add automated test suites for map path integrity, determinism, playability, and room map protocol synchronization
This commit is contained in:
Tronax 2026-08-16 15:03:34 +02:00
parent a7fb3efa61
commit ce2484bb8d
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
13 changed files with 911 additions and 132 deletions

View file

@ -1,4 +1,4 @@
import type { DifficultyDef, DifficultyId, EnemyDef, EnemyKind, TowerDef, TowerKind, WaveGroup } from './types'
import type { DifficultyDef, DifficultyId, EnemyDef, EnemyKind, MapDef, MapId, TowerDef, TowerKind, WaveGroup } from './types'
export const TILE = 48
export const COLS = 20
@ -22,27 +22,250 @@ export function playerColor(playerId: number | null): string {
return playerId === null || playerId === 0 ? PLAYER_COLORS[0] : PLAYER_COLORS[1]
}
/** 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],
]
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],
},
},
}
/** flying enemies fly straight across the middle */
export const FLY_WAYPOINTS: [number, number][] = [
[-1, 5],
[20, 5],
]
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']