From 568ce516e77b67e80e6ce4f90b1c4a650f2c6029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Untersch=C3=BCtz?= Date: Wed, 22 Apr 2026 19:53:15 +0200 Subject: [PATCH] add teacher and milestone quotes: implement random quotes, speech bubbles, and milestone achievements display --- cmd/client/game_render.go | 75 ++++++++++++++++++++++++++++++++++- cmd/client/gameover_native.go | 12 +++++- cmd/client/main.go | 31 +++++++++++++++ 3 files changed, 115 insertions(+), 3 deletions(-) diff --git a/cmd/client/game_render.go b/cmd/client/game_render.go index e4e714d..8f9b2bf 100644 --- a/cmd/client/game_render.go +++ b/cmd/client/game_render.go @@ -260,6 +260,9 @@ func (g *Game) UpdateGame() { if len(g.trail) > 8 { g.trail = g.trail[1:] } + + // --- Zitate & Meilensteine --- + g.updateQuotes() } // --- 6. KAMERA LOGIK (mit Smoothing) --- @@ -604,6 +607,13 @@ func (g *Game) drawStatusUI(screen *ebiten.Image, snap renderSnapshot) { if snap.isDead { g.drawSpectatorOverlay(screen, snap) } + + // --- MEILENSTEIN-QUOTE --- + if time.Now().Before(g.milestoneQuoteTime) { + msg := fmt.Sprintf("🎉 %d PUNKTE! \"%s\"", g.lastMilestone, g.milestoneQuote.Text) + tw := float32(len(msg) * 7) + text.Draw(screen, msg, basicfont.Face7x13, int(float32(snap.canvasW)/2-tw/2), 60, color.RGBA{255, 255, 0, 255}) + } } } @@ -781,6 +791,64 @@ func (g *Game) TriggerShake(frames int, intensity float64) { } } +// updateQuotes verarbeitet die Logik fĂŒr zufĂ€llige Lehrer-SprĂŒche und Meilensteine. +func (g *Game) updateQuotes() { + g.stateMutex.Lock() + status := g.gameState.Status + myScore := 0 + for _, p := range g.gameState.Players { + if p.Name == g.playerName { + myScore = p.Score + break + } + } + g.stateMutex.Unlock() + + if status != "RUNNING" { + return + } + + now := time.Now() + + // 1. ZufĂ€llige Lehrer-SprĂŒche (alle 10-25 Sekunden) + if now.After(g.teacherQuoteTime) { + g.teacherQuote = game.GetRandomQuote() + // NĂ€chster Spruch in 15-30 Sekunden + g.teacherQuoteTime = now.Add(time.Duration(15+rand.Intn(15)) * time.Second) + } + + // 2. Meilensteine (alle 1000 Punkte) + milestone := (myScore / 1000) * 1000 + if milestone > 0 && milestone > g.lastMilestone { + g.lastMilestone = milestone + g.milestoneQuote = game.GetRandomQuote() + g.milestoneQuoteTime = now.Add(4 * time.Second) // 4 Sekunden anzeigen + log.Printf("🎉 Meilenstein erreicht: %d Punkte!", milestone) + } +} + +// drawSpeechBubble zeichnet eine einfache Sprechblase mit Text. +func (g *Game) drawSpeechBubble(screen *ebiten.Image, x, y float32, msg string) { + // Text-Breite grob schĂ€tzen (Face7x13: ca 7px pro Zeichen) + tw := float32(len(msg) * 7) + th := float32(15) + padding := float32(8) + + bx := x + 10 + by := y - th - padding*2 + bw := tw + padding*2 + bh := th + padding*2 + + // Hintergrund + vector.DrawFilledRect(screen, bx, by, bw, bh, color.RGBA{255, 255, 255, 220}, false) + vector.StrokeRect(screen, bx, by, bw, bh, 2, color.Black, false) + + // Kleiner Pfeil + vector.DrawFilledCircle(screen, bx, by+bh/2, 5, color.RGBA{255, 255, 255, 220}, false) + + text.Draw(screen, msg, basicfont.Face7x13, int(bx+padding), int(by+padding+10), color.Black) +} + // drawTeacher zeichnet den Lehrer-Charakter am linken Bildschirmrand. func (g *Game) drawTeacher(screen *ebiten.Image, snap renderSnapshot) { if snap.status != "RUNNING" && snap.status != "COUNTDOWN" { @@ -825,8 +893,13 @@ func (g *Game) drawTeacher(screen *ebiten.Image, snap renderSnapshot) { // Legs vector.DrawFilledRect(screen, bodyX+1, groundY, bodyW/2-2, float32(snap.canvasH), color.RGBA{60, 10, 10, alpha}, false) vector.DrawFilledRect(screen, bodyX+bodyW/2+1, groundY, bodyW/2-2, float32(snap.canvasH), color.RGBA{60, 10, 10, alpha}, false) - } + // --- SPRECHBLASE --- + if time.Now().Before(g.teacherQuoteTime.Add(-10 * time.Second)) && g.teacherQuote.Text != "" { + // Wir zeigen den Spruch fĂŒr 5-10 Sekunden an + g.drawSpeechBubble(screen, teacherCX+15, bodyY-20, g.teacherQuote.Text) + } + } // Warning text — blinks when close if danger > 0.55 { if (time.Now().UnixMilli()/300)%2 == 0 { diff --git a/cmd/client/gameover_native.go b/cmd/client/gameover_native.go index b141583..3d5e27e 100644 --- a/cmd/client/gameover_native.go +++ b/cmd/client/gameover_native.go @@ -45,6 +45,14 @@ func (g *Game) drawGameOverScreen(screen *ebiten.Image, myScore int) { // Großes GAME OVER text.Draw(screen, "GAME OVER", basicfont.Face7x13, ScreenWidth/2-50, 60, color.RGBA{255, 0, 0, 255}) + // Lehrer-Spruch zum Abschied + if g.deathQuote.Text != "" { + quoteMsg := fmt.Sprintf("\"%s\"", g.deathQuote.Text) + quoteW := len(quoteMsg) * 7 + // Zentrieren und ggf. umbrechen wenn zu lang (hier erstmal einfach zentriert) + text.Draw(screen, quoteMsg, basicfont.Face7x13, ScreenWidth/2-quoteW/2, 80, color.RGBA{200, 200, 200, 255}) + } + // Highscore prĂŒfen und aktualisieren if myScore > g.localHighscore { g.localHighscore = myScore @@ -52,9 +60,9 @@ func (g *Game) drawGameOverScreen(screen *ebiten.Image, myScore int) { } // Persönlicher Highscore anzeigen if myScore == g.localHighscore && myScore > 0 { - text.Draw(screen, fmt.Sprintf("★ NEUER REKORD: %d ★", g.localHighscore), basicfont.Face7x13, ScreenWidth/2-80, 85, color.RGBA{255, 215, 0, 255}) + text.Draw(screen, fmt.Sprintf("★ NEUER REKORD: %d ★", g.localHighscore), basicfont.Face7x13, ScreenWidth/2-80, 105, color.RGBA{255, 215, 0, 255}) } else { - text.Draw(screen, fmt.Sprintf("Persönlicher Highscore: %d", g.localHighscore), basicfont.Face7x13, ScreenWidth/2-80, 85, color.Gray{Y: 180}) + text.Draw(screen, fmt.Sprintf("Persönlicher Highscore: %d", g.localHighscore), basicfont.Face7x13, ScreenWidth/2-100, 105, color.Gray{Y: 180}) } // Linke Seite: Raum-Ergebnisse - Daten KOPIEREN mit Lock, dann außerhalb zeichnen diff --git a/cmd/client/main.go b/cmd/client/main.go index 62e132b..1e82064 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -29,6 +29,7 @@ const ( StateLobby = 1 StateGame = 2 StateLeaderboard = 3 + StatePresentation = 4 RefFloorY = 540 // Server-Welt Boden-Position (unverĂ€nderlich) ) @@ -170,6 +171,20 @@ type Game struct { // Audio System audio *AudioSystem + // Zitate / SprĂŒche + teacherQuote game.Quote + teacherQuoteTime time.Time + milestoneQuote game.Quote + milestoneQuoteTime time.Time + deathQuote game.Quote + lastMilestone int + + // Presentation Mode + presQuote game.Quote + presQuoteTime time.Time + presAssets []presAssetInstance + lastPresUpdate time.Time + // Kamera camX float64 @@ -262,6 +277,17 @@ func (g *Game) Update() error { g.showDebug = !g.showDebug } + // Presentation Toggle (F1) + if inpututil.IsKeyJustPressed(ebiten.KeyF1) { + if g.appState == StatePresentation { + g.appState = StateMenu + } else { + g.appState = StatePresentation + g.presAssets = nil // Reset assets + g.presQuoteTime = time.Now() // Force immediate first quote + } + } + // Pending Inputs zĂ€hlen fĂŒr Debug g.predictionMutex.Lock() g.pendingInputCount = len(g.pendingInputs) @@ -302,6 +328,7 @@ func (g *Game) Update() error { } if currentStatus == "GAMEOVER" && g.lastStatus == "RUNNING" { g.audio.StopMusic() + g.deathQuote = game.GetRandomQuote() if g.gameMode == "solo" { g.verifyRoundResult() } @@ -317,6 +344,8 @@ func (g *Game) Update() error { g.UpdateGame() case StateLeaderboard: g.updateLeaderboard() + case StatePresentation: + g.updatePresentation() } return nil } @@ -478,6 +507,8 @@ func (g *Game) draw(screen *ebiten.Image) { g.DrawGame(screen) case StateLeaderboard: g.drawLeaderboard(screen) + case StatePresentation: + g.drawPresentation(screen) } }