Browse Source

first try at the game.

main
Stephanie Gredell 5 months ago
commit
863571b687
  1. 2
      .gitignore
  2. 21
      go.mod
  3. 64
      go.sum
  4. 84
      internal/handlers/auth.go
  5. 122
      internal/handlers/home.go
  6. 80
      main.go
  7. 491
      static/script.js
  8. 73
      templates/index.html
  9. 1
      tmp/build-errors.log
  10. BIN
      tmp/main

2
.gitignore vendored

@ -0,0 +1,2 @@
server.*
.env

21
go.mod

@ -0,0 +1,21 @@
module stick
go 1.22.2
require (
github.com/gorilla/sessions v1.1.1
github.com/gorilla/websocket v1.5.3
github.com/joho/godotenv v1.5.1
github.com/markbates/goth v1.81.0
)
require (
github.com/go-chi/chi/v5 v5.1.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/mux v1.6.2 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
golang.org/x/oauth2 v0.17.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.32.0 // indirect
)

64
go.sum

@ -0,0 +1,64 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0PrE=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/markbates/goth v1.81.0 h1:XVcCkeGWokynPV7MXvgb8pd2s3r7DS40P7931w6kdnE=
github.com/markbates/goth v1.81.0/go.mod h1:+6z31QyUms84EHmuBY7iuqYSxyoN3njIgg9iCF/lR1k=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

84
internal/handlers/auth.go

@ -0,0 +1,84 @@
package handlers
import (
"fmt"
"net/http"
"os"
"github.com/gorilla/sessions"
"github.com/markbates/goth/gothic"
)
func (h *Handler) Auth(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
q.Add("provider", "twitch")
r.URL.RawQuery = q.Encode()
key := os.Getenv("SESSION_SECRET")
fmt.Printf("my secret is this long: %v", len(key))
gothic.BeginAuthHandler(w, r)
}
func (h *Handler) Callback(w http.ResponseWriter, r *http.Request) {
user, err := gothic.CompleteUserAuth(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
key := os.Getenv("SESSION_SECRET") // Replace with your SESSION_SECRET or similar
maxAge := 86400 * 30 // 30 days
isProd := false // Set to true when serving over https
store := sessions.NewCookieStore([]byte(key))
store.MaxAge(maxAge)
store.Options.Path = "/"
store.Options.HttpOnly = true // HttpOnly should always be enabled
store.Options.Secure = isProd
gothic.Store = store
session, _ := gothic.Store.Get(r, "user-session")
session.Values["user_name"] = user.Name
session.Values["avatar_url"] = user.AvatarURL
session.Values["user_id"] = user.UserID
session.Values["provider"] = user.Provider
err = session.Save(r, w)
if err != nil {
fmt.Printf("error saving the session: %v", err)
}
http.Redirect(w, r, "/", http.StatusFound)
}
func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) {
session, err := gothic.Store.Get(r, "user-session")
if err != nil {
fmt.Printf("error retrieving session: %v", err)
return
}
// Clear the session data
session.Values = make(map[interface{}]interface{})
session.Options.MaxAge = -1
// Save the empty session
err = session.Save(r, w)
if err != nil {
return
}
http.Redirect(w, r, "/", http.StatusFound)
}
func RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, _ := gothic.Store.Get(r, "user-session")
userID, ok := session.Values["user_id"]
if !ok || userID == nil {
http.Redirect(w, r, "/", http.StatusFound)
return
}
next.ServeHTTP(w, r)
})
}

122
internal/handlers/home.go

