Browse Source

Refactoring

main
Stephanie Gredell 5 months ago
parent
commit
00bb43036d
  1. 176
      static/character.js
  2. 259
      static/inputhandler.js
  3. 44
      static/physics.js
  4. 575
      static/script.js
  5. 193
      static/stickfigure.js

176
static/character.js

@ -0,0 +1,176 @@ @@ -0,0 +1,176 @@
export class StickFigureRenderer {
draw(player, ctx, time) {
const headRadius = 10;
const x = player.x;
let y = player.y;
const crouch = !!player.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 + player.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);
if (player.action === "punch" && player.hitFrame < 10) {
ctx.lineTo(x + 30 * player.facing, y + 10);
} else {
ctx.lineTo(x + 20 * player.facing, y + 20);
}
ctx.moveTo(x, y + 20);
ctx.lineTo(x - 20 * player.facing, y + 20);
ctx.stroke();
// legs
ctx.beginPath();
if (player.vx !== 0) {
const step = Math.sin(time * 0.75);
const stride = 14;
const lift = 0;
// right leg
ctx.moveTo(x, hipY);
ctx.lineTo(x + stride * step, hipY + legLength + (-lift * Math.abs(step)));
// left leg
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();
ctx.strokeStyle = "black";
// speech bubble
if (player.talking) {
const padding = 12;
const maxMessageWidth = 100;
// set font BEFORE measuring
ctx.font = "12px sans-serif";
const lines = this._wrapText(ctx, player.message, maxMessageWidth);
const textWidth = ctx.measureText(player.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);
}
}
let nameY = y - 80;
if (player.isAlive) {
nameY = this._drawHealthBar(ctx, player)
}
// name tag
ctx.save();
ctx.font = "10px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillStyle = "black";
ctx.fillText(player.id, x, nameY);
ctx.restore();
}
_drawHealthBar(ctx, player) {
const x = player.x;
const headCenterY = player.y;
const topOfHead = headCenterY - 10;
const barWidth = 40;
const barHeight = 5;
const barX = x - barWidth / 2;
const barY = topOfHead - 10;
ctx.fillStyle = "#ddd";
ctx.fillRect(barX, barY, barWidth, barHeight);
const pct = Math.max(0, Math.min(1, player.hp / player.maxHp));
let color = '#27ae60';
if (pct <= 0.33) {
color = '#e74c3c';
} else if (pct <= 0.66) {
color = '#f1c40f';
}
ctx.fillStyle = color;
ctx.fillRect(barX, barY, barWidth * pct, barHeight);
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.strokeRect(barX, barY, barWidth, barHeight);
return barY;
}
_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;
}
}

259
static/inputhandler.js

@ -0,0 +1,259 @@ @@ -0,0 +1,259 @@
class Command {
execute() { }
toNetwork() {
return null;
}
}
class PunchCmd extends Command {
execute(p) {
p.punch();
}
toNetwork(p) {
return {
Id: p.id,
Action: ' ',
ActionType: "keyDown"
};
}
}
class JumpCmd extends Command {
execute(p) {
p.jump();
}
toNetwork(p) {
return {
Id: p.id,
Action: 'w',
ActionType: 'keyDown'
};
}
}
class CrouchPressCmd extends Command {
execute(p) {
p.crouching = true;
}
toNetwork(p) {
return {
Id: p.id,
Action: 's',
ActionType: 'keyDown'
};
}
}
class CrouchUnpressCmd extends Command {
execute(p) {
p.crouching = false;
}
toNetwork(p) {
return {
Id: p.id,
Action: 's',
ActionType: 'keyUp'
};
}
}
class MoveLeftPressCmd extends Command {
execute(p) {
p.keys.a = true;
}
toNetwork(p) {
return {
Id: p.id,
Action: 'a',
ActionType: 'keyDown'
};
}
}
class MoveLeftUnpressCmd extends Command {
execute(p) {
p.keys.a = false;
}
toNetwork(p) {
return {
Id: p.id,
Action: 'a',
ActionType: 'keyUp'
};
}
}
class MoveRightPressCmd extends Command {
execute(p) {
p.keys.d = true;
}
toNetwork(p) {
return {
Id: p.id,
Action: 'd',
ActionType: 'keyDown'
};
}
}
class MoveRightUnpressCmd extends Command {
execute(p) {
p.keys.d = false;
}
toNetwork(p) {
return {
Id: p.id,
Action: 'd',
ActionType: 'keyUp'
};
}
}
class TalkCmd extends Command {
constructor(message) {
super();
this.message = message;
}
execute(p) {
p.talk(this.message);
}
toNetwork(p) {
return {
Id: p.id,
Action: 't',
ActionType: '',
Message: this.message
}
}
}
class DisconnectedCmd extends Command {
execute() { }
toNetwork(p) {
return {
Id: p.id,
Action: "disconnected",
ActionType: ""
};
}
}
export class InputHandler {
constructor(game) {
this.game = game;
this.downMap = new Map([
[' ', () => new PunchCmd()],
['w', () => new JumpCmd()],
['s', () => new CrouchPressCmd()],
['a', () => new MoveLeftPressCmd()],
['d', () => new MoveRightPressCmd()],
]);
this.upMap = new Map([
['s', () => new CrouchUnpressCmd()],
['a', () => new MoveLeftUnpressCmd()],
['d', () => new MoveRightUnpressCmd()],
]);
}
handleKeyDown(e, player) {
const key = e.code === 'Space' ? ' ' : (e.key || '').toLowerCase();
const k = this.downMap.get(key);
if (!k) {
return;
}
e.preventDefault();
e.stopPropagation();
this.dispatch(k(), player)
}
handleKeyUp(e, player) {
const key = e.code === "Space" ? ' ' : e.key;
const k = this.upMap.get(key);
if (!k) {
return;
}
e.preventDefault();
e.stopPropagation();
this.dispatch(k(), player)
}
dispatch(cmd, player) {
if (!player) return;
cmd.execute(player, this.game);
if (player.isLocal) {
const msg = cmd.toNetwork(player);
if (msg) {
this.game._send({
...msg,
PosX: Math.floor(player.x),
PosY: Math.floor(player.y),
Facing: player.facing,
Ts: Date.now(),
});
}
}
}
handleNetworkMessage(data) {
this.game._ensurePlayer(data.Id);
const p = this.game.players?.[data.Id];
if (!p) {
return;
}
if (typeof data.PosX === "number") {
p._lastTx = p._tx ?? data.PosX;
p._tx = data.PosX;
}
if (typeof data.PosY === "number") {
p._lastTy = p._ty ?? data.PosY;
p._ty = data.PosY;
}
if (typeof data.Facing === "number") {
p._tFacing = data.Facing;
}
p._lastUpdateTime = Date.now();
const action = data.Action;
const actionType = data.ActionType;
const apply = (cmd) => cmd && cmd.execute(p, this.game);
switch (action) {
case 'disconnected':
delete this.game.players[data.Id];
break;
case 't':
apply(new TalkCmd(data.Message ?? ""));
break;
case ' ':
apply(new PunchCmd());
break;
case 'w':
apply(new JumpCmd());
break;
case 's':
apply(actionType === 'keyDown' ? new CrouchPressCmd() : new CrouchUnpressCmd());
break;
case 'a':
apply(actionType === 'keyDown' ? new MoveLeftPressCmd() : new MoveLeftUnpressCmd());
break;
case 'd':
apply(actionType === 'keyDown' ? new MoveRightPressCmd() : new MoveRightUnpressCmd());
break;
}
}
releaseAll(player) {
const cmds = [];
if (player.keys?.a) cmds.push(new MoveLeftUnpressCmd());
if (player.keys?.d) cmds.push(new MoveRightUnpressCmd());
if (player.crouching) cmds.push(new CrouchUnpressCmd());
for (const c of cmds) this.dispatch(c, player);
}
}

