Private
Public Access
1
0
Files
it232Abschied/websocket.go
Sebastian Unterschütz fd2497a342 fix Credits
2025-11-30 20:48:21 +01:00

337 lines
7.4 KiB
Go

package main
import (
"log"
"math"
"net/http"
"time"
"github.com/gorilla/websocket"
)
const (
ServerTickRate = 50 * time.Millisecond
BufferAhead = 60
SpawnXStart = 2000.0
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
// Protokoll
type WSInputMsg struct {
Type string `json:"type"`
Input string `json:"input"`
Tick int `json:"tick"`
PosY float64 `json:"y"`
VelY float64 `json:"vy"`
}
type WSServerMsg struct {
Type string `json:"type"`
Obstacles []ActiveObstacle `json:"obstacles"`
Platforms []ActivePlatform `json:"platforms"`
ServerTick int `json:"serverTick"`
Score int `json:"score"`
PowerUps PowerUpState `json:"powerups"`
SessionID string `json:"sessionId"`
Ts int `json:"ts,omitempty"`
CurrentSpeed float64 `json:"currentSpeed"`
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
// 1. Session Init
sessionID := "ws-" + time.Now().Format("150405999")
err = rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{
"score": 0, // Startwert
"is_dead": 0,
"created_at": time.Now().Format("02.01.2006 15:04"),
}).Err()
if err != nil {
log.Println("Redis Init Fehler:", err)
return
}
rdb.Expire(ctx, "session:"+sessionID, 24*time.Hour)
// Session ID senden
conn.WriteJSON(WSServerMsg{Type: "init", SessionID: sessionID})
state := SimState{
SessionID: sessionID,
RNG: NewRNG(time.Now().UnixNano()),
Score: 0,
Ticks: 0,
PosY: PlayerYBase,
NextSpawnTick: 0,
Chunks: loadChunksFromRedis(),
}
// Channel größer machen, damit bei Lag nichts blockiert
inputChan := make(chan WSInputMsg, 100)
closeChan := make(chan struct{})
// LESE-ROUTINE
go func() {
defer close(closeChan)
for {
var msg WSInputMsg
if err := conn.ReadJSON(&msg); err != nil {
return
}
inputChan <- msg
}
}()
// GAME LOOP (High Performance)
ticker := time.NewTicker(ServerTickRate)
defer ticker.Stop()
// Input State
var pendingJump bool
var isCrouching bool
generatedHeadTick := 0
for {
select {
case <-closeChan:
return
case <-ticker.C:
InputLoop:
for {
select {
case msg := <-inputChan:
if msg.Type == "input" {
if msg.Input == "JUMP" {
pendingJump = true
}
if msg.Input == "DUCK_START" {
isCrouching = true
}
if msg.Input == "DUCK_END" {
isCrouching = false
}
if msg.Input == "DEATH" {
state.IsDead = true
}
}
if msg.Type == "ping" {
conn.WriteJSON(WSServerMsg{Type: "pong", Ts: msg.Tick})
}
if msg.Type == "sync" {
diff := math.Abs(state.PosY - msg.PosY)
if diff < 100.0 {
state.PosY = msg.PosY
state.VelY = msg.VelY
}
}
if msg.Type == "debug" {
spd := calculateSpeed(state.Ticks)
conn.WriteJSON(WSServerMsg{
Type: "debug_sync",
ServerTick: state.Ticks,
Obstacles: state.Obstacles,
Platforms: state.Platforms,
Score: state.Score,
CurrentSpeed: spd,
})
log.Printf("🐞 Debug Snapshot an Client gesendet (Tick %d)", state.Ticks)
}
default:
break InputLoop
}
}
state.Ticks++
state.Score++
currentSpeed := calculateSpeed(state.Ticks)
updatePhysics(&state, pendingJump, isCrouching, currentSpeed)
pendingJump = false
checkCollisions(&state, isCrouching, currentSpeed)
if state.IsDead {
rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{
"score": state.Score,
"is_dead": 1,
})
rdb.Expire(ctx, "session:"+sessionID, 24*time.Hour)
conn.WriteJSON(WSServerMsg{Type: "dead", Score: state.Score})
return
}
moveWorld(&state, currentSpeed)
targetTick := state.Ticks + BufferAhead
var newObs []ActiveObstacle
var newPlats []ActivePlatform
loops := 0
for generatedHeadTick < targetTick && loops < 10 {
generatedHeadTick++
loops++
o, p := generateFutureObjects(&state, generatedHeadTick, currentSpeed)
if len(o) > 0 {
newObs = append(newObs, o...)
state.Obstacles = append(state.Obstacles, o...)
}
if len(p) > 0 {
newPlats = append(newPlats, p...)
state.Platforms = append(state.Platforms, p...)
}
}
if len(newObs) > 0 || len(newPlats) > 0 || state.Ticks%15 == 0 {
msg := WSServerMsg{
Type: "chunk",
ServerTick: state.Ticks,
Score: state.Score,
Obstacles: newObs,
Platforms: newPlats,
PowerUps: PowerUpState{
GodLives: state.GodLives, HasBat: state.HasBat, BootTicks: state.BootTicks,
},
}
conn.WriteJSON(msg)
}
}
}
}
func generateFutureObjects(s *SimState, tick int, speed float64) ([]ActiveObstacle, []ActivePlatform) {
var createdObs []ActiveObstacle
var createdPlats []ActivePlatform
// Initialisierung beim ersten Lauf
if s.NextSpawnTick == 0 {
s.NextSpawnTick = tick + 50
}
// Ist es Zeit für etwas Neues?
if tick >= s.NextSpawnTick {
spawnX := SpawnXStart
chunkCount := len(defaultConfig.Chunks)
if chunkCount > 0 && s.RNG.NextFloat() > 0.8 {
idx := int(s.RNG.NextRange(0, float64(chunkCount)))
chunk := defaultConfig.Chunks[idx]
for _, p := range chunk.Platforms {
createdPlats = append(createdPlats, ActivePlatform{
X: spawnX + p.X,
Y: p.Y,
Width: p.Width,
Height: p.Height,
})
}
for _, o := range chunk.Obstacles {
speech := ""
for _, def := range defaultConfig.Obstacles {
if def.ID == o.ID {
if def.CanTalk && len(def.SpeechLines) > 0 {
if s.RNG.NextFloat() > 0.7 {
sIdx := int(s.RNG.NextRange(0, float64(len(def.SpeechLines))))
speech = def.SpeechLines[sIdx]
}
}
break
}
}
createdObs = append(createdObs, ActiveObstacle{
ID: o.ID,
Type: o.Type,
X: spawnX + o.X,
Y: o.Y,
Width: o.Width,
Height: o.Height,
Speech: speech,
})
}
width := float64(chunk.TotalWidth)
if width == 0 {
width = 2000.0
}
s.NextSpawnTick = tick + int(width/speed)
} else {
gap := 400 + int(s.RNG.NextRange(0, 500))
s.NextSpawnTick = tick + int(float64(gap)/speed)
defs := defaultConfig.Obstacles
if len(defs) > 0 {
isBoss := (tick % 1500) > 1200
var pool []ObstacleDef
for _, d := range defs {
if isBoss {
if d.ID == "principal" || d.ID == "trashcan" {
pool = append(pool, d)
}
} else {
if d.ID != "principal" {
pool = append(pool, d)
}
}
}
def := s.RNG.PickDef(pool)
if def != nil {
if def.Type == "powerup" && s.RNG.NextFloat() > 0.1 {
def = nil
}
if def != nil {
// Speech Logik
speech := ""
if def.CanTalk && len(def.SpeechLines) > 0 {
if s.RNG.NextFloat() > 0.7 {
sIdx := int(s.RNG.NextRange(0, float64(len(def.SpeechLines))))
speech = def.SpeechLines[sIdx]
}
}
spawnY := GroundY - def.Height - def.YOffset
createdObs = append(createdObs, ActiveObstacle{
ID: def.ID,
Type: def.Type,
X: spawnX,
Y: spawnY,
Width: def.Width,
Height: def.Height,
Speech: speech,
})
}
}
}
}
}
return createdObs, createdPlats
}