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; this.sentJoin = false; this.hp = 10; this.isAlive = true; } update() { this.vx = 0; if (this.keys.h) { this.vx = -this.speed; this.facing = -1; } if (this.crouching && this.keys.l) { this.speed = 0.5; this.vx = this.speed; } if (this.crouching && this.keys.h) { this.speed = 0.5; this.vx = -this.speed; } 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; this.isPunching = false; this.punchHasHit = false; } } 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 // Set drawing styles ctx.strokeStyle = "black"; ctx.fillStyle = "black"; ctx.lineWidth = 2; // 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 = 100; // 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 + 10, maxMessageWidth + padding + 10); const lineHeight = 14; const bubbleHeight = lines.length * lineHeight + padding; const bubbleX = x - bubbleWidth - 15; const bubbleY = y - 20; 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); } } // name tag ctx.save(); ctx.font = "10px sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "bottom"; ctx.fillStyle = "black"; ctx.fillText(this.id, x, y - 19); ctx.restore(); } 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", leaveBtnId = "leave" }) { this.canvas = document.getElementById(canvasId); this.ctx = this.canvas.getContext('2d'); this.chatInput = document.getElementById(chatInputId); this.sendBtn = document.getElementById(sendBtnId); this.leaveBtn = document.getElementById(leaveBtnId); 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); this._leaveGame = this._leaveGame.bind(this); this._started = false; this._sentJoin = false; this._stopped = false; this._reconnectDelay = 1000; this._maxReconnectDelay = 15000; this._reconnectTimer = null; } start() { if (this._started) return; this._started = true; // FIXED: was "this.started = true" this._stopped = false; document.addEventListener("keydown", this._onKeyDown); document.addEventListener("keyup", this._onKeyUp); if (this.sendBtn) this.sendBtn.addEventListener("click", this._onSendClick); if (this.leaveBtn) this.leaveBtn.addEventListener("click", this._leaveGame); this._ensurePlayer(this.pageData.username); this._initWebSocket(); requestAnimationFrame(this.loop); } destroy() { this._stopped = true; this._started = false; if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; } 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(); } _scheduleReconnect() { if (this._stopped) return; if (this._reconnectTimer) { this._reconnectTimer = null; } const jitter = this._reconnectDelay * (Math.random() * 0.4 - 0.2); const delay = Math.max(250, Math.min(this._maxReconnectDelay, this._reconnectDelay + jitter)); console.log(`Reconnecting websocket...`) this._reconnectTimer = setTimeout(() => { this._reconnectTimer = null; this._initWebSocket(); }, delay); this._reconnectDelay = Math.min(this._maxReconnectDelay, Math.round(this._reconnectDelay * 1.8)); } 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 && !playerA.punchHasHit && rectangleOverlap(playerAPunch, playerBBody)) { playerA.punchHasHit = true; console.log("Player A hits player B"); playerB.hp -= 1; if (playerB.hp == 0) { playerB.isAlive = false; delete this.players[playerB.id] } } if (playerBPunch && !playerB.punchHasHit && rectangleOverlap(playerBPunch, playerABody)) { playerB.punchHasHit = true; console.log("Player B hits player A") playerA.hp -= 1; if (playerA.hp == 0) { playerA.isAlive = false; delete this.players[playerA.id] } } } } 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': 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 === 't') { this.chatInput.focus(); } if (e.key === 'j') { me.crouching = false; me.speed = 2; 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') } if (this._isTypingInChat()) { if (e.key === 'Enter') { this._onSendClick() } if (e.key === "Escape" && this._isTypingInChat()) { this.chatInput.blur(); } } } _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() { if (this._stopped) return; if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) { return; } const scheme = location.protocol === "https:" ? "wss://" : "ws://"; this.ws = new WebSocket(scheme + location.host + "/ws"); this.ws.onopen = () => { console.log('i am opening') this._reconnectDelay = 1000; if (!this._sentJoin && this.pageData.username !== "") { this._send({ Id: this.pageData.username, Action: "join", Message: "" }); this._sentJoin = true; } }; this.ws.onmessage = (e) => { if (e.data) { 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); this._scheduleReconnect() }; this.ws.onclose = (event) => { console.log(event.code) console.log("leaving game...") console.log(this.pageData) this.ws = null; this._sentJoin = false; this._scheduleReconnect() } } _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)); } } _leaveGame() { console.log('leave button was clicked'); this.ws.close(); } } const game = new Game({ canvasId: 'canvas', chatInputId: 'msg', sendBtnId: 'send', leaveBtnId: 'leave', }); 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 ) }