hockey-manager/src/systems/renderer.js
Pierre Wessman cb8d4919a7 ...
2025-09-16 20:57:39 +02:00

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