diff --git a/README.md b/README.md index 2917f90..d894ecc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,48 @@ -# base44-zip -base 44 app +# Par 3 Challenge + +A browser-based golf game where players try to complete 9 par-3 holes with the fewest strokes possible. + +## How to Play + +1. Open `index.html` in a web browser +2. Use the **Aim** slider to adjust your shot direction +3. Select the appropriate **Club** for the distance: + - **Driver**: Long distance shots + - **Iron**: Medium distance shots + - **Wedge**: Short distance, good for bunker escapes + - **Putter**: Very short distances, ideal near the hole +4. Click and hold the **Swing!** button to build power +5. Release to hit the ball + +## Game Features + +- 9 unique holes with different layouts +- Water hazards (penalty stroke if ball lands in water) +- Sand bunkers (reduced ball speed and power) +- Power meter with timing-based mechanics +- Score tracking relative to par + +## Scoring + +- **Hole in One**: Ball in the hole in 1 stroke +- **Eagle**: 2 under par (1 stroke on a par 3) +- **Birdie**: 1 under par (2 strokes on a par 3) +- **Par**: Expected strokes (3 strokes on a par 3) +- **Bogey**: 1 over par (4 strokes on a par 3) + +## Project Structure + +``` +├── index.html # Main game page +├── css/ +│ └── style.css # Game styling +├── js/ +│ └── game.js # Game logic and physics +└── README.md # This file +``` + +## Technologies + +- Pure HTML5, CSS3, and vanilla JavaScript +- Canvas API for game rendering +- No external dependencies required diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..6d1903d --- /dev/null +++ b/css/style.css @@ -0,0 +1,253 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #1a5f2c 0%, #0d3016 100%); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; +} + +#game-container { + background: rgba(255, 255, 255, 0.95); + border-radius: 16px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + overflow: hidden; + max-width: 850px; + width: 100%; +} + +header { + background: linear-gradient(135deg, #2d8a4e 0%, #1a5f2c 100%); + color: white; + padding: 15px 20px; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 10px; +} + +header h1 { + font-size: 1.8rem; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); +} + +#scoreboard { + display: flex; + gap: 20px; + font-size: 1.1rem; + font-weight: 600; +} + +#scoreboard span { + background: rgba(0, 0, 0, 0.2); + padding: 8px 15px; + border-radius: 8px; +} + +main { + padding: 20px; +} + +#game-canvas { + width: 100%; + height: auto; + border-radius: 12px; + border: 4px solid #1a5f2c; + cursor: crosshair; + display: block; + margin-bottom: 20px; +} + +#controls { + display: flex; + flex-wrap: wrap; + gap: 20px; + align-items: center; + justify-content: center; + padding: 15px; + background: #f5f5f5; + border-radius: 12px; + margin-bottom: 15px; +} + +#power-control { + display: flex; + align-items: center; + gap: 10px; +} + +#power-meter { + width: 150px; + height: 20px; + background: #ddd; + border-radius: 10px; + overflow: hidden; + border: 2px solid #333; +} + +#power-fill { + height: 100%; + width: 0%; + background: linear-gradient(90deg, #4CAF50, #FFC107, #f44336); + transition: width 0.05s linear; +} + +#power-value { + font-weight: bold; + min-width: 40px; +} + +#aim-control { + display: flex; + align-items: center; + gap: 10px; +} + +#aim { + width: 120px; + cursor: pointer; +} + +#club-selection { + display: flex; + align-items: center; + gap: 10px; +} + +#club { + padding: 8px 12px; + border-radius: 6px; + border: 2px solid #1a5f2c; + background: white; + font-size: 1rem; + cursor: pointer; +} + +#swing-btn { + padding: 12px 30px; + font-size: 1.2rem; + font-weight: bold; + background: linear-gradient(135deg, #f44336 0%, #c62828 100%); + color: white; + border: none; + border-radius: 10px; + cursor: pointer; + transition: transform 0.1s, box-shadow 0.1s; + user-select: none; +} + +#swing-btn:hover { + transform: scale(1.05); + box-shadow: 0 4px 15px rgba(244, 67, 54, 0.4); +} + +#swing-btn:active { + transform: scale(0.95); +} + +#swing-btn:disabled { + background: #999; + cursor: not-allowed; + transform: none; +} + +#message-area { + text-align: center; + padding: 10px; +} + +#game-message { + font-size: 1.1rem; + color: #333; + font-weight: 500; +} + +.hidden { + display: none !important; +} + +#game-over { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: white; + padding: 40px; + border-radius: 16px; + box-shadow: 0 10px 50px rgba(0, 0, 0, 0.5); + text-align: center; + z-index: 1000; +} + +#game-over::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + z-index: -1; +} + +#game-over h2 { + font-size: 2rem; + color: #1a5f2c; + margin-bottom: 20px; +} + +#game-over p { + font-size: 1.3rem; + margin: 10px 0; + color: #333; +} + +#score-description { + font-weight: bold; + color: #2d8a4e; +} + +#play-again-btn { + margin-top: 20px; + padding: 12px 40px; + font-size: 1.2rem; + font-weight: bold; + background: linear-gradient(135deg, #2d8a4e 0%, #1a5f2c 100%); + color: white; + border: none; + border-radius: 10px; + cursor: pointer; + transition: transform 0.1s; +} + +#play-again-btn:hover { + transform: scale(1.05); +} + +/* Mobile responsive styles */ +@media (max-width: 600px) { + header { + flex-direction: column; + text-align: center; + } + + #scoreboard { + flex-direction: column; + gap: 8px; + } + + #controls { + flex-direction: column; + } + + #power-meter { + width: 200px; + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..5c7445e --- /dev/null +++ b/index.html @@ -0,0 +1,66 @@ + + + + + + Par 3 Challenge + + + +
+
+