@ -0,0 +1,122 @@
package handlers
import (
"fmt"
"html/template"
"net/http"
"sync"
"github.com/gorilla/websocket"
"github.com/markbates/goth/gothic"
)
type Handler struct {
Template template.Template
}
type Message struct {
Id string
Action string
Message string
}
type PageData struct {
Username string
}
type hub struct {
clients map[*websocket.Conn]bool
Broadcast chan []byte
register chan *websocket.Conn
unregister chan *websocket.Conn
mutex sync.RWMutex
}
var Hub = &hub{
clients: make(map[*websocket.Conn]bool),
Broadcast: make(chan []byte),
register: make(chan *websocket.Conn),
unregister: make(chan *websocket.Conn),
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // this should change
},
}
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 conn := <-h.unregister:
h.mutex.Lock()
if _, ok := h.clients[conn]; ok {
delete(h.clients, conn)
conn.Close()
}
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()
}
}
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)
}
username, ok := session.Values["user_name"].(string)
var pagedata PageData
if ok {
pagedata.Username = username
} else {
pagedata.Username = ""
}
err = h.Template.ExecuteTemplate(w, "index.html", &pagedata)
if err != nil {
http.Error(w, "Template rendering error", http.StatusInternalServerError)
}
}
func (h *Handler) WsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
fmt.Println("upgrade error:", err)
return
}
Hub.register <- conn
defer conn.Close()
for {
_, messageBytes, err := conn.ReadMessage()
if err != nil {
fmt.Printf("error reading message: %v", err)
}
fmt.Printf("message being broadcasted: %v", string(messageBytes))
Hub.Broadcast <- messageBytes
}
}

80
main.go

@ -0,0 +1,80 @@
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"stick/internal/handlers"
"syscall"
"time"
"html/template"
"github.com/joho/godotenv"
"github.com/markbates/goth"
"github.com/markbates/goth/providers/twitch"
)
func main() {
err := godotenv.Load()
if err != nil {
fmt.Printf("error loading .env file: %v", err)
}
fs := http.FileServer(http.Dir("./static"))
tmpl, err := template.ParseGlob("templates/*.html")
mux := http.NewServeMux()
h := handlers.Handler{Template: *tmpl}
mux.Handle("/static/", http.StripPrefix("/static", fs))
mux.HandleFunc("/", h.Home)
goth.UseProviders(
twitch.New(
os.Getenv("TWITCH_CLIENT_ID"),
os.Getenv("TWITCH_SECRET"),
"https://localhost:8080/auth/twitch/callback",
),
)
if err != nil {
panic(fmt.Errorf("failed to parse templates: %w\n", err))
}
go func() {
hub := handlers.Hub
hub.Run()
}()
mux.HandleFunc("/ws", h.WsHandler)
mux.HandleFunc("/auth/twitch", h.Auth)
mux.HandleFunc("/auth/twitch/callback", h.Callback)
mux.HandleFunc("/logout", h.Logout)
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
go func() {
fmt.Println("Server started...")
if err := server.ListenAndServe(); err != http.ErrServerClosed {
fmt.Printf("Serve error: %v\n", err)
}
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
fmt.Println("Shutting down server")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
fmt.Printf("Server force to shutdown: %v\n", err)
} else {
fmt.Println("Server exit gracefully")
}
}

491
static/script.js

