540 lines
17 KiB
JavaScript
540 lines
17 KiB
JavaScript
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, 100, 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 = 60;
|
|
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 => {
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(dot.x, dot.y, 8, 0, Math.PI * 2);
|
|
this.ctx.fill();
|
|
|
|
this.ctx.strokeStyle = '#d32f2f';
|
|
this.ctx.lineWidth = 2;
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(dot.x, dot.y, 30, 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, color);
|
|
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) {
|
|
this.ctx.strokeStyle = color;
|
|
this.ctx.lineWidth = 4;
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
const angle = (i / 6) * Math.PI * 2;
|
|
const startX = position.x + Math.cos(angle) * 10;
|
|
const startY = position.y + Math.sin(angle) * 10;
|
|
const endX = position.x + Math.cos(angle) * 25;
|
|
const endY = position.y + Math.sin(angle) * 25;
|
|
|
|
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 and energy
|
|
this.ctx.fillStyle = player.team === 'home' ? '#ff4444' : '#4444ff';
|
|
this.ctx.fillText(`${player.role}`, x - 10, y);
|
|
|
|
// Draw energy bar
|
|
const barWidth = 20;
|
|
const barHeight = 3;
|
|
const energyPercent = player.state.energy / 100;
|
|
|
|
this.ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
|
this.ctx.fillRect(x - barWidth/2, y - 15, barWidth, barHeight);
|
|
|
|
this.ctx.fillStyle = energyPercent > 0.5 ? '#44ff44' : energyPercent > 0.25 ? '#ffff44' : '#ff4444';
|
|
this.ctx.fillRect(x - barWidth/2, y - 15, barWidth * energyPercent, barHeight);
|
|
|
|
// 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();
|
|
}
|
|
} |