You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

449 lines
14 KiB

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();
}
}
function rectangleOverlap(rect1, rect2) {
return rect1.x < rect2.x + rect2.w &&
rect1.x + rect1.w > rect2.x &&
rect1.y < rect2.y + rect2.h &&
rect1.y + rect1.h > rect2.y;
}
const game = new Game({
canvasId: 'canvas',
chatInputId: 'msg',
sendBtnId: 'send',
leaveBtnId: 'leave',
});
game.start();