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.state === 'ground' && 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 = 0; 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; } }