337 lines
7.5 KiB
Go
337 lines
7.5 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 // Client weg
|
|
|
|
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 { // 30% Wahrscheinlichkeit
|
|
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
|
|
}
|