Stephanie Gredell 5 months ago
parent
commit
30705c43d4
  1. 207
      internal/handlers/home.go
  2. 88
      static/script.js

207
internal/handlers/home.go

@ -1,10 +1,13 @@ @@ -1,10 +1,13 @@
package handlers
import (
"context"
"fmt"
"html/template"
"net/http"
"sync"
"sync/atomic"
"time"
"github.com/gorilla/websocket"
"github.com/markbates/goth/gothic"
@ -24,69 +27,178 @@ type PageData struct { @@ -24,69 +27,178 @@ type PageData struct {
Username string
}
type Client struct {
conn *websocket.Conn
send chan []byte
hub *hub
ctx context.Context
cancel context.CancelFunc
id uint64
mu sync.Mutex // Protect concurrent operations
}
type hub struct {
clients map[*websocket.Conn]bool
Broadcast chan []byte
register chan *websocket.Conn
unregister chan *websocket.Conn
mutex sync.RWMutex
clients sync.Map
broadcast chan []byte
register chan *Client
unregister chan *Client
// Message pool for reusing byte slices
messagePool sync.Pool
}
var Hub = &hub{
clients: make(map[*websocket.Conn]bool),
Broadcast: make(chan []byte),
register: make(chan *websocket.Conn),
unregister: make(chan *websocket.Conn),
broadcast: make(chan []byte, 1024),
register: make(chan *Client, 256),
unregister: make(chan *Client, 256),
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
CheckOrigin: func(r *http.Request) bool {
return true // this should change
return true
},
}
var clientIDCounter uint64
const (
writeWait = 10 * time.Second
pongWait = 60 * time.Second
pingPeriod = 54 * time.Second
maxMessageSize = 512
)
func init() {
Hub.messagePool.New = func() interface{} {
return make([]byte, 0, 1024)
}
}
func (h *hub) Run() {
for {
select {
case conn := <-h.register:
h.mutex.Lock()
h.clients[conn] = true
h.mutex.Unlock()
fmt.Printf("Client connected. Total clients: %d\n", len(h.clients))
case client := <-h.register:
h.clients.Store(client.id, client)
fmt.Printf("Client connected. ID: %d\n", client.id)
case client := <-h.unregister:
if _, loaded := h.clients.LoadAndDelete(client.id); loaded {
close(client.send)
fmt.Printf("Client disconnected. ID: %d\n", client.id)
}
case message := <-h.broadcast:
// Single-threaded broadcasting to avoid race conditions
h.clients.Range(func(key, value interface{}) bool {
client := value.(*Client)
select {
case client.send <- message:
// Message sent successfully
default:
// Client is slow, remove it
h.clients.Delete(client.id)
close(client.send)
fmt.Printf("Slow client removed. ID: %d\n", client.id)
}
return true
})
// Return message to pool after all clients processed
h.messagePool.Put(message)
}
}
}
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
c.cancel()
}()
c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error {
c.conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
select {
case <-c.ctx.Done():
return
default:
_, messageBytes, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
fmt.Printf("WebSocket error for client %d: %v\n", c.id, err)
}
return
}
// Get a message buffer from the pool
message := c.hub.messagePool.Get().([]byte)
message = message[:len(messageBytes)]
copy(message, messageBytes)
// Non-blocking broadcast
select {
case c.hub.broadcast <- message:
// Message will be returned to pool by hub.Run()
default:
// Broadcast buffer full, return message to pool and drop
c.hub.messagePool.Put(message)
}
}
}
}
func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case <-c.ctx.Done():
return
case conn := <-h.unregister:
h.mutex.Lock()
if _, ok := h.clients[conn]; ok {
delete(h.clients, conn)
conn.Close()
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
h.mutex.Unlock()
fmt.Printf("Client disconnected. Total clients: %d\n", len(h.clients))
case message := <-h.Broadcast:
h.mutex.RLock()
for conn := range h.clients {
if err := conn.WriteMessage(websocket.TextMessage, message); err != nil {
fmt.Printf("Error sending message to client: %v\n", err)
// Remove failed connection
delete(h.clients, conn)
conn.Close()
// Write the message
if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil {
fmt.Printf("Write error for client %d: %v\n", c.id, err)
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
h.mutex.RUnlock()
}
}
}
func (h *Handler) Home(w http.ResponseWriter, r *http.Request) {
session, err := gothic.Store.Get(r, "user-session")
if err != nil {
http.Error(w, "Error retrieving session for welcome page", http.StatusInternalServerError)
return
}
username, ok := session.Values["user_name"].(string)
var pagedata PageData
if ok {
pagedata.Username = username
} else {
@ -106,21 +218,22 @@ func (h *Handler) WsHandler(w http.ResponseWriter, r *http.Request) { @@ -106,21 +218,22 @@ func (h *Handler) WsHandler(w http.ResponseWriter, r *http.Request) {
return
}
Hub.register <- conn
ctx, cancel := context.WithCancel(context.Background())
client := &Client{
conn: conn,
send: make(chan []byte, 256),
hub: Hub,
ctx: ctx,
cancel: cancel,
id: atomic.AddUint64(&clientIDCounter, 1),
}
defer conn.Close()
Hub.register <- client
for {
_, messageBytes, err := conn.ReadMessage()
if err != nil && !websocket.IsCloseError(
err,
websocket.CloseNormalClosure,
websocket.CloseGoingAway,
websocket.CloseNoStatusReceived,
) {
fmt.Printf("error reading message: %v", err)
}
// Start pumps in separate goroutines
go client.writePump()
go client.readPump()
Hub.Broadcast <- messageBytes
}
// Wait for completion
<-ctx.Done()
}

88
static/script.js

@ -34,7 +34,9 @@ window.addEventListener("DOMContentLoaded", function() { @@ -34,7 +34,9 @@ window.addEventListener("DOMContentLoaded", function() {
}
update() {
this._prevY = this.y;
this.vx = 0;
if (this.keys.a) {
this.vx = -this.speed;
this.facing = -1;
@ -59,17 +61,6 @@ window.addEventListener("DOMContentLoaded", function() { @@ -59,17 +61,6 @@ window.addEventListener("DOMContentLoaded", function() {
this.vy += this.gravity;
this.y += this.vy;
const groundY = this.canvas.height;
const totalHeight = 60;
if (this.y + totalHeight >= groundY) {
this.y = groundY - totalHeight;
this.vy = 0;
this.onGround = true;
} else {
this.onGround = false;
}
this.x = Math.max(20, Math.min(this.canvas.width - 20, this.x));
if (this.action === "punch") {
@ -299,6 +290,54 @@ window.addEventListener("DOMContentLoaded", function() { @@ -299,6 +290,54 @@ window.addEventListener("DOMContentLoaded", function() {
return null;
}
resolveCollisions(platforms, groundY) {
const totalHeight = 60;
const halfFootW = 12;
const EPS = 0.5;
if (this._prevY === undefined) {
this._prevY = this.y;
}
let landedOnPlatform = false;
// one-way platforms (land only when falling and crossing from above)
if (this.vy >= 0) {
for (const pf of platforms) {
const feetLeft = this.x - halfFootW;
const feetRight = this.x + halfFootW;
const pfLeft = pf.x, pfRight = pf.x + pf.w;
const horizOverlap = feetRight > pfLeft && feetLeft < pfRight;
const wasAbove = (this._prevY + totalHeight) <= pf.y + EPS;
const nowBelowTop = (this.y + totalHeight) >= pf.y - EPS;
const dropping = this.crouching === true; // hold 's' to drop through
console.log({
feetPrev: this._prevY + 60, feetNow: this.y + 60, pfTop: pf.y,
horizOverlap, wasAbove, nowBelowTop, dropping
});
if (horizOverlap && wasAbove && nowBelowTop && !dropping) {
this.y = pf.y - totalHeight;
this.vy = 0;
landedOnPlatform = true;
break;
}
}
}
// --- ground after platforms ---
if (!landedOnPlatform && this.y + totalHeight >= groundY) {
this.y = groundY - totalHeight;
this.vy = 0;
this.onGround = true;
} else if (landedOnPlatform) {
this.onGround = true;
} else {
this.onGround = false;
}
}
}
class Game {
@ -330,6 +369,16 @@ window.addEventListener("DOMContentLoaded", function() { @@ -330,6 +369,16 @@ window.addEventListener("DOMContentLoaded", function() {
this._reconnectDelay = 1000;
this._maxReconnectDelay = 15000;
this._reconnectTimer = null;
this.platform = [
{ x: 120, y: this.canvas.height - 60, w: 240, h: 12 },
{ x: 200, y: this.canvas.height - 120, w: 240, h: 12 },
{ x: 280, y: this.canvas.height - 180, w: 240, h: 12 },
{ x: 460, y: this.canvas.height - 240, w: 240, h: 12 },
]
}
start() {
@ -392,8 +441,15 @@ window.addEventListener("DOMContentLoaded", function() { @@ -392,8 +441,15 @@ window.addEventListener("DOMContentLoaded", function() {
const playerArray = Object.values(this.players);
this.ctx.fillStyle = "#333"
for (const p of this.platform) {
this.ctx.fillRect(p.x, p.y, p.w, p.h)
}
playerArray.forEach(player => {
player.update(); // Call the update method for each player
player.resolveCollisions(this.platform, this.canvas.height);
});
for (let i = 0; i < playerArray.length; i++) {
@ -454,21 +510,16 @@ window.addEventListener("DOMContentLoaded", function() { @@ -454,21 +510,16 @@ window.addEventListener("DOMContentLoaded", function() {
switch (e.key) {
case 'e':
case ' ':
me.punch()
this._sendAction('e', "keyDown")
break;
case 'w':
me.jump();
this._sendAction('k', "keyDown")
break;
case 's':
me.crouching = true;
this._sendAction('j', 'keyDown')
break;
case 'a':
case 'd':
if (!me.keys) me.keys = {};
if (!me.keys[e.key]) me.keys[e.key] = true;
this._sendAction(e.key, 'keyDown')
break;
default:
@ -489,14 +540,10 @@ window.addEventListener("DOMContentLoaded", function() { @@ -489,14 +540,10 @@ window.addEventListener("DOMContentLoaded", function() {
}
if (e.key === 's') {
me.crouching = false;
me.speed = 2;
this._sendAction('j', 'keyUp')
}
if (e.key === 'a' || e.key === 'd') {
if (!me.keys) me.keys = {};
me.keys[e.key] = false;
this._sendAction(e.key, 'keyUp')
}
@ -558,7 +605,6 @@ window.addEventListener("DOMContentLoaded", function() { @@ -558,7 +605,6 @@ window.addEventListener("DOMContentLoaded", function() {
this.ws.onmessage = (e) => {
if (!e.data) return;
const data = JSON.parse(e.data);
if (data.Id === this.pageData.username) return;
this._ensurePlayer(data.Id)
const p = this.players[data.Id];

Loading…
Cancel
Save