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.