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 @@
+
+