@ -0,0 +1,491 @@
window.addEventListener("DOMContentLoaded", function() {
class StickFigure {
constructor(x, facing, id, canvas) {
this.canvas = canvas;
this.id = id;
this.x = x;
this.y = canvas.height - 60;
this.facing = facing;
this.action = "idle";
this.hitFrame = 0;
this.speed = 2;
this.vx = 0;
this.vy = 0;
this.gravity = 0.5;
this.jumpStrength = -10;
this.onGround = true;
this.talking = false;
this.talkTimer = 0;
this.message = "";
this.keys = {
h: false,
l: false,
};
this.crouching = false;
this.isPunching = false;
this.punchHasHit = false;
}
update() {
this.vx = 0;
if (this.keys.h) {
this.vx = -this.speed;
this.facing = -1;
}
if (this.keys.l) {
this.vx = this.speed;
this.facing = 1;
}
this.x += this.vx;
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") {
this.hitFrame++;
if (this.hitFrame > 15) {
this.action = 'idle';
this.hitFrame = 0;
}
}
if (this.talking) {
this.talkTimer--;
if (this.talkTimer <= 0) {
this.talking = false;
}
}
}
draw(ctx, time) {
const headRadius = 10;
const x = this.x;
let y = this.y;
const crouch = !!this.crouching;
const bodyLength = crouch ? 20 : 30;
const legLength = crouch ? 20 : 25;
if (crouch) y += 15; // shift down a touch so crouch looks grounded
// head
ctx.beginPath();
ctx.arc(x, y, headRadius, 0, Math.PI * 2);
ctx.stroke();
// eye
ctx.beginPath();
ctx.arc(x + this.facing * 5, y - 3, 2, 0, Math.PI * 2);
ctx.fill();
// body
const chestY = y + headRadius;
const hipY = y + headRadius + bodyLength;
ctx.beginPath();
ctx.moveTo(x, chestY);
ctx.lineTo(x, hipY);
ctx.stroke();
// arms
ctx.beginPath();
ctx.moveTo(x, y + 20);
const walking = this.vx !== 0;
const armSwing = walking ? Math.sin(time * 0.2) * 4 : 0; // subtle swing
if (this.action === "punch" && this.hitFrame < 10) {
ctx.lineTo(x + 30 * this.facing, y + 10);
} else {
ctx.lineTo(x + (20 + armSwing) * this.facing, y + 20);
}
ctx.moveTo(x, y + 20);
ctx.lineTo(x - (20 + armSwing) * this.facing, y + 20);
ctx.stroke();
// legs
ctx.beginPath();
if (this.vx !== 0) {
const step = Math.sin(time * 0.75); // cadence; lower = slower
const stride = 14; // how far forward/back feet go
const lift = 0; // little foot lift
// right leg
ctx.moveTo(x, hipY);
ctx.lineTo(x + stride * step, hipY + legLength + (-lift * Math.abs(step)));
// left leg (opposite phase)
ctx.moveTo(x, hipY);
ctx.lineTo(x - stride * step, hipY + legLength + (-lift * Math.abs(step)));
} else {
const idleSpread = 10;
ctx.moveTo(x, hipY);
ctx.lineTo(x + idleSpread, hipY + legLength);
ctx.moveTo(x, hipY);
ctx.lineTo(x - idleSpread, hipY + legLength);
}
ctx.stroke();
// speech bubble
if (this.talking) {
const padding = 12;
const maxMessageWidth = 150;
// set font BEFORE measuring
ctx.font = "12px sans-serif";
const lines = this._wrapText(ctx, this.message, maxMessageWidth);
const textWidth = ctx.measureText(this.message).width;
const widths = lines.map(line => ctx.measureText(line).width);
const contentWidth = Math.max(...widths, textWidth);
const bubbleWidth = Math.min(contentWidth + padding * 2, maxMessageWidth + padding * 2);
const lineHeight = 18;
const bubbleHeight = lines.length * lineHeight + padding;
const bubbleX = x - bubbleWidth;
const bubbleY = y - 30;
ctx.beginPath();
ctx.fillStyle = "white";
ctx.strokeStyle = "black";
if (typeof ctx.roundRect === "function") {
ctx.roundRect(bubbleX, bubbleY, bubbleWidth, bubbleHeight, 4);
} else {
ctx.rect(bubbleX, bubbleY, bubbleWidth, bubbleHeight);
}
ctx.fill();
ctx.stroke();
ctx.fillStyle = "black";
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], bubbleX + padding, bubbleY + padding + i * lineHeight);
}
}
}
punch() {
this.isPunching = true;
this.punchHasHit = false;
if (this.action === 'idle') {
this.action = 'punch';
this.hitFrame = 0;
}
}
talk(message) {
this.talking = true;
this.talkTimer = 120;
this.message = message;
}
jump() {
if (this.onGround) {
this.vy = this.jumpStrength;
this.onGround = false;
}
}
_wrapText(ctx, text, maxWidth) {
const words = text.split(" ");
const lines = [];
let line = "";
for (let i = 0; i < words.length; i++) {
const testLine = line + words[i] + " ";
const testWidth = ctx.measureText(testLine).width;
if (testWidth > maxWidth && i > 0) {
lines.push(line.trimEnd());
line = words[i] + " ";
} else {
line = testLine;
}
}
lines.push(line.trim());
return lines;
}
getBodyHitbox() {
const w = 24;
const h = 60 - (this.crouching ? 10 : 0);
return {
x: this.x - w / 2,
y: this.y - 10,
w,
h
};
}
getPunchHitbox() {
if (this.action === 'punch' && this.hitFrame < 10) {
const w = 18;
const h = 14;
const frontX = this.x + this.facing * 22;
const x = this.facing === 1 ? frontX : frontX - w;
const y = this.y + (this.crouching ? 18 : 10);
return {
x,
y,
w,
h
};
}
return null;
}
}
class Game {
constructor({ canvasId = 'canvas', chatInputId = 'msg', sendBtnId = "send" }) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.chatInput = document.getElementById(chatInputId);
this.sendBtn = document.getElementById(sendBtnId);
this.time = 0;
this.players = {};
this.pageData = this._getPageData();
this.ws = null;
this.loop = this.loop.bind(this);
this._onKeyDown = this._onKeyDown.bind(this);
this._onKeyUp = this._onKeyUp.bind(this);
this._onSendClick = this._onSendClick.bind(this);
}
start() {
document.addEventListener("keydown", this._onKeyDown);
document.addEventListener("keyup", this._onKeyUp);
if (this.sendBtn) this.sendBtn.addEventListener("click", this._onSendClick);
this._ensurePlayer(this.pageData.username);
this._initWebSocket();
requestAnimationFrame(this.loop);
}
destroy() {
document.removeEventListener("keydown", this._onKeyDown);
document.removeEventListener("keyup", this._onKeyUp);
if (this.sendBtn) this.sendBtn.removeEventListener("click", this._onSendClick);
if (this.ws) this.ws.close();
}
loop() {
this.time += 0.1;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
const playerArray = Object.values(this.players);
playerArray.forEach(player => {
player.update(); // Call the update method for each player
});
for (let i = 0; i < playerArray.length; i++) {
for (let j = i + 1; j < playerArray.length; j++) {
const playerA = playerArray[i];
const playerB = playerArray[j];
const playerAPunch = playerA.getPunchHitbox();
const playerBPunch = playerB.getPunchHitbox();
const playerABody = playerA.getBodyHitbox();
const playerBBody = playerB.getBodyHitbox();
if (playerAPunch && rectangleOverlap(playerAPunch, playerBBody && !playerA.punchHasHit)) {
playerA.punchHasHit = true;
console.log("Player A hits player B");
}
if (playerBPunch && rectangleOverlap(playerBPunch, playerABody) && !playerB.punchHasHit) {
playerB.punchHasHit = true;
console.log("Player B hits player A")
}
}
}
playerArray.forEach(player => {
player.draw(this.ctx, this.time); // Call the draw method for each player
})
requestAnimationFrame(this.loop)
}
_getPageData() {
const metaTag = document.querySelector('meta[name="pagedata"]');
const json = JSON.parse(metaTag.getAttribute("content"));
return json;
}
_isTypingInChat() {
return document.activeElement === this.chatInput;
}
_onKeyDown(e) {
if (this._isTypingInChat()) return;
const me = this._me()
if (!me) return;
switch (e.key) {
case 'e':
me.punch()
this._sendAction('e', "keyDown")
break;
case 'k':
me.jump();
this._sendAction('k', "keyDown")
break;
case 'j':
me.crouching = true;
this._sendAction('j', 'keyDown')
break;
case 'h':
case 'l':
// avoid redundant work if already true
if (!me.keys) me.keys = {};
if (!me.keys[e.key]) me.keys[e.key] = true;
this._sendAction(e.key, 'keyDown')
break;
default:
break;
}
}
_me() {
return this.players[this.pageData.username];
}
_onKeyUp(e) {
const me = this._me();
if (!me) return;
if (e.key === 'j') {
me.crouching = false;
this._sendAction('j', 'keyUp')
}
if (e.key === 'h' || e.key === 'l') {
if (!me.keys) me.keys = {};
me.keys[e.key] = false;
this._sendAction(e.key, 'keyUp')
}
}
_ensurePlayer(id) {
if (!this.players[id] && id !== "") {
this.players[id] = new StickFigure(250, 1, id, this.canvas)
}
}
_onSendClick() {
const inputEl = this.chatInput;
if (!inputEl) return;
const text = inputEl.value.trim();
if (!text) return;
const me = this._me();
if (me) me.talk(text);
this._sendAction("t", "", text);
inputEl.value = "";
}
_initWebSocket() {
const scheme = location.protocol === "https:" ? "wss://" : "ws://";
this.ws = new WebSocket(scheme + location.host + "/ws");
this.ws.onopen = () => {
if (this.pageData.username !== "") {
this._send({
Id: this.pageData.username,
Action: "join",
Message: ""
});
}
};
this.ws.onmessage = (e) => {
const data = JSON.parse(e.data);
this._ensurePlayer(data.Id);
// ignore echo
if (data.Id === this.pageData.username) return;
const p = this.players[data.Id];
switch (data.Action) {
case "t":
p.talk(data.Message);
break;
case "e":
p.punch();
break;
case "k":
p.jump();
break;
case "h":
case "l":
p.keys[data.Action] = data.ActionType === "keyDown";
break;
}
};
this.ws.onerror = (err) => {
console.log("ws error:", err);
};
}
_sendAction(action, actionType = "", message = "") {
this._send({
Id: this.pageData.username,
Action: action,
ActionType: actionType,
Message: message
});
}
_send(obj) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(obj));
}
}
}
const game = new Game({
canvasId: 'canvas',
chatInputId: 'msg',
sendBtnId: 'send',
});
game.start();
});
function rectangleOverlap(a, b) {
return (
a.x < b.x + b.w &&
a.x + a.w > b.x &&
a.y < b.y + b.h &&
a.y + a.h > b.y
)
}

