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:
parent
a7fb3efa61
commit
ce2484bb8d
13 changed files with 911 additions and 132 deletions
|
|
@ -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']
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue