270 lines
8.6 KiB
JavaScript
270 lines
8.6 KiB
JavaScript
window.Projectile = class Projectile {
|
|
constructor(x, y, row, type = 'pea', vy = 0, damage = 20, isCrit = false, targetX = 750) {
|
|
this.pierceHits = new Set();
|
|
this.init(x, y, row, type, vy, damage, isCrit, targetX);
|
|
}
|
|
|
|
init(x, y, row, type = 'pea', vy = 0, damage = 20, isCrit = false, targetX = 750) {
|
|
if (typeof isCrit === 'number' && isCrit > 1 && typeof damage === 'number' && damage >= 100) {
|
|
targetX = damage;
|
|
damage = (typeof vy === 'number') ? vy : 80;
|
|
vy = 0;
|
|
isCrit = false;
|
|
} else if (typeof isCrit === 'number' && typeof damage === 'number' && isCrit >= 100) {
|
|
targetX = isCrit;
|
|
isCrit = false;
|
|
}
|
|
|
|
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;
|
|
if (this.pierceHits) {
|
|
this.pierceHits.clear();
|
|
} else {
|
|
this.pierceHits = new Set();
|
|
}
|
|
this.lobProgress = 0;
|
|
this.toRemove = false;
|
|
this.animTime = 0;
|
|
this.hitCount = 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();
|
|
}
|
|
};
|
|
|
|
// Reusable Object Pool for Projectiles (Eliminates GC churn & frame drops)
|
|
window.ProjectilePool = class ProjectilePool {
|
|
constructor(initialCapacity = 60) {
|
|
this.pool = [];
|
|
for (let i = 0; i < initialCapacity; i++) {
|
|
this.pool.push(new window.Projectile(0, 0, 0));
|
|
}
|
|
}
|
|
|
|
get(x, y, row, type = 'pea', vy = 0, damage = 20, isCrit = false, targetX = 750) {
|
|
const proj = this.pool.length > 0 ? this.pool.pop() : new window.Projectile(x, y, row, type, vy, damage, isCrit, targetX);
|
|
proj.init(x, y, row, type, vy, damage, isCrit, targetX);
|
|
return proj;
|
|
}
|
|
|
|
release(proj) {
|
|
if (!proj) return;
|
|
proj.toRemove = false;
|
|
if (proj.pierceHits) proj.pierceHits.clear();
|
|
this.pool.push(proj);
|
|
}
|
|
|
|
releaseAll(projectiles) {
|
|
if (!Array.isArray(projectiles)) return;
|
|
for (const p of projectiles) {
|
|
this.release(p);
|
|
}
|
|
}
|
|
|
|
clear() {
|
|
this.pool = [];
|
|
}
|
|
};
|
|
|