- Refactor dirt and stone generation to optimize visible depth and adjust randomization.
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 8m32s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 8m32s
- Remove unused `StartWebSocketGateway` function from `websocket_gateway.go`. - Add security checks to track player-room mapping, enforce valid input, and prevent ID spoofing in `gateway.go`. - Refactor touch control logic to dynamically position joystick and buttons above gameplay floor. - Introduce dynamic floor Y-coordinate calculation (`GetFloorYFromHeight`) for better scaling across different screen sizes. - Adjust rendering logic to align assets, particles, and debug visuals with dynamic screen height transformations. - Update canvas CSS to support fullscreen scaling without center alignment.
This commit is contained in:
@@ -115,6 +115,10 @@ func (g *Game) UpdateGame() {
|
|||||||
func (g *Game) handleTouchInput() {
|
func (g *Game) handleTouchInput() {
|
||||||
touches := ebiten.TouchIDs()
|
touches := ebiten.TouchIDs()
|
||||||
|
|
||||||
|
// WICHTIG: Joystick-Base-Position wird jetzt beim Rendering gesetzt (DrawGame)
|
||||||
|
// um sicherzustellen dass die gleiche Canvas-Höhe verwendet wird!
|
||||||
|
// Wir verwenden hier nur die gecachte Position.
|
||||||
|
|
||||||
// Reset, wenn keine Finger mehr auf dem Display sind
|
// Reset, wenn keine Finger mehr auf dem Display sind
|
||||||
if len(touches) == 0 {
|
if len(touches) == 0 {
|
||||||
g.joyActive = false
|
g.joyActive = false
|
||||||
@@ -228,6 +232,10 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
|
|||||||
}
|
}
|
||||||
g.stateMutex.Unlock()
|
g.stateMutex.Unlock()
|
||||||
|
|
||||||
|
// Canvas-Größe und Scale-Faktor
|
||||||
|
canvasW, canvasH := screen.Size()
|
||||||
|
viewScale := GetScaleFromHeight(canvasH)
|
||||||
|
|
||||||
// 1. Hintergrund (wechselt alle 5000 Punkte)
|
// 1. Hintergrund (wechselt alle 5000 Punkte)
|
||||||
backgroundID := "background"
|
backgroundID := "background"
|
||||||
if myScore >= 10000 {
|
if myScore >= 10000 {
|
||||||
@@ -240,8 +248,6 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
|
|||||||
if bgImg, exists := g.assetsImages[backgroundID]; exists && bgImg != nil {
|
if bgImg, exists := g.assetsImages[backgroundID]; exists && bgImg != nil {
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
|
||||||
// Tatsächliche Canvas-Größe verwenden (nicht nur ScreenWidth/Height)
|
|
||||||
canvasW, canvasH := screen.Size()
|
|
||||||
bgW, bgH := bgImg.Size()
|
bgW, bgH := bgImg.Size()
|
||||||
|
|
||||||
// Skalierung berechnen, um Canvas komplett zu füllen
|
// Skalierung berechnen, um Canvas komplett zu füllen
|
||||||
@@ -265,12 +271,17 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Boden zeichnen (prozedural mit Dirt und Steinen, bewegt sich mit Kamera)
|
// Boden zeichnen (prozedural mit Dirt und Steinen, bewegt sich mit Kamera)
|
||||||
g.RenderGround(screen, g.camX)
|
// Mit viewScale multiplizieren damit auf Mobile mehr Welt sichtbar ist
|
||||||
|
effectiveCamX := g.camX / viewScale
|
||||||
|
g.RenderGround(screen, effectiveCamX)
|
||||||
|
|
||||||
// State Locken für Datenzugriff
|
// State Locken für Datenzugriff
|
||||||
g.stateMutex.Lock()
|
g.stateMutex.Lock()
|
||||||
defer g.stateMutex.Unlock()
|
defer g.stateMutex.Unlock()
|
||||||
|
|
||||||
|
// Screen-Höhe für Y-Transformation (canvasH bereits oben definiert)
|
||||||
|
// _, canvasH = screen.Size() // nicht nötig, bereits definiert
|
||||||
|
|
||||||
// 2. Chunks (Welt-Objekte)
|
// 2. Chunks (Welt-Objekte)
|
||||||
for _, activeChunk := range g.gameState.WorldChunks {
|
for _, activeChunk := range g.gameState.WorldChunks {
|
||||||
chunkDef, exists := g.world.ChunkLibrary[activeChunk.ChunkID]
|
chunkDef, exists := g.world.ChunkLibrary[activeChunk.ChunkID]
|
||||||
@@ -303,19 +314,19 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Asset zeichnen
|
// Asset zeichnen (mit Welt-zu-Screen-Y-Transformation)
|
||||||
g.DrawAsset(screen, obj.AssetID, activeChunk.X+obj.X, obj.Y)
|
g.DrawAsset(screen, obj.AssetID, activeChunk.X+obj.X, WorldToScreenYWithHeight(obj.Y, canvasH))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2.5 Bewegende Plattformen (von Server synchronisiert)
|
// 2.5 Bewegende Plattformen (von Server synchronisiert)
|
||||||
for _, mp := range g.gameState.MovingPlatforms {
|
for _, mp := range g.gameState.MovingPlatforms {
|
||||||
g.DrawAsset(screen, mp.AssetID, mp.X, mp.Y)
|
g.DrawAsset(screen, mp.AssetID, mp.X, WorldToScreenYWithHeight(mp.Y, canvasH))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2.6 DEBUG: Basis-Boden-Collider visualisieren (GRÜN) - NUR IM DEBUG-MODUS
|
// 2.6 DEBUG: Basis-Boden-Collider visualisieren (GRÜN) - NUR IM DEBUG-MODUS
|
||||||
if g.showDebug {
|
if g.showDebug {
|
||||||
vector.StrokeRect(screen, float32(-g.camX), float32(540), 10000, float32(5000), float32(2), color.RGBA{0, 255, 0, 255}, false)
|
vector.StrokeRect(screen, float32(-g.camX), float32(WorldToScreenYWithHeight(540, canvasH)), 10000, float32(5000), float32(2), color.RGBA{0, 255, 0, 255}, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Spieler
|
// 3. Spieler
|
||||||
@@ -351,31 +362,35 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
g.DrawAsset(screen, sprite, posX, posY)
|
// Konvertiere Welt-Y zu Screen-Y für korrektes Rendering
|
||||||
|
_, canvasH := screen.Size()
|
||||||
|
screenY := WorldToScreenYWithHeight(posY, canvasH)
|
||||||
|
|
||||||
|
g.DrawAsset(screen, sprite, posX, screenY)
|
||||||
|
|
||||||
// Name Tag
|
// Name Tag
|
||||||
name := p.Name
|
name := p.Name
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = id
|
name = id
|
||||||
}
|
}
|
||||||
text.Draw(screen, name, basicfont.Face7x13, int(posX-g.camX), int(posY-25), ColText)
|
text.Draw(screen, name, basicfont.Face7x13, int(posX-g.camX), int(screenY-25), ColText)
|
||||||
|
|
||||||
// HITBOX VISUALISIERUNG (NUR IM DEBUG-MODUS)
|
// HITBOX VISUALISIERUNG (NUR IM DEBUG-MODUS)
|
||||||
if g.showDebug {
|
if g.showDebug {
|
||||||
if def, ok := g.world.Manifest.Assets["player"]; ok {
|
if def, ok := g.world.Manifest.Assets["player"]; ok {
|
||||||
// Spieler-Hitbox (ROT)
|
// Spieler-Hitbox (ROT) - mit Screen-Y-Transformation
|
||||||
hx := float32(posX + def.DrawOffX + def.Hitbox.OffsetX - g.camX)
|
hx := float32(posX + def.DrawOffX + def.Hitbox.OffsetX - g.camX)
|
||||||
hy := float32(posY + def.DrawOffY + def.Hitbox.OffsetY)
|
hy := float32(screenY + def.DrawOffY + def.Hitbox.OffsetY)
|
||||||
vector.StrokeRect(screen, hx, hy, float32(def.Hitbox.W), float32(def.Hitbox.H), 3, color.RGBA{255, 0, 0, 255}, false)
|
vector.StrokeRect(screen, hx, hy, float32(def.Hitbox.W), float32(def.Hitbox.H), 3, color.RGBA{255, 0, 0, 255}, false)
|
||||||
|
|
||||||
// Spieler-Position als Punkt (GELB)
|
// Spieler-Position als Punkt (GELB) - mit Screen-Y-Transformation
|
||||||
vector.DrawFilledCircle(screen, float32(posX-g.camX), float32(posY), 5, color.RGBA{255, 255, 0, 255}, false)
|
vector.DrawFilledCircle(screen, float32(posX-g.camX), float32(screenY), 5, color.RGBA{255, 255, 0, 255}, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. UI Status (Canvas-relativ)
|
// 4. UI Status (Canvas-relativ)
|
||||||
canvasW, canvasH := screen.Size()
|
canvasW, canvasH = screen.Size()
|
||||||
|
|
||||||
if g.gameState.Status == "COUNTDOWN" {
|
if g.gameState.Status == "COUNTDOWN" {
|
||||||
msg := fmt.Sprintf("GO IN: %d", g.gameState.TimeLeft)
|
msg := fmt.Sprintf("GO IN: %d", g.gameState.TimeLeft)
|
||||||
@@ -430,25 +445,37 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
|
|||||||
|
|
||||||
// 8. TOUCH CONTROLS OVERLAY (nur wenn Tastatur nicht benutzt wurde)
|
// 8. TOUCH CONTROLS OVERLAY (nur wenn Tastatur nicht benutzt wurde)
|
||||||
if !g.keyboardUsed {
|
if !g.keyboardUsed {
|
||||||
canvasW, _ := screen.Size()
|
canvasW, canvasH := screen.Size()
|
||||||
|
|
||||||
|
// Cache Canvas-Höhe für Touch-Input (wird in UpdateGame/handleTouchInput verwendet)
|
||||||
|
g.lastCanvasHeight = canvasH
|
||||||
|
|
||||||
|
// A) Joystick Base (unten links, über dem Boden positioniert)
|
||||||
|
// WICHTIG: Verwende die gleiche Berechnung wie in handleTouchInput()!
|
||||||
|
g.joyBaseX = 150.0
|
||||||
|
floorY := GetFloorYFromHeight(canvasH)
|
||||||
|
g.joyBaseY = floorY - 50.0 // 50px über dem Boden (sichtbar über der Erde)
|
||||||
|
|
||||||
|
// Wenn Joystick nicht aktiv, Stick = Base
|
||||||
|
if !g.joyActive {
|
||||||
|
g.joyStickX = g.joyBaseX
|
||||||
|
g.joyStickY = g.joyBaseY
|
||||||
|
}
|
||||||
|
|
||||||
// A) Joystick Base (unten links, relativ zu Viewport)
|
|
||||||
joyX := 150.0
|
|
||||||
joyY := float64(ScreenHeight) - 150.0 // ScreenHeight statt canvasH!
|
|
||||||
baseCol := color.RGBA{80, 80, 80, 50}
|
baseCol := color.RGBA{80, 80, 80, 50}
|
||||||
vector.DrawFilledCircle(screen, float32(joyX), float32(joyY), 60, baseCol, false)
|
vector.DrawFilledCircle(screen, float32(g.joyBaseX), float32(g.joyBaseY), 60, baseCol, false)
|
||||||
vector.StrokeCircle(screen, float32(joyX), float32(joyY), 60, 2, color.RGBA{100, 100, 100, 100}, false)
|
vector.StrokeCircle(screen, float32(g.joyBaseX), float32(g.joyBaseY), 60, 2, color.RGBA{100, 100, 100, 100}, false)
|
||||||
|
|
||||||
// B) Joystick Knob (relativ zu Base, nicht zu Canvas)
|
// B) Joystick Knob (relativ zu Base)
|
||||||
knobCol := color.RGBA{100, 100, 100, 80}
|
knobCol := color.RGBA{100, 100, 100, 80}
|
||||||
if g.joyActive {
|
if g.joyActive {
|
||||||
knobCol = color.RGBA{100, 255, 100, 120}
|
knobCol = color.RGBA{100, 255, 100, 120}
|
||||||
}
|
}
|
||||||
vector.DrawFilledCircle(screen, float32(g.joyStickX), float32(g.joyStickY), 30, knobCol, false)
|
vector.DrawFilledCircle(screen, float32(g.joyStickX), float32(g.joyStickY), 30, knobCol, false)
|
||||||
|
|
||||||
// C) Jump Button (unten rechts, relativ zu Viewport)
|
// C) Jump Button (unten rechts, über dem Boden positioniert)
|
||||||
jumpX := float32(canvasW) - 150
|
jumpX := float32(canvasW) - 150
|
||||||
jumpY := float32(ScreenHeight) - 150 // ScreenHeight statt canvasH!
|
jumpY := float32(floorY) - 50 // 50px über dem Boden
|
||||||
vector.DrawFilledCircle(screen, jumpX, jumpY, 50, color.RGBA{255, 0, 0, 50}, false)
|
vector.DrawFilledCircle(screen, jumpX, jumpY, 50, color.RGBA{255, 0, 0, 50}, false)
|
||||||
vector.StrokeCircle(screen, jumpX, jumpY, 50, 2, color.RGBA{255, 0, 0, 100}, false)
|
vector.StrokeCircle(screen, jumpX, jumpY, 50, 2, color.RGBA{255, 0, 0, 100}, false)
|
||||||
text.Draw(screen, "JUMP", basicfont.Face7x13, int(jumpX)-15, int(jumpY)+5, color.RGBA{255, 255, 255, 150})
|
text.Draw(screen, "JUMP", basicfont.Face7x13, int(jumpX)-15, int(jumpY)+5, color.RGBA{255, 255, 255, 150})
|
||||||
@@ -465,13 +492,17 @@ func (g *Game) DrawAsset(screen *ebiten.Image, assetID string, worldX, worldY fl
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Screen Position berechnen (Welt - Kamera)
|
// Canvas-Größe und View-Scale
|
||||||
screenX := worldX - g.camX
|
canvasW, canvasH := screen.Size()
|
||||||
|
viewScale := GetScaleFromHeight(canvasH)
|
||||||
|
|
||||||
|
// 2. Screen Position berechnen (Welt - Kamera, mit Scale)
|
||||||
|
effectiveCamX := g.camX / viewScale
|
||||||
|
screenX := (worldX - effectiveCamX) * viewScale
|
||||||
screenY := worldY
|
screenY := worldY
|
||||||
|
|
||||||
// Optimierung: Nicht zeichnen, wenn komplett außerhalb (Canvas-Breite verwenden)
|
// Optimierung: Nicht zeichnen, wenn komplett außerhalb (Canvas-Breite verwenden)
|
||||||
// Großzügiger Culling-Bereich für früheres Spawning (800px statt 200px)
|
// Großzügiger Culling-Bereich für früheres Spawning (800px statt 200px)
|
||||||
canvasW, _ := screen.Size()
|
|
||||||
if screenX < -800 || screenX > float64(canvasW)+800 {
|
if screenX < -800 || screenX > float64(canvasW)+800 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -485,13 +516,14 @@ func (g *Game) DrawAsset(screen *ebiten.Image, assetID string, worldX, worldY fl
|
|||||||
// Filter für bessere Skalierung (besonders bei großen Sprites)
|
// Filter für bessere Skalierung (besonders bei großen Sprites)
|
||||||
op.Filter = ebiten.FilterLinear
|
op.Filter = ebiten.FilterLinear
|
||||||
|
|
||||||
// Skalieren
|
// Skalieren: Asset-Scale * View-Scale (auf Mobile kleiner)
|
||||||
op.GeoM.Scale(def.Scale, def.Scale)
|
finalScale := def.Scale * viewScale
|
||||||
|
op.GeoM.Scale(finalScale, finalScale)
|
||||||
|
|
||||||
// Positionieren: ScreenPos + DrawOffset
|
// Positionieren: ScreenPos + DrawOffset (auch skaliert)
|
||||||
op.GeoM.Translate(
|
op.GeoM.Translate(
|
||||||
screenX+def.DrawOffX,
|
screenX+(def.DrawOffX*viewScale),
|
||||||
screenY+def.DrawOffY,
|
screenY+(def.DrawOffY*viewScale),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Farbe anwenden (nur wenn explizit gesetzt)
|
// Farbe anwenden (nur wenn explizit gesetzt)
|
||||||
|
|||||||
@@ -58,27 +58,27 @@ func GenerateGroundTile(tileIdx int) GroundTile {
|
|||||||
Stones: make([]Stone, 0),
|
Stones: make([]Stone, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zufällige Dirt-Patches generieren (20-30 pro Tile, über die ganze Höhe)
|
// Zufällige Dirt-Patches generieren (15-25 pro Tile, kompakt)
|
||||||
numDirt := 20 + rng.Intn(10)
|
numDirt := 15 + rng.Intn(10)
|
||||||
dirtHeight := 5000.0 // Gesamte Dirt-Höhe bis tief in die Erde
|
maxDirtHeight := 100.0 // Kompakte Erde-Tiefe (100px)
|
||||||
for i := 0; i < numDirt; i++ {
|
for i := 0; i < numDirt; i++ {
|
||||||
darkness := uint8(70 + rng.Intn(40)) // Verschiedene Brauntöne
|
darkness := uint8(70 + rng.Intn(40)) // Verschiedene Brauntöne
|
||||||
tile.DirtVariants = append(tile.DirtVariants, DirtPatch{
|
tile.DirtVariants = append(tile.DirtVariants, DirtPatch{
|
||||||
OffsetX: rng.Float64() * 128,
|
OffsetX: rng.Float64() * 128,
|
||||||
OffsetY: rng.Float64()*dirtHeight + 20, // Über die ganze Dirt-Schicht verteilt
|
OffsetY: rng.Float64()*maxDirtHeight + 20, // Nur im sichtbaren Bereich
|
||||||
Width: 10 + rng.Float64()*30,
|
Width: 10 + rng.Float64()*30,
|
||||||
Height: 10 + rng.Float64()*25,
|
Height: 10 + rng.Float64()*25,
|
||||||
Color: color.RGBA{darkness, darkness - 10, darkness - 20, 255},
|
Color: color.RGBA{darkness, darkness - 10, darkness - 20, 255},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Steine IN der Erde generieren (30-50 pro Tile, nur eckig, tief verteilt)
|
// Steine IN der Erde generieren (20-30 pro Tile, nur eckig, kompakt)
|
||||||
numStones := 30 + rng.Intn(20)
|
numStones := 20 + rng.Intn(10)
|
||||||
for i := 0; i < numStones; i++ {
|
for i := 0; i < numStones; i++ {
|
||||||
tile.Stones = append(tile.Stones, Stone{
|
tile.Stones = append(tile.Stones, Stone{
|
||||||
X: rng.Float64() * 128,
|
X: rng.Float64() * 128,
|
||||||
Y: rng.Float64()*dirtHeight + 20, // Tief in der Erde verteilt
|
Y: rng.Float64()*maxDirtHeight + 20, // Nur im sichtbaren Bereich
|
||||||
Size: 4 + rng.Float64()*8, // Verschiedene Größen
|
Size: 4 + rng.Float64()*8, // Verschiedene Größen
|
||||||
Color: color.RGBA{100 + uint8(rng.Intn(50)), 100 + uint8(rng.Intn(50)), 100 + uint8(rng.Intn(50)), 255},
|
Color: color.RGBA{100 + uint8(rng.Intn(50)), 100 + uint8(rng.Intn(50)), 100 + uint8(rng.Intn(50)), 255},
|
||||||
Shape: 1, // Nur eckig (keine runden Steine mehr)
|
Shape: 1, // Nur eckig (keine runden Steine mehr)
|
||||||
})
|
})
|
||||||
@@ -92,18 +92,28 @@ func GenerateGroundTile(tileIdx int) GroundTile {
|
|||||||
// RenderGround rendert den Boden mit Bewegung
|
// RenderGround rendert den Boden mit Bewegung
|
||||||
func (g *Game) RenderGround(screen *ebiten.Image, cameraX float64) {
|
func (g *Game) RenderGround(screen *ebiten.Image, cameraX float64) {
|
||||||
// Tatsächliche Canvas-Größe verwenden
|
// Tatsächliche Canvas-Größe verwenden
|
||||||
canvasW, _ := screen.Size()
|
canvasW, canvasH := screen.Size()
|
||||||
|
|
||||||
// Boden bleibt an fester Position (RefFloorY) - wichtig für Spielphysik!
|
// Gameplay-Boden-Position (wo Spieler laufen) - mit echter Canvas-Höhe!
|
||||||
// Erweitere Boden nach unten weit über Canvas-Rand hinaus (5000 Pixel tief)
|
gameFloorY := float32(GetFloorYFromHeight(canvasH))
|
||||||
floorY := float32(RefFloorY)
|
|
||||||
floorH := float32(5000) // Tief in die Erde
|
// Erde-Tiefe - kompakt (100px) damit Gameplay optimal Platz hat
|
||||||
|
maxDirtDepth := float32(100.0)
|
||||||
|
|
||||||
|
// Berechne wie viel Erde sichtbar sein soll (bis zum Bildschirmrand)
|
||||||
|
remainingSpace := float32(canvasH) - gameFloorY
|
||||||
|
visibleDirtHeight := maxDirtDepth
|
||||||
|
if remainingSpace < maxDirtDepth {
|
||||||
|
visibleDirtHeight = remainingSpace
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Basis Gras-Schicht (volle Canvas-Breite, nur dünne Grasnarbe)
|
// 1. Basis Gras-Schicht (volle Canvas-Breite, nur dünne Grasnarbe)
|
||||||
vector.DrawFilledRect(screen, 0, floorY, float32(canvasW), 20, ColGrass, false)
|
vector.DrawFilledRect(screen, 0, gameFloorY, float32(canvasW), 20, ColGrass, false)
|
||||||
|
|
||||||
// 2. Dirt-Schicht (Basis, volle Canvas-Breite, tief nach unten)
|
// 2. Dirt-Schicht (Basis, volle Canvas-Breite, nur sichtbarer Bereich)
|
||||||
vector.DrawFilledRect(screen, 0, floorY+20, float32(canvasW), floorH-20, ColDirt, false)
|
if visibleDirtHeight > 20 {
|
||||||
|
vector.DrawFilledRect(screen, 0, gameFloorY+20, float32(canvasW), visibleDirtHeight-20, ColDirt, false)
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Prozedurale Dirt-Patches und Steine (bewegen sich mit Kamera)
|
// 3. Prozedurale Dirt-Patches und Steine (bewegen sich mit Kamera)
|
||||||
// Berechne welche Tiles sichtbar sind (basierend auf Canvas-Breite)
|
// Berechne welche Tiles sichtbar sind (basierend auf Canvas-Breite)
|
||||||
@@ -119,7 +129,7 @@ func (g *Game) RenderGround(screen *ebiten.Image, cameraX float64) {
|
|||||||
for _, dirt := range tile.DirtVariants {
|
for _, dirt := range tile.DirtVariants {
|
||||||
worldX := tile.X + dirt.OffsetX
|
worldX := tile.X + dirt.OffsetX
|
||||||
screenX := float32(worldX - cameraX)
|
screenX := float32(worldX - cameraX)
|
||||||
screenY := float32(RefFloorY) + float32(dirt.OffsetY)
|
screenY := gameFloorY + float32(dirt.OffsetY)
|
||||||
|
|
||||||
// Nur rendern wenn im sichtbaren Bereich (Canvas-Breite verwenden)
|
// Nur rendern wenn im sichtbaren Bereich (Canvas-Breite verwenden)
|
||||||
if screenX+float32(dirt.Width) > 0 && screenX < float32(canvasW) {
|
if screenX+float32(dirt.Width) > 0 && screenX < float32(canvasW) {
|
||||||
@@ -127,11 +137,11 @@ func (g *Game) RenderGround(screen *ebiten.Image, cameraX float64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Steine rendern (auf dem Gras)
|
// Steine rendern (in der Erde)
|
||||||
for _, stone := range tile.Stones {
|
for _, stone := range tile.Stones {
|
||||||
worldX := tile.X + stone.X
|
worldX := tile.X + stone.X
|
||||||
screenX := float32(worldX - cameraX)
|
screenX := float32(worldX - cameraX)
|
||||||
screenY := float32(RefFloorY) + float32(stone.Y)
|
screenY := gameFloorY + float32(stone.Y)
|
||||||
|
|
||||||
// Nur rendern wenn im sichtbaren Bereich (Canvas-Breite verwenden)
|
// Nur rendern wenn im sichtbaren Bereich (Canvas-Breite verwenden)
|
||||||
if screenX > -20 && screenX < float32(canvasW)+20 {
|
if screenX > -20 && screenX < float32(canvasW)+20 {
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ const (
|
|||||||
StateLobby = 1
|
StateLobby = 1
|
||||||
StateGame = 2
|
StateGame = 2
|
||||||
StateLeaderboard = 3
|
StateLeaderboard = 3
|
||||||
RefFloorY = 540
|
RefFloorY = 540 // Server-Welt Boden-Position (unveränderlich)
|
||||||
|
RefFloorYMobile = 270 // Nicht mehr verwendet
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -122,6 +123,7 @@ type Game struct {
|
|||||||
joyTouchID ebiten.TouchID
|
joyTouchID ebiten.TouchID
|
||||||
btnJumpActive bool
|
btnJumpActive bool
|
||||||
keyboardUsed bool // Wurde Tastatur benutzt?
|
keyboardUsed bool // Wurde Tastatur benutzt?
|
||||||
|
lastCanvasHeight int // Cache der Canvas-Höhe für Touch-Input
|
||||||
|
|
||||||
// Debug Stats
|
// Debug Stats
|
||||||
showDebug bool // Debug-Overlay anzeigen (F3 zum Umschalten)
|
showDebug bool // Debug-Overlay anzeigen (F3 zum Umschalten)
|
||||||
@@ -632,12 +634,104 @@ func (g *Game) DrawLobby(screen *ebiten.Image) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Layout(w, h int) (int, int) {
|
func (g *Game) Layout(w, h int) (int, int) {
|
||||||
// Nutze die GESAMTE Bildschirmfläche ohne Einschränkungen
|
// Nutze die echte Window-Größe (Mobile: ~360px Höhe, Desktop: 720px+ Höhe)
|
||||||
|
// Das erlaubt dynamische Anpassung an verschiedene Bildschirmgrößen
|
||||||
return w, h
|
return w, h
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- HELPER ---
|
// --- HELPER ---
|
||||||
|
|
||||||
|
// GetFloorY gibt die Y-Position des Bodens basierend auf der aktuellen Bildschirmhöhe zurück
|
||||||
|
// WICHTIG: Kann nicht direkt aufgerufen werden, braucht Screen-Höhe als Parameter!
|
||||||
|
func GetFloorYFromHeight(screenHeight int) float64 {
|
||||||
|
h := screenHeight
|
||||||
|
if h == 0 {
|
||||||
|
// Fallback wenn keine Höhe verfügbar
|
||||||
|
h = ScreenHeight // 720
|
||||||
|
log.Printf("⚠️ GetFloorY: Screen height is 0, using fallback: %d", h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ziel: Gameplay füllt den Bildschirm optimal aus
|
||||||
|
// Erde-Tiefe: ~100px (kompakt, damit mehr Gameplay-Raum bleibt)
|
||||||
|
dirtDepth := 100.0
|
||||||
|
|
||||||
|
// Berechne Boden-Position: möglichst weit unten
|
||||||
|
floorY := float64(h) - dirtDepth
|
||||||
|
|
||||||
|
// Minimum-Check: Bei sehr kleinen Bildschirmen (< 300px) mindestens 70% Höhe
|
||||||
|
minFloorY := float64(h) * 0.7
|
||||||
|
if floorY < minFloorY {
|
||||||
|
floorY = minFloorY
|
||||||
|
}
|
||||||
|
|
||||||
|
return floorY
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFloorY - Wrapper der versucht die Höhe zu bekommen (deprecated, benutze GetFloorYFromHeight)
|
||||||
|
func GetFloorY() float64 {
|
||||||
|
_, h := ebiten.WindowSize()
|
||||||
|
return GetFloorYFromHeight(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScale gibt den Scale-Faktor zurück um die Spielwelt an den Bildschirm anzupassen
|
||||||
|
// Auf Mobile: Scale < 1.0 (rauszoomen, damit mehr sichtbar ist)
|
||||||
|
// Auf Desktop: Scale = 1.0 (normale Größe)
|
||||||
|
func GetScale() float64 {
|
||||||
|
_, h := ebiten.WindowSize()
|
||||||
|
if h == 0 {
|
||||||
|
h = ScreenHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile Geräte (kleine Bildschirme): Rauszoomen für bessere Sicht
|
||||||
|
if h <= 400 {
|
||||||
|
// Sehr kleine Bildschirme: 0.7x Scale (30% rauszoomen)
|
||||||
|
return 0.7
|
||||||
|
} else if h <= 600 {
|
||||||
|
// Mittlere Bildschirme: 0.85x Scale (15% rauszoomen)
|
||||||
|
return 0.85
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desktop/große Bildschirme: Normale Größe
|
||||||
|
return 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScaleFromHeight - Scale mit expliziter Höhe
|
||||||
|
func GetScaleFromHeight(screenHeight int) float64 {
|
||||||
|
h := screenHeight
|
||||||
|
if h == 0 {
|
||||||
|
h = ScreenHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
if h <= 400 {
|
||||||
|
return 0.7
|
||||||
|
} else if h <= 600 {
|
||||||
|
return 0.85
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorldToScreenY konvertiert Server-Welt-Y-Koordinate zu Bildschirm-Y-Koordinate
|
||||||
|
// Nimmt die tatsächliche Bildschirmhöhe als Parameter (wichtig für WASM!)
|
||||||
|
func WorldToScreenYWithHeight(worldY float64, screenHeight int) float64 {
|
||||||
|
// Server arbeitet mit festen Koordinaten (Boden bei Y=540 für 720px Referenz)
|
||||||
|
// Client will Boden dynamisch positionieren
|
||||||
|
serverFloorY := float64(RefFloorY) // Server-Boden (540 bei 720px)
|
||||||
|
clientFloorY := GetFloorYFromHeight(screenHeight) // Client-Boden (dynamisch)
|
||||||
|
yOffset := clientFloorY - serverFloorY // Verschiebung (z.B. 620 - 540 = +80px nach unten)
|
||||||
|
|
||||||
|
return worldY + yOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorldToScreenY - Legacy wrapper (versucht WindowSize zu verwenden, funktioniert nicht in WASM!)
|
||||||
|
func WorldToScreenY(worldY float64) float64 {
|
||||||
|
_, h := ebiten.WindowSize()
|
||||||
|
if h == 0 {
|
||||||
|
h = ScreenHeight // Fallback
|
||||||
|
}
|
||||||
|
return WorldToScreenYWithHeight(worldY, h)
|
||||||
|
}
|
||||||
|
|
||||||
func isHit(x, y, w, h int) bool {
|
func isHit(x, y, w, h int) bool {
|
||||||
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
|
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
|
||||||
mx, my := ebiten.CursorPosition()
|
mx, my := ebiten.CursorPosition()
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func main() {
|
|||||||
// Das Spiel wartet im Hintergrund bis startGame() von JavaScript aufgerufen wird
|
// Das Spiel wartet im Hintergrund bis startGame() von JavaScript aufgerufen wird
|
||||||
log.Println("⏳ Warte auf Start-Signal vom HTML-Menü...")
|
log.Println("⏳ Warte auf Start-Signal vom HTML-Menü...")
|
||||||
|
|
||||||
ebiten.SetWindowSize(ScreenWidth, ScreenHeight)
|
// WICHTIG: Keine feste WindowSize auf WASM - Layout() regelt die Größe dynamisch
|
||||||
ebiten.SetWindowTitle("Escape From Teacher")
|
ebiten.SetWindowTitle("Escape From Teacher")
|
||||||
ebiten.SetTPS(20)
|
ebiten.SetTPS(20)
|
||||||
ebiten.SetVsyncEnabled(true)
|
ebiten.SetVsyncEnabled(true)
|
||||||
|
|||||||
@@ -186,6 +186,9 @@ func (g *Game) RenderParticles(screen *ebiten.Image) {
|
|||||||
g.particlesMutex.Lock()
|
g.particlesMutex.Lock()
|
||||||
defer g.particlesMutex.Unlock()
|
defer g.particlesMutex.Unlock()
|
||||||
|
|
||||||
|
// Canvas-Höhe für Y-Transformation
|
||||||
|
_, canvasH := screen.Size()
|
||||||
|
|
||||||
for i := range g.particles {
|
for i := range g.particles {
|
||||||
p := &g.particles[i]
|
p := &g.particles[i]
|
||||||
|
|
||||||
@@ -197,9 +200,9 @@ func (g *Game) RenderParticles(screen *ebiten.Image) {
|
|||||||
|
|
||||||
col := color.RGBA{p.Color.R, p.Color.G, p.Color.B, alpha}
|
col := color.RGBA{p.Color.R, p.Color.G, p.Color.B, alpha}
|
||||||
|
|
||||||
// Position relativ zur Kamera
|
// Position relativ zur Kamera und mit Y-Transformation
|
||||||
screenX := float32(p.X - g.camX)
|
screenX := float32(p.X - g.camX)
|
||||||
screenY := float32(p.Y)
|
screenY := float32(WorldToScreenYWithHeight(p.Y, canvasH))
|
||||||
|
|
||||||
// Partikel als Kreis zeichnen
|
// Partikel als Kreis zeichnen
|
||||||
vector.DrawFilledCircle(screen, screenX, screenY, float32(p.Size), col, false)
|
vector.DrawFilledCircle(screen, screenX, screenY, float32(p.Size), col, false)
|
||||||
|
|||||||
@@ -357,7 +357,7 @@
|
|||||||
"OffsetY": 12,
|
"OffsetY": 12,
|
||||||
"W": 55,
|
"W": 55,
|
||||||
"H": 113,
|
"H": 113,
|
||||||
"Type": ""
|
"Type": "wall"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
|||||||
@font-face{font-display:swap;font-family:'Press Start 2P';font-style:normal;font-weight:400;src:url('../assets/fonts/press-start-2p-v16-latin-regular.woff2') format('woff2')}
|
@font-face{font-display:swap;font-family:'Press Start 2P';font-style:normal;font-weight:400;src:url('../assets/fonts/press-start-2p-v16-latin-regular.woff2') format('woff2')}
|
||||||
body,html{margin:0;padding:0;width:100%;height:100%;background-color:#1a1a1a;color:#fff;overflow:hidden;font-family:'Press Start 2P',cursive;font-size:14px}
|
body,html{margin:0;padding:0;width:100%;height:100%;background-color:#1a1a1a;color:#fff;overflow:hidden;font-family:'Press Start 2P',cursive;font-size:14px}
|
||||||
#game-container{position:relative;width:100%;height:100%;box-shadow:0 0 50px rgba(0,0,0,.8);border:4px solid #444;background:#000}
|
#game-container{position:relative;width:100%;height:100%;box-shadow:0 0 50px rgba(0,0,0,.8);border:4px solid #444;background:#000}
|
||||||
canvas{position:fixed!important;top:50%!important;left:50%!important;transform:translate(-50%,-50%)!important;width:100%!important;height:100%!important;max-width:100vw!important;max-height:100vh!important;object-fit:contain!important;z-index:1!important;background:#000;image-rendering:pixelated;opacity:0;pointer-events:none;transition:opacity .3s;visibility:hidden}
|
canvas{position:fixed!important;top:0!important;left:0!important;width:100vw!important;height:100vh!important;z-index:1!important;background:#000;image-rendering:pixelated;opacity:0;pointer-events:none;transition:opacity .3s;visibility:hidden}
|
||||||
canvas.game-active{opacity:1;pointer-events:auto;z-index:2000!important;visibility:visible}
|
canvas.game-active{opacity:1;pointer-events:auto;z-index:2000!important;visibility:visible}
|
||||||
.overlay-screen{position:fixed!important;top:0;left:0;width:100%;height:100%;background:url('background.jpg') center/cover no-repeat,rgba(0,0,0,.85);display:flex;justify-content:center;align-items:center;z-index:1000;box-sizing:border-box;padding:20px}
|
.overlay-screen{position:fixed!important;top:0;left:0;width:100%;height:100%;background:url('background.jpg') center/cover no-repeat,rgba(0,0,0,.85);display:flex;justify-content:center;align-items:center;z-index:1000;box-sizing:border-box;padding:20px}
|
||||||
.overlay-screen.hidden{display:none!important}
|
.overlay-screen.hidden{display:none!important}
|
||||||
|
|||||||
@@ -248,15 +248,3 @@ func (c *WebSocketClient) handleMessage(msg WebSocketMessage) {
|
|||||||
log.Printf("⚠️ Unbekannter Nachrichtentyp: %s", msg.Type)
|
log.Printf("⚠️ Unbekannter Nachrichtentyp: %s", msg.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartWebSocketGateway startet den WebSocket-Server
|
|
||||||
func StartWebSocketGateway(port string, ec *nats.EncodedConn) {
|
|
||||||
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
handleWebSocket(w, r, ec)
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Printf("🌐 WebSocket-Gateway läuft auf http://localhost:%s/ws", port)
|
|
||||||
if err := http.ListenAndServe(":"+port, nil); err != nil {
|
|
||||||
log.Fatal("❌ WebSocket-Server Fehler: ", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,13 +22,18 @@ type Gateway struct {
|
|||||||
// In einer echten Microservice Welt wäre das separat,
|
// In einer echten Microservice Welt wäre das separat,
|
||||||
// aber hier hostet der Gateway auch Räume.
|
// aber hier hostet der Gateway auch Räume.
|
||||||
LocalRooms map[string]*Room
|
LocalRooms map[string]*Room
|
||||||
|
|
||||||
|
// Security: Tracking welcher Spieler in welchem Raum ist
|
||||||
|
// PlayerID -> RoomID Mapping
|
||||||
|
PlayerRooms map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGateway(nc *nats.Conn, w *game.World) *Gateway {
|
func NewGateway(nc *nats.Conn, w *game.World) *Gateway {
|
||||||
return &Gateway{
|
return &Gateway{
|
||||||
NC: nc,
|
NC: nc,
|
||||||
World: w,
|
World: w,
|
||||||
LocalRooms: make(map[string]*Room),
|
LocalRooms: make(map[string]*Room),
|
||||||
|
PlayerRooms: make(map[string]string),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +61,13 @@ func (gw *Gateway) HandleWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
playerID := fmt.Sprintf("p_%d", time.Now().UnixNano())
|
playerID := fmt.Sprintf("p_%d", time.Now().UnixNano())
|
||||||
roomID := login.RoomID
|
roomID := login.RoomID
|
||||||
|
|
||||||
|
// 🔒 SECURITY: Prüfe ob dieser Spieler bereits in einem Raum ist
|
||||||
|
if existingRoom, exists := gw.PlayerRooms[playerID]; exists {
|
||||||
|
log.Printf("🚫 SECURITY: Player %s already in room %s, rejecting new connection", playerID, existingRoom)
|
||||||
|
conn.WriteMessage(websocket.TextMessage, []byte(`{"error":"Already connected to a room"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 2. RAUM LOGIK
|
// 2. RAUM LOGIK
|
||||||
if login.Action == "CREATE" {
|
if login.Action == "CREATE" {
|
||||||
// Raum ID generieren (4 Zeichen Random)
|
// Raum ID generieren (4 Zeichen Random)
|
||||||
@@ -69,6 +81,9 @@ func (gw *Gateway) HandleWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Spieler lokal hinzufügen (Hack für Demo, sauberer wäre via NATS Event)
|
// Spieler lokal hinzufügen (Hack für Demo, sauberer wäre via NATS Event)
|
||||||
newRoom.AddPlayer(playerID, login.Name)
|
newRoom.AddPlayer(playerID, login.Name)
|
||||||
|
|
||||||
|
// 🔒 SECURITY: Spieler in Raum registrieren
|
||||||
|
gw.PlayerRooms[playerID] = roomID
|
||||||
|
|
||||||
} else if login.Action == "JOIN" {
|
} else if login.Action == "JOIN" {
|
||||||
// Wir müssen dem Raum (egal wo er läuft) sagen: Hier ist ein Neuer!
|
// Wir müssen dem Raum (egal wo er läuft) sagen: Hier ist ein Neuer!
|
||||||
// Da wir hier keine verteilte DB haben, tricksen wir:
|
// Da wir hier keine verteilte DB haben, tricksen wir:
|
||||||
@@ -78,12 +93,15 @@ func (gw *Gateway) HandleWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if room, ok := gw.LocalRooms[roomID]; ok {
|
if room, ok := gw.LocalRooms[roomID]; ok {
|
||||||
room.AddPlayer(playerID, login.Name)
|
room.AddPlayer(playerID, login.Name)
|
||||||
|
|
||||||
|
// 🔒 SECURITY: Spieler in Raum registrieren
|
||||||
|
gw.PlayerRooms[playerID] = roomID
|
||||||
} else {
|
} else {
|
||||||
// Falls Raum nicht lokal: Senden wir ein "JOIN REQUEST" über NATS?
|
// Falls Raum nicht lokal: Senden wir ein "JOIN REQUEST" über NATS?
|
||||||
// Für jetzt: Wir lassen es simpel. Wenn Raum nicht auf diesem Server -> Error.
|
// Für jetzt: Wir lassen es simpel. Wenn Raum nicht auf diesem Server -> Error.
|
||||||
// (Für echtes Scaling bräuchten wir Redis oder NATS Request/Reply zur Raumsuche)
|
// (Für echtes Scaling bräuchten wir Redis oder NATS Request/Reply zur Raumsuche)
|
||||||
log.Println("Raum nicht gefunden (oder auf anderem Node):", roomID)
|
log.Println("❌ Raum nicht gefunden (oder auf anderem Node):", roomID)
|
||||||
// Optional: Error an Client senden
|
conn.WriteMessage(websocket.TextMessage, []byte(`{"error":"Room not found"}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,14 +125,37 @@ func (gw *Gateway) HandleWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Wir parsen kurz, um den Typ zu prüfen, oder leiten blind weiter?
|
// Wir parsen kurz, um den Typ zu prüfen, oder leiten blind weiter?
|
||||||
// Besser: Wir wrappen es in ClientInput struct
|
// Besser: Wir wrappen es in ClientInput struct
|
||||||
var raw map[string]interface{}
|
var raw map[string]interface{}
|
||||||
json.Unmarshal(data, &raw)
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
log.Printf("⚠️ Invalid JSON from player %s: %v", playerID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
inputType, _ := raw["type"].(string)
|
inputType, _ := raw["type"].(string)
|
||||||
|
|
||||||
|
// 🔒 SECURITY: Prüfe ob der Spieler versucht für jemand anderen zu sprechen
|
||||||
|
claimedPlayerID, hasPlayerID := raw["player_id"].(string)
|
||||||
|
claimedRoomID, hasRoomID := raw["room_id"].(string)
|
||||||
|
|
||||||
|
if hasPlayerID && claimedPlayerID != playerID {
|
||||||
|
log.Printf("🚫 SECURITY BREACH: Player %s tried to send input as %s", playerID, claimedPlayerID)
|
||||||
|
continue // Ignoriere böswilligen Input
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasRoomID && claimedRoomID != roomID {
|
||||||
|
log.Printf("🚫 SECURITY BREACH: Player %s tried to send input to room %s (is in %s)", playerID, claimedRoomID, roomID)
|
||||||
|
continue // Ignoriere böswilligen Input
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔒 SECURITY: Setze IMMER die korrekten IDs (überschreibe Client-Werte)
|
||||||
input := game.ClientInput{
|
input := game.ClientInput{
|
||||||
Type: inputType,
|
Type: inputType,
|
||||||
RoomID: roomID,
|
RoomID: roomID, // Server setzt den Raum (nicht Client!)
|
||||||
PlayerID: playerID,
|
PlayerID: playerID, // Server setzt die Player-ID (nicht Client!)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sequence-Nummer vom Client übernehmen (für Client Prediction)
|
||||||
|
if seq, ok := raw["sequence"].(float64); ok {
|
||||||
|
input.Sequence = uint32(seq)
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes, _ := json.Marshal(input)
|
bytes, _ := json.Marshal(input)
|
||||||
@@ -122,6 +163,11 @@ func (gw *Gateway) HandleWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup beim Disconnect
|
// Cleanup beim Disconnect
|
||||||
|
log.Printf("✋ Player %s (%s) disconnected from Room %s", playerID, login.Name, roomID)
|
||||||
|
|
||||||
|
// 🔒 SECURITY: Entferne Spieler aus PlayerRooms Tracking
|
||||||
|
delete(gw.PlayerRooms, playerID)
|
||||||
|
|
||||||
if room, ok := gw.LocalRooms[roomID]; ok {
|
if room, ok := gw.LocalRooms[roomID]; ok {
|
||||||
room.RemovePlayer(playerID)
|
room.RemovePlayer(playerID)
|
||||||
// Wenn leer -> Raum löschen?
|
// Wenn leer -> Raum löschen?
|
||||||
|
|||||||
Reference in New Issue
Block a user