import { StickFigureRenderer } from "./character.js"; import { InputHandler } from "./inputhandler.js"; import { PhysicsSystems } from "./physics.js"; import { StickFigure } from "./stickfigure.js" class Game { constructor({ canvasId = 'canvas', chatInputId = 'msg', sendBtnId = "send", leaveBtnId = "leave" }) { this.renderer = new StickFigureRenderer(); this.physics = new PhysicsSystems(); this.input = new InputHandler(this); 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; this.platform = [ { x: 300, y: this.canvas.height - 90, w: 240, h: 12 }, { x: 200, y: this.canvas.height - 160, w: 240, h: 12 }, { x: 280, y: this.canvas.height - 240, w: 240, h: 12 }, { x: 460, y: this.canvas.height - 320, w: 240, h: 12 }, ] this.pickups = [ { id: 'p1', x: 160, y: this.canvas.height - 15, r: 15, type: 'atk', amount: 10 }, { id: 'p2', x: 350, y: this.canvas.height - 15, r: 15, type: 'def', amount: 20 }, ] } start() { if (this._started) return; this._started = true; // FIXED: was "this.started = true" this._stopped = false; document.addEventListener("keydown", this._onKeyDown, { passive: false }); document.addEventListener("keyup", this._onKeyUp, { passive: false }); window.addEventListener("blur", this._onBlur); // Send disconnect message on page unload window.addEventListener("beforeunload", () => { this._sendAction("disconnected", "", ""); }); 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) { clearTimeout(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)); this._reconnectTimer = setTimeout(() => { this._reconnectTimer = null; this._initWebSocket(); }, delay); this._reconnectDelay = Math.min(this._maxReconnectDelay, Math.round(this._reconnectDelay * 1.8)); } _getPlayerArray() { if (!this._cachedPlayerArray || this._playersChanged) { this._cachedPlayerArray = Object.values(this.players); this._playersChanged = false; } return this._cachedPlayerArray; } loop() { if (this._stopped) return; this.time += 0.1; this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); const playerArray = this._getPlayerArray() 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 if (player.isLocal) { this.physics.resolveCollisions(player, this.platform, this.canvas.height); } }); const alivePlayers = playerArray.filter(p => p.isAlive); for (let i = 0; i < alivePlayers.length; i++) { for (let j = i + 1; j < alivePlayers.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; playerB.hp -= Math.pow(playerA.baseAttackPower, 2) / (playerA.baseAttackPower + playerB.baseDefense); if (playerB.hp <= 0) { playerB.isAlive = false; delete this.players[playerB.id] this._playersChanged = true; } } if (playerBPunch && !playerB.punchHasHit && rectangleOverlap(playerBPunch, playerABody)) { playerB.punchHasHit = true; playerA.hp -= Math.pow(playerB.baseAttackPower, 2) / (playerB.baseAttackPower + playerA.baseDefense); if (playerA.hp <= 0) { playerA.isAlive = false; delete this.players[playerA.id] this._playersChanged = true; } } } } playerArray.forEach(player => { this.renderer.draw(player, this.ctx, this.time); // Call the draw method for each player }) const me = this._me(); if (me && me.isLocal) { if (this.time % 0.2 < 0.1) { this._send({ Id: this.pageData.username, Action: "position_update", PosX: Math.floor(me.x), PosY: Math.floor(me.y), Facing: me.facing, Ts: Date.now(), }) } } this._drawPickups(this.ctx); if (me && me.isLocal) { this._checkPickupCollision(me) } requestAnimationFrame(this.loop) } _drawPickups(ctx) { for (const p of this.pickups) { ctx.beginPath(); ctx.fillStyle = p.type === 'atk' ? '#e74c3c' : '#3498db'; ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = 'white'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(p.type === 'atk' ? 'A' : 'D', p.x, p.y + 0.5); } } _checkPickupCollision(player) { const headRadius = 10; const bodyLength = player.crouching ? 20 : 30; const legLength = player.crouching ? 20 : 25; const px = Math.floor(player.x); const py = Math.floor(player.y + headRadius + bodyLength + legLength); for (let i = this.pickups.length - 1; i >= 0; i--) { const p = this.pickups[i]; const dx = px - p.x; const dy = py - p.y; const dist2 = dx * dx + dy * dy; const playerPickupRadius = 8; const r = p.r + playerPickupRadius; if (dist2 <= r * r) { this._send({ Id: this.pageData.username, Action: 'pickup', PickupId: p.id, PickupIndex: i, Type: p.type, Amount: p.amount, Ts: Date.now() }) } } } _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; this.input.handleKeyDown(e, me); } _me() { return this.players[this.pageData.username]; } _onKeyUp(e) { const me = this._me(); if (!me) return; if (e.key === 't') { this.chatInput.focus(); } if (this._isTypingInChat()) { if (e.key === 'Enter') { this._onSendClick() } if (e.key === "Escape" && this._isTypingInChat()) { this.chatInput.blur(); } return; } this.input.handleKeyUp(e, me); } _ensurePlayer(id) { if (!this.players[id] && id !== "") { this.players[id] = new StickFigure(250, 1, id, this.canvas) this._playersChanged = true; this.players[id].isLocal = (id === this.pageData.username) } } _onBlur() { const me = this._me(); if (!me) return; // ensure local state matches network: clear any stuck presses this.input.releaseAll(me); } _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 = () => { 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) return; let data; try { data = JSON.parse(e.data); } catch (error) { console.error('JSON parse error:', error); console.error('Message length:', e.data.length); console.error('Raw message:', e.data); // Check character codes around position 106 const around106 = e.data.substring(100, 115); console.error('Around position 106:', around106); console.error('Character codes around 106:', Array.from(around106).map((char, i) => `${i + 100}: '${char}' (${char.charCodeAt(0)})`)); // Try to find the actual error position for (let i = 0; i < e.data.length; i++) { try { JSON.parse(e.data.substring(0, i + 1)); } catch (partialError) { if (i > 105 && i < 110) { console.error(`Parse fails at position ${i}: character '${e.data[i]}' (code: ${e.data.charCodeAt(i)})`); } } } return; } this._ensurePlayer(data.Id); this.input.handleNetworkMessage(data); switch (data.Action) { case 'pickup': const idx = this.pickups.findIndex(pp => pp.id === data.PickupId); if (idx !== -1) { this.pickups.splice(idx, 1); } const who = this.players[data.Id]; if (who) { const kind = data.Type; who.applyBuff(kind, data.Amount) } break; } }; this.ws.onerror = (err) => { console.log("ws error:", err); this._scheduleReconnect() }; this.ws.onclose = () => { console.log("leaving game...") this.ws = null; this._sentJoin = false; delete this.players[this.pageData.username] this._playersChanged = true; this._scheduleReconnect() } } _sendAction(action, actionType = "", message = "") { const me = this._me(); this._send({ Id: this.pageData.username, Action: action, ActionType: actionType, Message: message, PosX: me ? Math.floor(me.x) : undefined, PosY: me ? Math.floor(me.y) : undefined, Facing: me ? me.facing : undefined, Ts: Date.now(), }); } _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();