Private
Public Access
1
0

fix game
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 6m49s

This commit is contained in:
Sebastian Unterschütz
2026-03-22 18:46:54 +01:00
parent 656f279a89
commit 6d0d31824e
6 changed files with 265 additions and 9 deletions

View File

@@ -5,6 +5,7 @@ import (
"image/color"
"log"
"math"
"math/rand"
"time"
"github.com/hajimehoshi/ebiten/v2"
@@ -67,6 +68,15 @@ type renderSnapshot struct {
collectedPowerups map[string]bool
players map[string]game.PlayerState
// Powerup Timer HUD
myHasDoubleJump bool
myDoubleJumpRemaining float64
myDoubleJumpUsed bool
myHasGodMode bool
myGodModeRemaining float64
myHasMagnet bool
myMagnetRemaining float64
// Client-Prediction (aus predictionMutex)
prevPredX, prevPredY float64
predX, predY float64
@@ -109,6 +119,13 @@ func (g *Game) takeRenderSnapshot(screen *ebiten.Image) renderSnapshot {
if p.Name == g.playerName {
snap.isDead = !p.IsAlive || p.IsSpectator
snap.myScore = p.Score
snap.myHasDoubleJump = p.HasDoubleJump
snap.myDoubleJumpRemaining = p.DoubleJumpRemainingSeconds
snap.myDoubleJumpUsed = p.DoubleJumpUsed
snap.myHasGodMode = p.HasGodMode
snap.myGodModeRemaining = p.GodModeRemainingSeconds
snap.myHasMagnet = p.HasMagnet
snap.myMagnetRemaining = p.MagnetRemainingSeconds
break
}
}
@@ -227,6 +244,12 @@ func (g *Game) UpdateGame() {
g.predictionMutex.Unlock()
g.SendInputWithSequence(input)
// Trail: store predicted position every physics step
g.trail = append(g.trail, trailPoint{X: g.predictedX, Y: g.predictedY})
if len(g.trail) > 8 {
g.trail = g.trail[1:]
}
}
// --- 6. KAMERA LOGIK (mit Smoothing) ---
@@ -363,19 +386,46 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
snap := g.takeRenderSnapshot(screen)
g.drawBackground(screen, snap)
g.RenderGround(screen, g.camX/snap.viewScale)
g.drawWorldObjects(screen, snap)
g.drawPlayers(screen, snap)
g.drawStatusUI(screen, snap)
g.drawDeathZoneLine(screen, snap.canvasH)
g.RenderParticles(screen)
// Screen Shake: draw to offscreen buffer when active
target := screen
if g.shakeFrames > 0 {
w, h := screen.Size()
if g.shakeBuffer == nil {
g.shakeBuffer = ebiten.NewImage(w, h)
} else if bw, bh := g.shakeBuffer.Size(); bw != w || bh != h {
g.shakeBuffer = ebiten.NewImage(w, h)
}
g.shakeBuffer.Clear()
target = g.shakeBuffer
g.shakeFrames--
}
g.drawBackground(target, snap)
g.RenderGround(target, g.camX/snap.viewScale)
g.drawTeacher(target, snap)
g.drawWorldObjects(target, snap)
g.drawPlayers(target, snap)
g.drawStatusUI(target, snap)
g.drawDeathZoneLine(target, snap.canvasH)
g.RenderParticles(target)
if g.showDebug {
g.drawDebugOverlay(screen)
g.drawDebugOverlay(target)
}
if !g.keyboardUsed {
g.drawTouchControls(screen)
g.drawTouchControls(target)
}
// Blit shakeBuffer to screen with random offset
if target != screen {
ox := (rand.Float64()*2 - 1) * g.shakeIntensity
oy := (rand.Float64()*2 - 1) * g.shakeIntensity
if g.shakeFrames == 0 {
g.shakeIntensity = 0
}
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(ox, oy)
screen.DrawImage(g.shakeBuffer, op)
}
}
@@ -483,6 +533,17 @@ func (g *Game) drawPlayers(screen *ebiten.Image, snap renderSnapshot) {
sprite := g.selectPlayerSprite(p.OnGround, p.VY)
screenY := WorldToScreenYWithHeight(posY, snap.canvasH)
if p.Name == g.playerName && len(g.trail) > 1 {
for i, tp := range g.trail {
ratio := float32(i+1) / float32(len(g.trail))
alpha := uint8(ratio * 80)
r := float32(8 * ratio)
tx := float32(tp.X - g.camX)
ty := float32(WorldToScreenYWithHeight(tp.Y, snap.canvasH))
vector.DrawFilledCircle(screen, tx, ty, r, color.RGBA{200, 220, 255, alpha}, false)
}
}
g.DrawAsset(screen, sprite, posX, screenY)
name := p.Name
@@ -527,6 +588,7 @@ func (g *Game) drawStatusUI(screen *ebiten.Image, snap renderSnapshot) {
msg := fmt.Sprintf("GO IN: %d", snap.timeLeft)
text.Draw(screen, msg, basicfont.Face7x13, snap.canvasW/2-40, snap.canvasH/2, color.RGBA{255, 255, 0, 255})
case "RUNNING":
g.drawPowerupHUD(screen, snap)
g.drawDangerOverlay(screen, snap)
g.drawScoreBox(screen, snap)
if snap.isDead {
@@ -579,6 +641,54 @@ func (g *Game) drawSpectatorOverlay(screen *ebiten.Image, snap renderSnapshot) {
text.Draw(screen, fmt.Sprintf("Dein Final Score: %d", snap.myScore), basicfont.Face7x13, snap.canvasW/2-90, 55, color.RGBA{255, 255, 0, 255})
}
// drawPowerupHUD zeichnet Timer-Balken für aktive Powerups (oben links).
func (g *Game) drawPowerupHUD(screen *ebiten.Image, snap renderSnapshot) {
type bar struct {
label string
remaining float64
maxTime float64
col color.RGBA
active bool
used bool
}
bars := []bar{
{"JUMP x2", snap.myDoubleJumpRemaining, 15.0, color.RGBA{100, 200, 255, 255}, snap.myHasDoubleJump, snap.myDoubleJumpUsed},
{"GODMODE", snap.myGodModeRemaining, 10.0, color.RGBA{255, 215, 0, 255}, snap.myHasGodMode, false},
{"MAGNET", snap.myMagnetRemaining, 8.0, color.RGBA{255, 80, 220, 255}, snap.myHasMagnet, false},
}
x := float32(10)
y := float32(60)
barW := float32(110)
barH := float32(13)
for _, b := range bars {
if !b.active {
continue
}
ratio := float32(b.remaining / b.maxTime)
if ratio > 1 {
ratio = 1
}
if ratio < 0 {
ratio = 0
}
// Background
vector.DrawFilledRect(screen, x, y, barW, barH, color.RGBA{30, 30, 30, 200}, false)
// Fill — blinks red when < 30%
fillCol := b.col
if b.used {
fillCol = color.RGBA{fillCol.R / 3, fillCol.G / 3, fillCol.B / 3, fillCol.A}
}
if ratio < 0.3 && (time.Now().UnixMilli()/250)%2 == 0 {
fillCol = color.RGBA{255, 60, 60, 255}
}
vector.DrawFilledRect(screen, x, y, barW*ratio, barH, fillCol, false)
vector.StrokeRect(screen, x, y, barW, barH, 1, color.RGBA{140, 140, 140, 180}, false)
label := fmt.Sprintf("%s %.0fs", b.label, b.remaining)
text.Draw(screen, label, basicfont.Face7x13, int(x)+2, int(y)+10, color.White)
y += barH + 5
}
}
// drawDeathZoneLine zeichnet die rote Todes-Linie am linken Bildschirmrand.
func (g *Game) drawDeathZoneLine(screen *ebiten.Image, canvasH int) {
vector.StrokeLine(screen, 0, 0, 0, float32(canvasH), 10, color.RGBA{255, 0, 0, 128}, false)
@@ -651,6 +761,74 @@ func (g *Game) drawTouchControls(screen *ebiten.Image) {
text.Draw(screen, "▼", basicfont.Face7x13, int(downX)-4, int(downY)+5, color.RGBA{200, 220, 255, 180})
}
// TriggerShake aktiviert den Screen-Shake-Effekt.
func (g *Game) TriggerShake(frames int, intensity float64) {
if frames > g.shakeFrames {
g.shakeFrames = frames
}
if intensity > g.shakeIntensity {
g.shakeIntensity = intensity
}
}
// drawTeacher zeichnet den Lehrer-Charakter am linken Bildschirmrand.
func (g *Game) drawTeacher(screen *ebiten.Image, snap renderSnapshot) {
if snap.status != "RUNNING" && snap.status != "COUNTDOWN" {
return
}
danger := snap.difficultyFactor
groundY := float32(GetFloorYFromHeight(snap.canvasH))
// Teacher slides in from the left as danger increases
// At danger=0: fully offscreen (-70). At danger=1: at X=5.
teacherCX := float32(-70 + danger*75)
bodyW := float32(28)
bodyH := float32(55 + danger*15)
headR := float32(14)
bodyX := teacherCX - bodyW/2
bodyY := groundY - bodyH
alpha := uint8(40 + danger*215)
// Shadow on left edge (red vignette)
shadowW := int(20 + danger*40)
for i := 0; i < shadowW; i++ {
a := uint8(float64(70) * float64(shadowW-i) / float64(shadowW) * danger)
vector.DrawFilledRect(screen, float32(i), 0, 1, float32(snap.canvasH), color.RGBA{200, 0, 0, a}, false)
}
// Only draw body when partially visible
if teacherCX > -40 {
// Body (dark suit)
vector.DrawFilledRect(screen, bodyX, bodyY, bodyW, bodyH, color.RGBA{100, 10, 10, alpha}, false)
// Tie
vector.DrawFilledRect(screen, teacherCX-3, bodyY+4, 6, bodyH-8, color.RGBA{180, 0, 0, alpha}, false)
// Head
vector.DrawFilledCircle(screen, teacherCX, bodyY-headR, headR, color.RGBA{210, 160, 110, alpha}, false)
// Angry eyes
eyeA := uint8(200)
vector.DrawFilledCircle(screen, teacherCX-5, bodyY-headR-1, 3, color.RGBA{255, 0, 0, eyeA}, false)
vector.DrawFilledCircle(screen, teacherCX+5, bodyY-headR-1, 3, color.RGBA{255, 0, 0, eyeA}, false)
// 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)
}
// Warning text — blinks when close
if danger > 0.55 {
if (time.Now().UnixMilli()/300)%2 == 0 {
warnX := int(teacherCX) - 20
if warnX < 2 {
warnX = 2
}
text.Draw(screen, "LEHRER!", basicfont.Face7x13, warnX, int(bodyY)-20, color.RGBA{255, 50, 50, 255})
}
}
}
// --- ASSET HELPER ---
// DrawAsset zeichnet ein Asset an einer Welt-Position auf den Screen.

View File

@@ -43,6 +43,18 @@ 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})
// Highscore prüfen und aktualisieren
if myScore > g.localHighscore {
g.localHighscore = myScore
g.saveHighscore(myScore)
}
// 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})
} else {
text.Draw(screen, fmt.Sprintf("Persönlicher Highscore: %d", g.localHighscore), basicfont.Face7x13, ScreenWidth/2-80, 85, color.Gray{Y: 180})
}
// Linke Seite: Raum-Ergebnisse - Daten KOPIEREN mit Lock, dann außerhalb zeichnen
text.Draw(screen, "=== RAUM ERGEBNISSE ===", basicfont.Face7x13, 50, 120, color.RGBA{255, 255, 0, 255})

View File

@@ -40,6 +40,11 @@ var (
ColDirt = color.RGBA{101, 67, 33, 255}
)
// trailPoint speichert eine Position für den Player-Trail
type trailPoint struct {
X, Y float64
}
// InputState speichert einen einzelnen Input für Replay
type InputState struct {
Sequence uint32
@@ -118,6 +123,11 @@ type Game struct {
correctionOffsetX float64
correctionOffsetY float64
// Screen Shake
shakeFrames int
shakeIntensity float64
shakeBuffer *ebiten.Image
// Particle System
particles []Particle
particlesMutex sync.Mutex
@@ -125,6 +135,10 @@ type Game struct {
lastCollectedCoins map[string]bool // Für Coin-Partikel
lastCollectedPowerups map[string]bool // Für Powerup-Partikel
lastPlayerStates map[string]game.PlayerState // Für Death-Partikel
trail []trailPoint // Player Trail
// Highscore
localHighscore int
// Audio System
audio *AudioSystem
@@ -193,6 +207,7 @@ func NewGame() *Game {
}
g.loadAssets()
g.loadOrCreatePlayerCode()
g.localHighscore = g.loadHighscore()
// Gespeicherten Namen laden
savedName := g.loadPlayerName()
@@ -446,6 +461,11 @@ func (g *Game) DrawMenu(screen *ebiten.Image) {
title := "ESCAPE FROM TEACHER"
text.Draw(screen, title, basicfont.Face7x13, ScreenWidth/2-80, 100, ColText)
if g.localHighscore > 0 {
hsText := fmt.Sprintf("Persönlicher Highscore: %d", g.localHighscore)
text.Draw(screen, hsText, basicfont.Face7x13, ScreenWidth/2-70, 120, color.RGBA{255, 215, 0, 255})
}
// Name-Feld
fieldW := 250
nameX := ScreenWidth/2 - fieldW/2

View File

@@ -154,6 +154,7 @@ func (g *Game) SpawnDeathParticles(x, y float64) {
FadeOut: true,
})
}
g.TriggerShake(12, 7.0)
}
// UpdateParticles aktualisiert alle Partikel

View File

@@ -6,6 +6,7 @@ package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"io/ioutil"
"log"
"strings"
@@ -61,3 +62,19 @@ func (g *Game) savePlayerName(name string) {
log.Printf("💾 Spielername gespeichert: %s", name)
}
}
func (g *Game) loadHighscore() int {
const hsFile = "highscore.txt"
if data, err := ioutil.ReadFile(hsFile); err == nil {
var score int
if _, err2 := fmt.Sscanf(strings.TrimSpace(string(data)), "%d", &score); err2 == nil {
return score
}
}
return 0
}
func (g *Game) saveHighscore(score int) {
const hsFile = "highscore.txt"
ioutil.WriteFile(hsFile, []byte(fmt.Sprintf("%d", score)), 0644)
}

View File

@@ -6,6 +6,7 @@ package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"syscall/js"
)
@@ -73,3 +74,30 @@ func (g *Game) savePlayerName(name string) {
}
}
}
func (g *Game) loadHighscore() int {
const storageKey = "escape_from_teacher_highscore"
if jsGlobal := js.Global(); !jsGlobal.IsUndefined() {
localStorage := jsGlobal.Get("localStorage")
if !localStorage.IsUndefined() {
stored := localStorage.Call("getItem", storageKey)
if !stored.IsNull() && stored.String() != "" {
var score int
if _, err := fmt.Sscanf(stored.String(), "%d", &score); err == nil {
return score
}
}
}
}
return 0
}
func (g *Game) saveHighscore(score int) {
const storageKey = "escape_from_teacher_highscore"
if jsGlobal := js.Global(); !jsGlobal.IsUndefined() {
localStorage := jsGlobal.Get("localStorage")
if !localStorage.IsUndefined() {
localStorage.Call("setItem", storageKey, fmt.Sprintf("%d", score))
}
}
}