Par 3 Challenge

+
+ Hole: 1/9 + Strokes: 0 + Total: 0 +
+
+ +
+ + +
+
+ +
+
+
+ 0% +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+

Click and hold "Swing!" to build power, release to hit!

+
+
+ + +
+ + + + diff --git a/js/game.js b/js/game.js new file mode 100644 index 0000000..0276d24 --- /dev/null +++ b/js/game.js @@ -0,0 +1,643 @@ +/** + * Par 3 Challenge - Golf Game + * A simple browser-based golf game where players try to complete + * 9 par-3 holes with the fewest strokes possible. + */ + +// Game configuration +const CONFIG = { + TOTAL_HOLES: 9, + PAR_PER_HOLE: 3, + CANVAS_WIDTH: 800, + CANVAS_HEIGHT: 600, + BALL_RADIUS: 6, + HOLE_RADIUS: 10, + FLAG_HEIGHT: 40, + FRICTION: 0.98, + BUNKER_FRICTION: 0.92, + MIN_VELOCITY: 0.1, + MAX_SPEED: 15, + MAX_HOLE_ENTRY_SPEED: 3, + PIXELS_PER_YARD: 5, + DIMPLE_ANIMATION_SPEED: 100, + CLUBS: { + driver: { power: 1.0, name: 'Driver' }, + iron: { power: 0.7, name: 'Iron' }, + wedge: { power: 0.4, name: 'Wedge' }, + putter: { power: 0.15, name: 'Putter' } + } +}; + +// Hole configurations - each hole has different layouts +const HOLES = [ + { + name: "The Opener", + distance: 150, + tee: { x: 100, y: 500 }, + hole: { x: 700, y: 100 }, + hazards: [], + bunkers: [{ x: 600, y: 150, radius: 40 }] + }, + { + name: "Water Hazard", + distance: 130, + tee: { x: 100, y: 500 }, + hole: { x: 650, y: 150 }, + hazards: [{ type: 'water', x: 350, y: 300, width: 100, height: 150 }], + bunkers: [] + }, + { + name: "The Bunker Challenge", + distance: 140, + tee: { x: 100, y: 300 }, + hole: { x: 700, y: 300 }, + hazards: [], + bunkers: [ + { x: 400, y: 250, radius: 50 }, + { x: 550, y: 350, radius: 45 } + ] + }, + { + name: "Island Green", + distance: 120, + tee: { x: 100, y: 500 }, + hole: { x: 600, y: 200 }, + hazards: [ + { type: 'water', x: 450, y: 100, width: 250, height: 250 } + ], + bunkers: [] + }, + { + name: "Dogleg Left", + distance: 160, + tee: { x: 700, y: 500 }, + hole: { x: 150, y: 100 }, + hazards: [], + bunkers: [ + { x: 300, y: 300, radius: 60 } + ] + }, + { + name: "The Narrow", + distance: 145, + tee: { x: 100, y: 550 }, + hole: { x: 700, y: 50 }, + hazards: [ + { type: 'water', x: 0, y: 200, width: 300, height: 80 }, + { type: 'water', x: 500, y: 350, width: 300, height: 80 } + ], + bunkers: [] + }, + { + name: "Triple Bunker", + distance: 135, + tee: { x: 400, y: 550 }, + hole: { x: 400, y: 80 }, + hazards: [], + bunkers: [ + { x: 250, y: 250, radius: 35 }, + { x: 400, y: 300, radius: 35 }, + { x: 550, y: 250, radius: 35 } + ] + }, + { + name: "Around the Lake", + distance: 155, + tee: { x: 100, y: 300 }, + hole: { x: 700, y: 300 }, + hazards: [ + { type: 'water', x: 300, y: 200, width: 200, height: 200 } + ], + bunkers: [ + { x: 600, y: 250, radius: 30 }, + { x: 600, y: 350, radius: 30 } + ] + }, + { + name: "The Finale", + distance: 170, + tee: { x: 100, y: 550 }, + hole: { x: 700, y: 100 }, + hazards: [ + { type: 'water', x: 250, y: 250, width: 150, height: 100 } + ], + bunkers: [ + { x: 500, y: 150, radius: 50 }, + { x: 650, y: 200, radius: 35 } + ] + } +]; + +// Game state +let gameState = { + currentHole: 0, + currentStrokes: 0, + totalScore: 0, + scores: [], + ball: { x: 0, y: 0, vx: 0, vy: 0 }, + isSwinging: false, + isBallMoving: false, + power: 0, + powerDirection: 1, + gameOver: false, + inBunker: false +}; + +// DOM elements +let canvas, ctx; +let swingBtn, aimSlider, clubSelect; +let powerFill, powerValue, aimValue; +let currentHoleEl, currentStrokesEl, totalScoreEl; +let gameMessage, gameOverEl, finalScoreEl, scoreDescEl; + +// Initialize the game +function init() { + // Get DOM elements + canvas = document.getElementById('game-canvas'); + ctx = canvas.getContext('2d'); + + swingBtn = document.getElementById('swing-btn'); + aimSlider = document.getElementById('aim'); + clubSelect = document.getElementById('club'); + + powerFill = document.getElementById('power-fill'); + powerValue = document.getElementById('power-value'); + aimValue = document.getElementById('aim-value'); + + currentHoleEl = document.getElementById('current-hole'); + currentStrokesEl = document.getElementById('current-strokes'); + totalScoreEl = document.getElementById('total-score'); + + gameMessage = document.getElementById('game-message'); + gameOverEl = document.getElementById('game-over'); + finalScoreEl = document.getElementById('final-score'); + scoreDescEl = document.getElementById('score-description'); + + // Set up event listeners + swingBtn.addEventListener('mousedown', startSwing); + swingBtn.addEventListener('mouseup', executeSwing); + swingBtn.addEventListener('mouseleave', executeSwing); + swingBtn.addEventListener('touchstart', startSwing); + swingBtn.addEventListener('touchend', executeSwing); + + aimSlider.addEventListener('input', updateAimDisplay); + + document.getElementById('play-again-btn').addEventListener('click', resetGame); + + // Start the game + startHole(); + requestAnimationFrame(gameLoop); +} + +// Start a new hole +function startHole() { + const hole = HOLES[gameState.currentHole]; + gameState.ball.x = hole.tee.x; + gameState.ball.y = hole.tee.y; + gameState.ball.vx = 0; + gameState.ball.vy = 0; + gameState.currentStrokes = 0; + gameState.power = 0; + gameState.inBunker = false; + + updateUI(); + updateMessage(`Hole ${gameState.currentHole + 1}: ${hole.name} - Par ${CONFIG.PAR_PER_HOLE}`); +} + +// Start the swing (power building) +function startSwing(e) { + e.preventDefault(); + if (gameState.isBallMoving || gameState.gameOver) return; + + gameState.isSwinging = true; + gameState.power = 0; + gameState.powerDirection = 1; + + buildPower(); +} + +// Build power while holding +function buildPower() { + if (!gameState.isSwinging) return; + + gameState.power += 2 * gameState.powerDirection; + + if (gameState.power >= 100) { + gameState.powerDirection = -1; + } else if (gameState.power <= 0) { + gameState.powerDirection = 1; + } + + powerFill.style.width = gameState.power + '%'; + powerValue.textContent = Math.round(gameState.power) + '%'; + + requestAnimationFrame(buildPower); +} + +// Execute the swing +function executeSwing(e) { + e.preventDefault(); + if (!gameState.isSwinging || gameState.isBallMoving) return; + + gameState.isSwinging = false; + + if (gameState.power < 5) { + updateMessage("Too weak! Hold longer for more power."); + gameState.power = 0; + powerFill.style.width = '0%'; + powerValue.textContent = '0%'; + return; + } + + // Calculate ball velocity based on power, aim, and club + const aim = parseInt(aimSlider.value); + const club = clubSelect.value; + const clubPower = CONFIG.CLUBS[club].power; + + // Convert aim angle to radians (aim towards the right side of screen by default) + const hole = HOLES[gameState.currentHole]; + const dx = hole.hole.x - gameState.ball.x; + const dy = hole.hole.y - gameState.ball.y; + const baseAngle = Math.atan2(dy, dx); + const aimRadians = (aim * Math.PI) / 180; + const finalAngle = baseAngle + aimRadians; + + // Calculate velocity + let speed = (gameState.power / 100) * CONFIG.MAX_SPEED * clubPower; + + // Reduce speed if in bunker + if (gameState.inBunker) { + speed *= 0.5; + updateMessage("Bunker shot! Reduced power."); + } + + gameState.ball.vx = Math.cos(finalAngle) * speed; + gameState.ball.vy = Math.sin(finalAngle) * speed; + + gameState.currentStrokes++; + gameState.isBallMoving = true; + + // Reset power display + gameState.power = 0; + powerFill.style.width = '0%'; + powerValue.textContent = '0%'; + + updateUI(); + updateMessage("Nice swing!"); +} + +// Update aim display +function updateAimDisplay() { + aimValue.textContent = aimSlider.value + '°'; +} + +// Main game loop +function gameLoop() { + update(); + render(); + requestAnimationFrame(gameLoop); +} + +// Update game physics +function update() { + if (!gameState.isBallMoving) return; + + const hole = HOLES[gameState.currentHole]; + + // Apply friction + let friction = CONFIG.FRICTION; + + // Check if in bunker (more friction) + gameState.inBunker = false; + for (const bunker of hole.bunkers) { + const dist = Math.sqrt( + Math.pow(gameState.ball.x - bunker.x, 2) + + Math.pow(gameState.ball.y - bunker.y, 2) + ); + if (dist < bunker.radius) { + friction = CONFIG.BUNKER_FRICTION; + gameState.inBunker = true; + break; + } + } + + gameState.ball.vx *= friction; + gameState.ball.vy *= friction; + + // Update position + gameState.ball.x += gameState.ball.vx; + gameState.ball.y += gameState.ball.vy; + + // Check boundaries + if (gameState.ball.x < CONFIG.BALL_RADIUS) { + gameState.ball.x = CONFIG.BALL_RADIUS; + gameState.ball.vx *= -0.5; + } + if (gameState.ball.x > CONFIG.CANVAS_WIDTH - CONFIG.BALL_RADIUS) { + gameState.ball.x = CONFIG.CANVAS_WIDTH - CONFIG.BALL_RADIUS; + gameState.ball.vx *= -0.5; + } + if (gameState.ball.y < CONFIG.BALL_RADIUS) { + gameState.ball.y = CONFIG.BALL_RADIUS; + gameState.ball.vy *= -0.5; + } + if (gameState.ball.y > CONFIG.CANVAS_HEIGHT - CONFIG.BALL_RADIUS) { + gameState.ball.y = CONFIG.CANVAS_HEIGHT - CONFIG.BALL_RADIUS; + gameState.ball.vy *= -0.5; + } + + // Check water hazards + for (const hazard of hole.hazards) { + if (hazard.type === 'water') { + if (gameState.ball.x > hazard.x && + gameState.ball.x < hazard.x + hazard.width && + gameState.ball.y > hazard.y && + gameState.ball.y < hazard.y + hazard.height) { + // Ball in water - penalty stroke and reset + gameState.currentStrokes++; + gameState.ball.x = hole.tee.x; + gameState.ball.y = hole.tee.y; + gameState.ball.vx = 0; + gameState.ball.vy = 0; + gameState.isBallMoving = false; + updateUI(); + updateMessage("Splash! In the water. Penalty stroke added."); + return; + } + } + } + + // Check if ball is in hole + const distToHole = Math.sqrt( + Math.pow(gameState.ball.x - hole.hole.x, 2) + + Math.pow(gameState.ball.y - hole.hole.y, 2) + ); + + const ballSpeed = Math.sqrt( + Math.pow(gameState.ball.vx, 2) + + Math.pow(gameState.ball.vy, 2) + ); + + if (distToHole < CONFIG.HOLE_RADIUS && ballSpeed < CONFIG.MAX_HOLE_ENTRY_SPEED) { + // Ball is in the hole! + ballInHole(); + return; + } + + // Check if ball has stopped + if (ballSpeed < CONFIG.MIN_VELOCITY) { + gameState.ball.vx = 0; + gameState.ball.vy = 0; + gameState.isBallMoving = false; + + if (gameState.inBunker) { + updateMessage("In the bunker! Use a wedge to escape."); + } else { + const distDisplay = Math.round(distToHole / CONFIG.PIXELS_PER_YARD); + updateMessage(`Ball stopped. ${distDisplay} yards to the hole.`); + } + } +} + +// Handle ball going in hole +function ballInHole() { + gameState.isBallMoving = false; + gameState.ball.vx = 0; + gameState.ball.vy = 0; + + const strokes = gameState.currentStrokes; + const par = CONFIG.PAR_PER_HOLE; + const diff = strokes - par; + + gameState.scores.push(strokes); + gameState.totalScore += strokes; + + let message = ""; + if (strokes === 1) { + message = "🎉 HOLE IN ONE! Amazing!"; + } else if (diff === -2) { + message = "🦅 EAGLE! Incredible shot!"; + } else if (diff === -1) { + message = "🐦 BIRDIE! Great job!"; + } else if (diff === 0) { + message = "👍 PAR! Nice work!"; + } else if (diff === 1) { + message = "BOGEY. Keep trying!"; + } else if (diff === 2) { + message = "DOUBLE BOGEY. You can do better!"; + } else { + message = `+${diff}. Tough hole!`; + } + + updateMessage(message); + updateUI(); + + // Move to next hole after delay + setTimeout(() => { + gameState.currentHole++; + + if (gameState.currentHole >= CONFIG.TOTAL_HOLES) { + endGame(); + } else { + startHole(); + } + }, 2000); +} + +// End the game +function endGame() { + gameState.gameOver = true; + + const totalPar = CONFIG.TOTAL_HOLES * CONFIG.PAR_PER_HOLE; + const diff = gameState.totalScore - totalPar; + + finalScoreEl.textContent = gameState.totalScore; + + let description = ""; + if (diff < -5) { + description = "🏆 Legendary! You're a golf master!"; + } else if (diff < 0) { + description = "⭐ Under par! Excellent round!"; + } else if (diff === 0) { + description = "👏 Right on par! Solid game!"; + } else if (diff <= 5) { + description = "💪 Over par, but not bad!"; + } else { + description = "🎯 Keep practicing! You'll improve!"; + } + + scoreDescEl.textContent = description; + gameOverEl.classList.remove('hidden'); +} + +// Reset the game +function resetGame() { + gameState = { + currentHole: 0, + currentStrokes: 0, + totalScore: 0, + scores: [], + ball: { x: 0, y: 0, vx: 0, vy: 0 }, + isSwinging: false, + isBallMoving: false, + power: 0, + powerDirection: 1, + gameOver: false, + inBunker: false + }; + + gameOverEl.classList.add('hidden'); + aimSlider.value = 0; + updateAimDisplay(); + startHole(); +} + +// Render the game +function render() { + const hole = HOLES[gameState.currentHole]; + + // Clear canvas + ctx.fillStyle = '#4a9c59'; + ctx.fillRect(0, 0, CONFIG.CANVAS_WIDTH, CONFIG.CANVAS_HEIGHT); + + // Draw fairway pattern + ctx.fillStyle = '#5ab067'; + for (let i = 0; i < CONFIG.CANVAS_WIDTH; i += 40) { + if ((i / 40) % 2 === 0) { + ctx.fillRect(i, 0, 20, CONFIG.CANVAS_HEIGHT); + } + } + + // Draw water hazards + for (const hazard of hole.hazards) { + if (hazard.type === 'water') { + ctx.fillStyle = '#3498db'; + ctx.fillRect(hazard.x, hazard.y, hazard.width, hazard.height); + + // Water ripple effect + ctx.strokeStyle = '#2980b9'; + ctx.lineWidth = 2; + for (let i = 0; i < 3; i++) { + ctx.beginPath(); + ctx.moveTo(hazard.x + 10 + i * 30, hazard.y + hazard.height / 2); + ctx.quadraticCurveTo( + hazard.x + 25 + i * 30, hazard.y + hazard.height / 2 - 10, + hazard.x + 40 + i * 30, hazard.y + hazard.height / 2 + ); + ctx.stroke(); + } + } + } + + // Draw bunkers + for (const bunker of hole.bunkers) { + ctx.fillStyle = '#f5d89a'; + ctx.beginPath(); + ctx.arc(bunker.x, bunker.y, bunker.radius, 0, Math.PI * 2); + ctx.fill(); + + // Bunker edge + ctx.strokeStyle = '#d4b870'; + ctx.lineWidth = 3; + ctx.stroke(); + } + + // Draw tee box + ctx.fillStyle = '#2d7a3e'; + ctx.fillRect(hole.tee.x - 20, hole.tee.y - 10, 40, 20); + + // Draw putting green + ctx.fillStyle = '#7ec850'; + ctx.beginPath(); + ctx.arc(hole.hole.x, hole.hole.y, 50, 0, Math.PI * 2); + ctx.fill(); + + // Draw hole + ctx.fillStyle = '#1a1a1a'; + ctx.beginPath(); + ctx.arc(hole.hole.x, hole.hole.y, CONFIG.HOLE_RADIUS, 0, Math.PI * 2); + ctx.fill(); + + // Draw flag + ctx.fillStyle = '#8b4513'; + ctx.fillRect(hole.hole.x - 1, hole.hole.y - CONFIG.FLAG_HEIGHT, 3, CONFIG.FLAG_HEIGHT); + + ctx.fillStyle = '#e74c3c'; + ctx.beginPath(); + ctx.moveTo(hole.hole.x + 2, hole.hole.y - CONFIG.FLAG_HEIGHT); + ctx.lineTo(hole.hole.x + 25, hole.hole.y - CONFIG.FLAG_HEIGHT + 12); + ctx.lineTo(hole.hole.x + 2, hole.hole.y - CONFIG.FLAG_HEIGHT + 24); + ctx.closePath(); + ctx.fill(); + + // Draw aim line when not moving + if (!gameState.isBallMoving && !gameState.gameOver) { + const aim = parseInt(aimSlider.value); + const dx = hole.hole.x - gameState.ball.x; + const dy = hole.hole.y - gameState.ball.y; + const baseAngle = Math.atan2(dy, dx); + const aimRadians = (aim * Math.PI) / 180; + const finalAngle = baseAngle + aimRadians; + + const lineLength = 60; + const endX = gameState.ball.x + Math.cos(finalAngle) * lineLength; + const endY = gameState.ball.y + Math.sin(finalAngle) * lineLength; + + ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'; + ctx.lineWidth = 2; + ctx.setLineDash([5, 5]); + ctx.beginPath(); + ctx.moveTo(gameState.ball.x, gameState.ball.y); + ctx.lineTo(endX, endY); + ctx.stroke(); + ctx.setLineDash([]); + } + + // Draw ball + ctx.fillStyle = '#ffffff'; + ctx.beginPath(); + ctx.arc(gameState.ball.x, gameState.ball.y, CONFIG.BALL_RADIUS, 0, Math.PI * 2); + ctx.fill(); + + // Ball outline + ctx.strokeStyle = '#cccccc'; + ctx.lineWidth = 1; + ctx.stroke(); + + // Draw ball dimples + ctx.fillStyle = '#f0f0f0'; + const dimpleAngle = Date.now() / CONFIG.DIMPLE_ANIMATION_SPEED; + for (let i = 0; i < 4; i++) { + const angle = dimpleAngle + (i * Math.PI / 2); + const dimpleX = gameState.ball.x + Math.cos(angle) * 3; + const dimpleY = gameState.ball.y + Math.sin(angle) * 3; + ctx.beginPath(); + ctx.arc(dimpleX, dimpleY, 1, 0, Math.PI * 2); + ctx.fill(); + } + + // Draw hole info + ctx.fillStyle = 'rgba(0, 0, 0, 0.6)'; + ctx.fillRect(10, 10, 180, 60); + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 14px Arial'; + ctx.fillText(`Hole ${gameState.currentHole + 1}: ${hole.name}`, 20, 30); + ctx.font = '12px Arial'; + ctx.fillText(`Distance: ${hole.distance} yards`, 20, 50); + ctx.fillText(`Par: ${CONFIG.PAR_PER_HOLE}`, 20, 65); +} + +// Update UI elements +function updateUI() { + currentHoleEl.textContent = gameState.currentHole + 1; + currentStrokesEl.textContent = gameState.currentStrokes; + totalScoreEl.textContent = gameState.totalScore; +} + +// Update message +function updateMessage(message) { + gameMessage.textContent = message; +} + +// Start the game when DOM is loaded +document.addEventListener('DOMContentLoaded', init);