73
templates/index.html

@ -0,0 +1,73 @@
<html>
<head>
<meta name="pagedata" content='{"username": "{{ .Username}}"}' />
<style>
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji";
padding: 12px 0 0;
}
canvas {
margin: 0 auto;
width: 1024px;
display: block;
border: 1px solid #000;
}
.chat {
width: 1024px;
margin: 20px auto 0;
border: 1px solid #000;
padding: 8px;
box-sizing: border-box;
display: flex;
}
#msg {
display: flex;
flex-grow: 1;
margin-right: 8px;
}
#send {
height: 30px;
}
.header {
width: 1024px;
margin: 0 auto 12px;
justify-content: flex-end;
display: flex;
}
.login:link,
.login:visited {
color: #ffffff;
background-color: #310f69;
border-radius: 12px;
padding: 8px;
}
</style>
</head>
<body>
<div class="header">
{{ if eq .Username ""}}
<a href="/auth/twitch" class="login">Login with Twitch</a>
{{ else }}
Logged In as {{ .Username}}
{{ end }}
</div>
<canvas id="canvas" width="1024" height="400"></canvas>
<div class="chat">
<input type="text" id="msg" placeholder="Type in a message to chat" />
<button type="button" id="send">Send</button>
</div>
<script src="/static/script.js"></script>
</body>
</html>

1
tmp/build-errors.log

@ -0,0 +1 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1

BIN
tmp/main

Binary file not shown.
Loading…
Cancel
Save