window.Projectile = class Projectile { constructor(x, y, row, type = 'pea', vy = 0, damage = 20, isCrit = false, targetX = 750) { this.x = x; this.y = y; this.startX = x; this.baseY = y; this.targetX = targetX; this.row = row; this.type = type; this.vy = vy; // for bouncing bowling ball this.isCrit = isCrit; this.isLobbed = type === 'cabbage' || type === 'melon'; this.apex = type === 'melon' ? 95 : (type === 'cabbage' ? 85 : 0); let baseRadius = 9; let baseSpeed = 380; let baseDamage = damage; if (type === 'bowling') { baseRadius = 22; baseSpeed = 440; baseDamage = 400; } else if (type === 'fume') { baseRadius = 16; baseSpeed = 280; } else if (type === 'plasma' || type === 'firepea') { baseRadius = 12; baseSpeed = 460; } else if (type === 'cabbage') { baseRadius = 13; baseSpeed = 340; baseDamage = 40; } else if (type === 'melon') { baseRadius = 17; baseSpeed = 320; baseDamage = 80; } this.radius = isCrit ? baseRadius + 2 : baseRadius; this.speed = baseSpeed; this.damage = type === 'bowling' ? 400 : (isCrit ? baseDamage * 2 : baseDamage); this.splashDamage = type === 'melon' ? 26 : 0; this.splashRadius = type === 'melon' ? 80 : 0; this.slows = type === 'snowpea'; this.freezes = type === 'freeze_snowpea'; this.pierces = type === 'fume' || type === 'plasma'; this.maxPierces = type === 'plasma' ? 2 : 999; this.pierceHits = new Set(); this.lobProgress = 0; this.toRemove = false; this.animTime = 0; } update(dt) { this.animTime += dt; this.x += this.speed * dt; if (this.isLobbed) { const totalDist = Math.max(80, this.targetX - this.startX); const curDist = this.x - this.startX; const t = Math.max(0, Math.min(1, curDist / totalDist)); this.lobProgress = t; this.y = this.baseY - 4 * this.apex * t * (1 - t); } else { this.y += this.vy * dt; } // Fume fades out after ~340px if (this.type === 'fume' && (this.x - this.startX) > 340) { this.toRemove = true; } // Bowling wall bounce if (this.type === 'bowling') { if (this.y < 120 && this.vy < 0) { this.vy = -this.vy; this.row = 0; } else if (this.y > 550 && this.vy > 0) { this.vy = -this.vy; this.row = 4; } } if (this.x > 960 || this.x < -40) { this.toRemove = true; } } draw(ctx) { ctx.save(); if (this.type === 'snowpea' || this.type === 'freeze_snowpea') { // Ice pea glow const grad = ctx.createRadialGradient(this.x - 2, this.y - 2, 2, this.x, this.y, this.radius); grad.addColorStop(0, '#e0f7fa'); grad.addColorStop(0.6, '#4dd0e1'); grad.addColorStop(1, '#00838f'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fill(); // Ice sparkle trail ctx.fillStyle = 'rgba(178, 235, 242, 0.5)'; ctx.beginPath(); ctx.arc(this.x - 8, this.y, this.radius * 0.5, 0, Math.PI * 2); ctx.fill(); } else if (this.type === 'plasma') { // Tier 3 Plasma Peashooter const grad = ctx.createRadialGradient(this.x, this.y, 2, this.x, this.y, this.radius + 4); grad.addColorStop(0, '#ffffff'); grad.addColorStop(0.4, '#00e5ff'); grad.addColorStop(0.8, '#2979ff'); grad.addColorStop(1, 'rgba(41, 121, 255, 0)'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(this.x, this.y, this.radius + 4, 0, Math.PI * 2); ctx.fill(); // Electric spark trail ctx.strokeStyle = '#e0f7fa'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(this.x - 12, this.y); ctx.lineTo(this.x - 4, this.y + (Math.sin(this.animTime * 20) * 4)); ctx.stroke(); } else if (this.type === 'firepea') { // Tier 3 Fiery Repeater const grad = ctx.createRadialGradient(this.x, this.y, 2, this.x, this.y, this.radius + 4); grad.addColorStop(0, '#fff9c4'); grad.addColorStop(0.4, '#ff9800'); grad.addColorStop(0.8, '#d50000'); grad.addColorStop(1, 'rgba(213, 0, 0, 0)'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(this.x, this.y, this.radius + 4, 0, Math.PI * 2); ctx.fill(); } else if (this.type === 'fume') { // Purple spore cloud const alpha = Math.max(0.2, 1 - (this.x - this.startX) / 340); ctx.globalAlpha = alpha; const grad = ctx.createRadialGradient(this.x, this.y, 2, this.x, this.y, this.radius); grad.addColorStop(0, '#f3e5f5'); grad.addColorStop(0.5, '#ab47bc'); grad.addColorStop(1, '#6a1b9a'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fill(); // Bubbles ctx.fillStyle = '#ea80fc'; ctx.beginPath(); ctx.arc(this.x - 6, this.y - 4, 4, 0, Math.PI * 2); ctx.arc(this.x + 5, this.y + 3, 3, 0, Math.PI * 2); ctx.fill(); } else if (this.type === 'cabbage') { // Tumbling green cabbage head ctx.translate(this.x, this.y); ctx.rotate(this.animTime * 7); ctx.fillStyle = '#66bb6a'; ctx.beginPath(); ctx.arc(0, 0, this.radius, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#2e7d32'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(-2, -2, this.radius * 0.6, 0, Math.PI); ctx.arc(3, 1, this.radius * 0.5, 0, Math.PI); ctx.stroke(); } else if (this.type === 'melon') { // Tumbling striped watermelon ctx.translate(this.x, this.y); ctx.rotate(this.animTime * 6); ctx.fillStyle = '#2e7d32'; ctx.beginPath(); ctx.ellipse(0, 0, this.radius, this.radius * 0.8, 0, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#a5d6a7'; ctx.lineWidth = 2.5; for (let i = -1; i <= 1; i++) { ctx.beginPath(); ctx.arc(i * 5, 0, this.radius * 0.7, -Math.PI / 2, Math.PI / 2); ctx.stroke(); } } else if (this.type === 'bowling') { // Rolling Wall-nut ctx.translate(this.x, this.y); ctx.rotate(this.animTime * 10); ctx.fillStyle = '#8d6e63'; ctx.beginPath(); ctx.arc(0, 0, this.radius, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#5d4037'; ctx.beginPath(); ctx.arc(6, -6, 5, 0, Math.PI * 2); ctx.fill(); } else { // Standard or Critical pea const grad = ctx.createRadialGradient(this.x - 3, this.y - 3, 2, this.x, this.y, this.radius); if (this.isCrit) { grad.addColorStop(0, '#fff59d'); grad.addColorStop(0.5, '#fbc02d'); grad.addColorStop(1, '#f57f17'); } else { grad.addColorStop(0, '#a5d6a7'); grad.addColorStop(0.5, '#4caf50'); grad.addColorStop(1, '#2e7d32'); } ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fill(); // Highlight spot ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; ctx.beginPath(); ctx.arc(this.x - 3, this.y - 3, 2.5, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); } };