package main import ( "bytes" "fmt" "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" "git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game" ) type presAssetInstance struct { AssetID string X, Y float64 VX float64 Scale float64 } // generateQRCode erstellt ein ebiten.Image aus einem QR-Code func generateQRCode(url string) *ebiten.Image { pngData, err := qrcode.Encode(url, qrcode.Medium, 256) if err != nil { fmt.Println("Error generating QR code:", err) return nil } img, _, err := image.Decode(bytes.NewReader(pngData)) if err != nil { fmt.Println("Error decoding QR code:", err) return nil } return ebiten.NewImageFromImage(img) } // updatePresentation verarbeitet die Logik für den Präsentationsmodus. func (g *Game) updatePresentation() { now := time.Now() // Auto-Start Presentation Room when connected g.stateMutex.Lock() status := g.gameState.Status g.stateMutex.Unlock() if g.connected && status == "LOBBY" && g.isHost { g.SendCommand("START_PRESENTATION") } // 1. Zitat-Wechsel alle 6 Sekunden if now.After(g.presQuoteTime) { g.presQuote = game.GetRandomQuote() g.presQuoteTime = now.Add(6 * time.Second) } // 2. Assets spawnen (wenn zu wenige da sind) if len(g.presAssets) < 8 && rand.Float64() < 0.05 { // Wähle zufälliges Asset (Schüler, Lehrer, Items) assetList := []string{"player", "coin", "eraser", "pc-trash", "godmode", "jumpboost", "magnet"} id := assetList[rand.Intn(len(assetList))] g.presAssets = append(g.presAssets, presAssetInstance{ AssetID: id, X: float64(ScreenWidth + 100), Y: float64(ScreenHeight - 150 - rand.Intn(100)), VX: -(2.0 + rand.Float64()*4.0), Scale: 1.0 + rand.Float64()*0.5, }) } // 3. Assets bewegen newAssets := g.presAssets[:0] for _, a := range g.presAssets { a.X += a.VX if a.X > -200 { // Noch im Bildbereich (mit Puffer) newAssets = append(newAssets, a) } } 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) { words := strings.Split(str, " ") line := "" currY := y for _, w := range words { testLine := line + w + " " if len(testLine)*7 > maxWidth { // Grobe Schätzung Breite text.Draw(screen, line, basicfont.Face7x13, x-len(line)*7/2, currY, col) line = w + " " currY += 20 } else { line = testLine } } text.Draw(screen, line, basicfont.Face7x13, x-len(line)*7/2, currY, col) }