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
|
|
@ -2,7 +2,7 @@ import { GameEngine, engine } from './engine'
|
|||
import { MPClient, serverUrl } from './net'
|
||||
import { sound } from './sound'
|
||||
import { store } from './store'
|
||||
import type { MPAction, MPMode, TargetingMode } from './types'
|
||||
import type { MapId, MPAction, MPMode, TargetingMode } from './types'
|
||||
|
||||
/** simulation tick rate in multiplayer (both clients advance identical ticks) */
|
||||
const TICK_DT = 1 / 30
|
||||
|
|
@ -73,9 +73,10 @@ class MpGameController {
|
|||
}
|
||||
}
|
||||
|
||||
private handleRoom(info: { code: string; mode: MPMode; players: { id: number; name: string }[]; you: number }): void {
|
||||
private handleRoom(info: { code: string; mode: MPMode; mapId?: MapId; players: { id: number; name: string }[]; you: number }): void {
|
||||
store.mp.roomCode = info.code
|
||||
store.mp.mode = info.mode
|
||||
store.mp.mapId = info.mapId || 'meadow'
|
||||
store.mp.players = info.players
|
||||
store.mp.myId = info.you
|
||||
store.mp.isHost = info.you === 0
|
||||
|
|
@ -84,15 +85,16 @@ class MpGameController {
|
|||
store.screen = 'lobby'
|
||||
}
|
||||
|
||||
async create(mode: MPMode, name: string): Promise<void> {
|
||||
async create(mode: MPMode, name: string, mapId: MapId = 'meadow'): Promise<void> {
|
||||
if (this.connecting) return
|
||||
this.connecting = true
|
||||
store.mp.status = 'Verbinde mit Server…'
|
||||
try {
|
||||
await this.net.connect(serverUrl())
|
||||
store.mp.name = name
|
||||
store.mp.mapId = mapId
|
||||
store.mp.active = false
|
||||
this.net.createRoom(mode, name)
|
||||
this.net.createRoom(mode, name, mapId)
|
||||
} catch (err) {
|
||||
store.mp.status = err instanceof Error ? err.message : 'Verbindung fehlgeschlagen.'
|
||||
} finally {
|
||||
|
|
@ -100,6 +102,13 @@ class MpGameController {
|
|||
}
|
||||
}
|
||||
|
||||
setMap(mapId: MapId): void {
|
||||
if (store.mp.isHost) {
|
||||
store.mp.mapId = mapId
|
||||
this.net.setMap(mapId)
|
||||
}
|
||||
}
|
||||
|
||||
async join(code: string, name: string): Promise<void> {
|
||||
if (this.connecting) return
|
||||
this.connecting = true
|
||||
|
|
@ -139,7 +148,7 @@ class MpGameController {
|
|||
|
||||
// ---------------------------------------------------------------- game start
|
||||
|
||||
private handleStart(info: { you: number; mode: MPMode; players: { id: number; name: string }[] }): void {
|
||||
private handleStart(info: { you: number; mode: MPMode; mapId?: MapId; players: { id: number; name: string }[] }): void {
|
||||
this.active = true
|
||||
this.ended = false
|
||||
this.mode = info.mode
|
||||
|
|
@ -150,9 +159,10 @@ class MpGameController {
|
|||
|
||||
const me = info.players.find((p) => p.id === info.you)
|
||||
const peer = info.players.find((p) => p.id !== info.you)
|
||||
const chosenMap = info.mapId || store.mp.mapId || 'meadow'
|
||||
|
||||
if (info.mode === 'coop') {
|
||||
engine.startGame('normal')
|
||||
engine.startGame('normal', chosenMap)
|
||||
engine.mpGameActive = true
|
||||
engine.mpMode = 'coop'
|
||||
engine.localPlayerId = info.you
|
||||
|
|
@ -161,13 +171,13 @@ class MpGameController {
|
|||
this.engines = [engine]
|
||||
this.remoteEngine = null
|
||||
} else {
|
||||
engine.startGame('normal')
|
||||
engine.startGame('normal', chosenMap)
|
||||
engine.mpGameActive = true
|
||||
engine.mpMode = 'duel'
|
||||
engine.localPlayerId = info.you
|
||||
engine.mpController = this
|
||||
const remote = new GameEngine()
|
||||
remote.startGame('normal')
|
||||
remote.startGame('normal', chosenMap)
|
||||
remote.mpGameActive = true
|
||||
remote.mpMode = 'duel'
|
||||
remote.localPlayerId = 1 - info.you
|
||||
|
|
@ -182,6 +192,7 @@ class MpGameController {
|
|||
|
||||
store.mp.active = true
|
||||
store.mp.mode = info.mode
|
||||
store.mp.mapId = chosenMap
|
||||
store.mp.name = me?.name ?? 'Du'
|
||||
store.mp.peerName = peer?.name ?? 'Gegner'
|
||||
store.mp.myId = info.you
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue