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; } }