class Renderer { constructor(canvas) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.goalXOffset = 80; // Distance from rink edge this.setupCanvas(); this.setupFixedCamera(); } setupCanvas() { this.canvas.style.imageRendering = 'pixelated'; this.ctx.imageSmoothingEnabled = false; } setupFixedCamera() { const rinkWidth = 1000; const rinkHeight = 600; const padding = 50; const scaleX = this.canvas.width / (rinkWidth + padding * 2); const scaleY = this.canvas.height / (rinkHeight + padding * 2); const zoom = Math.min(scaleX, scaleY); const x = (this.canvas.width - rinkWidth * zoom) / 2; const y = (this.canvas.height - rinkHeight * zoom) / 2; this.camera = { x: x, y: y, zoom: zoom, target: null, smoothing: 0.1 }; } clear() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); } drawRink(gameState) { const rink = gameState.rink; this.ctx.save(); this.applyCamera(); this.ctx.fillStyle = '#f8f8f8'; this.ctx.fillRect(0, 0, rink.width, rink.height); this.drawRinkLines(rink); this.drawGoals(rink); this.drawFaceoffDots(rink); this.drawCreases(rink); this.ctx.restore(); } drawRinkLines(rink) { // Outer lines this.ctx.strokeStyle = '#d32f2f'; this.ctx.lineWidth = 3; // Zone lines this.ctx.beginPath(); this.ctx.rect(0, 0, rink.width, rink.height); this.ctx.stroke(); this.ctx.strokeStyle = '#d32f2f'; this.ctx.lineWidth = 3; this.ctx.beginPath(); this.ctx.moveTo(rink.centerX, 0); this.ctx.lineTo(rink.centerX, rink.height); this.ctx.stroke(); const zoneWidth = rink.width / 3; this.ctx.strokeStyle = '#2196f3'; this.ctx.beginPath(); this.ctx.moveTo(zoneWidth, 0); this.ctx.lineTo(zoneWidth, rink.height); this.ctx.stroke(); this.ctx.beginPath(); this.ctx.moveTo(rink.width - zoneWidth, 0); this.ctx.lineTo(rink.width - zoneWidth, rink.height); this.ctx.stroke(); this.ctx.beginPath(); this.ctx.arc(rink.centerX, rink.centerY, RINK_CIRCLES.CENTER_ICE_RADIUS, 0, Math.PI * 2); this.ctx.stroke(); // Goal lines this.ctx.strokeStyle = '#d32f2f'; this.ctx.lineWidth = 3; this.ctx.beginPath(); this.ctx.moveTo(this.goalXOffset, 0); this.ctx.lineTo(this.goalXOffset, rink.height); this.ctx.stroke(); this.ctx.beginPath(); this.ctx.moveTo(rink.width - this.goalXOffset, 0); this.ctx.lineTo(rink.width - this.goalXOffset, rink.height); this.ctx.stroke(); } drawGoals(rink) { this.ctx.strokeStyle = '#d32f2f'; this.ctx.lineWidth = 4; const goalY = rink.centerY; const goalHeight = rink.goalHeight; const goalDepth = 25; this.ctx.beginPath(); this.ctx.moveTo(this.goalXOffset, goalY - goalHeight); this.ctx.lineTo(this.goalXOffset - goalDepth, goalY - goalHeight); this.ctx.lineTo(this.goalXOffset - goalDepth, goalY + goalHeight); this.ctx.lineTo(this.goalXOffset, goalY + goalHeight); this.ctx.stroke(); this.ctx.beginPath(); this.ctx.moveTo(rink.width - this.goalXOffset, goalY - goalHeight); this.ctx.lineTo(rink.width - this.goalXOffset + goalDepth, goalY - goalHeight); this.ctx.lineTo(rink.width - this.goalXOffset + goalDepth, goalY + goalHeight); this.ctx.lineTo(rink.width - this.goalXOffset, goalY + goalHeight); this.ctx.stroke(); this.ctx.fillStyle = 'rgba(211, 47, 47, 0.1)'; this.ctx.fillRect(this.goalXOffset - goalDepth, goalY - goalHeight, goalDepth, goalHeight * 2); this.ctx.fillRect(rink.width - this.goalXOffset, goalY - goalHeight, goalDepth, goalHeight * 2); } drawCreases(rink) { this.ctx.strokeStyle = '#4fc3f7'; this.ctx.lineWidth = 2; this.ctx.fillStyle = 'rgba(79, 195, 247, 0.1)'; const creaseRadius = RINK_CIRCLES.GOAL_CREASE_RADIUS; const goalY = rink.centerY; this.ctx.beginPath(); this.ctx.arc(this.goalXOffset, goalY, creaseRadius, -Math.PI/2, Math.PI/2); this.ctx.fill(); this.ctx.stroke(); this.ctx.beginPath(); this.ctx.arc(rink.width - this.goalXOffset, goalY, creaseRadius, Math.PI/2, -Math.PI/2); this.ctx.fill(); this.ctx.stroke(); } drawFaceoffDots(rink) { this.ctx.fillStyle = '#d32f2f'; rink.faceoffDots.forEach((dot, index) => { this.ctx.beginPath(); this.ctx.arc(dot.x, dot.y, RINK_CIRCLES.FACEOFF_DOT_RADIUS, 0, Math.PI * 2); this.ctx.fill(); // Skip drawing faceoff circle for center ice (index 2) - only use blue center circle if (index !== 2) { this.ctx.strokeStyle = '#d32f2f'; this.ctx.lineWidth = 2; this.ctx.beginPath(); this.ctx.arc(dot.x, dot.y, RINK_CIRCLES.FACEOFF_CIRCLE_RADIUS, 0, Math.PI * 2); this.ctx.stroke(); } }); } drawPlayers(players) { this.ctx.save(); this.applyCamera(); players.forEach(player => { player.render(this.ctx); }); this.ctx.restore(); } drawPuck(puck) { this.ctx.save(); this.applyCamera(); puck.render(this.ctx); this.ctx.restore(); } drawUI(gameState) { this.updateScoreBoard(gameState); this.updateGameStats(gameState); } updateScoreBoard(gameState) { document.querySelector('.team.home .score').textContent = gameState.homeScore; document.querySelector('.team.away .score').textContent = gameState.awayScore; document.getElementById('period').textContent = gameState.getPeriodName(); document.getElementById('clock').textContent = gameState.formatTime(gameState.timeRemaining); } updateGameStats(gameState) { document.getElementById('home-shots').textContent = gameState.stats.home.shots; document.getElementById('away-shots').textContent = gameState.stats.away.shots; } drawParticleEffect(position, type, color = '#ffff00') { this.ctx.save(); this.applyCamera(); switch (type) { case 'goal': this.drawGoalEffect(position); break; case 'hit': this.drawHitEffect(position, '#00000038', 10); break; case 'save': this.drawSaveEffect(position); break; } this.ctx.restore(); } drawGoalEffect(position) { this.ctx.fillStyle = '#ffff00'; this.ctx.strokeStyle = '#ff8800'; this.ctx.lineWidth = 3; for (let i = 0; i < 8; i++) { const angle = (i / 8) * Math.PI * 2; const x = position.x + Math.cos(angle) * 30; const y = position.y + Math.sin(angle) * 30; this.ctx.beginPath(); this.ctx.arc(x, y, 5, 0, Math.PI * 2); this.ctx.fill(); this.ctx.stroke(); } } drawHitEffect(position, color, size = 25) { this.ctx.strokeStyle = color; this.ctx.lineWidth = 4; const innerRadius = size * 0.4; const outerRadius = size; for (let i = 0; i < 6; i++) { const angle = (i / 6) * Math.PI * 2; const startX = position.x + Math.cos(angle) * innerRadius; const startY = position.y + Math.sin(angle) * innerRadius; const endX = position.x + Math.cos(angle) * outerRadius; const endY = position.y + Math.sin(angle) * outerRadius; this.ctx.beginPath(); this.ctx.moveTo(startX, startY); this.ctx.lineTo(endX, endY); this.ctx.stroke(); } } drawSaveEffect(position) { this.ctx.fillStyle = '#4fc3f7'; this.ctx.strokeStyle = '#0288d1'; this.ctx.lineWidth = 2; this.ctx.beginPath(); this.ctx.arc(position.x, position.y, 20, 0, Math.PI * 2); this.ctx.stroke(); this.ctx.beginPath(); this.ctx.arc(position.x, position.y, 15, 0, Math.PI * 2); this.ctx.stroke(); } updateCamera(target) { // Camera is now fixed - no updates needed } applyCamera() { this.ctx.translate(this.camera.x, this.camera.y); this.ctx.scale(this.camera.zoom, this.camera.zoom); } setZoom(zoom) { this.camera.zoom = Math.max(0.5, Math.min(2.0, zoom)); } screenToWorld(screenPos) { return new Vector2( (screenPos.x - this.camera.x) / this.camera.zoom, (screenPos.y - this.camera.y) / this.camera.zoom ); } worldToScreen(worldPos) { return new Vector2( worldPos.x * this.camera.zoom + this.camera.x, worldPos.y * this.camera.zoom + this.camera.y ); } drawDebugInfo(gameState, players, puck, selectedPlayer = null) { // Always draw selected player target line if there is one, even when debug mode is off if (selectedPlayer) { this.ctx.save(); this.applyCamera(); this.drawSelectedPlayerTarget(selectedPlayer); this.ctx.restore(); } // Only draw the rest of debug info if debug mode is on if (!window.debugMode) return; // Draw basic debug info overlay this.ctx.save(); this.ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; this.ctx.fillRect(10, 10, 250, 180); this.ctx.fillStyle = '#fff'; this.ctx.font = '12px monospace'; this.ctx.fillText(`FPS: ${Math.round(1000 / 16)}`, 20, 30); this.ctx.fillText(`Players: ${players.length}`, 20, 50); this.ctx.fillText(`Puck Speed: ${Math.round(puck.velocity.magnitude())}`, 20, 70); this.ctx.fillText(`Game Time: ${gameState.formatTime(gameState.timeRemaining)}`, 20, 90); this.ctx.fillText(`Period: ${gameState.period}`, 20, 110); this.ctx.fillText(`Paused: ${gameState.isPaused}`, 20, 130); this.ctx.fillText(`Faceoff: ${gameState.faceoff.isActive}`, 20, 150); this.ctx.fillText(`Puck Active: ${gameState.puckActive !== undefined ? gameState.puckActive : 'N/A'}`, 20, 170); this.ctx.restore(); // Draw enhanced debug visualizations on the rink this.ctx.save(); this.applyCamera(); this.drawDebugVectors(players, puck); this.drawDebugPlayerInfo(players); this.drawDebugPuckInfo(puck); this.ctx.restore(); } drawDebugVectors(players, puck) { // Draw velocity vectors for players players.forEach(player => { if (player.velocity.magnitude() > 5) { this.drawVector(player.position, player.velocity, '#ff4444', 0.5); } // Draw target position if (player.targetPosition) { this.drawLine(player.position, player.targetPosition, '#44ff44', 1, [5, 5]); } // Draw AI target if exists if (player.aiState.target && player.aiState.target.position) { this.drawLine(player.position, player.aiState.target.position, '#ffff44', 1, [2, 2]); } }); // Draw puck velocity vector if (puck.velocity.magnitude() > 10) { this.drawVector(puck.position, puck.velocity, '#4444ff', 0.3); } } drawDebugPlayerInfo(players) { this.ctx.font = '10px Arial'; players.forEach(player => { const x = player.position.x; const y = player.position.y - player.radius - 5; // Draw player ID this.ctx.fillStyle = player.team === 'home' ? '#ff4444' : '#4444ff'; this.ctx.fillText(`${player.role}`, x - 10, y); // Highlight puck carrier if (player.state.hasPuck) { this.ctx.strokeStyle = '#ffff00'; this.ctx.lineWidth = 3; this.ctx.setLineDash([3, 3]); this.ctx.beginPath(); this.ctx.arc(x, player.position.y, player.radius + 5, 0, Math.PI * 2); this.ctx.stroke(); this.ctx.setLineDash([]); } }); } drawDebugPuckInfo(puck) { const x = puck.position.x; const y = puck.position.y - 20; this.ctx.font = '8px Arial'; this.ctx.fillStyle = '#ffffff'; this.ctx.fillText(`Speed: ${Math.round(puck.velocity.magnitude())}`, x - 20, y); if (puck.lastTouchedBy) { this.ctx.fillText(`Last: ${puck.lastTouchedBy}`, x - 20, y + 10); } } drawVector(position, vector, color = '#ffffff', scale = 1) { const endX = position.x + vector.x * scale; const endY = position.y + vector.y * scale; this.ctx.strokeStyle = color; this.ctx.lineWidth = 2; this.ctx.setLineDash([]); // Draw vector line this.ctx.beginPath(); this.ctx.moveTo(position.x, position.y); this.ctx.lineTo(endX, endY); this.ctx.stroke(); // Draw arrow head const angle = Math.atan2(vector.y, vector.x); const arrowSize = 8; this.ctx.beginPath(); this.ctx.moveTo(endX, endY); this.ctx.lineTo( endX - arrowSize * Math.cos(angle - Math.PI / 6), endY - arrowSize * Math.sin(angle - Math.PI / 6) ); this.ctx.moveTo(endX, endY); this.ctx.lineTo( endX - arrowSize * Math.cos(angle + Math.PI / 6), endY - arrowSize * Math.sin(angle + Math.PI / 6) ); this.ctx.stroke(); } drawLine(start, end, color = '#ffffff', width = 1, dash = []) { this.ctx.strokeStyle = color; this.ctx.lineWidth = width; this.ctx.setLineDash(dash); this.ctx.beginPath(); this.ctx.moveTo(start.x, start.y); this.ctx.lineTo(end.x, end.y); this.ctx.stroke(); this.ctx.setLineDash([]); } drawSelectedPlayerTarget(selectedPlayer) { if (!selectedPlayer) { return; } if (!selectedPlayer.targetPosition) { console.log('Selected player has no target position:', selectedPlayer.name); return; } // Check if target is different from current position const distance = Math.sqrt( Math.pow(selectedPlayer.targetPosition.x - selectedPlayer.position.x, 2) + Math.pow(selectedPlayer.targetPosition.y - selectedPlayer.position.y, 2) ); if (distance < 5) { // Target too close to player, not worth drawing line return; } // Save current context state this.ctx.save(); // Draw a bright, prominent line from selected player to their target this.ctx.strokeStyle = '#00ffff'; // Cyan color for high visibility this.ctx.lineWidth = 6; this.ctx.setLineDash([12, 6]); // Larger dashed line pattern this.ctx.lineCap = 'round'; this.ctx.beginPath(); this.ctx.moveTo(selectedPlayer.position.x, selectedPlayer.position.y); this.ctx.lineTo(selectedPlayer.targetPosition.x, selectedPlayer.targetPosition.y); this.ctx.stroke(); // Also draw a solid white line underneath for extra visibility this.ctx.strokeStyle = '#ffffff'; this.ctx.lineWidth = 2; this.ctx.setLineDash([]); this.ctx.beginPath(); this.ctx.moveTo(selectedPlayer.position.x, selectedPlayer.position.y); this.ctx.lineTo(selectedPlayer.targetPosition.x, selectedPlayer.targetPosition.y); this.ctx.stroke(); // Draw target position marker (circle) this.ctx.setLineDash([]); this.ctx.fillStyle = '#00ffff'; this.ctx.strokeStyle = '#ffffff'; this.ctx.lineWidth = 3; this.ctx.beginPath(); this.ctx.arc(selectedPlayer.targetPosition.x, selectedPlayer.targetPosition.y, 10, 0, Math.PI * 2); this.ctx.fill(); this.ctx.stroke(); // Draw crosshair in the target circle this.ctx.strokeStyle = '#ffffff'; this.ctx.lineWidth = 2; const targetX = selectedPlayer.targetPosition.x; const targetY = selectedPlayer.targetPosition.y; this.ctx.beginPath(); this.ctx.moveTo(targetX - 6, targetY); this.ctx.lineTo(targetX + 6, targetY); this.ctx.moveTo(targetX, targetY - 6); this.ctx.lineTo(targetX, targetY + 6); this.ctx.stroke(); // Highlight the selected player with a special border this.ctx.strokeStyle = '#00ffff'; this.ctx.lineWidth = 4; this.ctx.setLineDash([8, 4]); this.ctx.beginPath(); this.ctx.arc(selectedPlayer.position.x, selectedPlayer.position.y, selectedPlayer.radius + 10, 0, Math.PI * 2); this.ctx.stroke(); // Restore context state this.ctx.restore(); } }