|
|
|
|
@ -1,342 +1,13 @@
@@ -1,342 +1,13 @@
|
|
|
|
|
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 = { |
|
|
|
|
a: false, |
|
|
|
|
d: false, |
|
|
|
|
}; |
|
|
|
|
this.crouching = false; |
|
|
|
|
this.isPunching = false; |
|
|
|
|
this.punchHasHit = false; |
|
|
|
|
this.sentJoin = false; |
|
|
|
|
this.hp = 10; |
|
|
|
|
this.isAlive = true; |
|
|
|
|
this._tx = x; |
|
|
|
|
this._ty = canvas.height - 60; |
|
|
|
|
this._lastTs = 0; |
|
|
|
|
this._tFacing = undefined; |
|
|
|
|
this._lastUpdateTime = Date.now(); |
|
|
|
|
this._updateInterval = 100; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
update() { |
|
|
|
|
this._prevY = this.y; |
|
|
|
|
this.vx = 0; |
|
|
|
|
|
|
|
|
|
if (this.keys.a) { |
|
|
|
|
this.vx = -this.speed; |
|
|
|
|
this.facing = -1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (this.crouching && this.keys.d) { |
|
|
|
|
this.speed = 0.5; |
|
|
|
|
this.vx = this.speed; |
|
|
|
|
} |
|
|
|
|
if (this.crouching && this.keys.a) { |
|
|
|
|
this.speed = 0.5; |
|
|
|
|
this.vx = -this.speed; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (this.keys.d) { |
|
|
|
|
this.vx = this.speed; |
|
|
|
|
this.facing = 1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
this.x += this.vx; |
|
|
|
|
|
|
|
|
|
if (this.isLocal) { |
|
|
|
|
this.vy += this.gravity; |
|
|
|
|
this.y += this.vy; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!this.isLocal && typeof this._tx === "number" |
|
|
|
|
&& typeof this._ty === "number") { |
|
|
|
|
const now = Date.now(); |
|
|
|
|
const timeSinceUpdate = now - this._lastUpdateTime; |
|
|
|
|
|
|
|
|
|
const targetLerpTime = 100; |
|
|
|
|
const deltaTime = 16.67; |
|
|
|
|
const lerpFactor = Math.min(1, deltaTime / targetLerpTime); |
|
|
|
|
const stalenessFactor = Math.min(2, timeSinceUpdate / this._updateInterval); |
|
|
|
|
const adjustedLerpFactor = lerpFactor * stalenessFactor; |
|
|
|
|
|
|
|
|
|
const lerp = (a, b, t) => a + (b - a) * Math.min(1, t); |
|
|
|
|
|
|
|
|
|
this.x = lerp(this.x, this._tx, adjustedLerpFactor); |
|
|
|
|
this.y = lerp(this.y, this._ty, adjustedLerpFactor); |
|
|
|
|
this.facing = this._tFacing ?? this.facing; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
resolveCollisions(platforms, groundY) { |
|
|
|
|
const totalHeight = 60; |
|
|
|
|
const halfFootW = 12; |
|
|
|
|
const EPS = 0.5; |
|
|
|
|
|
|
|
|
|
if (this._prevY === undefined) { |
|
|
|
|
this._prevY = this.y; |
|
|
|
|
} |
|
|
|
|
import { StickFigureRenderer } from "./character.js"; |
|
|
|
|
import { InputHandler } from "./inputhandler.js"; |
|
|
|
|
import { PhysicsSystems } from "./physics.js"; |
|
|
|
|
import { StickFigure } from "./stickfigure.js" |
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
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'); |
|
|
|
|
|
|
|
|
|
@ -366,13 +37,16 @@ window.addEventListener("DOMContentLoaded", function() {
@@ -366,13 +37,16 @@ window.addEventListener("DOMContentLoaded", function() {
|
|
|
|
|
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 }, |
|
|
|
|
|
|
|
|
|
{ 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 }, |
|
|
|
|
] |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -381,8 +55,9 @@ window.addEventListener("DOMContentLoaded", function() {
@@ -381,8 +55,9 @@ window.addEventListener("DOMContentLoaded", function() {
|
|
|
|
|
this._started = true; // FIXED: was "this.started = true"
|
|
|
|
|
this._stopped = false; |
|
|
|
|
|
|
|
|
|
document.addEventListener("keydown", this._onKeyDown); |
|
|
|
|
document.addEventListener("keyup", this._onKeyUp); |
|
|
|
|
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", "", ""); |
|
|
|
|
@ -428,12 +103,21 @@ window.addEventListener("DOMContentLoaded", function() {
@@ -428,12 +103,21 @@ window.addEventListener("DOMContentLoaded", function() {
|
|
|
|
|
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 = Object.values(this.players); |
|
|
|
|
const playerArray = this._getPlayerArray() |
|
|
|
|
|
|
|
|
|
this.ctx.fillStyle = "#333" |
|
|
|
|
|
|
|
|
|
@ -444,12 +128,14 @@ window.addEventListener("DOMContentLoaded", function() {
@@ -444,12 +128,14 @@ window.addEventListener("DOMContentLoaded", function() {
|
|
|
|
|
playerArray.forEach(player => { |
|
|
|
|
player.update(); // Call the update method for each player
|
|
|
|
|
if (player.isLocal) { |
|
|
|
|
player.resolveCollisions(this.platform, this.canvas.height); |
|
|
|
|
this.physics.resolveCollisions(player, this.platform, this.canvas.height); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
for (let i = 0; i < playerArray.length; i++) { |
|
|
|
|
for (let j = i + 1; j < playerArray.length; j++) { |
|
|
|
|
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]; |
|
|
|
|
|
|
|
|
|
@ -458,28 +144,32 @@ window.addEventListener("DOMContentLoaded", function() {
@@ -458,28 +144,32 @@ window.addEventListener("DOMContentLoaded", function() {
|
|
|
|
|
const playerABody = playerA.getBodyHitbox(); |
|
|
|
|
const playerBBody = playerB.getBodyHitbox(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (playerAPunch && !playerA.punchHasHit && rectangleOverlap(playerAPunch, playerBBody)) { |
|
|
|
|
playerA.punchHasHit = true; |
|
|
|
|
playerB.hp -= 1; |
|
|
|
|
if (playerB.hp == 0) { |
|
|
|
|
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 -= 1; |
|
|
|
|
if (playerA.hp == 0) { |
|
|
|
|
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 => { |
|
|
|
|
player.draw(this.ctx, this.time); // Call the draw method for each player
|
|
|
|
|
this.renderer.draw(player, this.ctx, this.time); // Call the draw method for each player
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const me = this._me(); |
|
|
|
|
@ -496,9 +186,64 @@ window.addEventListener("DOMContentLoaded", function() {
@@ -496,9 +186,64 @@ window.addEventListener("DOMContentLoaded", function() {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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")); |
|
|
|
|
@ -515,24 +260,7 @@ window.addEventListener("DOMContentLoaded", function() {
@@ -515,24 +260,7 @@ window.addEventListener("DOMContentLoaded", function() {
|
|
|
|
|
const me = this._me() |
|
|
|
|
if (!me) return; |
|
|
|
|
|
|
|
|
|
switch (e.key) { |
|
|
|
|
case 'e': |
|
|
|
|
case ' ': |
|
|
|
|
this._sendAction('e', "keyDown") |
|
|
|
|
break; |
|
|
|
|
case 'w': |
|
|
|
|
this._sendAction('k', "keyDown") |
|
|
|
|
break; |
|
|
|
|
case 's': |
|
|
|
|
this._sendAction('j', 'keyDown') |
|
|
|
|
break; |
|
|
|
|
case 'a': |
|
|
|
|
case 'd': |
|
|
|
|
this._sendAction(e.key, 'keyDown') |
|
|
|
|
break; |
|
|
|
|
default: |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
this.input.handleKeyDown(e, me); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
_me() { |
|
|
|
|
@ -547,14 +275,6 @@ window.addEventListener("DOMContentLoaded", function() {
@@ -547,14 +275,6 @@ window.addEventListener("DOMContentLoaded", function() {
|
|
|
|
|
this.chatInput.focus(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (e.key === 's') { |
|
|
|
|
this._sendAction('j', 'keyUp') |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (e.key === 'a' || e.key === 'd') { |
|
|
|
|
this._sendAction(e.key, 'keyUp') |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (this._isTypingInChat()) { |
|
|
|
|
if (e.key === 'Enter') { |
|
|
|
|
this._onSendClick() |
|
|
|
|
@ -562,16 +282,27 @@ window.addEventListener("DOMContentLoaded", function() {
@@ -562,16 +282,27 @@ window.addEventListener("DOMContentLoaded", function() {
|
|
|
|
|
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; |
|
|
|
|
@ -612,48 +343,50 @@ window.addEventListener("DOMContentLoaded", function() {
@@ -612,48 +343,50 @@ window.addEventListener("DOMContentLoaded", function() {
|
|
|
|
|
|
|
|
|
|
this.ws.onmessage = (e) => { |
|
|
|
|
if (!e.data) return; |
|
|
|
|
const data = JSON.parse(e.data); |
|
|
|
|
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); |
|
|
|
|
|
|
|
|
|
this._ensurePlayer(data.Id) |
|
|
|
|
const p = this.players[data.Id]; |
|
|
|
|
// 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)})`)); |
|
|
|
|
|
|
|
|
|
if (typeof data.PosX === "number" && typeof data.PosY === "number") { |
|
|
|
|
p._tx = data.PosX; |
|
|
|
|
p._ty = data.PosY; |
|
|
|
|
p._lastUpdateTime = Date.now(); |
|
|
|
|
// 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)})`); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (typeof data.Facing === "number") { |
|
|
|
|
p._tFacing = data.Facing; |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
this._ensurePlayer(data.Id); |
|
|
|
|
this.input.handleNetworkMessage(data); |
|
|
|
|
|
|
|
|
|
switch (data.Action) { |
|
|
|
|
case "disconnected": |
|
|
|
|
delete this.players[data.Id] |
|
|
|
|
break; |
|
|
|
|
case "t": |
|
|
|
|
p.talk(data.Message); |
|
|
|
|
break; |
|
|
|
|
case "e": |
|
|
|
|
p.punch(); |
|
|
|
|
break; |
|
|
|
|
case "k": |
|
|
|
|
case "w": |
|
|
|
|
p.jump(); |
|
|
|
|
break; |
|
|
|
|
case "j": |
|
|
|
|
case "s": |
|
|
|
|
p.crouching = data.ActionType === "keyDown" |
|
|
|
|
p.speed = 2; |
|
|
|
|
break; |
|
|
|
|
case "h": |
|
|
|
|
case "a": |
|
|
|
|
p.keys[data.Action === "h" ? "h" : "a"] = data.ActionType === "keyDown"; |
|
|
|
|
break; |
|
|
|
|
case "l": |
|
|
|
|
case "d": |
|
|
|
|
p.keys[data.Action === "l" ? "l" : "d"] = data.ActionType === "keyDown"; |
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
@ -668,6 +401,7 @@ window.addEventListener("DOMContentLoaded", function() {
@@ -668,6 +401,7 @@ window.addEventListener("DOMContentLoaded", function() {
|
|
|
|
|
this.ws = null; |
|
|
|
|
this._sentJoin = false; |
|
|
|
|
delete this.players[this.pageData.username] |
|
|
|
|
this._playersChanged = true; |
|
|
|
|
this._scheduleReconnect() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -696,22 +430,13 @@ window.addEventListener("DOMContentLoaded", function() {
@@ -696,22 +430,13 @@ window.addEventListener("DOMContentLoaded", function() {
|
|
|
|
|
this.ws.close(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const game = new Game({ |
|
|
|
|
const game = new Game({ |
|
|
|
|
canvasId: 'canvas', |
|
|
|
|
chatInputId: 'msg', |
|
|
|
|
sendBtnId: 'send', |
|
|
|
|
leaveBtnId: 'leave', |
|
|
|
|
}); |
|
|
|
|
game.start(); |
|
|
|
|
}); |
|
|
|
|
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 |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
|