fix game
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 6m49s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 6m49s
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -154,6 +154,7 @@ func (g *Game) SpawnDeathParticles(x, y float64) {
|
||||
FadeOut: true,
|
||||
})
|
||||
}
|
||||
g.TriggerShake(12, 7.0)
|
||||
}
|
||||
|
||||
// UpdateParticles aktualisiert alle Partikel
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user