modified: admin.html
modified: index.html modified: js/config.js modified: js/entities.js modified: js/game.js modified: js/helpers.js modified: js/server.js modified: js/storage.js modified: js/ui.js modified: style.css
This commit is contained in:
@ -28,6 +28,7 @@
|
||||
<button onclick="adminSpawn('planet')">Planet</button>
|
||||
<button onclick="adminSpawn('giant')">Giant</button>
|
||||
<button onclick="adminSpawn('mtype')">M-Type</button>
|
||||
<button onclick="adminSpawn('ktype')">K-Type</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
47
index.html
47
index.html
@ -38,19 +38,32 @@ a1.7 1.7 0 0 0-1.5 1z"></path>
|
||||
</svg>
|
||||
|
||||
<div id="settings-menu">
|
||||
<div class="menu-title">Game Settings</div>
|
||||
<div class="menu-text">
|
||||
This game uses a browser cookie to save your progress across sessions.
|
||||
By using this website, you consent to storing a cookie on your device.
|
||||
<div class="menu-subtitle">Hoel</div>
|
||||
<div class="menu-text">This game uses cookies to save your progress across sessions. By using this website, you consent to storing cookies on your device.</div>
|
||||
<div class="menu-text">The game periodically sends pseudonymous information about your session to the server to populate the neighbour chart.</div>
|
||||
<div class="menu-text">Sessions are hidden from the chart after 5 days and deleted from the database after 30 days of inactivity.</div>
|
||||
<div class="menu-divider"></div>
|
||||
<div class="menu-subtitle">Session Reset</div>
|
||||
<div class="menu-text">Terminate progress, purge registers, annihilate archives, unweave circuits, shatter syntax, invert polarity, nullify existence, restart systems, initialize framework.</div>
|
||||
<button id="reset-btn" class="menu-btn danger">Reset</button>
|
||||
<div id="save-status" style="margin-top: 12px; font-size: 10px; color: rgba(100, 200, 100, 0.6);"></div>
|
||||
<div class="menu-divider"></div>
|
||||
<div class="menu-subtitle">Session Transfer</div>
|
||||
<div class="menu-text">Duplicate progress, start migration, redirect access, shift registry, authorize device, clone instance, restore snapshot, deploy replica, mount payload, fuse endpoints.</div>
|
||||
<button id="export-session-btn" class="menu-btn-export">Export</button>
|
||||
<div id="export-code-display" style="display:none; margin-top:8px;">
|
||||
<div class="transfer-warning">This code provides full access to your save state. Handle with care.</div>
|
||||
<div class="transfer-code-box">
|
||||
<span id="transfer-code-value" class="transfer-code">-----</span>
|
||||
</div>
|
||||
<div id="transfer-code-expiry" class="transfer-expiry"></div>
|
||||
</div>
|
||||
<div class="menu-text">
|
||||
The game periodically sends parts of your session to the server to populate the neighbour chart.
|
||||
<div style="margin-top:8px;">
|
||||
<div class="transfer-input-row">
|
||||
<button id="transfer-claim-btn" class="menu-btn-import">Import</button>
|
||||
<input type="text" id="transfer-code-input" class="transfer-input" placeholder="ACA8E" maxlength="5" spellcheck="false" autocomplete="off" style="display:none;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-text">
|
||||
Sessions are hidden from the chart after 5 days and deleted from the database after 30 days of inactivity.
|
||||
</div>
|
||||
<button id="reset-btn" class="menu-btn danger">Reset</button>
|
||||
<div id="save-status" style="margin-top: 12px; font-size: 10px; color: rgba(100, 200, 100, 0.6);"></div>
|
||||
</div>
|
||||
|
||||
<div id="stats-panel">
|
||||
@ -68,24 +81,28 @@ a1.7 1.7 0 0 0-1.5 1z"></path>
|
||||
<div id="spendable-mass">Available to Spend: 0 kg</div>
|
||||
</br>
|
||||
<div id="asteroid-level"></div>
|
||||
<button id="asteroid-upgrade-btn" class="upgrade-btn" disabled>Upgrade (Cost: 1 tonne)</button>
|
||||
<button id="asteroid-upgrade-btn" class="upgrade-btn" disabled>Upgrade</button>
|
||||
</div>
|
||||
<div class="upgrade-row">
|
||||
<div id="comet-level"></div>
|
||||
<button id="comet-upgrade-btn" class="upgrade-btn" disabled>Upgrade (Cost: 10 tonnes)</button>
|
||||
<button id="comet-upgrade-btn" class="upgrade-btn" disabled>Upgrade</button>
|
||||
</div>
|
||||
<div class="upgrade-row">
|
||||
<div id="planet-level"></div>
|
||||
<button id="planet-upgrade-btn" class="upgrade-btn" disabled>Upgrade (Cost: 100 tonnes)</button>
|
||||
<button id="planet-upgrade-btn" class="upgrade-btn" disabled>Upgrade</button>
|
||||
</div>
|
||||
<div class="upgrade-row">
|
||||
<div id="giant-level"></div>
|
||||
<button id="giant-upgrade-btn" class="upgrade-btn" disabled>Upgrade (Cost: 1000 tonnes)</button>
|
||||
<button id="giant-upgrade-btn" class="upgrade-btn" disabled>Upgrade</button>
|
||||
</div>
|
||||
<div class="upgrade-row">
|
||||
<div id="mtype-level"></div>
|
||||
<button id="mtype-upgrade-btn" class="upgrade-btn" disabled>Upgrade</button>
|
||||
</div>
|
||||
<div class="upgrade-row">
|
||||
<div id="ktype-level"></div>
|
||||
<button id="ktype-upgrade-btn" class="upgrade-btn" disabled>Upgrade</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaderboard Toggle -->
|
||||
|
||||
22
js/config.js
22
js/config.js
@ -16,11 +16,12 @@ var CONFIG = {
|
||||
STAR_UPDATE_INTERVAL: 1312,
|
||||
|
||||
// Spawn intervals (in milliseconds)
|
||||
BASE_ASTEROID_SPAWN_INTERVAL: 1000,
|
||||
BASE_ASTEROID_SPAWN_INTERVAL: 2000,
|
||||
BASE_COMET_SPAWN_INTERVAL: 120000, // 2 minutes
|
||||
BASE_PLANET_SPAWN_INTERVAL: 3600000, // 1 hour
|
||||
BASE_GIANT_SPAWN_INTERVAL: 21600000, // 6 hours
|
||||
BASE_MTYPE_SPAWN_INTERVAL: 86400000, // 1 day
|
||||
BASE_MTYPE_SPAWN_INTERVAL: 86400000, // 1 day
|
||||
BASE_KTYPE_SPAWN_INTERVAL: 259200000, // 3 days
|
||||
|
||||
// Rate tracking windows (in milliseconds)
|
||||
RATE_WINDOWS: {
|
||||
@ -34,7 +35,8 @@ var CONFIG = {
|
||||
COMET_BASE_COST: 1e7,
|
||||
PLANET_BASE_COST: 1e24,
|
||||
GIANT_BASE_COST: 1e27,
|
||||
MTYPE_BASE_COST: 1e28,
|
||||
MTYPE_BASE_COST: 1e29,
|
||||
KTYPE_BASE_COST: 1e30,
|
||||
|
||||
// Upgrade scaling
|
||||
UPGRADE_COST_MULTIPLIER_ASTER: 1.25,
|
||||
@ -42,12 +44,14 @@ var CONFIG = {
|
||||
UPGRADE_COST_MULTIPLIER_PLANT: 1.75,
|
||||
UPGRADE_COST_MULTIPLIER_GIANT: 2,
|
||||
UPGRADE_COST_MULTIPLIER_MTYPE: 2.1,
|
||||
UPGRADE_COST_MULTIPLIER_KTYPE: 2.2,
|
||||
|
||||
UPGRADE_BONUS_PER_LEVEL_ASTER: 0.1,
|
||||
UPGRADE_BONUS_PER_LEVEL_COMET: 0.1,
|
||||
UPGRADE_BONUS_PER_LEVEL_PLANT: 0.05,
|
||||
UPGRADE_BONUS_PER_LEVEL_GIANT: 0.03,
|
||||
UPGRADE_BONUS_PER_LEVEL_MTYPE: 0.03,
|
||||
UPGRADE_BONUS_PER_LEVEL_KTYPE: 0.03,
|
||||
|
||||
// Asteroid spawn patterns
|
||||
ASTEROID_SPAWN_PATTERNS: {
|
||||
@ -62,7 +66,8 @@ var CONFIG = {
|
||||
comet: [1e7, 1e8], // 10000-100000 t
|
||||
planet: [1e22, 1e25], // Moons & Planets
|
||||
giant: [1e26, 1e28], // Gas & Ice Giants
|
||||
mtype: [1.589e29, 8.95e29] // M-Type: 0.08-0.45 M☉
|
||||
mtype: [1.589e29, 8.95e29],// M-Type: 0.08-0.45 M☉
|
||||
ktype: [8.95e29, 1.59e30] // K-Type: 0.45-0.8 M☉
|
||||
},
|
||||
|
||||
// Planet color schemes
|
||||
@ -108,6 +113,15 @@ var CONFIG = {
|
||||
'rgba(70, 40, 22, 0.55)',
|
||||
'rgba(45, 28, 15, 0.5)'
|
||||
],
|
||||
|
||||
// K-Type star color schemes (warm orange to pale yellow-orange)
|
||||
KTYPE_COLORS: [
|
||||
{ light: '#ffe4a0', mid: '#ffb347', dark: '#e07820' },
|
||||
{ light: '#ffd580', mid: '#f0a030', dark: '#d07010' },
|
||||
{ light: '#ffc870', mid: '#e89030', dark: '#c86818' },
|
||||
{ light: '#ffe8b0', mid: '#ffc055', dark: '#e08820' },
|
||||
{ light: '#ffd070', mid: '#e8952a', dark: '#c87018' }
|
||||
],
|
||||
|
||||
// Star color distribution
|
||||
STAR_COLORS: [
|
||||
|
||||
239
js/entities.js
239
js/entities.js
@ -54,8 +54,8 @@ function Asteroid(type, blackHole, canvas) {
|
||||
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.orbitSpeed = 0.004 + Math.random() * 0.004;
|
||||
this.decayRate = 0.08 + Math.random() * 0.08;
|
||||
|
||||
this.blackHole = blackHole;
|
||||
|
||||
@ -68,7 +68,7 @@ function Asteroid(type, blackHole, canvas) {
|
||||
Asteroid.prototype.initializeTypeSpecificProperties = function() {
|
||||
// Visual size (unchanged)
|
||||
if (this.type === 'comet') {
|
||||
this.size = 3 + Math.random() * 3;
|
||||
this.size = 3 + Math.random() * 1;
|
||||
} else if (this.type === 'mtype') {
|
||||
this.size = 15 + Math.random() * 3;
|
||||
this.orbitSpeed *= 0.2;
|
||||
@ -99,6 +99,64 @@ Asteroid.prototype.initializeTypeSpecificProperties = function() {
|
||||
phase: Math.random() * Math.PI * 2 // random phase for variety
|
||||
});
|
||||
}
|
||||
} else if (this.type === 'ktype') {
|
||||
this.size = 16 + Math.random() * 4;
|
||||
this.orbitSpeed *= 0.15;
|
||||
this.decayRate *= 0.22;
|
||||
this.planetColors = CONFIG.KTYPE_COLORS[Math.floor(Math.random() * CONFIG.KTYPE_COLORS.length)];
|
||||
|
||||
// Surface granulation cells
|
||||
this.granules = [];
|
||||
var granCount = 8 + Math.floor(Math.random() * 10);
|
||||
for (var i = 0; i < granCount; i++) {
|
||||
var angle = Math.random() * Math.PI * 2;
|
||||
var dist = Math.random() * this.size * 0.8;
|
||||
this.granules.push({
|
||||
x: Math.cos(angle) * dist,
|
||||
y: Math.sin(angle) * dist,
|
||||
radius: 1.5 + Math.random() * this.size * 0.2,
|
||||
phase: Math.random() * Math.PI * 2
|
||||
});
|
||||
}
|
||||
|
||||
// Spikes — each one fully independent
|
||||
this.spikes = [];
|
||||
var spikeCount = 11 + Math.floor(Math.random() * 11); // 11–22
|
||||
for (var s = 0; s < spikeCount; s++) {
|
||||
this.spikes.push({
|
||||
baseAngle: (s / spikeCount) * Math.PI * 2 + (Math.random() - 0.5) * 0.4,
|
||||
rotSpeed: (Math.random() - 0.5) * 0.015, // some drift CW, some CCW
|
||||
lenBase: 0.25 + Math.random() * 0.45, // 0.25–0.70 × size
|
||||
lenAmp: 0.05 + Math.random() * 0.20, // oscillation range
|
||||
lenSpeed: 0.3 + Math.random() * 1.0, // oscillation speed
|
||||
lenPhase: Math.random() * Math.PI * 2,
|
||||
widthBase: 0.025 + Math.random() * 0.045, // tip width
|
||||
alphaBase: 0.10 + Math.random() * 0.14,
|
||||
alphaAmp: 0.03 + Math.random() * 0.07,
|
||||
alphaPhase: Math.random() * Math.PI * 2,
|
||||
alphaSpeed: 0.4 + Math.random() * 0.9
|
||||
});
|
||||
}
|
||||
|
||||
// Sparkles — simulating spikes toward the viewer
|
||||
this.sparkles = [];
|
||||
var sparkleCount = 4 + Math.floor(Math.random() * 4); // 4–8
|
||||
for (var k = 0; k < sparkleCount; k++) {
|
||||
var angle = Math.random() * Math.PI * 2;
|
||||
var dist = Math.random() * this.size * 0.75;
|
||||
this.sparkles.push({
|
||||
x: Math.cos(angle) * dist,
|
||||
y: Math.sin(angle) * dist,
|
||||
dist: dist, // ← add
|
||||
angle: angle, // ← add
|
||||
wanderSpeed: (Math.random() - 0.5) * 0.008, // ← add, slow angular drift
|
||||
size: 0.4 + Math.random() * 0.8,
|
||||
phase: Math.random() * Math.PI * 2,
|
||||
speed: 0.05 + Math.random() * 1.8,
|
||||
armLen: 0.4 + Math.random() * 8,
|
||||
armWidth: 0.2 + Math.random() * 0.4
|
||||
});
|
||||
}
|
||||
} else if (this.type === 'giant') {
|
||||
this.size = 10 + Math.random() * 5;
|
||||
this.orbitSpeed *= 0.3;
|
||||
@ -146,6 +204,8 @@ Asteroid.prototype.draw = function(ctx) {
|
||||
this.drawComet(ctx);
|
||||
} else if (this.type === 'mtype') {
|
||||
this.drawMType(ctx, performance.now() * 0.002);
|
||||
} else if (this.type === 'ktype') {
|
||||
this.drawKType(ctx, performance.now() * 0.002);
|
||||
} else if (this.type === 'giant') {
|
||||
// Initialize per-giant tilt if not already done
|
||||
if (this._ringTiltBase === undefined) {
|
||||
@ -348,6 +408,179 @@ Asteroid.prototype.drawMType = function(ctx, time) {
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
Asteroid.prototype.drawKType = function(ctx, time) {
|
||||
var massRange = CONFIG.ASTEROID_MASS_RANGES.ktype;
|
||||
var massNormalized = (this.massKg - massRange[0]) / (massRange[1] - massRange[0]);
|
||||
|
||||
var pulse = 0.06 * Math.sin(time * 1.4 + this.massKg * 0.001) + 1;
|
||||
var slowPulse = 0.04 * Math.sin(time * 0.4) + 1;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(this.x, this.y);
|
||||
|
||||
// ── Layer 1: tight ambient halo ──────────────────────────────────────
|
||||
var haloSize = this.size * (2.2 + massNormalized * 0.5) * slowPulse;
|
||||
var halo = ctx.createRadialGradient(0, 0, this.size * 0.9, 0, 0, haloSize);
|
||||
halo.addColorStop(0, 'rgba(255, 160, 40, 0.10)');
|
||||
halo.addColorStop(0.5, 'rgba(255, 130, 20, 0.04)');
|
||||
halo.addColorStop(1, 'rgba(240, 100, 10, 0)');
|
||||
ctx.fillStyle = halo;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, haloSize, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// ── Layer 2: spikes ──────────────────────────────────────────────────
|
||||
for (var i = 0; i < this.spikes.length; i++) {
|
||||
var sp = this.spikes[i];
|
||||
|
||||
var spikeAngle = sp.baseAngle + time * sp.rotSpeed;
|
||||
var spikeLen = this.size * (sp.lenBase + sp.lenAmp * Math.sin(time * sp.lenSpeed + sp.lenPhase));
|
||||
var alpha = sp.alphaBase + sp.alphaAmp * Math.sin(time * sp.alphaSpeed + sp.alphaPhase);
|
||||
var tipWidth = this.size * sp.widthBase;
|
||||
|
||||
ctx.save();
|
||||
ctx.rotate(spikeAngle);
|
||||
|
||||
var spikeGrad = ctx.createLinearGradient(
|
||||
this.size, 0,
|
||||
this.size + spikeLen, 0
|
||||
);
|
||||
spikeGrad.addColorStop(0, 'rgba(255, 210, 110, ' + alpha + ')');
|
||||
spikeGrad.addColorStop(0.4, 'rgba(255, 185, 70, ' + (alpha * 0.6) + ')');
|
||||
spikeGrad.addColorStop(1, 'rgba(240, 150, 30, 0)');
|
||||
|
||||
ctx.fillStyle = spikeGrad;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(this.size * 0.95, -tipWidth);
|
||||
ctx.quadraticCurveTo(
|
||||
this.size + spikeLen * 0.5, -tipWidth * 0.3,
|
||||
this.size + spikeLen, 0
|
||||
);
|
||||
ctx.quadraticCurveTo(
|
||||
this.size + spikeLen * 0.5, tipWidth * 0.3,
|
||||
this.size * 0.95, tipWidth
|
||||
);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// ── Layer 3: inner corona ring ───────────────────────────────────────
|
||||
var coronaGrad = ctx.createRadialGradient(0, 0, this.size * 0.9, 0, 0, this.size * 1.3 * pulse);
|
||||
coronaGrad.addColorStop(0, 'rgba(255, 200, 100, 0.18)');
|
||||
coronaGrad.addColorStop(0.5, 'rgba(255, 170, 60, 0.07)');
|
||||
coronaGrad.addColorStop(1, 'rgba(255, 140, 30, 0)');
|
||||
ctx.fillStyle = coronaGrad;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, this.size * 1.3 * pulse, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// ── Layer 4: main body ───────────────────────────────────────────────
|
||||
var bodyGrad = ctx.createRadialGradient(
|
||||
-this.size * 0.15, -this.size * 0.15, 0,
|
||||
0, 0, this.size
|
||||
);
|
||||
bodyGrad.addColorStop(0, this.planetColors.light);
|
||||
bodyGrad.addColorStop(0.45, this.planetColors.mid);
|
||||
bodyGrad.addColorStop(1, this.planetColors.dark);
|
||||
ctx.fillStyle = bodyGrad;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// ── Layer 5: surface granulation ────────────────────────────────────
|
||||
for (var g = 0; g < this.granules.length; g++) {
|
||||
var gran = this.granules[g];
|
||||
var gAlpha = 0.05 + 0.03 * Math.sin(time * 0.35 + gran.phase);
|
||||
var granGrad = ctx.createRadialGradient(
|
||||
gran.x, gran.y, 0,
|
||||
gran.x, gran.y, gran.radius
|
||||
);
|
||||
granGrad.addColorStop(0, 'rgba(255, 248, 200, ' + gAlpha + ')');
|
||||
granGrad.addColorStop(1, 'rgba(255, 200, 100, 0)');
|
||||
ctx.fillStyle = granGrad;
|
||||
ctx.beginPath();
|
||||
ctx.arc(gran.x, gran.y, gran.radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// ── Layer 6a: limb darkening ──────────────────────────────────────────
|
||||
var limbGrad = ctx.createRadialGradient(0, 0, this.size * 0.45, 0, 0, this.size * 1.04);
|
||||
limbGrad.addColorStop(0, 'rgba(0, 0, 0, 0)');
|
||||
limbGrad.addColorStop(0.6, 'rgba(0, 0, 0, 0)');
|
||||
limbGrad.addColorStop(0.82, 'rgba(0, 0, 0, 0.12)');
|
||||
limbGrad.addColorStop(0.93, 'rgba(0, 0, 0, 0.20)');
|
||||
limbGrad.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||||
|
||||
// ── Layer 6b: sparkles (viewer-facing spikes) ────────────────────────
|
||||
for (var k = 0; k < this.sparkles.length; k++) {
|
||||
var sp = this.sparkles[k];
|
||||
|
||||
// Drift angle slowly over time
|
||||
sp.angle += sp.wanderSpeed;
|
||||
sp.x = Math.cos(sp.angle) * sp.dist;
|
||||
sp.y = Math.sin(sp.angle) * sp.dist;
|
||||
|
||||
var bright = 0.5 + 0.5 * Math.sin(time * sp.speed + sp.phase);
|
||||
var sAlpha = bright * bright; // squaring gives snappier flash
|
||||
|
||||
if (sAlpha < 0.05) continue; // skip when nearly invisible
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
|
||||
ctx.clip(); // keep sparkles inside the disc
|
||||
|
||||
ctx.translate(sp.x, sp.y);
|
||||
|
||||
// Four-pointed cross — two perpendicular arms
|
||||
for (var arm = 0; arm < 2; arm++) {
|
||||
ctx.save();
|
||||
ctx.rotate(arm * Math.PI / 2);
|
||||
var armGrad = ctx.createLinearGradient(-sp.armLen, 0, sp.armLen, 0);
|
||||
armGrad.addColorStop(0, 'rgba(255, 255, 240, 0)');
|
||||
armGrad.addColorStop(0.35, 'rgba(255, 250, 210, ' + (sAlpha * 0.5) + ')');
|
||||
armGrad.addColorStop(0.5, 'rgba(255, 255, 255, ' + sAlpha + ')');
|
||||
armGrad.addColorStop(0.65, 'rgba(255, 250, 210, ' + (sAlpha * 0.5) + ')');
|
||||
armGrad.addColorStop(1, 'rgba(255, 255, 240, 0)');
|
||||
ctx.fillStyle = armGrad;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-sp.armLen, 0);
|
||||
ctx.lineTo(0, -sp.armWidth);
|
||||
ctx.lineTo( sp.armLen, 0);
|
||||
ctx.lineTo(0, sp.armWidth);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Bright centre dot
|
||||
var dotGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, sp.size);
|
||||
dotGrad.addColorStop(0, 'rgba(255, 255, 255, ' + sAlpha + ')');
|
||||
dotGrad.addColorStop(0.5, 'rgba(255, 245, 200, ' + (sAlpha * 0.5) + ')');
|
||||
dotGrad.addColorStop(1, 'rgba(255, 230, 150, 0)');
|
||||
ctx.fillStyle = dotGrad;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, sp.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// ── Layer 7: bright core ─────────────────────────────────────────────
|
||||
var coreGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, this.size * 0.45 * pulse);
|
||||
coreGrad.addColorStop(0, 'rgba(255, 255, 230, 0.55)');
|
||||
coreGrad.addColorStop(0.35,'rgba(255, 235, 160, 0.25)');
|
||||
coreGrad.addColorStop(0.7, 'rgba(255, 200, 100, 0.08)');
|
||||
coreGrad.addColorStop(1, 'rgba(255, 170, 50, 0)');
|
||||
ctx.fillStyle = coreGrad;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, this.size * 0.45 * pulse, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
Asteroid.prototype.drawGiant = function(ctx) {
|
||||
ctx.save();
|
||||
ctx.translate(this.x, this.y);
|
||||
|
||||
81
js/game.js
81
js/game.js
@ -18,17 +18,21 @@ var Game = (function() {
|
||||
giantUpgradeCost: CONFIG.GIANT_BASE_COST,
|
||||
mtypeUpgradeLevel: 0,
|
||||
mtypeUpgradeCost: CONFIG.MTYPE_BASE_COST,
|
||||
ktypeUpgradeLevel: 0,
|
||||
ktypeUpgradeCost: CONFIG.KTYPE_BASE_COST,
|
||||
currentAsteroidSpawnInterval: CONFIG.BASE_ASTEROID_SPAWN_INTERVAL,
|
||||
currentCometSpawnInterval: CONFIG.BASE_COMET_SPAWN_INTERVAL,
|
||||
currentPlanetSpawnInterval: CONFIG.BASE_PLANET_SPAWN_INTERVAL,
|
||||
currentGiantSpawnInterval: CONFIG.BASE_GIANT_SPAWN_INTERVAL,
|
||||
currentMtypeSpawnInterval: CONFIG.BASE_MTYPE_SPAWN_INTERVAL,
|
||||
currentKtypeSpawnInterval: CONFIG.BASE_KTYPE_SPAWN_INTERVAL,
|
||||
asteroidSpawnCount: 0,
|
||||
lastAsteroidSpawn: Date.now(),
|
||||
lastCometSpawn: Date.now(),
|
||||
lastPlanetSpawn: Date.now(),
|
||||
lastGiantSpawn: Date.now(),
|
||||
lastMtypeSpawn: Date.now(),
|
||||
lastKtypeSpawn: Date.now(),
|
||||
sM: 0, sT: Date.now(),
|
||||
mM: 0, mT: Date.now(),
|
||||
lM: 0, lT: Date.now(),
|
||||
@ -40,7 +44,8 @@ var Game = (function() {
|
||||
cometUnlocked: false,
|
||||
planetUnlocked: false,
|
||||
giantUnlocked: false,
|
||||
mtypeUnlocked: false
|
||||
mtypeUnlocked: false,
|
||||
ktypeUnlocked: false
|
||||
};
|
||||
|
||||
var blackHole;
|
||||
@ -87,7 +92,8 @@ var Game = (function() {
|
||||
comet: handleCometUpgrade,
|
||||
planet: handlePlanetUpgrade,
|
||||
giant: handleGiantUpgrade,
|
||||
mtype: handleMtypeUpgrade
|
||||
mtype: handleMtypeUpgrade,
|
||||
ktype: handleKtypeUpgrade
|
||||
});
|
||||
|
||||
Server.init()
|
||||
@ -157,7 +163,9 @@ var Game = (function() {
|
||||
state.giantUpgradeLevel = savedState.giantUpgradeLevel;
|
||||
state.giantUpgradeCost = savedState.giantUpgradeCost;
|
||||
state.mtypeUpgradeLevel = savedState.mtypeUpgradeLevel || 0;
|
||||
state.ktypeUpgradeLevel = savedState.ktypeUpgradeLevel || 0;
|
||||
state.mtypeUpgradeCost = savedState.mtypeUpgradeCost || CONFIG.MTYPE_BASE_COST;
|
||||
state.ktypeUpgradeCost = savedState.ktypeUpgradeCost || CONFIG.KTYPE_BASE_COST;
|
||||
state.asteroidSpawnCount = savedState.asteroidSpawnCount;
|
||||
|
||||
// Load unlock states (with backward compatibility)
|
||||
@ -165,12 +173,14 @@ var Game = (function() {
|
||||
state.planetUnlocked = savedState.planetUnlocked !== undefined ? savedState.planetUnlocked : (savedState.cometUpgradeLevel >= 15);
|
||||
state.giantUnlocked = savedState.giantUnlocked !== undefined ? savedState.giantUnlocked : (savedState.planetUpgradeLevel >= 10);
|
||||
state.mtypeUnlocked = savedState.mtypeUnlocked !== undefined ? savedState.mtypeUnlocked : (savedState.giantUpgradeLevel >= 5);
|
||||
state.ktypeUnlocked = savedState.ktypeUnlocked !== undefined ? savedState.ktypeUnlocked : (savedState.mtypeUpgradeLevel >= 5);
|
||||
|
||||
state.lastAsteroidSpawn = savedState.lastAsteroidSpawn || Date.now();
|
||||
state.lastCometSpawn = savedState.lastCometSpawn || Date.now();
|
||||
state.lastPlanetSpawn = savedState.lastPlanetSpawn || Date.now();
|
||||
state.lastGiantSpawn = savedState.lastGiantSpawn || Date.now();
|
||||
state.lastMtypeSpawn = savedState.lastMtypeSpawn || Date.now();
|
||||
state.lastKtypeSpawn = savedState.lastKtypeSpawn || Date.now();
|
||||
|
||||
// Load consumption rates
|
||||
state.sM = savedState.sM || 0;
|
||||
@ -262,6 +272,8 @@ var Game = (function() {
|
||||
(1 + state.giantUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_GIANT);
|
||||
state.currentMtypeSpawnInterval = CONFIG.BASE_MTYPE_SPAWN_INTERVAL /
|
||||
(1 + state.mtypeUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_MTYPE);
|
||||
state.currentKtypeSpawnInterval = CONFIG.BASE_KTYPE_SPAWN_INTERVAL /
|
||||
(1 + state.ktypeUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_KTYPE);
|
||||
}
|
||||
|
||||
function handleAsteroidUpgrade() {
|
||||
@ -347,6 +359,26 @@ var Game = (function() {
|
||||
state.mtypeUpgradeCost = Math.floor(
|
||||
CONFIG.MTYPE_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_MTYPE, state.mtypeUpgradeLevel)
|
||||
);
|
||||
|
||||
// Unlock K-Type at m-type level 5
|
||||
if (state.mtypeUpgradeLevel >= 5 && !state.ktypeUnlocked) {
|
||||
state.ktypeUnlocked = true;
|
||||
state.lastKtypeSpawn = Date.now();
|
||||
showUnlockNotification('ktype');
|
||||
asteroids.push(new Asteroid('ktype', blackHole, canvas));
|
||||
}
|
||||
updateSpawnIntervals();
|
||||
UI.update(state, CONFIG);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKtypeUpgrade() {
|
||||
if (state.totalMassConsumed >= state.ktypeUpgradeCost) {
|
||||
state.totalMassConsumed -= state.ktypeUpgradeCost;
|
||||
state.ktypeUpgradeLevel++;
|
||||
state.ktypeUpgradeCost = Math.floor(
|
||||
CONFIG.KTYPE_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_KTYPE, state.ktypeUpgradeLevel)
|
||||
);
|
||||
updateSpawnIntervals();
|
||||
UI.update(state, CONFIG);
|
||||
}
|
||||
@ -391,12 +423,20 @@ var Game = (function() {
|
||||
asteroids.push(new Asteroid('giant', blackHole, canvas));
|
||||
}
|
||||
}
|
||||
|
||||
if (state.mtypeUnlocked) {
|
||||
if (currentTime - state.lastMtypeSpawn > state.currentMtypeSpawnInterval) {
|
||||
state.lastMtypeSpawn = currentTime;
|
||||
asteroids.push(new Asteroid('mtype', blackHole, canvas));
|
||||
}
|
||||
}
|
||||
|
||||
if (state.ktypeUnlocked) {
|
||||
if (currentTime - state.lastKtypeSpawn > state.currentKtypeSpawnInterval) {
|
||||
state.lastKtypeSpawn = currentTime;
|
||||
asteroids.push(new Asteroid('ktype', blackHole, canvas));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -492,6 +532,8 @@ var Server = {
|
||||
this.playerId = 'p_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
Storage.setCookie('playerId', this.playerId, 365);
|
||||
}
|
||||
// Get or create session token
|
||||
this.sessionToken = Storage.getOrCreateToken();
|
||||
|
||||
// Start checkpoints
|
||||
this.startCheckpoints();
|
||||
@ -507,6 +549,7 @@ var Server = {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
playerId: this.playerId,
|
||||
secretToken: this.sessionToken,
|
||||
gameState: gameState
|
||||
})
|
||||
});
|
||||
@ -574,6 +617,40 @@ var Server = {
|
||||
}
|
||||
}, 15000); // Check every 15s, but only send if changed
|
||||
},
|
||||
|
||||
createTransferCode: async function() {
|
||||
try {
|
||||
const resp = await fetch('/api/transfer/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
playerId: this.playerId,
|
||||
secretToken: this.sessionToken
|
||||
})
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) return { error: data.error };
|
||||
return data;
|
||||
}catch (err) {
|
||||
console.error('Failed to create transfer code', err);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
claimTransferCode: async function(code) {
|
||||
try {
|
||||
const resp = await fetch('/api/transfer/claim', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code })
|
||||
});
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
return await resp.json();
|
||||
} catch (err) {
|
||||
console.error('Failed to claim transfer code', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Start the game when the page loads
|
||||
|
||||
@ -181,7 +181,8 @@ function showUnlockNotification(type) {
|
||||
comet: 'Comets Unlocked',
|
||||
planet: 'Planets Unlocked',
|
||||
giant: 'Gas & Ice Giants Unlocked',
|
||||
mtype: 'M-Type Stars Unlocked'
|
||||
mtype: 'M-Type Stars Unlocked',
|
||||
ktype: 'K-Type Stars Unlocked'
|
||||
};
|
||||
|
||||
showNotification({
|
||||
|
||||
123
js/server.js
123
js/server.js
@ -8,7 +8,25 @@ app.use(express.json());
|
||||
|
||||
const LEADERBOARD_FILE = './leaderboard.json';
|
||||
const INACTIVE_DAYS = 30; // Completely remove from JSON after 5 days
|
||||
const LEADERBOARD_VISIBLE_DAYS = 3; // Only show on leaderboard if active within 1 day
|
||||
const LEADERBOARD_VISIBLE_DAYS = 5; // Only show on leaderboard if active within 5 days
|
||||
|
||||
// In-memory transfer codes: Map<code, { playerId, secretToken, expiresAt }>
|
||||
const transferCodes = new Map();
|
||||
|
||||
// Strip secret fields before sending player data to clients
|
||||
function sanitizePlayer(player) {
|
||||
const p = Object.assign({}, player);
|
||||
delete p.secretToken;
|
||||
return p;
|
||||
}
|
||||
|
||||
// Remove expired transfer codes every minute
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [code, data] of transferCodes) {
|
||||
if (data.expiresAt < now) transferCodes.delete(code);
|
||||
}
|
||||
}, 60 * 1000);
|
||||
|
||||
const BOT_USER_AGENTS = [
|
||||
'googlebot', 'bingbot', 'slurp', 'duckduckbot', 'baiduspider',
|
||||
@ -64,7 +82,7 @@ app.post('/api/checkpoint', (req, res) => {
|
||||
return res.status(403).json({ error: 'Bot traffic not allowed' });
|
||||
}
|
||||
|
||||
const { playerId, gameState } = req.body;
|
||||
const { playerId, gameState, secretToken } = req.body;
|
||||
// Ignore players who haven't consumed any mass
|
||||
if (gameState.totalMassConsumedEver === 0) {
|
||||
return res.json({ success: true }); // Accept but don't save
|
||||
@ -93,9 +111,19 @@ app.post('/api/checkpoint', (req, res) => {
|
||||
const now = Date.now();
|
||||
|
||||
// Find or create player
|
||||
let code, attempts = 0;
|
||||
|
||||
let player = leaderboard.find(p => p.id === playerId);
|
||||
if (player) {
|
||||
// Update existing player - store full game state
|
||||
// Token validation with backward-compatible migration
|
||||
if (player.secretToken) {
|
||||
if (secretToken !== player.secretToken) {
|
||||
return res.status(403).json({ error: 'Invalid session token' });
|
||||
}
|
||||
} else if (secretToken) {
|
||||
player.secretToken = secretToken; // migrate: adopt token on first checkpoint
|
||||
}
|
||||
|
||||
player.gameState = gameState;
|
||||
player.mass = gameState.blackHoleTotalMass || 0;
|
||||
player.level = (gameState.asteroidUpgradeLevel || 0) +
|
||||
@ -104,12 +132,12 @@ app.post('/api/checkpoint', (req, res) => {
|
||||
(gameState.giantUpgradeLevel || 0);
|
||||
player.lastSeen = now;
|
||||
player.holeAge = now - player.firstSeen;
|
||||
|
||||
|
||||
} else {
|
||||
// New player
|
||||
const timestamp = new Date().toLocaleString();
|
||||
const timestamp = new Date().toLocaleString();
|
||||
const newPlayer = {
|
||||
id: playerId,
|
||||
secretToken: secretToken || null,
|
||||
gameState: gameState,
|
||||
mass: gameState.blackHoleTotalMass || 0,
|
||||
level: (gameState.asteroidUpgradeLevel || 0) +
|
||||
@ -120,9 +148,8 @@ app.post('/api/checkpoint', (req, res) => {
|
||||
lastSeen: now,
|
||||
holeAge: 0
|
||||
};
|
||||
|
||||
|
||||
console.log(`${timestamp} Created new player: `, newPlayer.id);
|
||||
|
||||
leaderboard.push(newPlayer);
|
||||
}
|
||||
|
||||
@ -180,7 +207,7 @@ app.get('/api/leaderboard', (req, res) => {
|
||||
saveLeaderboard(leaderboard);
|
||||
|
||||
// Return only visible players (top 50)
|
||||
res.json(visiblePlayers.slice(0, 50));
|
||||
res.json(visiblePlayers.slice(0, 50).map(sanitizePlayer));
|
||||
});
|
||||
|
||||
// Get player info (works even if not visible on leaderboard)
|
||||
@ -204,7 +231,7 @@ app.get('/api/player/:playerId', (req, res) => {
|
||||
const fiveDaysAgo = Date.now() - (LEADERBOARD_VISIBLE_DAYS * 24 * 60 * 60 * 1000);
|
||||
player.visibleOnLeaderboard = player.lastSeen > fiveDaysAgo;
|
||||
|
||||
res.json(player);
|
||||
res.json(sanitizePlayer(player));
|
||||
});
|
||||
|
||||
// Get player's full game state
|
||||
@ -272,6 +299,82 @@ setInterval(cleanupBotAccounts, 24 * 60 * 60 * 1000);
|
||||
// Run cleanup on startup too
|
||||
cleanupBotAccounts();
|
||||
|
||||
// Block direct access to leaderboard.json
|
||||
app.get('/leaderboard.json', (req, res) => {
|
||||
res.status(403).json({ error: 'Access denied' });
|
||||
});
|
||||
|
||||
// Create a transfer code for session migration
|
||||
app.post('/api/transfer/create', (req, res) => {
|
||||
const { playerId, secretToken } = req.body;
|
||||
if (!playerId || !secretToken) {
|
||||
return res.status(400).json({ error: 'Missing credentials' });
|
||||
}
|
||||
|
||||
const leaderboard = loadLeaderboard();
|
||||
const player = leaderboard.find(p => p.id === playerId);
|
||||
if (!player) {
|
||||
return res.status(404).json({ error: 'Player not found' });
|
||||
}
|
||||
|
||||
if (player.secretToken && player.secretToken !== secretToken) {
|
||||
return res.status(403).json({ error: 'Invalid session token' });
|
||||
}
|
||||
|
||||
// Return existing active code if one exists
|
||||
for (const [existingCode, data] of transferCodes) {
|
||||
if (data.playerId === playerId && data.expiresAt > Date.now()) {
|
||||
return res.json({ code: existingCode, expiresAt: data.expiresAt });
|
||||
}
|
||||
}
|
||||
|
||||
let code, attempts = 0;
|
||||
do {
|
||||
code = Math.floor(Math.random() * 0x100000).toString(16).padStart(5, '0');
|
||||
attempts++;
|
||||
} while (transferCodes.has(code) && attempts < 100);
|
||||
|
||||
const expiresAt = Date.now() + 5 * 60 * 1000;
|
||||
transferCodes.set(code, { playerId, secretToken, expiresAt });
|
||||
|
||||
const timestamp = new Date().toLocaleString();
|
||||
console.log(`${timestamp} Transfer code created for player ...${playerId.slice(-5)}`);
|
||||
|
||||
res.json({ code, expiresAt });
|
||||
});
|
||||
|
||||
// Claim a transfer code
|
||||
app.post('/api/transfer/claim', (req, res) => {
|
||||
const { code } = req.body;
|
||||
if (!code) return res.status(400).json({ error: 'Missing code' });
|
||||
|
||||
const key = code.toLowerCase();
|
||||
|
||||
// Check key
|
||||
if (!/^[0-9a-f]{5}$/.test(key)) {
|
||||
return res.status(400).json({ error: 'Invalid code format' });
|
||||
}
|
||||
|
||||
const data = transferCodes.get(key);
|
||||
|
||||
if (!data) return res.status(404).json({ error: 'Invalid or expired code' });
|
||||
|
||||
if (data.expiresAt < Date.now()) {
|
||||
transferCodes.delete(key);
|
||||
return res.status(410).json({ error: 'Code has expired' });
|
||||
}
|
||||
|
||||
transferCodes.delete(key); // one-time use
|
||||
|
||||
const timestamp = new Date().toLocaleString();
|
||||
console.log(`${timestamp} Transfer code claimed for player ...${data.playerId.slice(-5)}`);
|
||||
|
||||
res.json({
|
||||
playerId: data.playerId,
|
||||
sessionToken: data.secretToken
|
||||
});
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
const timestamp = new Date().toLocaleString();
|
||||
|
||||
@ -43,5 +43,22 @@ var Storage = {
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getOrCreateToken: function() {
|
||||
var token = this.getCookie('sessionToken');
|
||||
if (!token) {
|
||||
var arr = new Uint8Array(16);
|
||||
if (window.crypto && window.crypto.getRandomValues) {
|
||||
window.crypto.getRandomValues(arr);
|
||||
token = Array.from(arr).map(function(b) {
|
||||
return b.toString(16).padStart(2, '0');
|
||||
}).join('');
|
||||
} else {
|
||||
token = Date.now().toString(36) + Math.random().toString(36).substr(2, 16) + Math.random().toString(36).substr(2, 16);
|
||||
}
|
||||
this.setCookie('sessionToken', token, 365);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
};
|
||||
|
||||
146
js/ui.js
146
js/ui.js
@ -14,11 +14,13 @@ var UI = {
|
||||
planetLevel: document.getElementById('planet-level'),
|
||||
giantLevel: document.getElementById('giant-level'),
|
||||
mtypeLevel: document.getElementById('mtype-level'),
|
||||
ktypeLevel: document.getElementById('ktype-level'),
|
||||
asteroidUpgradeBtn: document.getElementById('asteroid-upgrade-btn'),
|
||||
cometUpgradeBtn: document.getElementById('comet-upgrade-btn'),
|
||||
planetUpgradeBtn: document.getElementById('planet-upgrade-btn'),
|
||||
giantUpgradeBtn: document.getElementById('giant-upgrade-btn'),
|
||||
mtypeUpgradeBtn: document.getElementById('mtype-upgrade-btn'),
|
||||
ktypeUpgradeBtn: document.getElementById('ktype-upgrade-btn'),
|
||||
gearIcon: document.getElementById('gear-icon'),
|
||||
settingsMenu: document.getElementById('settings-menu'),
|
||||
resetBtn: document.getElementById('reset-btn'),
|
||||
@ -105,7 +107,70 @@ var UI = {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ---------- Session Transfer ----------
|
||||
var exportBtn = document.getElementById('export-session-btn');
|
||||
var claimBtn = document.getElementById('transfer-claim-btn');
|
||||
|
||||
if (exportBtn) {
|
||||
exportBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
self.exportSession();
|
||||
});
|
||||
}
|
||||
|
||||
if (claimBtn) {
|
||||
claimBtn.addEventListener('click', async function(e) {
|
||||
e.stopPropagation();
|
||||
var input = document.getElementById('transfer-code-input');
|
||||
var isVisible = input.style.display !== 'none';
|
||||
|
||||
if (!isVisible) {
|
||||
input.style.display = 'block';
|
||||
claimBtn.classList.add('active');
|
||||
input.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
var code = input.value.trim();
|
||||
|
||||
if (!code) {
|
||||
input.style.display = 'none';
|
||||
claimBtn.classList.remove('active');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[0-9a-fA-F]{5}$/.test(code)) {
|
||||
showErrorNotification('Invalid or expired code');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var result = await Server.claimTransferCode(code.toLowerCase());
|
||||
if (!result || !result.playerId) throw new Error();
|
||||
|
||||
Storage.setCookie('playerId', result.playerId, 365);
|
||||
Storage.setCookie('sessionToken', result.sessionToken, 365);
|
||||
|
||||
showSuccessNotification('Session imported — reloading...');
|
||||
setTimeout(function() { location.reload(); }, 1500);
|
||||
|
||||
} catch (err) {
|
||||
showErrorNotification('Invalid or expired code');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var transferInput = document.getElementById('transfer-code-input');
|
||||
if (transferInput) {
|
||||
transferInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.stopPropagation();
|
||||
claimBtn.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Click outside to close ----------
|
||||
document.addEventListener('click', () => {
|
||||
// Close settings menu
|
||||
@ -139,6 +204,7 @@ var UI = {
|
||||
this.elements.planetUpgradeBtn.addEventListener('click', handlers.planet);
|
||||
this.elements.giantUpgradeBtn.addEventListener('click', handlers.giant);
|
||||
this.elements.mtypeUpgradeBtn.addEventListener('click', handlers.mtype);
|
||||
this.elements.ktypeUpgradeBtn.addEventListener('click', handlers.ktype);
|
||||
},
|
||||
|
||||
update: function(gameState, config) {
|
||||
@ -246,12 +312,21 @@ var UI = {
|
||||
this.elements.mtypeLevel.parentElement.style.display = 'none';
|
||||
}
|
||||
|
||||
// Only show ktype upgrade if unlocked
|
||||
if (gameState.ktypeUnlocked) {
|
||||
this.updateKtypeUpgrade(gameState);
|
||||
this.elements.ktypeLevel.parentElement.style.display = '';
|
||||
} else {
|
||||
this.elements.ktypeLevel.parentElement.style.display = 'none';
|
||||
}
|
||||
|
||||
var canAffordAny =
|
||||
gameState.totalMassConsumed >= gameState.asteroidUpgradeCost ||
|
||||
(gameState.cometUnlocked && gameState.totalMassConsumed >= gameState.cometUpgradeCost) ||
|
||||
(gameState.planetUnlocked && gameState.totalMassConsumed >= gameState.planetUpgradeCost) ||
|
||||
(gameState.giantUnlocked && gameState.totalMassConsumed >= gameState.giantUpgradeCost) ||
|
||||
(gameState.mtypeUnlocked && gameState.totalMassConsumed >= gameState.mtypeUpgradeCost);
|
||||
(gameState.mtypeUnlocked && gameState.totalMassConsumed >= gameState.mtypeUpgradeCost) ||
|
||||
(gameState.ktypeUnlocked && gameState.totalMassConsumed >= gameState.ktypeUpgradeCost);
|
||||
|
||||
var holeIcon = document.getElementById('hole-icon');
|
||||
if (holeIcon) {
|
||||
@ -271,7 +346,9 @@ var UI = {
|
||||
gameState.asteroidUpgradeLevel +
|
||||
gameState.cometUpgradeLevel +
|
||||
gameState.planetUpgradeLevel +
|
||||
gameState.giantUpgradeLevel;
|
||||
gameState.giantUpgradeLevel +
|
||||
gameState.mtypeUpgradeLevel +
|
||||
gameState.ktypeUpgradeLevel;
|
||||
|
||||
el.innerHTML =
|
||||
'<span class="tooltip-trigger">' +
|
||||
@ -390,6 +467,19 @@ var UI = {
|
||||
gameState.totalMassConsumed < gameState.mtypeUpgradeCost;
|
||||
},
|
||||
|
||||
updateKtypeUpgrade: function(gameState) {
|
||||
var rate = (259200000 / gameState.currentKtypeSpawnInterval).toFixed(2);
|
||||
var bonusPercent = (gameState.ktypeUpgradeLevel * 3);
|
||||
var tooltipText = 'Spawn Rate: ' + rate + '/3days <br>Bonus: ' + bonusPercent + '%';
|
||||
this.elements.ktypeLevel.innerHTML = '<span class="tooltip-trigger">K-Type: Level ' +
|
||||
gameState.ktypeUpgradeLevel +
|
||||
'<span class="tooltip">' + tooltipText + '</span></span>';
|
||||
this.elements.ktypeUpgradeBtn.textContent =
|
||||
'Upgrade (Cost: ' + this.formatMass(gameState.ktypeUpgradeCost) + ')';
|
||||
this.elements.ktypeUpgradeBtn.disabled =
|
||||
gameState.totalMassConsumed < gameState.ktypeUpgradeCost;
|
||||
},
|
||||
|
||||
formatMass: function(massKg, options = {}) {
|
||||
if (massKg == null) return '0'; // fallback
|
||||
const SOLAR_SWITCH_KG = CONFIG.SOLAR_MASS_KG * 0.01; // ~1% solar mass
|
||||
@ -543,5 +633,55 @@ var UI = {
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
|
||||
exportSession: async function() {
|
||||
var btn = document.getElementById('export-session-btn');
|
||||
var display = document.getElementById('export-code-display');
|
||||
var codeEl = document.getElementById('transfer-code-value');
|
||||
var expiryEl = document.getElementById('transfer-code-expiry');
|
||||
|
||||
if (!btn || !display || !codeEl) return;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Export';
|
||||
display.style.display = 'none';
|
||||
|
||||
try {
|
||||
var result = await Server.createTransferCode();
|
||||
if (!result || !result.code) {
|
||||
showErrorNotification(result && result.error ? result.error : 'Failed to generate transfer code');
|
||||
return;
|
||||
}
|
||||
codeEl.textContent = result.code.toUpperCase();
|
||||
display.style.display = 'block';
|
||||
btn.classList.add('menu-btn-export-active');
|
||||
|
||||
var expiresAt = result.expiresAt;
|
||||
if (this._transferTimer) clearInterval(this._transferTimer);
|
||||
var self = this;
|
||||
|
||||
var tick = function() {
|
||||
var remaining = Math.max(0, expiresAt - Date.now());
|
||||
var mins = Math.floor(remaining / 60000);
|
||||
var secs = Math.floor((remaining % 60000) / 1000);
|
||||
if (expiryEl) expiryEl.textContent = 'Expires in ' + mins + ':' + (secs < 10 ? '0' : '') + secs;
|
||||
if (remaining === 0) {
|
||||
clearInterval(self._transferTimer);
|
||||
if (codeEl) codeEl.textContent = 'EXPIRED';
|
||||
if (expiryEl) expiryEl.textContent = '';
|
||||
btn.classList.remove('menu-btn-export-active');
|
||||
}
|
||||
};
|
||||
|
||||
tick(); // run immediately
|
||||
this._transferTimer = setInterval(tick, 1000);
|
||||
|
||||
} catch (e) {
|
||||
showErrorNotification('Failed to generate transfer code');
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Export';
|
||||
}
|
||||
};
|
||||
|
||||
122
style.css
122
style.css
@ -125,9 +125,6 @@ canvas {
|
||||
}
|
||||
}
|
||||
|
||||
#hole-icon.pulse:hover {
|
||||
}
|
||||
|
||||
#gear-icon:hover,
|
||||
.score-icon:hover,
|
||||
#hole-icon:hover {
|
||||
@ -183,7 +180,7 @@ canvas {
|
||||
}
|
||||
|
||||
.menu-btn.danger:hover {
|
||||
background: rgba(255, 0, 0, 0.42);
|
||||
background: rgba(255, 0, 0, 0.24);
|
||||
border-color: rgba(255, 0, 0, 0.69);
|
||||
}
|
||||
|
||||
@ -419,3 +416,120 @@ canvas {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* SESSION TRANSFER */
|
||||
.menu-divider {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin: 14px 0;
|
||||
}
|
||||
|
||||
.menu-subtitle {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.transfer-warning {
|
||||
color: rgba(255, 200, 100, 0.9);
|
||||
font-size: 10px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.transfer-code-box {
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
cursor: text;
|
||||
text-align: center;
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
|
||||
.transfer-code {
|
||||
font-size: 26px;
|
||||
letter-spacing: 7px;
|
||||
color: rgba(0, 255, 255, 0.69);
|
||||
}
|
||||
|
||||
.transfer-expiry {
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
text-align: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.transfer-input-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-btn-export {
|
||||
background: rgba(60, 60, 80, 0.5);
|
||||
border: 1px solid rgba(255, 192, 0, 0.42);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
padding: 6px 12px;
|
||||
margin-top: 8px;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.menu-btn-export:hover {
|
||||
background: rgba(255, 192, 0, 0.24);
|
||||
border-color: rgba(255, 192, 0, 0.69);
|
||||
}
|
||||
|
||||
.menu-btn-export-active {
|
||||
background: rgba(255, 192, 0, 0.13);
|
||||
border-color: rgba(255, 192, 0, 0.42);
|
||||
}
|
||||
|
||||
.menu-btn-import {
|
||||
background: rgba(60, 60, 80, 0.5);
|
||||
border: 1px solid rgba(0, 255, 255, 0.42);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
padding: 6px 12px;
|
||||
margin-top: 8px;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
transition: all 0.2s;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.menu-btn-import:hover {
|
||||
background: rgba(0, 255, 255, 0.24);
|
||||
border-color: rgba(0, 255, 255, 0.69);
|
||||
}
|
||||
|
||||
.menu-btn-import.active {
|
||||
background: rgba(0, 255, 255, 0.13);
|
||||
border-color: rgba(0, 255, 255, 0.42);
|
||||
}
|
||||
|
||||
.transfer-input {
|
||||
flex: 1;
|
||||
background: rgba(30, 30, 50, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
padding: 6px 8px;
|
||||
font-size: 15px;
|
||||
letter-spacing: 3px;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.transfer-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.transfer-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(0, 255, 255, 0.42);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user