function renderGrid(ctx, grid) { const cellSize = canvas.width / 20; // Draw grid lines ctx.strokeStyle = '#ccc'; ctx.lineWidth = 1; for (let i = 0; i <= 20; i++) { // Vertical lines ctx.beginPath(); ctx.moveTo(i * cellSize, 0); ctx.lineTo(i * cellSize, canvas.height); ctx.stroke(); // Horizontal lines ctx.beginPath(); ctx.moveTo(0, i * cellSize); ctx.lineTo(canvas.width, i * cellSize); ctx.stroke(); } // Draw cells grid.forEach((row, y) => { row.forEach((cell, x) => { if (cell === 'path') { ctx.fillStyle = '#f4a460'; ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); } }); }); // Draw hover preview if (gameState.phase === GamePhase.PLACEMENT && draggedTowerType && hoverCell) { const tower = TowerTypes[draggedTowerType]; const canPlace = grid[hoverCell.y][hoverCell.x] === 'empty' && gameState.playerCurrency >= tower.cost; ctx.fillStyle = canPlace ? tower.color + '80' : 'rgba(255, 0, 0, 0.3)'; ctx.fillRect( hoverCell.x * cellSize, hoverCell.y * cellSize, cellSize, cellSize ); // Draw range preview ctx.beginPath(); ctx.arc( (hoverCell.x + 0.5) * cellSize, (hoverCell.y + 0.5) * cellSize, tower.range * cellSize, 0, Math.PI * 2 ); ctx.strokeStyle = canPlace ? tower.color + '40' : 'rgba(255, 0, 0, 0.2)'; ctx.stroke(); } } function renderEnemies(ctx, enemies) { const cellSize = canvas.width / 20; enemies.forEach(enemy => { const healthPercent = enemy.currentHealth / enemy.maxHealth; const opacity = 0.3 + (healthPercent * 0.7); // Use enemy type color ctx.fillStyle = `${EnemyTypes[enemy.type].color}${Math.floor(opacity * 255).toString(16).padStart(2, '0')}`; ctx.strokeStyle = 'rgba(0, 0, 0, 0.8)'; ctx.lineWidth = 2; // Draw enemy body ctx.beginPath(); ctx.arc( (enemy.position.x + 0.5) * cellSize, (enemy.position.y + 0.5) * cellSize, cellSize / 3, 0, Math.PI * 2 ); ctx.fill(); ctx.stroke(); // Draw range indicator for ranged enemies if (EnemyTypes[enemy.type].isRanged) { ctx.beginPath(); ctx.arc( (enemy.position.x + 0.5) * cellSize, (enemy.position.y + 0.5) * cellSize, EnemyTypes[enemy.type].attackRange * cellSize, 0, Math.PI * 2 ); ctx.strokeStyle = `${EnemyTypes[enemy.type].color}40`; ctx.stroke(); } }); } function renderUI(ctx, gameState) { ctx.fillStyle = 'black'; ctx.font = '20px Arial'; ctx.fillText(`Currency: $${gameState.currency}`, 10, 30); ctx.fillText(`Phase: ${gameState.phase}`, 10, 60); // Add enemy stats ctx.fillText(`Destroyed: ${gameState.enemiesDestroyed}`, 10, 90); ctx.fillText(`Escaped: ${gameState.enemiesEscaped}`, 10, 120); } function renderTowers(ctx, towers) { const cellSize = canvas.width / 20; towers.forEach(tower => { const healthPercent = tower.currentHealth / tower.maxHealth; // Draw tower body with opacity based on health ctx.fillStyle = tower.color + Math.floor(healthPercent * 255).toString(16).padStart(2, '0'); ctx.fillRect( tower.position.x * cellSize + cellSize * 0.1, tower.position.y * cellSize + cellSize * 0.1, cellSize * 0.8, cellSize * 0.8 ); // Draw range indicator if (gameState.phase === GamePhase.PLACEMENT) { ctx.beginPath(); ctx.arc( (tower.position.x + 0.5) * cellSize, (tower.position.y + 0.5) * cellSize, tower.range * cellSize, 0, Math.PI * 2 ); ctx.strokeStyle = tower.color + '40'; ctx.stroke(); } }); } // Add new render function for particles function renderParticles(ctx, particles) { particles.forEach(particle => { const age = performance.now() - particle.createdAt; const lifePercent = age / particle.lifetime; if (lifePercent <= 1) { if (particle.type === 'SLIME_TRAIL') { // Calculate opacity based on lifetime and fade start let opacity = 1; if (lifePercent > particle.fadeStart) { opacity = 1 - ((lifePercent - particle.fadeStart) / (1 - particle.fadeStart)); } opacity *= 0.3; // Make it translucent ctx.globalAlpha = opacity; ctx.fillStyle = particle.color; // Draw a circular slime splat ctx.beginPath(); ctx.arc( particle.position.x, particle.position.y, particle.size * (1 - lifePercent * 0.3), // Slightly shrink over time 0, Math.PI * 2 ); ctx.fill(); // Add some variation to the splat for (let i = 0; i < 3; i++) { const angle = (Math.PI * 2 * i) / 3; const distance = particle.size * 0.4; ctx.beginPath(); ctx.arc( particle.position.x + Math.cos(angle) * distance, particle.position.y + Math.sin(angle) * distance, particle.size * 0.4 * (1 - lifePercent * 0.3), 0, Math.PI * 2 ); ctx.fill(); } } else if (particle.type === 'AOE_EXPLOSION') { // Draw expanding circle const radius = particle.initialRadius + (particle.finalRadius - particle.initialRadius) * lifePercent; // Draw multiple rings for better effect const numRings = 3; for (let i = 0; i < numRings; i++) { const ringRadius = radius * (1 - (i * 0.2)); const ringAlpha = (1 - lifePercent) * (1 - (i * 0.3)); ctx.beginPath(); ctx.arc( particle.position.x, particle.position.y, ringRadius, 0, Math.PI * 2 ); ctx.strokeStyle = particle.color; ctx.lineWidth = particle.ringWidth * (1 - (i * 0.2)); ctx.globalAlpha = ringAlpha; ctx.stroke(); } // Draw affected area ctx.beginPath(); ctx.arc( particle.position.x, particle.position.y, radius, 0, Math.PI * 2 ); ctx.fillStyle = particle.color + '20'; // Very transparent fill ctx.fill(); } else { // Original particle rendering ctx.fillStyle = particle.color; ctx.beginPath(); ctx.arc( particle.position.x, particle.position.y, particle.size * (1 - lifePercent), 0, Math.PI * 2 ); ctx.fill(); } } }); ctx.globalAlpha = 1; } // Add new render function for projectiles function renderProjectiles(ctx, projectiles) { const cellSize = canvas.width / 20; projectiles.forEach(projectile => { const age = performance.now() - projectile.createdAt; const progress = age / projectile.lifetime; if (progress <= 1) { // Draw projectile trail ctx.beginPath(); ctx.moveTo( projectile.startPos.x * cellSize + cellSize / 2, projectile.startPos.y * cellSize + cellSize / 2 ); const currentX = projectile.startPos.x + (projectile.targetPos.x - projectile.startPos.x) * progress; const currentY = projectile.startPos.y + (projectile.targetPos.y - projectile.startPos.y) * progress; ctx.lineTo( currentX * cellSize + cellSize / 2, currentY * cellSize + cellSize / 2 ); ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke(); // Draw projectile head ctx.beginPath(); ctx.arc( currentX * cellSize + cellSize / 2, currentY * cellSize + cellSize / 2, 4, 0, Math.PI * 2 ); ctx.fillStyle = '#fff'; ctx.fill(); } }); }