From d5c1e2ec8226be4350dfc0e264423f8d88105224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Untersch=C3=BCtz?= Date: Wed, 22 Apr 2026 20:28:16 +0200 Subject: [PATCH] add presentation mode enhancements: refine visuals, integrate HTML-based interface for presentation mode, align assets display and player states, and handle real-time JS callbacks --- cmd/client/draw_native.go | 95 ++++++++++++++++++++ cmd/client/draw_wasm.go | 5 ++ cmd/client/main.go | 7 +- cmd/client/main_native.go | 3 + cmd/client/main_wasm.go | 15 ++++ cmd/client/presentation.go | 115 +----------------------- cmd/client/wasm_bridge.go | 39 +++++++++ cmd/client/web/game.js | 152 +++++++++++++++++++++++++++++++- cmd/client/web/index.html | 33 +++++++ cmd/client/web/style.css | 173 +++++++++++++++++++++++++++++++++++++ pkg/server/room.go | 4 + 11 files changed, 526 insertions(+), 115 deletions(-) diff --git a/cmd/client/draw_native.go b/cmd/client/draw_native.go index 351f190..842d43c 100644 --- a/cmd/client/draw_native.go +++ b/cmd/client/draw_native.go @@ -4,7 +4,16 @@ package main import ( + "fmt" + "image/color" + "math" + "strings" + "time" + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/text" + "github.com/hajimehoshi/ebiten/v2/vector" + "golang.org/x/image/font/basicfont" ) // In Native: Nutze die normalen Draw-Funktionen @@ -20,3 +29,89 @@ func (g *Game) drawLobby(screen *ebiten.Image) { func (g *Game) drawLeaderboard(screen *ebiten.Image) { g.DrawLeaderboard(screen) } + +func (g *Game) drawPresentation(screen *ebiten.Image) { + // Hintergrund: Retro Dunkelblau + screen.Fill(color.RGBA{10, 15, 30, 255}) + + // Animierte Scanlines / Raster-Effekt (Retro Style) + for i := 0; i < ScreenHeight; i += 4 { + vector.DrawFilledRect(screen, 0, float32(i), float32(ScreenWidth), 1, color.RGBA{0, 0, 0, 40}, false) + } + + // Überschrift + text.Draw(screen, "PRESENTATION MODE", basicfont.Face7x13, ScreenWidth/2-80, 50, color.RGBA{255, 255, 0, 255}) + vector.StrokeLine(screen, ScreenWidth/2-90, 60, ScreenWidth/2+90, 60, 2, color.RGBA{255, 255, 0, 255}, false) + + // Zitat groß in der Mitte + if g.presQuote.Text != "" { + quoteMsg := fmt.Sprintf("\"%s\"", g.presQuote.Text) + authorMsg := fmt.Sprintf("- %s", g.presQuote.Author) + + g.DrawWrappedText(screen, quoteMsg, ScreenWidth/2, ScreenHeight/2-20, 600, color.White) + text.Draw(screen, authorMsg, basicfont.Face7x13, ScreenWidth/2+100, ScreenHeight/2+50, color.RGBA{200, 200, 200, 255}) + } + + // Assets laufen unten durch + for _, a := range g.presAssets { + img, ok := g.assetsImages[a.AssetID] + if !ok { continue } + + op := &ebiten.DrawImageOptions{} + op.GeoM.Scale(a.Scale, a.Scale) + op.GeoM.Translate(a.X, a.Y) + + bob := math.Sin(float64(time.Now().UnixMilli())/200.0) * 5.0 + op.GeoM.Translate(0, bob) + + screen.DrawImage(img, op) + } + + // Draw connected players (no names) + g.stateMutex.Lock() + for _, p := range g.gameState.Players { + if !p.IsAlive || p.Name == "PRESENTATION" { + continue + } + + playerX := p.X + playerY := p.Y + + if playerX > ScreenWidth { + playerX = math.Mod(playerX, ScreenWidth) + } else if playerX < 0 { + playerX = ScreenWidth - math.Mod(-playerX, ScreenWidth) + } + + img, ok := g.assetsImages["player"] + if ok { + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(playerX, playerY) + screen.DrawImage(img, op) + } + + if p.State != "" && strings.HasPrefix(p.State, "EMOTE_") { + emoteStr := p.State[6:] + emoteMap := map[string]string{"1": "❤️", "2": "😂", "3": "😡", "4": "👍"} + if emoji, ok := emoteMap[emoteStr]; ok { + text.Draw(screen, emoji, basicfont.Face7x13, int(playerX+10), int(playerY-10), color.White) + } + } + } + g.stateMutex.Unlock() + + // Draw QR Code + if g.presQRCode != nil { + qrSize := 150.0 + qrW, _ := g.presQRCode.Size() + scale := float64(qrSize) / float64(qrW) + op := &ebiten.DrawImageOptions{} + op.GeoM.Scale(scale, scale) + op.GeoM.Translate(20, 20) + screen.DrawImage(g.presQRCode, op) + + text.Draw(screen, "SCANNEN ZUM MITMACHEN!", basicfont.Face7x13, 20, 190, color.RGBA{255, 255, 0, 255}) + } + + text.Draw(screen, "DRÜCKE [F1] ZUM BEENDEN", basicfont.Face7x13, ScreenWidth-250, ScreenHeight-30, color.RGBA{255, 255, 255, 100}) +} diff --git a/cmd/client/draw_wasm.go b/cmd/client/draw_wasm.go index 98a6e68..64b7a12 100644 --- a/cmd/client/draw_wasm.go +++ b/cmd/client/draw_wasm.go @@ -26,3 +26,8 @@ func (g *Game) drawLeaderboard(screen *ebiten.Image) { // Schwarzer Hintergrund - HTML-Leaderboard ist darüber screen.Fill(color.RGBA{0, 0, 0, 255}) } + +func (g *Game) drawPresentation(screen *ebiten.Image) { + // Schwarzer Hintergrund - HTML-Präsentation ist darüber + screen.Fill(color.RGBA{0, 0, 0, 255}) +} diff --git a/cmd/client/main.go b/cmd/client/main.go index 32af19a..a1767ab 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -300,6 +300,9 @@ func (g *Game) Update() error { // Generate QR Code URL joinURL := "https://escape-from-school.de/?room=" + g.roomID g.presQRCode = generateQRCode(joinURL) + + // WASM: Notify JS + g.notifyPresentationStarted_Platform(g.roomID) } } @@ -361,9 +364,11 @@ func (g *Game) Update() error { g.updateLeaderboard() case StatePresentation: g.updatePresentation() + g.updatePresentationState_Platform() } + return nil -} + } func (g *Game) updateMenu() { g.handleMenuInput() diff --git a/cmd/client/main_native.go b/cmd/client/main_native.go index 8c8a079..b3a61ac 100644 --- a/cmd/client/main_native.go +++ b/cmd/client/main_native.go @@ -9,6 +9,9 @@ import ( "github.com/hajimehoshi/ebiten/v2" ) +func (g *Game) notifyPresentationStarted_Platform(roomID string) {} +func (g *Game) updatePresentationState_Platform() {} + func main() { ebiten.SetWindowSize(ScreenWidth, ScreenHeight) ebiten.SetWindowTitle("Escape From Teacher") diff --git a/cmd/client/main_wasm.go b/cmd/client/main_wasm.go index 84770a4..694ce96 100644 --- a/cmd/client/main_wasm.go +++ b/cmd/client/main_wasm.go @@ -7,8 +7,23 @@ import ( "log" "github.com/hajimehoshi/ebiten/v2" + "github.com/skip2/go-qrcode" ) +func (g *Game) notifyPresentationStarted_Platform(roomID string) { + // Im WASM: QR Code aus dem Game-Struct nehmen + var qrData []byte + // Wir generieren den QR Code hier nochmal als PNG Bytes oder wir speichern die Bytes im Game Struct + // Der Einfachheit halber generieren wir ihn in Go und übergeben ihn. + joinURL := "https://escape-from-school.de/?room=" + roomID + pngData, _ := qrcode.Encode(joinURL, qrcode.Medium, 256) + g.notifyPresentationStarted(roomID, pngData) +} + +func (g *Game) updatePresentationState_Platform() { + g.updatePresentationState() +} + func main() { log.Println("🚀 WASM Version startet...") diff --git a/cmd/client/presentation.go b/cmd/client/presentation.go index 289e9d8..052869a 100644 --- a/cmd/client/presentation.go +++ b/cmd/client/presentation.go @@ -6,14 +6,12 @@ import ( "image" _ "image/png" "image/color" - "math" "math/rand" "strings" "time" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/text" - "github.com/hajimehoshi/ebiten/v2/vector" "github.com/skip2/go-qrcode" "golang.org/x/image/font/basicfont" @@ -88,117 +86,8 @@ func (g *Game) updatePresentation() { g.presAssets = newAssets } -// drawPresentation zeichnet den Präsentationsmodus. -func (g *Game) drawPresentation(screen *ebiten.Image) { - // Hintergrund: Retro Dunkelblau - screen.Fill(color.RGBA{10, 15, 30, 255}) - - // Animierte Scanlines / Raster-Effekt (Retro Style) - for i := 0; i < ScreenHeight; i += 4 { - vector.DrawFilledRect(screen, 0, float32(i), float32(ScreenWidth), 1, color.RGBA{0, 0, 0, 40}, false) - } - - // Überschrift - text.Draw(screen, "PRESENTATION MODE", basicfont.Face7x13, ScreenWidth/2-80, 50, color.RGBA{255, 255, 0, 255}) - vector.StrokeLine(screen, ScreenWidth/2-90, 60, ScreenWidth/2+90, 60, 2, color.RGBA{255, 255, 0, 255}, false) - - // Zitat groß in der Mitte - if g.presQuote.Text != "" { - quoteMsg := fmt.Sprintf("\"%s\"", g.presQuote.Text) - authorMsg := fmt.Sprintf("- %s", g.presQuote.Author) - - // Einfaches Word-Wrap (sehr rudimentär) - drawWrappedText(screen, quoteMsg, ScreenWidth/2, ScreenHeight/2-20, 600, color.White) - text.Draw(screen, authorMsg, basicfont.Face7x13, ScreenWidth/2+100, ScreenHeight/2+50, color.RGBA{200, 200, 200, 255}) - } - - // Assets laufen unten durch - for _, a := range g.presAssets { - def, ok := g.world.Manifest.Assets[a.AssetID] - if !ok { continue } - - img, ok := g.assetsImages[a.AssetID] - if !ok { continue } - - op := &ebiten.DrawImageOptions{} - op.GeoM.Scale(a.Scale, a.Scale) - op.GeoM.Translate(a.X, a.Y) - - // Leichtes Pulsieren/Animation - bob := math.Sin(float64(time.Now().UnixMilli())/200.0) * 5.0 - op.GeoM.Translate(0, bob) - - screen.DrawImage(img, op) - - // Name des Assets drunter schreiben - text.Draw(screen, def.ID, basicfont.Face7x13, int(a.X), int(a.Y+80), color.RGBA{100, 200, 255, 150}) - } - - // Draw connected players (no names) - g.stateMutex.Lock() - for _, p := range g.gameState.Players { - if !p.IsAlive || p.Name == "PRESENTATION" { - continue // Skip Host and dead players - } - - // Map player X/Y to screen - playerX := p.X - playerY := p.Y - - // Keep players somewhat in bounds if they walk too far - if playerX > ScreenWidth { - playerX = math.Mod(playerX, ScreenWidth) - } else if playerX < 0 { - playerX = ScreenWidth - math.Mod(-playerX, ScreenWidth) - } - - // Draw simple player sprite - img, ok := g.assetsImages["player"] - if ok { - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(playerX, playerY) - screen.DrawImage(img, op) - } else { - // Fallback rect - vector.DrawFilledRect(screen, float32(playerX), float32(playerY), 40, 60, color.RGBA{0, 255, 0, 255}, false) - } - - // Draw Emote if active - if p.State != "" && strings.HasPrefix(p.State, "EMOTE_") { - emoteStr := p.State[6:] // e.g. EMOTE_1 -> "1" - emoteMap := map[string]string{ - "1": "❤️", - "2": "😂", - "3": "😡", - "4": "👍", - } - if emoji, ok := emoteMap[emoteStr]; ok { - text.Draw(screen, emoji, basicfont.Face7x13, int(playerX+10), int(playerY-10), color.White) - } - } - } - g.stateMutex.Unlock() - - // Draw QR Code - if g.presQRCode != nil { - qrSize := 150.0 - qrW, _ := g.presQRCode.Size() - scale := float64(qrSize) / float64(qrW) - op := &ebiten.DrawImageOptions{} - op.GeoM.Scale(scale, scale) - op.GeoM.Translate(20, 20) - screen.DrawImage(g.presQRCode, op) - - // Instruction - text.Draw(screen, "SCANNEN ZUM MITMACHEN!", basicfont.Face7x13, 20, 190, color.RGBA{255, 255, 0, 255}) - } - - // Hotkey Info - text.Draw(screen, "DRÜCKE [F1] ZUM BEENDEN", basicfont.Face7x13, ScreenWidth-250, ScreenHeight-30, color.RGBA{255, 255, 255, 100}) -} - -// drawWrappedText zeichnet Text mit automatischem Zeilenumbruch. -func drawWrappedText(screen *ebiten.Image, str string, x, y, maxWidth int, col color.Color) { +// DrawWrappedText zeichnet Text mit automatischem Zeilenumbruch. +func (g *Game) DrawWrappedText(screen *ebiten.Image, str string, x, y, maxWidth int, col color.Color) { words := strings.Split(str, " ") line := "" currY := y diff --git a/cmd/client/wasm_bridge.go b/cmd/client/wasm_bridge.go index aa3c962..b913f45 100644 --- a/cmd/client/wasm_bridge.go +++ b/cmd/client/wasm_bridge.go @@ -4,6 +4,7 @@ package main import ( + "encoding/base64" "log" "syscall/js" ) @@ -181,3 +182,41 @@ func (g *Game) sendLobbyPlayersToJS() { log.Printf("🏷️ Team-Name an JavaScript gesendet: '%s' (isHost: %v)", teamName, isHost) } } + +// notifyPresentationStarted benachrichtigt JS dass der Presi-Modus aktiv ist +func (g *Game) notifyPresentationStarted(roomID string, qrCode []byte) { + if presFunc := js.Global().Get("onPresentationStarted"); !presFunc.IsUndefined() { + // Konvertiere QR Code zu Base64 für JS + qrBase64 := "" + if qrCode != nil { + qrBase64 = "data:image/png;base64," + base64.StdEncoding.EncodeToString(qrCode) + } + presFunc.Invoke(roomID, qrBase64) + log.Printf("📺 Präsentationsmodus an JS signalisiert: %s", roomID) + } +} + +// updatePresentationState sendet den aktuellen Status (Spieler, Emotes) an JS +func (g *Game) updatePresentationState() { + if updateFunc := js.Global().Get("onPresentationUpdate"); !updateFunc.IsUndefined() { + g.stateMutex.Lock() + players := g.gameState.Players + g.stateMutex.Unlock() + + // Vereinfachte Spieler-Daten für JS + jsPlayers := make([]interface{}, 0) + for _, p := range players { + if !p.IsAlive || p.Name == "PRESENTATION" { + continue + } + jsPlayers = append(jsPlayers, map[string]interface{}{ + "id": p.ID, + "x": p.X, + "y": p.Y, + "vy": p.VY, + "state": p.State, + }) + } + updateFunc.Invoke(jsPlayers) + } +} diff --git a/cmd/client/web/game.js b/cmd/client/web/game.js index 1209e60..2ed6583 100644 --- a/cmd/client/web/game.js +++ b/cmd/client/web/game.js @@ -17,10 +17,16 @@ const UIState = { COOP_MENU: 'coop_menu', MY_CODES: 'mycodes', IMPRESSUM: 'impressum', - DATENSCHUTZ: 'datenschutz' + DATENSCHUTZ: 'datenschutz', + PRESENTATION: 'presentation' }; let currentUIState = UIState.LOADING; +let assetsManifest = null; +let presiAssets = []; +let presiPlayers = new Map(); +let presiQuoteInterval = null; +let presiAssetInterval = null; // Central UI State Manager function setUIState(newState) { @@ -133,6 +139,15 @@ function setUIState(newState) { } document.getElementById('datenschutzMenu').classList.remove('hidden'); break; + + case UIState.PRESENTATION: + if (canvas) { + canvas.classList.remove('game-active'); + canvas.style.visibility = 'hidden'; + } + document.getElementById('presentationScreen').classList.remove('hidden'); + startPresentationLogic(); + break; } } @@ -919,3 +934,138 @@ window.restartGame = restartGame; initWASM(); console.log('🎮 Game.js loaded - Retro Edition'); + +// ===== PRESENTATION MODE LOGIC ===== + +function startPresentationLogic() { + if (presiQuoteInterval) clearInterval(presiQuoteInterval); + if (presiAssetInterval) clearInterval(presiAssetInterval); + + // Initial Quote + showNextPresiQuote(); + presiQuoteInterval = setInterval(showNextPresiQuote, 8000); + + // Asset Spawning + presiAssetInterval = setInterval(spawnPresiAsset, 2500); +} + +function showNextPresiQuote() { + if (!SPRUECHE || SPRUECHE.length === 0) return; + const q = SPRUECHE[Math.floor(Math.random() * SPRUECHE.length)]; + document.getElementById('presiQuoteText').textContent = `"${q.text}"`; + document.getElementById('presiQuoteAuthor').textContent = `- ${q.author}`; + + // Simple pulse effect + const box = document.getElementById('presiQuoteBox'); + box.style.animation = 'none'; + box.offsetHeight; // trigger reflow + box.style.animation = 'emotePop 0.8s ease-out'; +} + +async function spawnPresiAsset() { + if (!assetsManifest) { + try { + const resp = await fetchWithCache('assets/assets.json'); + const data = await resp.json(); + assetsManifest = data.assets; + } catch(e) { return; } + } + + const track = document.querySelector('.presi-assets-track'); + if (!track) return; + + const assetKeys = Object.keys(assetsManifest).filter(k => + ['player', 'coin', 'eraser', 'pc-trash', 'godmode', 'jumpboost', 'magnet', 'baskeball', 'desk'].includes(k) + ); + const key = assetKeys[Math.floor(Math.random() * assetKeys.length)]; + const def = assetsManifest[key]; + + const el = document.createElement('div'); + el.className = 'presi-asset'; + + const img = document.createElement('img'); + img.src = `assets/${def.Filename || 'playernew.png'}`; + + // Scale based on JSON and screen height + const baseScale = def.Scale || 1.0; + const responsiveScale = (window.innerHeight / 720) * 3.0; // scale up for presentation + img.style.transform = `scale(${baseScale * responsiveScale})`; + + el.appendChild(img); + track.appendChild(el); + + const duration = 12 + Math.random() * 8; + el.style.animation = `assetSlide ${duration}s linear forwards`; + + // Add random vertical bobbing + el.style.bottom = `${Math.random() * 40}px`; + + setTimeout(() => el.remove(), duration * 1000); +} + +// WASM Callbacks for Presentation +window.onPresentationStarted = function(roomID, qrBase64) { + console.log('📺 Presentation started:', roomID); + document.getElementById('presiRoomCode').textContent = roomID; + const qrEl = document.getElementById('presiQRCode'); + if (qrEl) qrEl.innerHTML = qrBase64 ? `` : ''; + setUIState(UIState.PRESENTATION); +}; + +window.onPresentationUpdate = function(players) { + if (currentUIState !== UIState.PRESENTATION) return; + + const layer = document.querySelector('.presi-players-layer'); + if (!layer) return; + + const currentIds = new Set(players.map(p => p.id)); + + // Remove left players + for (let [id, el] of presiPlayers) { + if (!currentIds.has(id)) { + el.remove(); + presiPlayers.delete(id); + } + } + + // Update or add players + players.forEach(p => { + let el = presiPlayers.get(p.id); + if (!el) { + el = document.createElement('div'); + el.className = 'presi-player'; + el.innerHTML = ``; + layer.appendChild(el); + presiPlayers.set(p.id, el); + } + + // Map world coords to screen + // World width is roughly 1280, height 720 + const screenX = (p.x % 1280) / 1280 * window.innerWidth; + const screenY = (p.y / 720) * window.innerHeight; + + el.style.left = `${screenX}px`; + el.style.top = `${screenY}px`; + + // Handle Emotes + if (p.state && p.state.startsWith('EMOTE_')) { + const emoteNum = p.state.split('_')[1]; + const emotes = ["❤️", "😂", "😡", "👍"]; + const emoji = emotes[parseInt(emoteNum)-1] || "❓"; + + let emoteEl = el.querySelector('.presi-player-emote'); + if (!emoteEl) { + emoteEl = document.createElement('div'); + emoteEl.className = 'presi-player-emote'; + el.appendChild(emoteEl); + } + emoteEl.textContent = emoji; + + // Auto-remove emote text after 2s + clearTimeout(el.emoteTimeout); + el.emoteTimeout = setTimeout(() => { + if (emoteEl) emoteEl.remove(); + }, 2000); + } + }); +}; diff --git a/cmd/client/web/index.html b/cmd/client/web/index.html index 9999281..40131ea 100644 --- a/cmd/client/web/index.html +++ b/cmd/client/web/index.html @@ -293,6 +293,39 @@ + + +
diff --git a/cmd/client/web/style.css b/cmd/client/web/style.css index 00c6bc4..e82b291 100644 --- a/cmd/client/web/style.css +++ b/cmd/client/web/style.css @@ -165,3 +165,176 @@ input[type=range]{width:100%;max-width:300px} #rotate-overlay{display:flex} #game-container{display:none!important} } + +/* PRESENTATION MODE */ +.presentation-mode { + background: #0a0f1e!important; + flex-direction: column; + padding: 0!important; + overflow: hidden; +} + +.presi-background { + position: absolute; + top: 0; left: 0; width: 100%; height: 100%; + background: radial-gradient(circle at center, #1a2a4a 0%, #0a0f1e 100%); + z-index: -2; +} + +.presi-scanlines { + position: absolute; + top: 0; left: 0; width: 100%; height: 100%; + background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 100, 0.06)); + background-size: 100% 4px, 3px 100%; + pointer-events: none; + z-index: 10; +} + +.presi-header { + margin-top: 40px; + text-align: center; + z-index: 5; +} + +.presi-header h1 { + font-size: 36px; + color: #ff0; + text-shadow: 0 0 10px rgba(255, 255, 0, 0.5); + margin: 0; +} + +#presiRoomInfo { + margin-top: 10px; + background: rgba(0, 0, 0, 0.6); + padding: 5px 15px; + border: 2px solid #ff0; + display: inline-block; +} + +#presiRoomCode { + font-size: 24px; + color: #ff0; + letter-spacing: 2px; +} + +.presi-content { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 40px; + z-index: 5; +} + +#presiQuoteBox { + max-width: 800px; + text-align: center; + background: rgba(0, 0, 0, 0.4); + padding: 30px; + border-radius: 10px; + border-left: 5px solid #ff0; +} + +#presiQuoteText { + font-family: sans-serif; + font-size: 28px; + line-height: 1.4; + font-style: italic; + margin-bottom: 20px; +} + +#presiQuoteAuthor { + color: #ffcc00; + font-size: 18px; +} + +.presi-qr-container { + position: absolute; + bottom: 40px; + left: 40px; + background: white; + padding: 15px; + border-radius: 5px; + text-align: center; + z-index: 20; + box-shadow: 0 0 20px rgba(0,0,0,0.5); +} + +#presiQRCode { + width: 150px; + height: 150px; +} + +#presiQRCode img { + width: 100%; + height: 100%; +} + +.presi-qr-container p { + color: black; + font-size: 10px; + margin-top: 10px; + font-weight: bold; +} + +.presi-assets-track { + position: absolute; + bottom: 120px; + width: 100%; + height: 100px; + pointer-events: none; + z-index: 2; +} + +.presi-asset { + position: absolute; + bottom: 0; + transition: transform 0.1s linear; +} + +.presi-asset img { + display: block; + filter: drop-shadow(0 5px 15px rgba(0,0,0,0.5)); +} + +.presi-players-layer { + position: absolute; + top: 0; left: 0; width: 100%; height: 100%; + pointer-events: none; + z-index: 3; +} + +.presi-player { + position: absolute; + transition: all 0.05s linear; +} + +.presi-player-emote { + position: absolute; + top: -40px; + left: 50%; + transform: translateX(-50%); + font-size: 30px; + animation: emotePop 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +@keyframes emotePop { + 0% { transform: translateX(-50%) scale(0); opacity: 0; } + 70% { transform: translateX(-50%) scale(1.2); opacity: 1; } + 100% { transform: translateX(-50%) scale(1); opacity: 1; } +} + +.presi-footer { + position: absolute; + bottom: 20px; + right: 20px; + color: rgba(255, 255, 255, 0.4); + font-size: 10px; +} + +/* Animations */ +@keyframes assetSlide { + from { transform: translateX(100vw); } + to { transform: translateX(-200px); } +} + diff --git a/pkg/server/room.go b/pkg/server/room.go index c0a02ab..1014808 100644 --- a/pkg/server/room.go +++ b/pkg/server/room.go @@ -483,6 +483,10 @@ func (r *Room) Update() { } // === SERVER-SPEZIFISCHE LOGIK === + if r.Status == "PRESENTATION" { + // Im Präsentationsmodus: Unverwundbar und keine Grenzen + continue + } // Obstacle-Kollision prüfen -> Spieler töten hitObstacle, obstacleType := r.CheckCollision(