470 lines
16 KiB
JavaScript
470 lines
16 KiB
JavaScript
// Entity classes for game objects
|
||
|
||
function Star(canvas) {
|
||
this.canvas = canvas
|
||
this.reset();
|
||
this.baseBrightness = 0.6 + Math.random() * 0.4;
|
||
this.brightness = this.baseBrightness;
|
||
this.twinkleSpeed = Math.random() * 0.003 + 0.001;
|
||
this.twinkleAmount = 0.1 + Math.random() * 0.1;
|
||
|
||
var colorRand = Math.random();
|
||
for (var i = 0; i < CONFIG.STAR_COLORS.length; i++) {
|
||
if (colorRand < CONFIG.STAR_COLORS[i].threshold) {
|
||
this.color = CONFIG.STAR_COLORS[i].rgb;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
Star.prototype.reset = function() {
|
||
this.x = Math.random() * this.canvas.width;
|
||
this.y = Math.random() * this.canvas.height;
|
||
this.size = Math.random() * 1;
|
||
};
|
||
|
||
Star.prototype.update = function() {
|
||
this.brightness += this.twinkleSpeed;
|
||
if (this.brightness > this.baseBrightness + this.twinkleAmount ||
|
||
this.brightness < this.baseBrightness - this.twinkleAmount) {
|
||
this.twinkleSpeed = -this.twinkleSpeed;
|
||
}
|
||
};
|
||
|
||
Star.prototype.draw = function(ctx) {
|
||
ctx.fillStyle = 'rgba(' + this.color + ', ' + this.brightness + ')';
|
||
ctx.beginPath();
|
||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
};
|
||
|
||
function Asteroid(type, blackHole, canvas) {
|
||
this.canvas = canvas
|
||
if (!type) type = 'small';
|
||
this.type = type;
|
||
|
||
var angle = Math.random() * Math.PI * 2;
|
||
var distance = Math.max(canvas.width, canvas.height) * 0.7;
|
||
|
||
this.x = this.canvas.width / 2 + Math.cos(angle) * distance;
|
||
this.y = this.canvas.height / 2 + Math.sin(angle) * distance;
|
||
|
||
this.angle = Math.atan2(this.y - blackHole.y, this.x - blackHole.x);
|
||
this.distance = Math.sqrt(
|
||
Math.pow(this.x - blackHole.x, 2) +
|
||
Math.pow(this.y - blackHole.y, 2)
|
||
);
|
||
this.orbitSpeed = 0.005 + Math.random() * 0.003;
|
||
this.decayRate = 0.08 + Math.random() * 0.05;
|
||
|
||
this.blackHole = blackHole;
|
||
|
||
this.initializeTypeSpecificProperties();
|
||
|
||
this.rotation = Math.random() * Math.PI * 2;
|
||
this.rotationSpeed = (Math.random() - 0.5) * 0.1;
|
||
}
|
||
|
||
Asteroid.prototype.initializeTypeSpecificProperties = function() {
|
||
// Visual size (unchanged)
|
||
if (this.type === 'comet') {
|
||
this.size = 5 + Math.random() * 3;
|
||
} else if (this.type === 'giant') {
|
||
this.size = 15 + Math.random() * 10;
|
||
this.orbitSpeed *= 0.3;
|
||
this.decayRate *= 0.4;
|
||
this.planetColors = CONFIG.GIANT_COLORS[Math.floor(Math.random() * CONFIG.GIANT_COLORS.length)];
|
||
// Initialize rings
|
||
this.rings = [];
|
||
var ringCount = Math.floor(Math.random() * 4); // 0-3 rings
|
||
for (var i = 0; i < ringCount; i++) {
|
||
this.rings.push({
|
||
color: CONFIG.GIANT_RING_COLORS[Math.floor(Math.random() * CONFIG.GIANT_RING_COLORS.length)],
|
||
thickness: 1 + Math.random() * 9, // random thickness
|
||
radiusOffset: 10 + i * 5 + Math.random() * 5, // spread rings outward
|
||
alpha: 0.3 + Math.random() * 0.4 // stored permanently
|
||
});
|
||
}
|
||
} else if (this.type === 'planet') {
|
||
this.size = 10 + Math.random() * 5;
|
||
this.orbitSpeed *= 0.5;
|
||
this.decayRate *= 0.6;
|
||
this.planetColors = CONFIG.PLANET_COLORS[Math.floor(Math.random() * CONFIG.PLANET_COLORS.length)];
|
||
} else if (this.type === 'large') {
|
||
this.size = 5 + Math.random() * 2;
|
||
} else if (this.type === 'medium') {
|
||
this.size = 3 + Math.random() * 2;
|
||
} else {
|
||
this.size = 1 + Math.random() * 2;
|
||
}
|
||
|
||
// Physical mass (kg)
|
||
var range = CONFIG.ASTEROID_MASS_RANGES[this.type] || CONFIG.ASTEROID_MASS_RANGES.small;
|
||
this.massKg = range[0] + Math.random() * (range[1] - range[0]);
|
||
};
|
||
|
||
Asteroid.prototype.update = function() {
|
||
this.angle += this.orbitSpeed;
|
||
this.distance -= this.decayRate;
|
||
this.x = this.blackHole.x + Math.cos(this.angle) * this.distance;
|
||
this.y = this.blackHole.y + Math.sin(this.angle) * this.distance;
|
||
this.rotation += this.rotationSpeed;
|
||
};
|
||
|
||
Asteroid.prototype.draw = function(ctx) {
|
||
if (this.type === 'comet') {
|
||
this.drawComet(ctx);
|
||
} else if (this.type === 'giant') {
|
||
// Initialize per-giant tilt if not already done
|
||
if (this._ringTiltBase === undefined) {
|
||
this._ringTiltBase = (Math.random() - 0.5) * 0.3; // ±0.15 rad
|
||
this._tiltOscillationSpeed = 0.001 + Math.random() * 0.002;
|
||
this._tiltScale = 0.1 + Math.random() * 0.69; // vertical squash
|
||
}
|
||
|
||
const tilt = this._ringTiltBase + Math.sin(Date.now() * this._tiltOscillationSpeed) * 0.05;
|
||
|
||
if (this.rings && this.rings.length > 0) {
|
||
// Draw back halves of rings
|
||
this.rings.forEach((ring) => {
|
||
ctx.save();
|
||
ctx.translate(this.x, this.y);
|
||
ctx.rotate(tilt);
|
||
ctx.scale(1, this._tiltScale);
|
||
|
||
ctx.strokeStyle = ring.color.replace(/;?\s*$/, '') // just in case
|
||
ctx.strokeStyle = ring.color.startsWith('rgba')
|
||
? ring.color.replace(/rgba\(([^)]+),[^)]+\)/, `rgba($1,${ring.alpha})`)
|
||
: `rgba(${parseInt(ring.color.slice(1,3),16)},${parseInt(ring.color.slice(3,5),16)},${parseInt(ring.color.slice(5,7),16)},${ring.alpha})`;
|
||
|
||
ctx.lineWidth = ring.thickness;
|
||
ctx.beginPath();
|
||
ctx.arc(0, 0, this.size + ring.radiusOffset, Math.PI, 2 * Math.PI); // back half
|
||
ctx.stroke();
|
||
ctx.restore();
|
||
});
|
||
}
|
||
|
||
// Draw the giant planet
|
||
this.drawGiant(ctx);
|
||
|
||
if (this.rings && this.rings.length > 0) {
|
||
// Draw front halves of rings
|
||
this.rings.forEach((ring) => {
|
||
ctx.save();
|
||
ctx.translate(this.x, this.y);
|
||
ctx.rotate(tilt);
|
||
ctx.scale(1, this._tiltScale);
|
||
|
||
ctx.strokeStyle = ring.color.startsWith('rgba')
|
||
? ring.color.replace(/rgba\(([^)]+),[^)]+\)/, `rgba($1,${rings.alpha})`)
|
||
: `rgba(${parseInt(ring.color.slice(1,3),16)},${parseInt(ring.color.slice(3,5),16)},${parseInt(ring.color.slice(5,7),16)},${ring.alpha})`;
|
||
|
||
ctx.lineWidth = ring.thickness;
|
||
ctx.beginPath();
|
||
ctx.arc(0, 0, this.size + ring.radiusOffset, 0, Math.PI); // front half
|
||
ctx.stroke();
|
||
ctx.restore();
|
||
});
|
||
}
|
||
|
||
} else if (this.type === 'planet') {
|
||
this.drawPlanet(ctx);
|
||
} else {
|
||
this.drawAsteroid(ctx);
|
||
}
|
||
};
|
||
|
||
|
||
//Asteroid.prototype.draw = function(ctx) {
|
||
// if (this.type === 'comet') {
|
||
// this.drawComet(ctx);
|
||
// } else if (this.type === 'giant') {
|
||
// // Draw the giant at its absolute position
|
||
// this.drawGiant(ctx);
|
||
//
|
||
// // Draw rings if present
|
||
// if (this.rings && this.rings.length > 0) {
|
||
// this.rings.forEach((ring, index) => {
|
||
// ctx.save();
|
||
//
|
||
// // Move origin to giant's center
|
||
// ctx.translate(this.x, this.y);
|
||
//
|
||
// // Independent tilt + oscillation per ring
|
||
// if (ring._tiltBase === undefined) {
|
||
// ring._tiltBase = (Math.random() - 0.5) * 0.4; // ±0.2 rad
|
||
// ring._oscillationSpeed = 0.001 + Math.random() * 0.002;
|
||
// }
|
||
// const tilt = ring._tiltBase + Math.sin(Date.now() * ring._oscillationSpeed) * 0.05;
|
||
//
|
||
// ctx.rotate(tilt);
|
||
//
|
||
// ctx.strokeStyle = ring.color;
|
||
// ctx.lineWidth = ring.thickness;
|
||
// ctx.beginPath();
|
||
// ctx.arc(0, 0, this.size + ring.radiusOffset, 0, Math.PI * 2);
|
||
// ctx.stroke();
|
||
//
|
||
// ctx.restore();
|
||
// });
|
||
// }
|
||
// } else if (this.type === 'planet') {
|
||
// this.drawPlanet(ctx);
|
||
// } else {
|
||
// this.drawAsteroid(ctx);
|
||
// }
|
||
//};
|
||
|
||
//Asteroid.prototype.draw = function(ctx) {
|
||
// if (this.type === 'comet') {
|
||
// this.drawComet(ctx);
|
||
// } else if (this.type === 'giant') {
|
||
// this.drawGiant(ctx);
|
||
// if (this.rings && this.rings.length > 0) {
|
||
// this.rings.forEach(function(ring) {
|
||
// ctx.strokeStyle = ring.color;
|
||
// ctx.lineWidth = ring.thickness;
|
||
// ctx.beginPath();
|
||
// ctx.arc(0, 0, this.size + ring.radiusOffset, 0, Math.PI * 2);
|
||
// ctx.stroke();
|
||
// }, this);
|
||
// }
|
||
// } else if (this.type === 'planet') {
|
||
// this.drawPlanet(ctx);
|
||
// } else {
|
||
// this.drawAsteroid(ctx);
|
||
// }
|
||
//};
|
||
|
||
Asteroid.prototype.drawComet = function(ctx) {
|
||
var orbitTangentAngle = this.angle + Math.PI / 2;
|
||
var tailLength = this.size * 5;
|
||
|
||
ctx.save();
|
||
ctx.translate(this.x, this.y);
|
||
ctx.rotate(orbitTangentAngle + Math.PI);
|
||
|
||
var gradient = ctx.createLinearGradient(0, 0, tailLength, 0);
|
||
gradient.addColorStop(0, 'rgba(80, 140, 200, 0.5)');
|
||
gradient.addColorStop(0.5, 'rgba(60, 100, 150, 0.25)');
|
||
gradient.addColorStop(1, 'rgba(40, 80, 120, 0)');
|
||
|
||
ctx.fillStyle = gradient;
|
||
ctx.fillRect(0, -this.size * 0.4, tailLength, this.size * 0.8);
|
||
ctx.restore();
|
||
|
||
var nucleusSize = this.size * 0.5;
|
||
ctx.save();
|
||
ctx.translate(this.x, this.y);
|
||
ctx.fillStyle = '#5a7a8a';
|
||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
|
||
ctx.lineWidth = 0.5;
|
||
ctx.beginPath();
|
||
ctx.arc(0, 0, nucleusSize, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.stroke();
|
||
|
||
ctx.fillStyle = 'rgba(80, 120, 140, 0.2)';
|
||
ctx.beginPath();
|
||
ctx.arc(0, 0, nucleusSize * 1.3, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.restore();
|
||
};
|
||
|
||
Asteroid.prototype.drawGiant = function(ctx) {
|
||
ctx.save();
|
||
ctx.translate(this.x, this.y);
|
||
|
||
var gradient = ctx.createRadialGradient(-this.size * 0.25, -this.size * 0.25, 0, 0, 0, this.size);
|
||
gradient.addColorStop(0, this.planetColors.light);
|
||
gradient.addColorStop(0.5, this.planetColors.mid);
|
||
gradient.addColorStop(1, this.planetColors.dark);
|
||
|
||
ctx.fillStyle = gradient;
|
||
ctx.beginPath();
|
||
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
|
||
ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
|
||
ctx.lineWidth = 1;
|
||
for (var band = -this.size * 0.6; band < this.size * 0.6; band += this.size * 0.15) {
|
||
ctx.beginPath();
|
||
var bandWidth = Math.sqrt(this.size * this.size - band * band);
|
||
ctx.moveTo(-bandWidth, band);
|
||
ctx.lineTo(bandWidth, band);
|
||
ctx.stroke();
|
||
}
|
||
|
||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
|
||
ctx.lineWidth = 0.5;
|
||
ctx.beginPath();
|
||
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
|
||
ctx.restore();
|
||
};
|
||
|
||
Asteroid.prototype.drawPlanet = function(ctx) {
|
||
ctx.save();
|
||
ctx.translate(this.x, this.y);
|
||
|
||
var gradient = ctx.createRadialGradient(-this.size * 0.2, -this.size * 0.2, 0, 0, 0, this.size);
|
||
gradient.addColorStop(0, this.planetColors.light);
|
||
gradient.addColorStop(0.5, this.planetColors.mid);
|
||
gradient.addColorStop(1, this.planetColors.dark);
|
||
|
||
ctx.fillStyle = gradient;
|
||
ctx.beginPath();
|
||
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
|
||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
|
||
ctx.lineWidth = 0.5;
|
||
ctx.stroke();
|
||
|
||
ctx.restore();
|
||
};
|
||
|
||
Asteroid.prototype.drawAsteroid = function(ctx) {
|
||
ctx.save();
|
||
ctx.translate(this.x, this.y);
|
||
ctx.rotate(this.rotation);
|
||
|
||
var asteroidColor = CONFIG.ASTEROID_COLORS[this.type] || CONFIG.ASTEROID_COLORS.small;
|
||
|
||
ctx.fillStyle = asteroidColor;
|
||
ctx.beginPath();
|
||
ctx.moveTo(0, -this.size);
|
||
ctx.lineTo(this.size * 0.8, this.size * 0.6);
|
||
ctx.lineTo(-this.size * 0.8, this.size * 0.6);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.restore();
|
||
};
|
||
|
||
Asteroid.prototype.isDestroyed = function() {
|
||
return this.distance < this.blackHole.radius;
|
||
};
|
||
|
||
function BlackHole(x, y, radius) {
|
||
this.x = x;
|
||
this.y = y;
|
||
this.radius = radius;
|
||
this.pulse = 0;
|
||
this.pulseColor = '#000000';
|
||
}
|
||
|
||
BlackHole.prototype.draw = function(ctx) {
|
||
if (this.pulse > 0) {
|
||
this.pulse -= 0.02;
|
||
if (this.pulse < 0) this.pulse = 0;
|
||
}
|
||
if (this.pulse === 0) {
|
||
this.pulseColor = '#000000';
|
||
}
|
||
ctx.fillStyle = '#000000';
|
||
ctx.beginPath();
|
||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
|
||
var visualPulse = Math.min(this.pulse, 1);
|
||
var glowIntensity = 0.2 + (visualPulse * 0.3);
|
||
var glowRadius = this.radius * (1.01 + visualPulse * 0.1);
|
||
|
||
var gradient = ctx.createRadialGradient(
|
||
this.x, this.y, this.radius * 0.97,
|
||
this.x, this.y, glowRadius
|
||
);
|
||
|
||
var r, g, b;
|
||
if (this.pulseColor.startsWith('#')) {
|
||
var hex = this.pulseColor;
|
||
r = parseInt(hex.substr(1, 2), 16);
|
||
g = parseInt(hex.substr(3, 2), 16);
|
||
b = parseInt(hex.substr(5, 2), 16);
|
||
} else {
|
||
r = 60; g = 60; b = 80;
|
||
}
|
||
|
||
// Increase saturation by 3x when pulsing
|
||
var saturated = saturateColor(r, g, b, 3);
|
||
r = saturated.r;
|
||
g = saturated.g;
|
||
b = saturated.b;
|
||
|
||
gradient.addColorStop(0, 'rgba(' + r + ', ' + g + ', ' + b + ', 0)');
|
||
gradient.addColorStop(1, 'rgba(' + r + ', ' + g + ', ' + b + ', ' + glowIntensity + ')');
|
||
|
||
ctx.fillStyle = gradient;
|
||
ctx.beginPath();
|
||
ctx.arc(this.x, this.y, glowRadius, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
};
|
||
|
||
BlackHole.prototype.consumeAsteroid = function(asteroid) {
|
||
// Calculate pulse contribution (same as before)
|
||
var normalized = (Math.log10(asteroid.massKg) - 3) / 6;
|
||
var pulseContribution = Math.max(0.1, normalized);
|
||
this.pulse = Math.min(this.pulse + pulseContribution, 9);
|
||
|
||
// Determine new pulse color based on asteroid type/size
|
||
var newColor;
|
||
if (asteroid.planetColors) {
|
||
newColor = asteroid.planetColors.mid;
|
||
} else if (asteroid.type === 'comet') {
|
||
newColor = '#5a7a8a';
|
||
} else if (asteroid.size === 'large') {
|
||
newColor = '#f8f8f8';
|
||
} else if (asteroid.size === 'medium') {
|
||
newColor = '#888888';
|
||
} else {
|
||
newColor = '#444444';
|
||
}
|
||
|
||
// Blend with existing pulseColor instead of replacing
|
||
if (!this.pulseColor) this.pulseColor = newColor;
|
||
else this.pulseColor = this.blendColors(this.pulseColor, newColor, 0.3);
|
||
};
|
||
|
||
// Helper function to blend two hex colors by weight (0–1)
|
||
BlackHole.prototype.blendColors = function(c1, c2, weight) {
|
||
var d2h = d => d.toString(16).padStart(2, '0');
|
||
var h2d = h => parseInt(h, 16);
|
||
|
||
// Remove #
|
||
c1 = c1.replace('#','');
|
||
c2 = c2.replace('#','');
|
||
|
||
var r = Math.round(h2d(c1.substring(0,2)) * (1-weight) + h2d(c2.substring(0,2)) * weight);
|
||
var g = Math.round(h2d(c1.substring(2,4)) * (1-weight) + h2d(c2.substring(2,4)) * weight);
|
||
var b = Math.round(h2d(c1.substring(4,6)) * (1-weight) + h2d(c2.substring(4,6)) * weight);
|
||
|
||
return '#' + d2h(r) + d2h(g) + d2h(b);
|
||
};
|
||
|
||
|
||
//BlackHole.prototype.consumeAsteroid = function(asteroid) {
|
||
// // Calculate pulse contribution based on mass
|
||
// // Using log scale: small asteroids (1e3 kg) give ~0.1, giants (5e14 kg) give ~2.0
|
||
// var normalized = (Math.log10(asteroid.massKg) - 3) / 6; // Range: 0 to ~2
|
||
// var pulseContribution = Math.max(0.1, normalized); // Minimum 0.05 for visibility
|
||
//
|
||
// // Add to existing pulse (cumulative effect)
|
||
// this.pulse = Math.min(this.pulse + pulseContribution, 9); // Cap at 9 for very large accumulations
|
||
//
|
||
// // Update color - blend with existing color if already glowing
|
||
// if (asteroid.planetColors) {
|
||
// this.pulseColor = asteroid.planetColors.mid;
|
||
// } else if (asteroid.type === 'comet') {
|
||
// this.pulseColor = '#5a7a8a';
|
||
// } else if (asteroid.size === 'large') {
|
||
// this.pulseColor = '#ffffff';
|
||
// } else if (asteroid.size === 'medium') {
|
||
// this.pulseColor = '#888888';
|
||
// } else {
|
||
// this.pulseColor = '#444444';
|
||
// }
|
||
//};
|
||
|