44
static/physics.js

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
export class PhysicsSystems {
resolveCollisions(player, platforms, groundY) {
const totalHeight = 60;
const halfFootW = 12;
const EPS = 0.5;
if (player._prevY === undefined) {
player._prevY = player.y;
}
let landedOnPlatform = false;
// one-way platforms (land only when falling and crossing from above)
if (player.vy >= 0) {
for (const pf of platforms) {
const feetLeft = player.x - halfFootW;
const feetRight = player.x + halfFootW;
const pfLeft = pf.x, pfRight = pf.x + pf.w;
const horizOverlap = feetRight > pfLeft && feetLeft < pfRight;
const wasAbove = (player._prevY + totalHeight) <= pf.y + EPS;
const nowBelowTop = (player.y + totalHeight) >= pf.y - EPS;
const dropping = player.crouching === true; // hold 's' to drop through
if (horizOverlap && wasAbove && nowBelowTop && !dropping) {
player.y = pf.y - totalHeight;
player.vy = 0;
landedOnPlatform = true;
break;
}
}
}
// --- ground after platforms ---
if (!landedOnPlatform && player.y + totalHeight >= groundY) {
player.y = groundY - totalHeight;
player.vy = 0;
player.onGround = true;
} else if (landedOnPlatform) {
player.onGround = true;
} else {
player.onGround = false;
}
}
}

575
static/script.js

@ -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
)
}

193
static/stickfigure.js

@ -0,0 +1,193 @@ @@ -0,0 +1,193 @@
export 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._lastTx = x;
this._lastTy = this.y;
this._lastUpdateTime = 0;
this._walkTTL = 0;
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 = 100;
this.maxHp = 100;
this.isAlive = true;
this._tx = x;
this._ty = canvas.height - 60;
this._lastTs = 0;
this._tFacing = undefined;
this._lastUpdateTime = Date.now();
this._updateInterval = 10;
this.baseAttackPower = 14;
this.baseDefense = 14;
}
update() {
const prevX = this.x;
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.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 = 200;
const deltaTime = 16.67;
const lerpFactor = Math.min(1, deltaTime / targetLerpTime);
const stalenessFactor = Math.min(2, timeSinceUpdate / this._updateInterval);
const adjustedLerpFactor = lerpFactor * stalenessFactor;
const deltaY = Math.abs(this.y - this._ty);
if (deltaY > 8 || !this.onGround) {
this.x = this._tx;
this.y = this._ty;
} else {
this.x = this.lerp(this.x, this._tx, adjustedLerpFactor);
this.y = this.lerp(this.y, this._ty, adjustedLerpFactor);
}
this.facing = this._tFacing ?? this.facing;
this.vx = this.x - prevX;
if (Math.abs(this.vx) < 0.5) {
this.vx = 0;
}
}
}
lerp(a, b, t) {
return a + (b - a) * Math.min(1, t);
}
applyBuff(type, amount) {
if (type === 'atk') {
this.baseAttackPower += amount;
}
if (type === 'def') {
this.baseDefense += amount;
}
}
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;
}
}
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;
}
}
Loading…
Cancel
Save