loel
This commit is contained in:
195
admin.css
Normal file
195
admin.css
Normal file
@ -0,0 +1,195 @@
|
||||
/* Admin Panel Styles */
|
||||
#admin-panel {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 320px;
|
||||
max-height: calc(100vh - 40px);
|
||||
background: rgba(15, 15, 25, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#admin-panel.collapsed #admin-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: rgba(30, 30, 50, 0.95);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.admin-header h2 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: rgba(100, 200, 255, 0.9);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
#admin-toggle {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
#admin-toggle:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
#admin-content {
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.admin-section {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.admin-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.admin-section h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 12px;
|
||||
color: rgba(200, 200, 220, 0.9);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.admin-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.admin-buttons:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.admin-buttons button {
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
background: rgba(60, 60, 80, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 10px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.admin-buttons button:hover {
|
||||
background: rgba(80, 80, 100, 0.8);
|
||||
color: rgba(255, 255, 255, 1);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.admin-buttons button:active {
|
||||
background: rgba(100, 100, 120, 0.8);
|
||||
}
|
||||
|
||||
.admin-input-group {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-input-group label {
|
||||
min-width: 70px;
|
||||
color: rgba(200, 200, 220, 0.8);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.admin-input-group 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-family: 'Courier New', monospace;
|
||||
font-size: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.admin-input-group input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(100, 200, 255, 0.6);
|
||||
}
|
||||
|
||||
.admin-input-group button {
|
||||
min-width: 50px;
|
||||
background: rgba(60, 60, 80, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 10px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.admin-input-group button:hover {
|
||||
background: rgba(80, 80, 100, 0.8);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.admin-info {
|
||||
background: rgba(20, 20, 30, 0.6);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.admin-info div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: rgba(180, 180, 200, 0.9);
|
||||
}
|
||||
|
||||
.admin-info span {
|
||||
color: rgba(100, 200, 255, 0.9);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
#admin-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
#admin-content::-webkit-scrollbar-track {
|
||||
background: rgba(20, 20, 30, 0.5);
|
||||
}
|
||||
|
||||
#admin-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(100, 100, 120, 0.6);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#admin-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(120, 120, 140, 0.8);
|
||||
}
|
||||
136
admin.html
Normal file
136
admin.html
Normal file
@ -0,0 +1,136 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>hoel - Admin</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Admin Panel -->
|
||||
<div id="admin-panel">
|
||||
<div class="admin-header">
|
||||
<h2>ADMIN PANEL</h2>
|
||||
<button id="admin-toggle" title="Toggle Panel">▼</button>
|
||||
</div>
|
||||
|
||||
<div id="admin-content">
|
||||
<!-- Spawn Objects -->
|
||||
<div class="admin-section">
|
||||
<h3>Spawn Objects</h3>
|
||||
<div class="admin-buttons">
|
||||
<button onclick="adminSpawn('small')">Small Asteroid</button>
|
||||
<button onclick="adminSpawn('medium')">Medium Asteroid</button>
|
||||
<button onclick="adminSpawn('large')">Large Asteroid</button>
|
||||
<button onclick="adminSpawn('comet')">Comet</button>
|
||||
<button onclick="adminSpawn('planet')">Planet</button>
|
||||
<button onclick="adminSpawn('giant')">Giant</button>
|
||||
<button onclick="adminSpawn('mtype')">M-Type</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Set Levels -->
|
||||
<div class="admin-section">
|
||||
<h3>Set Levels</h3>
|
||||
<div class="admin-input-group">
|
||||
<label>Asteroids:</label>
|
||||
<input type="number" id="admin-asteroid-level" min="0" value="0">
|
||||
<button onclick="adminSetLevel('asteroid')">Set</button>
|
||||
</div>
|
||||
<div class="admin-input-group">
|
||||
<label>Comets:</label>
|
||||
<input type="number" id="admin-comet-level" min="0" value="0">
|
||||
<button onclick="adminSetLevel('comet')">Set</button>
|
||||
</div>
|
||||
<div class="admin-input-group">
|
||||
<label>Planets:</label>
|
||||
<input type="number" id="admin-planet-level" min="0" value="0">
|
||||
<button onclick="adminSetLevel('planet')">Set</button>
|
||||
</div>
|
||||
<div class="admin-input-group">
|
||||
<label>Giants:</label>
|
||||
<input type="number" id="admin-giant-level" min="0" value="0">
|
||||
<button onclick="adminSetLevel('giant')">Set</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Mass -->
|
||||
<div class="admin-section">
|
||||
<h3>Add Mass</h3>
|
||||
<div class="admin-input-group">
|
||||
<input type="number" id="admin-mass-input" placeholder="Amount in kg" value="1000000">
|
||||
<button onclick="adminAddMass()">Add Mass</button>
|
||||
</div>
|
||||
<div class="admin-buttons">
|
||||
<button onclick="adminAddMass(1e9)">+1 Gt</button>
|
||||
<button onclick="adminAddMass(1e12)">+1 Tt</button>
|
||||
<button onclick="adminAddMass(1e15)">+1 Pt</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notifications -->
|
||||
<div class="admin-section">
|
||||
<h3>Test Notifications</h3>
|
||||
<div class="admin-buttons">
|
||||
<button onclick="showUnlockNotification('comet')">Unlock: Comet</button>
|
||||
<button onclick="showUnlockNotification('planet')">Unlock: Planet</button>
|
||||
<button onclick="showUnlockNotification('giant')">Unlock: Giant</button>
|
||||
</div>
|
||||
<div class="admin-buttons">
|
||||
<button onclick="showOfflineNotification('5 minutes', '1.23 Tt')">Offline Progress</button>
|
||||
<button onclick="showErrorNotification('Test error message')">Error</button>
|
||||
<button onclick="showSuccessNotification('Test success message')">Success</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unlocks -->
|
||||
<div class="admin-section">
|
||||
<h3>Toggle Unlocks</h3>
|
||||
<div class="admin-buttons">
|
||||
<button onclick="adminToggleUnlock('comet')">Toggle Comets</button>
|
||||
<button onclick="adminToggleUnlock('planet')">Toggle Planets</button>
|
||||
<button onclick="adminToggleUnlock('giant')">Toggle Giants</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game State -->
|
||||
<div class="admin-section">
|
||||
<h3>Game State</h3>
|
||||
<div class="admin-buttons">
|
||||
<button onclick="adminSaveGame()">Save Now</button>
|
||||
<button onclick="adminLoadGame()">Reload State</button>
|
||||
<button onclick="adminResetGame()">Reset Game</button>
|
||||
</div>
|
||||
<div class="admin-buttons">
|
||||
<button onclick="adminExportState()">Export JSON</button>
|
||||
<button onclick="adminImportState()">Import JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Display -->
|
||||
<div class="admin-section">
|
||||
<h3>Current State</h3>
|
||||
<div id="admin-info" class="admin-info">
|
||||
<div>Player ID: <span id="admin-player-id">-</span></div>
|
||||
<div>Mass: <span id="admin-mass">-</span></div>
|
||||
<div>Total Level: <span id="admin-total-level">-</span></div>
|
||||
<div>Asteroids: <span id="admin-asteroids">-</span></div>
|
||||
<div>Active Objects: <span id="admin-objects">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Canvas -->
|
||||
<canvas id="space"></canvas>
|
||||
|
||||
<!-- Same scripts as index.html -->
|
||||
<script src="js/config.js"></script>
|
||||
<script src="js/helpers.js"></script>
|
||||
<script src="js/entities.js"></script>
|
||||
<script src="js/storage.js"></script>
|
||||
<script src="js/admin.js"></script>
|
||||
<script src="js/game.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
favicon.png
Normal file
BIN
favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
40
index.html
40
index.html
@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>hoel</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="icon" href="favicon.png" type="image/png">
|
||||
</head>
|
||||
<body>
|
||||
<svg id="gear-icon" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1">
|
||||
@ -40,22 +41,28 @@ a1.7 1.7 0 0 0-1.5 1z"></path>
|
||||
<div class="menu-title">Game Settings</div>
|
||||
<div class="menu-text">
|
||||
This game uses a browser cookie to save your progress across sessions.
|
||||
The cookie stores your black hole mass, consumed resources, upgrade levels and spawn timers.
|
||||
By using this website, you consent to storing a cookie on your device.
|
||||
</div>
|
||||
<div class="menu-text">
|
||||
By using this website, you consent to storing a cookie on your device.
|
||||
The game periodically sends parts of 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>
|
||||
<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">
|
||||
<div id="total-mass"></div>
|
||||
</br>
|
||||
<div id="total-level" class="stat-line"></div>
|
||||
<div id="total-mass"></div>
|
||||
<br>
|
||||
<div id="active-objects"></div>
|
||||
<span id="massRate"></span>
|
||||
</div>
|
||||
|
||||
<div id="upgrade-panel">
|
||||
<div class="menu-title">Upgrades</div>
|
||||
<div id="total-level" class="stat-line"></div>
|
||||
<div class="upgrade-row">
|
||||
</br>
|
||||
<div id="spendable-mass">Available to Spend: 0 kg</div>
|
||||
@ -75,6 +82,10 @@ a1.7 1.7 0 0 0-1.5 1z"></path>
|
||||
<div id="giant-level"></div>
|
||||
<button id="giant-upgrade-btn" class="upgrade-btn" disabled>Upgrade (Cost: 1000 tonnes)</button>
|
||||
</div>
|
||||
<div class="upgrade-row">
|
||||
<div id="mtype-level"></div>
|
||||
<button id="mtype-upgrade-btn" class="upgrade-btn" disabled>Upgrade</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaderboard Toggle -->
|
||||
@ -106,16 +117,23 @@ a1.7 1.7 0 0 0-1.5 1z"></path>
|
||||
<!-- Leaderboard Panel -->
|
||||
<div id="leaderboardPanel" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="leaderboard-title">
|
||||
<span class="rank" style="width:20px;"></span>
|
||||
<span class="name" style="flex:1; min-width:80px;">Name</span>
|
||||
<span id="sortMass" class="value mass" style="min-width: 115px; cursor:pointer">Mass</span>
|
||||
<span id="sortAge" class="value age" style="padding-right: 30px; cursor:pointer">Age</span>
|
||||
</div>
|
||||
<div class="menu-title">Neighbour Chart</div>
|
||||
<div class="leaderboard-title" style="">
|
||||
<span class="rank"></span>
|
||||
<span class="name">Name</span>
|
||||
<span class="value last-seen">Seen</span>
|
||||
<span id="sortMass" class="value mass">Mass</span>
|
||||
<span id="sortAge" class="value age">Age</span>
|
||||
</div>
|
||||
<div id="leaderboardList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svg id="hole-icon" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="7" stroke="white" stroke-width="1" fill="none" />
|
||||
<line x1="0" y1="12" x2="24" y2="12" stroke="white" stroke-width="1" />
|
||||
</svg>
|
||||
|
||||
<canvas id="space"></canvas>
|
||||
|
||||
<script src="js/config.js"></script>
|
||||
|
||||
309
js/admin.js
Normal file
309
js/admin.js
Normal file
@ -0,0 +1,309 @@
|
||||
// ===== Admin Mode Setup =====
|
||||
|
||||
// Admin mode flags
|
||||
window.isAdminMode = true;
|
||||
window.adminPlayerId = 'admin_dev';
|
||||
|
||||
// Create complete UI replacement
|
||||
window.UI = {
|
||||
elements: {},
|
||||
|
||||
init: function() {
|
||||
console.log('Admin: Using stub UI');
|
||||
},
|
||||
|
||||
setUpgradeHandlers: function(handlers) {
|
||||
// Store handlers for admin to call
|
||||
window.adminUpgradeHandlers = handlers;
|
||||
},
|
||||
|
||||
update: function(state, config) {
|
||||
// Update admin info display if it exists
|
||||
if (document.getElementById('admin-mass')) {
|
||||
updateAdminInfo();
|
||||
}
|
||||
},
|
||||
|
||||
formatMass: function(mass, options) {
|
||||
if (options && options.forceSolarMass) {
|
||||
var solarMasses = mass / CONFIG.SOLAR_MASS_KG;
|
||||
return solarMasses.toFixed(3) + ' M☉';
|
||||
}
|
||||
if (mass >= 1e18) return (mass / 1e18).toFixed(2) + ' Et';
|
||||
if (mass >= 1e15) return (mass / 1e15).toFixed(2) + ' Pt';
|
||||
if (mass >= 1e12) return (mass / 1e12).toFixed(2) + ' Tt';
|
||||
if (mass >= 1e9) return (mass / 1e9).toFixed(2) + ' Gt';
|
||||
if (mass >= 1e6) return (mass / 1e6).toFixed(2) + ' Mt';
|
||||
if (mass >= 1e3) return (mass / 1e3).toFixed(2) + ' tonnes';
|
||||
return mass.toFixed(0) + ' kg';
|
||||
},
|
||||
|
||||
updateMassDisplay: function() {},
|
||||
updateUpgradeDisplay: function() {},
|
||||
updateTotalLevel: function() {},
|
||||
formatTime: function(ms) {
|
||||
var seconds = Math.floor(ms / 1000);
|
||||
var minutes = Math.floor(seconds / 60);
|
||||
var hours = Math.floor(minutes / 60);
|
||||
var days = Math.floor(hours / 24);
|
||||
if (days > 0) return days + 'd ' + (hours % 24) + 'h';
|
||||
if (hours > 0) return hours + 'h ' + (minutes % 60) + 'm';
|
||||
if (minutes > 0) return minutes + 'm';
|
||||
return seconds + 's';
|
||||
},
|
||||
formatAge: function(ms) {
|
||||
var days = Math.floor(ms / (24 * 60 * 60 * 1000));
|
||||
var hours = Math.floor((ms % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000));
|
||||
if (days > 0) return days + 'd ' + hours + 'h old';
|
||||
if (hours > 0) return hours + 'h old';
|
||||
return 'New';
|
||||
}
|
||||
};
|
||||
window.UI.updateActiveObjects = function() {
|
||||
if (window.gameAsteroids) {
|
||||
var el = document.getElementById('admin-objects');
|
||||
if (el) el.textContent = window.gameAsteroids.length;
|
||||
}
|
||||
};
|
||||
|
||||
// ===== Wait for game to load =====
|
||||
window.addEventListener('load', function() {
|
||||
|
||||
// Override Server.init
|
||||
if (window.Server) {
|
||||
var originalInit = Server.init;
|
||||
Server.init = function() {
|
||||
this.playerId = window.adminPlayerId;
|
||||
Storage.setCookie('playerId', this.playerId, 365);
|
||||
console.log('Admin mode: Player ID =', this.playerId);
|
||||
}
|
||||
// Override checkpoint to do nothing
|
||||
Server.checkpoint = function() {
|
||||
console.log('Admin: Checkpoint skipped');
|
||||
return Promise.resolve({ success: true });
|
||||
};
|
||||
}
|
||||
|
||||
// Toggle admin panel
|
||||
var toggleBtn = document.getElementById('admin-toggle');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', function() {
|
||||
var panel = document.getElementById('admin-panel');
|
||||
panel.classList.toggle('collapsed');
|
||||
this.textContent = panel.classList.contains('collapsed') ? '▲' : '▼';
|
||||
});
|
||||
}
|
||||
|
||||
console.log('%c[ADMIN MODE ENABLED]', 'color: #64c8ff; font-weight: bold; font-size: 14px;');
|
||||
});
|
||||
|
||||
// ===== Admin Functions =====
|
||||
|
||||
// Spawn objects
|
||||
function adminSpawn(type) {
|
||||
if (!window.Game || !Game.getState) {
|
||||
console.error('Game not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
var canvas = document.getElementById('space');
|
||||
var blackHole = window.gameBlackHole;
|
||||
|
||||
if (!blackHole) {
|
||||
console.error('Black hole not available');
|
||||
return;
|
||||
}
|
||||
|
||||
window.gameAsteroids.push(new Asteroid(type, blackHole, canvas));
|
||||
console.log('Spawned:', type);
|
||||
}
|
||||
|
||||
// Set upgrade levels
|
||||
function adminSetLevel(upgradeType) {
|
||||
if (!window.Game || !Game.getState) return;
|
||||
|
||||
var state = Game.getState();
|
||||
var inputId = 'admin-' + upgradeType + '-level';
|
||||
var input = document.getElementById(inputId);
|
||||
if (!input) return;
|
||||
|
||||
var level = parseInt(input.value) || 0;
|
||||
|
||||
switch(upgradeType) {
|
||||
case 'asteroid':
|
||||
state.asteroidUpgradeLevel = level;
|
||||
state.asteroidUpgradeCost = Math.floor(
|
||||
CONFIG.ASTEROID_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_ASTER, level)
|
||||
);
|
||||
if (level >= 25) state.cometUnlocked = true;
|
||||
break;
|
||||
case 'comet':
|
||||
state.cometUpgradeLevel = level;
|
||||
state.cometUpgradeCost = Math.floor(
|
||||
CONFIG.COMET_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER, level)
|
||||
);
|
||||
if (level >= 20) state.planetUnlocked = true;
|
||||
break;
|
||||
case 'planet':
|
||||
state.planetUpgradeLevel = level;
|
||||
state.planetUpgradeCost = Math.floor(
|
||||
CONFIG.PLANET_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER, level)
|
||||
);
|
||||
if (level >= 15) state.giantUnlocked = true;
|
||||
break;
|
||||
case 'giant':
|
||||
state.giantUpgradeLevel = level;
|
||||
state.giantUpgradeCost = Math.floor(
|
||||
CONFIG.GIANT_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER, level)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
UI.update(state, CONFIG);
|
||||
console.log('Set', upgradeType, 'level to:', level);
|
||||
}
|
||||
|
||||
// Add mass
|
||||
function adminAddMass(amount) {
|
||||
if (!window.Game || !Game.getState) return;
|
||||
|
||||
var state = Game.getState();
|
||||
|
||||
if (!amount) {
|
||||
var input = document.getElementById('admin-mass-input');
|
||||
amount = input ? (parseFloat(input.value) || 0) : 0;
|
||||
}
|
||||
|
||||
state.blackHoleTotalMass += amount;
|
||||
state.totalMassConsumedEver += amount;
|
||||
state.totalMassConsumed += amount;
|
||||
|
||||
// Update black hole size
|
||||
var solarMasses = state.blackHoleTotalMass / CONFIG.SOLAR_MASS_KG;
|
||||
if (window.gameBlackHole) {
|
||||
window.gameBlackHole.radius = CONFIG.INITIAL_BLACK_HOLE_RADIUS +
|
||||
(solarMasses - CONFIG.INITIAL_BLACK_HOLE_MASS);
|
||||
}
|
||||
|
||||
UI.update(state, CONFIG);
|
||||
console.log('Added mass:', UI.formatMass(amount));
|
||||
}
|
||||
|
||||
// Toggle unlocks
|
||||
function adminToggleUnlock(type) {
|
||||
if (!window.Game || !Game.getState) return;
|
||||
|
||||
var state = Game.getState();
|
||||
|
||||
switch(type) {
|
||||
case 'comet':
|
||||
state.cometUnlocked = !state.cometUnlocked;
|
||||
break;
|
||||
case 'planet':
|
||||
state.planetUnlocked = !state.planetUnlocked;
|
||||
break;
|
||||
case 'giant':
|
||||
state.giantUnlocked = !state.giantUnlocked;
|
||||
break;
|
||||
}
|
||||
|
||||
UI.update(state, CONFIG);
|
||||
console.log(type + ' unlocked:', state[type + 'Unlocked']);
|
||||
}
|
||||
|
||||
// Game state management
|
||||
function adminSaveGame() {
|
||||
if (!window.Game || !Game.getState || !window.Server) return;
|
||||
Server.checkpoint(Game.getState());
|
||||
console.log('Game saved');
|
||||
showSuccessNotification('Game saved');
|
||||
}
|
||||
|
||||
function adminLoadGame() {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
function adminResetGame() {
|
||||
if (confirm('Reset game state? This will delete all progress.')) {
|
||||
Storage.resetGame();
|
||||
}
|
||||
}
|
||||
|
||||
function adminExportState() {
|
||||
if (!window.Game || !Game.getState) return;
|
||||
|
||||
var state = Game.getState();
|
||||
var json = JSON.stringify(state, null, 2);
|
||||
|
||||
var blob = new Blob([json], { type: 'application/json' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'game-state-' + Date.now() + '.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
console.log('Exported game state');
|
||||
}
|
||||
|
||||
function adminImportState() {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'application/json';
|
||||
|
||||
input.onchange = function(e) {
|
||||
var file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
try {
|
||||
var importedState = JSON.parse(event.target.result);
|
||||
var currentState = Game.getState();
|
||||
|
||||
for (var key in importedState) {
|
||||
currentState[key] = importedState[key];
|
||||
}
|
||||
|
||||
UI.update(currentState, CONFIG);
|
||||
console.log('Imported game state');
|
||||
showSuccessNotification('State imported');
|
||||
} catch (err) {
|
||||
console.error('Failed to import:', err);
|
||||
showErrorNotification('Invalid JSON file');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
input.click();
|
||||
}
|
||||
|
||||
// Update admin info display
|
||||
function updateAdminInfo() {
|
||||
if (!window.Game || !Game.getState) return;
|
||||
|
||||
var state = Game.getState();
|
||||
|
||||
var el = document.getElementById('admin-player-id');
|
||||
if (el && window.Server) el.textContent = Server.playerId;
|
||||
|
||||
el = document.getElementById('admin-mass');
|
||||
if (el) el.textContent = UI.formatMass(state.blackHoleTotalMass, {forceSolarMass: true});
|
||||
|
||||
el = document.getElementById('admin-total-level');
|
||||
if (el) {
|
||||
el.textContent = state.asteroidUpgradeLevel + state.cometUpgradeLevel +
|
||||
state.planetUpgradeLevel + state.giantUpgradeLevel;
|
||||
}
|
||||
|
||||
el = document.getElementById('admin-asteroids');
|
||||
if (el) el.textContent = state.asteroidSpawnCount;
|
||||
|
||||
el = document.getElementById('admin-objects');
|
||||
if (el) el.textContent = window.gameAsteroids ? window.gameAsteroids.length : 0;
|
||||
}
|
||||
|
||||
// Update info every second
|
||||
setInterval(updateAdminInfo, 1000);
|
||||
setTimeout(updateAdminInfo, 500);
|
||||
53
js/config.js
53
js/config.js
@ -6,7 +6,7 @@ var CONFIG = {
|
||||
|
||||
// Initial values
|
||||
INITIAL_BLACK_HOLE_MASS: 42, // in solar masses
|
||||
INITIAL_BLACK_HOLE_RADIUS: 42,
|
||||
INITIAL_BLACK_HOLE_RADIUS: 21,
|
||||
|
||||
// Performance
|
||||
LOGIC_INTERVAL: 10,
|
||||
@ -20,6 +20,7 @@ var CONFIG = {
|
||||
BASE_COMET_SPAWN_INTERVAL: 120000, // 2 minutes
|
||||
BASE_PLANET_SPAWN_INTERVAL: 3600000, // 1 hour
|
||||
BASE_GIANT_SPAWN_INTERVAL: 21600000, // 6 hours
|
||||
ASE_MTYPE_SPAWN_INTERVAL: 86400000, // 1 day
|
||||
|
||||
// Rate tracking windows (in milliseconds)
|
||||
RATE_WINDOWS: {
|
||||
@ -29,15 +30,24 @@ var CONFIG = {
|
||||
},
|
||||
|
||||
// Upgrade costs (in kg)
|
||||
ASTEROID_BASE_COST: 1e3,
|
||||
COMET_BASE_COST: 1e4,
|
||||
PLANET_BASE_COST: 1e5,
|
||||
GIANT_BASE_COST: 1e6,
|
||||
ASTEROID_BASE_COST: 1e5,
|
||||
COMET_BASE_COST: 1e7,
|
||||
PLANET_BASE_COST: 1e24,
|
||||
GIANT_BASE_COST: 1e27,
|
||||
MTYPE_BASE_COST: 1e28,
|
||||
|
||||
// Upgrade scaling
|
||||
UPGRADE_COST_MULTIPLIER: 1.5,
|
||||
UPGRADE_COST_MULTIPLIER_ASTER: 1.25,
|
||||
UPGRADE_BONUS_PER_LEVEL: 0.1, // 10% per level
|
||||
UPGRADE_COST_MULTIPLIER_COMET: 1.5,
|
||||
UPGRADE_COST_MULTIPLIER_PLANT: 1.75,
|
||||
UPGRADE_COST_MULTIPLIER_GIANT: 2,
|
||||
UPGRADE_COST_MULTIPLIER_MTYPE: 2.1,
|
||||
|
||||
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,
|
||||
|
||||
// Asteroid spawn patterns
|
||||
ASTEROID_SPAWN_PATTERNS: {
|
||||
@ -46,12 +56,13 @@ var CONFIG = {
|
||||
},
|
||||
|
||||
ASTEROID_MASS_RANGES: {
|
||||
small: [1e3, 5e3], // 1-5 t
|
||||
medium: [5e3, 5e4], // 5-50 t
|
||||
large: [5e4, 5e5], // 50-500 t
|
||||
comet: [1e6, 5e7], // 1000-50000 t
|
||||
planet: [1e12, 1e13], // Moons / small planets
|
||||
giant: [5e13, 5e14] // Gas giants / huge bodies
|
||||
small: [1e3, 1e4], // 1-10 t
|
||||
medium: [1e4, 1e5], // 10-100 t
|
||||
large: [1e5, 1e6], // 100-1000 t
|
||||
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☉
|
||||
},
|
||||
|
||||
// Planet color schemes
|
||||
@ -81,6 +92,22 @@ var CONFIG = {
|
||||
'#d0e0e3',
|
||||
'#f4cccc'
|
||||
],
|
||||
// M-Type dwarf star color schemes
|
||||
MTYPE_COLORS: [
|
||||
{ light: '#e8734a', mid: '#c4522e', dark: '#8b3a1f' }, // Orange-red
|
||||
{ light: '#d4602a', mid: '#a84520', dark: '#7a3015' }, // Deep orange
|
||||
{ light: '#c45a35', mid: '#9e4428', dark: '#6b2e1a' }, // Burnt orange
|
||||
{ light: '#b8533d', mid: '#934030', dark: '#652a1e' }, // Brownish red
|
||||
{ light: '#a84e3a', mid: '#854030', dark: '#5c2c1f' } // Dark brown-red
|
||||
],
|
||||
|
||||
// M-Type spot colors (dark brownish spots)
|
||||
MTYPE_SPOT_COLORS: [
|
||||
'rgba(60, 35, 20, 0.6)',
|
||||
'rgba(50, 30, 18, 0.5)',
|
||||
'rgba(70, 40, 22, 0.55)',
|
||||
'rgba(45, 28, 15, 0.5)'
|
||||
],
|
||||
|
||||
// Star color distribution
|
||||
STAR_COLORS: [
|
||||
|
||||
364
js/entities.js
364
js/entities.js
@ -68,9 +68,39 @@ function Asteroid(type, blackHole, canvas) {
|
||||
Asteroid.prototype.initializeTypeSpecificProperties = function() {
|
||||
// Visual size (unchanged)
|
||||
if (this.type === 'comet') {
|
||||
this.size = 5 + Math.random() * 3;
|
||||
this.size = 3 + Math.random() * 3;
|
||||
} else if (this.type === 'mtype') {
|
||||
this.size = 15 + Math.random() * 3;
|
||||
this.orbitSpeed *= 0.2;
|
||||
this.decayRate *= 0.3;
|
||||
this.planetColors = CONFIG.MTYPE_COLORS[Math.floor(Math.random() * CONFIG.MTYPE_COLORS.length)];
|
||||
|
||||
// Generate spots (dark brownish surface features)
|
||||
this.spots = [];
|
||||
var spotCount = 0 + Math.floor(Math.random() * 9); // 0-9 spots
|
||||
for (var i = 0; i < spotCount; i++) {
|
||||
var angle = Math.random() * Math.PI * 2;
|
||||
var dist = Math.random() * this.size * 0.7;
|
||||
this.spots.push({
|
||||
x: Math.cos(angle) * dist,
|
||||
y: Math.sin(angle) * dist,
|
||||
radius: 1 + Math.random() * (this.size * 0.3),
|
||||
color: CONFIG.MTYPE_SPOT_COLORS[Math.floor(Math.random() * CONFIG.MTYPE_SPOT_COLORS.length)]
|
||||
});
|
||||
}
|
||||
// Generate sparkles
|
||||
this.sparkles = [];
|
||||
for (var s = 0; s < 20; s++) { // more sparkles if you want
|
||||
var angle = Math.random() * Math.PI * 2;
|
||||
var radius = Math.random() * this.size * 0.9;
|
||||
this.sparkles.push({
|
||||
x: Math.cos(angle) * radius,
|
||||
y: Math.sin(angle) * radius,
|
||||
phase: Math.random() * Math.PI * 2 // random phase for variety
|
||||
});
|
||||
}
|
||||
} else if (this.type === 'giant') {
|
||||
this.size = 15 + Math.random() * 10;
|
||||
this.size = 10 + Math.random() * 5;
|
||||
this.orbitSpeed *= 0.3;
|
||||
this.decayRate *= 0.4;
|
||||
this.planetColors = CONFIG.GIANT_COLORS[Math.floor(Math.random() * CONFIG.GIANT_COLORS.length)];
|
||||
@ -86,16 +116,16 @@ Asteroid.prototype.initializeTypeSpecificProperties = function() {
|
||||
});
|
||||
}
|
||||
} else if (this.type === 'planet') {
|
||||
this.size = 10 + Math.random() * 5;
|
||||
this.size = 6 + Math.random() * 4;
|
||||
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;
|
||||
this.size = 3 + Math.random() * 1;
|
||||
} else if (this.type === 'medium') {
|
||||
this.size = 3 + Math.random() * 2;
|
||||
this.size = 2 + Math.random() * 1;
|
||||
} else {
|
||||
this.size = 1 + Math.random() * 2;
|
||||
this.size = 1 + Math.random() * 1;
|
||||
}
|
||||
|
||||
// Physical mass (kg)
|
||||
@ -114,6 +144,8 @@ Asteroid.prototype.update = function() {
|
||||
Asteroid.prototype.draw = function(ctx) {
|
||||
if (this.type === 'comet') {
|
||||
this.drawComet(ctx);
|
||||
} else if (this.type === 'mtype') {
|
||||
this.drawMType(ctx, performance.now() * 0.002);
|
||||
} else if (this.type === 'giant') {
|
||||
// Initialize per-giant tilt if not already done
|
||||
if (this._ringTiltBase === undefined) {
|
||||
@ -175,68 +207,6 @@ Asteroid.prototype.draw = function(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;
|
||||
@ -272,6 +242,112 @@ Asteroid.prototype.drawComet = function(ctx) {
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
Asteroid.prototype.drawMType = function(ctx, time) {
|
||||
var massRange = CONFIG.ASTEROID_MASS_RANGES.mtype;
|
||||
var massNormalized = (this.massKg - massRange[0]) / (massRange[1] - massRange[0]); // 0-1
|
||||
|
||||
// Pulsing multiplier for glow
|
||||
var pulse = 0.2 * Math.sin(time * 2 + this.massKg) + 1;
|
||||
|
||||
// Glow
|
||||
var glowIntensity = (0.5 + massNormalized * 0.8) * pulse;
|
||||
var glowSize = this.size * (1.5 + massNormalized * 0.8) * pulse;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(this.x, this.y);
|
||||
|
||||
// Outer glow (soft)
|
||||
var outerGlow = ctx.createRadialGradient(0, 0, this.size * 0.5, 0, 0, glowSize);
|
||||
outerGlow.addColorStop(0, 'rgba(255, 200, 150,' + (glowIntensity * 0.4) + ')');
|
||||
outerGlow.addColorStop(0.5, 'rgba(200, 120, 80,' + (glowIntensity * 0.2) + ')');
|
||||
outerGlow.addColorStop(1, 'rgba(140, 80, 50, 0)');
|
||||
ctx.fillStyle = outerGlow;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, glowSize, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Main body
|
||||
var bodyGradient = ctx.createRadialGradient(
|
||||
-this.size * 0.1, -this.size * 0.1, 0,
|
||||
0, 0, this.size
|
||||
);
|
||||
bodyGradient.addColorStop(0, this.planetColors.light);
|
||||
bodyGradient.addColorStop(0.5, this.planetColors.mid);
|
||||
bodyGradient.addColorStop(1, this.planetColors.dark);
|
||||
ctx.fillStyle = bodyGradient;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Gritty surface texture (tiny semi-random dots)
|
||||
for (var i = 0; i < this.size * 4; i++) {
|
||||
ctx.fillStyle = 'rgba(0,0,0,' + (Math.random() * 0.05) + ')';
|
||||
var rx = (Math.random() - 0.5) * this.size * 2;
|
||||
var ry = (Math.random() - 0.5) * this.size * 2;
|
||||
if (rx*rx + ry*ry <= this.size*this.size) ctx.fillRect(rx, ry, 1, 1);
|
||||
}
|
||||
|
||||
// Swirling bands (subtle, irregular)
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0.06)';
|
||||
ctx.lineWidth = 1;
|
||||
var amplitude = this.size * 0.06;
|
||||
var frequency = 0.25;
|
||||
for (var band = -this.size * 0.7; band < this.size * 0.7; band += this.size * 0.15) {
|
||||
ctx.beginPath();
|
||||
for (var x = -this.size; x <= this.size; x += 1) {
|
||||
var y = band + Math.sin(x * frequency + band + time) * amplitude
|
||||
+ (Math.random()-0.5) * 1.5; // gritty randomness
|
||||
if (x === -this.size) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Spots
|
||||
for (var i = 0; i < this.spots.length; i++) {
|
||||
var spot = this.spots[i];
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
|
||||
ctx.clip();
|
||||
|
||||
ctx.fillStyle = spot.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(spot.x, spot.y, spot.radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Inner core glow
|
||||
var coreGlow = ctx.createRadialGradient(0, 0, 0, 0, 0, this.size * 0.5 * pulse);
|
||||
coreGlow.addColorStop(0, 'rgba(255, 230, 160,' + (glowIntensity * 0.5) + ')');
|
||||
coreGlow.addColorStop(1, 'rgba(255, 150, 80, 0)');
|
||||
ctx.fillStyle = coreGlow;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, this.size * 0.5 * pulse, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Subtle edge glow instead of hard stroke
|
||||
var edgeGlow = ctx.createRadialGradient(0, 0, this.size * 0.9, 0, 0, this.size);
|
||||
edgeGlow.addColorStop(0, 'rgba(255,200,120,0.15)');
|
||||
edgeGlow.addColorStop(1, 'rgba(255,200,120,0)');
|
||||
ctx.fillStyle = edgeGlow;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Sparkles (tiny random twinkles)
|
||||
for (var i = 0; i < this.sparkles.length; i++) {
|
||||
var s = this.sparkles[i];
|
||||
var sparkleFrequency = 0.1;
|
||||
var sparkleIntensity = 0.4 + 0.4 * Math.sin(time * sparkleFrequency + s.phase);
|
||||
ctx.fillStyle = 'rgba(255,255,200,' + sparkleIntensity + ')';
|
||||
ctx.fillRect(s.x, s.y, 1.5, 1.5);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
Asteroid.prototype.drawGiant = function(ctx) {
|
||||
ctx.save();
|
||||
ctx.translate(this.x, this.y);
|
||||
@ -353,16 +429,59 @@ function BlackHole(x, y, radius) {
|
||||
this.radius = radius;
|
||||
this.pulse = 0;
|
||||
this.pulseColor = '#000000';
|
||||
}
|
||||
};
|
||||
|
||||
BlackHole.prototype.draw = function(ctx) {
|
||||
BlackHole.prototype.draw = function(ctx, consumptionRate) {
|
||||
// Calculate jet intensity based on consumption rate
|
||||
var jetMin = 1e7; // Start triggering at 10 Mt/s (Comets)
|
||||
var jetMax = 1e22; // max intensity
|
||||
var minVisible = 0.1; // 10% intensity floor
|
||||
|
||||
var targetIntensity = 0;
|
||||
|
||||
if (consumptionRate && consumptionRate > jetMin) {
|
||||
var logMin = 10; // log10(1e10)
|
||||
var logMax = 22; // log10(1e22)
|
||||
var logRate = Math.log10(consumptionRate);
|
||||
|
||||
// Log-scaled 0..1
|
||||
targetIntensity = Math.max(0, Math.min(
|
||||
(logRate - logMin) / (logMax - logMin),
|
||||
1
|
||||
));
|
||||
|
||||
// Perceptual tweaks
|
||||
targetIntensity = Math.pow(targetIntensity, 0.35); // boost low end
|
||||
targetIntensity = minVisible + (1 - minVisible) * targetIntensity;
|
||||
}
|
||||
|
||||
// Smooth transition for jet intensity
|
||||
if (!this.jetIntensity) this.jetIntensity = 0;
|
||||
|
||||
var transitionSpeed = 0.05; // Lower = slower fade (0.01 = very slow, 0.05 = fast)
|
||||
|
||||
if (targetIntensity > this.jetIntensity) {
|
||||
// Fade in
|
||||
this.jetIntensity = Math.min(this.jetIntensity + transitionSpeed, targetIntensity);
|
||||
} else if (targetIntensity < this.jetIntensity) {
|
||||
// Fade out
|
||||
this.jetIntensity = Math.max(this.jetIntensity - transitionSpeed, targetIntensity);
|
||||
}
|
||||
|
||||
// Draw jets first (behind the black hole)
|
||||
if (this.jetIntensity > 0) {
|
||||
this.drawJets(ctx, this.jetIntensity);
|
||||
}
|
||||
|
||||
// Existing black hole drawing code
|
||||
if (this.pulse > 0) {
|
||||
this.pulse -= 0.02;
|
||||
if (this.pulse < 0) this.pulse = 0;
|
||||
}
|
||||
if (this.pulse === 0) {
|
||||
this.pulseColor = '#000000';
|
||||
this.pulseColor = '#000000';
|
||||
}
|
||||
|
||||
ctx.fillStyle = '#000000';
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||||
@ -443,27 +562,86 @@ BlackHole.prototype.blendColors = function(c1, c2, weight) {
|
||||
return '#' + d2h(r) + d2h(g) + d2h(b);
|
||||
};
|
||||
|
||||
BlackHole.prototype.drawJets = function(ctx, intensity) {
|
||||
// Jet configuration (easily adjustable)
|
||||
var config = {
|
||||
color: 'rgba(100, 150, 255, 0.8)', // Blue-white jet color
|
||||
coreColor: 'rgba(200, 220, 255, 0.9)', // Bright core
|
||||
length: this.radius * 8, // Jet length (8x black hole radius)
|
||||
width: this.radius * 0.2, // Jet width at base
|
||||
coreWidth: this.radius * 0.05, // Bright core width
|
||||
particleCount: 15, // Number of particles per jet
|
||||
particleSize: 2, // Size of individual particles
|
||||
rotation: 0, // Rotation angle (for spinning jets)
|
||||
flickerSpeed: 0.1 // How fast jets flicker
|
||||
};
|
||||
|
||||
//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';
|
||||
// }
|
||||
//};
|
||||
// Intensity affects opacity and size (0-1 scale)
|
||||
var alpha = Math.min(intensity, 1);
|
||||
var sizeMultiplier = 0.5 + (intensity * 0.5); // 50% to 100% size based on intensity
|
||||
|
||||
// Draw both jets (top and bottom)
|
||||
for (var direction = -1; direction <= 1; direction += 2) {
|
||||
ctx.save();
|
||||
ctx.translate(this.x, this.y);
|
||||
ctx.rotate(config.rotation);
|
||||
|
||||
// Outer jet cone (gradient)
|
||||
var jetGradient = ctx.createLinearGradient(
|
||||
0, 0,
|
||||
0, direction * config.length * sizeMultiplier
|
||||
);
|
||||
|
||||
var jetColor = config.color.replace(/[\d.]+\)/, (alpha * 0.6) + ')');
|
||||
jetGradient.addColorStop(0, jetColor);
|
||||
jetGradient.addColorStop(0.3, jetColor.replace(/[\d.]+\)/, (alpha * 0.4) + ')'));
|
||||
jetGradient.addColorStop(1, 'rgba(100, 150, 255, 0)');
|
||||
|
||||
ctx.fillStyle = jetGradient;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-config.width * sizeMultiplier, 0);
|
||||
ctx.lineTo(0, direction * config.length * sizeMultiplier);
|
||||
ctx.lineTo(config.width * sizeMultiplier, 0);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Inner bright core
|
||||
var coreGradient = ctx.createLinearGradient(
|
||||
0, 0,
|
||||
0, direction * config.length * sizeMultiplier * 0.7
|
||||
);
|
||||
|
||||
var coreColor = config.coreColor.replace(/[\d.]+\)/, alpha + ')');
|
||||
coreGradient.addColorStop(0, coreColor);
|
||||
coreGradient.addColorStop(0.5, coreColor.replace(/[\d.]+\)/, (alpha * 0.5) + ')'));
|
||||
coreGradient.addColorStop(1, 'rgba(200, 220, 255, 0)');
|
||||
|
||||
ctx.fillStyle = coreGradient;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-config.coreWidth * sizeMultiplier, 0);
|
||||
ctx.lineTo(0, direction * config.length * sizeMultiplier * 0.7);
|
||||
ctx.lineTo(config.coreWidth * sizeMultiplier, 0);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Particles along the jet (for dynamic effect)
|
||||
ctx.fillStyle = config.coreColor.replace(/[\d.]+\)/, alpha + ')');
|
||||
for (var i = 0; i < config.particleCount; i++) {
|
||||
var particlePos = (i / config.particleCount) * config.length * sizeMultiplier;
|
||||
var particleOffset = (Math.sin(Date.now() * 0.01 + i) * config.width * 0.3) * sizeMultiplier;
|
||||
var particleSize = config.particleSize * (1 - i / config.particleCount); // Smaller as they go out
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
particleOffset,
|
||||
direction * particlePos,
|
||||
particleSize * alpha,
|
||||
0,
|
||||
Math.PI * 2
|
||||
);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
|
||||
448
js/game.js
448
js/game.js
@ -16,21 +16,31 @@ var Game = (function() {
|
||||
planetUpgradeCost: CONFIG.PLANET_BASE_COST,
|
||||
giantUpgradeLevel: 0,
|
||||
giantUpgradeCost: CONFIG.GIANT_BASE_COST,
|
||||
mtypeUpgradeLevel: 0,
|
||||
mtypeUpgradeCost: CONFIG.MTYPE_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,
|
||||
asteroidSpawnCount: 0,
|
||||
lastAsteroidSpawn: Date.now(),
|
||||
lastCometSpawn: Date.now(),
|
||||
lastPlanetSpawn: Date.now() - ((0 + Math.random() * 30) * 60 * 1000),
|
||||
lastGiantSpawn: Date.now() - ((60 + Math.random() * 240) * 60 * 1000),
|
||||
lastPlanetSpawn: Date.now(),
|
||||
lastGiantSpawn: Date.now(),
|
||||
lastMtypeSpawn: Date.now(),
|
||||
sM: 0, sT: Date.now(),
|
||||
mM: 0, mT: Date.now(),
|
||||
lM: 0, lT: Date.now(),
|
||||
rateShort: 0, // 1 second average
|
||||
rateMedium: 0, // 1 hour average
|
||||
rateLong: 0 // 1 day average
|
||||
rateLong: 0, // 1 day average
|
||||
isReady: false,
|
||||
tabHiddenAt: null,
|
||||
cometUnlocked: false,
|
||||
planetUnlocked: false,
|
||||
giantUnlocked: false,
|
||||
mtypeUnlocked: false
|
||||
};
|
||||
|
||||
var blackHole;
|
||||
@ -41,7 +51,11 @@ var Game = (function() {
|
||||
var needsRender = true;
|
||||
var currentTime = Date.now();
|
||||
var lastLogicUpdate = Date.now();
|
||||
|
||||
|
||||
// Expose to admin interface
|
||||
window.gameBlackHole = null;
|
||||
window.gameAsteroids = asteroids;
|
||||
|
||||
function resizeCanvas() {
|
||||
// Set canvas to actual viewport size
|
||||
canvas.width = window.innerWidth
|
||||
@ -57,9 +71,14 @@ var Game = (function() {
|
||||
CONFIG.INITIAL_BLACK_HOLE_RADIUS
|
||||
);
|
||||
|
||||
window.gameBlackHole = blackHole;
|
||||
|
||||
for (var i = 0; i < CONFIG.STAR_COUNT; i++) {
|
||||
stars.push(new Star(canvas));
|
||||
}
|
||||
setInterval(function() {
|
||||
UI.updateActiveObjects();
|
||||
}, 1000);
|
||||
|
||||
UI.init();
|
||||
|
||||
@ -67,9 +86,11 @@ var Game = (function() {
|
||||
asteroid: handleAsteroidUpgrade,
|
||||
comet: handleCometUpgrade,
|
||||
planet: handlePlanetUpgrade,
|
||||
giant: handleGiantUpgrade
|
||||
giant: handleGiantUpgrade,
|
||||
mtype: handleMtypeUpgrade
|
||||
});
|
||||
|
||||
Server.init()
|
||||
loadGameState();
|
||||
|
||||
window.addEventListener('resize', function() {
|
||||
@ -81,7 +102,6 @@ var Game = (function() {
|
||||
});
|
||||
});
|
||||
|
||||
// ADD THIS BLOCK - Prevent zoom on desktop
|
||||
window.addEventListener('wheel', function(e) {
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
@ -96,141 +116,152 @@ var Game = (function() {
|
||||
|
||||
animate(currentTime);
|
||||
UI.update(state, CONFIG);
|
||||
Server.init()
|
||||
|
||||
// Save when page becomes hidden (tab switch, minimize, close)
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (document.hidden) {
|
||||
Storage.saveGame(state);
|
||||
Server.checkpoint(state);
|
||||
} else if (!document.hidden) {
|
||||
loadGameState();
|
||||
}
|
||||
});
|
||||
|
||||
// Also save on beforeunload as backup
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (document.hidden) {
|
||||
state.tabHiddenAt = Date.now(); // NEW: Record when hidden
|
||||
Server.checkpoint(state);
|
||||
} else if (!document.hidden) {
|
||||
loadGameState();
|
||||
Server.checkpoint(state);
|
||||
}
|
||||
});
|
||||
|
||||
// Save when page is about to unload (close tab, navigate away)
|
||||
window.addEventListener('beforeunload', function() {
|
||||
Storage.saveGame(state);
|
||||
state.tabHiddenAt = Date.now();
|
||||
Server.checkpoint(state);
|
||||
});
|
||||
|
||||
// Periodic auto-save every 30 seconds as final safety net
|
||||
setInterval(function() {
|
||||
Storage.saveGame(state);
|
||||
}, 15000);
|
||||
}
|
||||
|
||||
function loadGameState() {
|
||||
var savedState = Storage.loadGame();
|
||||
if (savedState) {
|
||||
state.blackHoleTotalMass = savedState.blackHoleTotalMass;
|
||||
state.totalMassConsumedEver = savedState.totalMassConsumedEver;
|
||||
state.totalMassConsumed = savedState.totalMassConsumed;
|
||||
state.asteroidUpgradeLevel = savedState.asteroidUpgradeLevel;
|
||||
state.asteroidUpgradeCost = savedState.asteroidUpgradeCost;
|
||||
state.cometUpgradeLevel = savedState.cometUpgradeLevel;
|
||||
state.cometUpgradeCost = savedState.cometUpgradeCost;
|
||||
state.planetUpgradeLevel = savedState.planetUpgradeLevel;
|
||||
state.planetUpgradeCost = savedState.planetUpgradeCost;
|
||||
state.giantUpgradeLevel = savedState.giantUpgradeLevel;
|
||||
state.giantUpgradeCost = savedState.giantUpgradeCost;
|
||||
state.asteroidSpawnCount = savedState.asteroidSpawnCount;
|
||||
|
||||
// Load spawn timers if they exist, otherwise generate from save timestamp
|
||||
state.lastAsteroidSpawn = savedState.lastAsteroidSpawn || Date.now();
|
||||
state.lastCometSpawn = savedState.lastCometSpawn || Date.now();
|
||||
|
||||
// For old saves without spawn timers, use savedAt timestamp to generate consistent times
|
||||
if (savedState.lastPlanetSpawn) {
|
||||
state.lastPlanetSpawn = savedState.lastPlanetSpawn;
|
||||
} else {
|
||||
// Use savedAt to create a consistent offset (not random)
|
||||
var planetOffset = (savedState.savedAt % 30) * 60 * 1000; // 0-30 minutes
|
||||
state.lastPlanetSpawn = Date.now() - planetOffset;
|
||||
}
|
||||
|
||||
if (savedState.lastGiantSpawn) {
|
||||
state.lastGiantSpawn = savedState.lastGiantSpawn;
|
||||
} else {
|
||||
// Use savedAt to create a consistent offset (not random)
|
||||
var giantOffset = (60 + (savedState.savedAt % 240)) * 60 * 1000; // 60-300 minutes
|
||||
state.lastGiantSpawn = Date.now() - giantOffset;
|
||||
}
|
||||
|
||||
// Load consumption rates
|
||||
state.sM = savedState.sM || 0;
|
||||
state.mM = savedState.mM || 0;
|
||||
state.lM = savedState.lM || 0;
|
||||
|
||||
var now = Date.now();
|
||||
|
||||
state.sT = savedState.sT || (now - CONFIG.RATE_WINDOWS.SHORT);
|
||||
state.mT = savedState.mT || (now - CONFIG.RATE_WINDOWS.MEDIUM);
|
||||
state.lT = savedState.lT || (now - CONFIG.RATE_WINDOWS.LONG);
|
||||
|
||||
state.rateShort = savedState.rateShort || 0;
|
||||
state.rateMedium = savedState.rateMedium || 0;
|
||||
state.rateLong = savedState.rateLong || 0;
|
||||
|
||||
// Calculate offline progression
|
||||
var now = Date.now();
|
||||
var offlineTime = now - savedState.savedAt; // milliseconds offline
|
||||
|
||||
if (offlineTime > 1000) {
|
||||
// Pick the longest non-zero rate
|
||||
var shortRate = state.sM / ((now - state.sT) / 1000);
|
||||
var mediumRate = state.mM / ((now - state.mT) / 1000);
|
||||
var longRate = state.lM / ((now - state.lT) / 1000);
|
||||
var rateToUse = longRate || mediumRate || shortRate || 0;
|
||||
|
||||
if (rateToUse > 0) {
|
||||
var offlineMass = rateToUse * (offlineTime / 1000);
|
||||
|
||||
// Update totals
|
||||
state.blackHoleTotalMass += offlineMass;
|
||||
state.totalMassConsumedEver += offlineMass;
|
||||
state.totalMassConsumed += offlineMass;
|
||||
|
||||
// Add to rolling windows so rates are meaningful
|
||||
state.sM += offlineMass;
|
||||
state.mM += offlineMass;
|
||||
state.lM += offlineMass;
|
||||
|
||||
|
||||
// Optional notification
|
||||
showOfflineNotification(
|
||||
formatOfflineTime(offlineTime),
|
||||
UI.formatMass(offlineMass),
|
||||
'offline'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateSpawnIntervals();
|
||||
|
||||
// Restore black hole size
|
||||
var solarMasses = state.blackHoleTotalMass / CONFIG.SOLAR_MASS_KG;
|
||||
blackHole.radius = CONFIG.INITIAL_BLACK_HOLE_RADIUS + (solarMasses - CONFIG.INITIAL_BLACK_HOLE_MASS);
|
||||
|
||||
// Save state
|
||||
Storage.saveGame(state);
|
||||
|
||||
} else {
|
||||
// No save exists - initialize spawn times with random values
|
||||
state.lastAsteroidSpawn = Date.now();
|
||||
state.lastCometSpawn = Date.now();
|
||||
state.lastPlanetSpawn = Date.now() - ((0 + Math.random() * 30) * 60 * 1000);
|
||||
state.lastGiantSpawn = Date.now() - ((60 + Math.random() * 240) * 60 * 1000);
|
||||
// Save and update scores when window gains focus
|
||||
window.addEventListener('focus', function() {
|
||||
if (window.Server && window.Game && Game.getState) {
|
||||
Server.checkpoint(Game.getState());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadGameState() {
|
||||
Server.loadGameState().then(function(savedState) {
|
||||
if (savedState) {
|
||||
state.blackHoleTotalMass = savedState.blackHoleTotalMass;
|
||||
state.totalMassConsumedEver = savedState.totalMassConsumedEver;
|
||||
state.totalMassConsumed = savedState.totalMassConsumed;
|
||||
state.asteroidUpgradeLevel = savedState.asteroidUpgradeLevel;
|
||||
state.asteroidUpgradeCost = savedState.asteroidUpgradeCost;
|
||||
state.cometUpgradeLevel = savedState.cometUpgradeLevel;
|
||||
state.cometUpgradeCost = savedState.cometUpgradeCost;
|
||||
state.planetUpgradeLevel = savedState.planetUpgradeLevel;
|
||||
state.planetUpgradeCost = savedState.planetUpgradeCost;
|
||||
state.giantUpgradeLevel = savedState.giantUpgradeLevel;
|
||||
state.giantUpgradeCost = savedState.giantUpgradeCost;
|
||||
state.mtypeUpgradeLevel = savedState.mtypeUpgradeLevel || 0;
|
||||
state.mtypeUpgradeCost = savedState.mtypeUpgradeCost || CONFIG.MTYPE_BASE_COST;
|
||||
state.asteroidSpawnCount = savedState.asteroidSpawnCount;
|
||||
|
||||
// Load unlock states (with backward compatibility)
|
||||
state.cometUnlocked = savedState.cometUnlocked !== undefined ? savedState.cometUnlocked : (savedState.asteroidUpgradeLevel >= 20);
|
||||
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.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();
|
||||
|
||||
// Load consumption rates
|
||||
state.sM = savedState.sM || 0;
|
||||
state.mM = savedState.mM || 0;
|
||||
state.lM = savedState.lM || 0;
|
||||
|
||||
var now = Date.now();
|
||||
|
||||
state.sT = savedState.sT || now;
|
||||
state.mT = savedState.mT || now;
|
||||
state.lT = savedState.lT || now;
|
||||
|
||||
state.rateShort = savedState.rateShort || 0;
|
||||
state.rateShortTrend = savedState.rateShortTrend || 'same';
|
||||
state.rateMedium = savedState.rateMedium || 0;
|
||||
state.rateLong = savedState.rateLong || 0;
|
||||
|
||||
state.tabHiddenAt = savedState.tabHiddenAt || null;
|
||||
|
||||
// Calculate offline progression ONLY if tab was actually hidden
|
||||
var offlineTime = 0;
|
||||
|
||||
if (savedState.tabHiddenAt) {
|
||||
// Tab was hidden - calculate time between hiding and now
|
||||
offlineTime = now - savedState.tabHiddenAt;
|
||||
}
|
||||
|
||||
if (offlineTime > 1000) {
|
||||
var rateToUse = 0;
|
||||
|
||||
if (state.rateLong > 0 && (now - state.lT) > 3600000) {
|
||||
rateToUse = state.rateLong;
|
||||
} else if (state.rateMedium > 0 && (now - state.mT) > 300000) {
|
||||
rateToUse = state.rateMedium;
|
||||
} else {
|
||||
rateToUse = state.rateShort || 0;
|
||||
}
|
||||
|
||||
if (rateToUse > 0) {
|
||||
var offlineMass = rateToUse * (offlineTime / 1000);
|
||||
|
||||
state.blackHoleTotalMass += offlineMass;
|
||||
state.totalMassConsumedEver += offlineMass;
|
||||
state.totalMassConsumed += offlineMass;
|
||||
|
||||
state.sM += offlineMass;
|
||||
state.mM += offlineMass;
|
||||
state.lM += offlineMass;
|
||||
|
||||
showOfflineNotification(
|
||||
formatOfflineTime(offlineTime),
|
||||
UI.formatMass(offlineMass),
|
||||
'offline'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the hidden timestamp after processing
|
||||
state.tabHiddenAt = null;
|
||||
|
||||
updateSpawnIntervals();
|
||||
|
||||
// Restore black hole size
|
||||
blackHole.radius = 0.5 * (state.blackHoleTotalMass / CONFIG.SOLAR_MASS_KG)
|
||||
|
||||
// Send checkpoint after loading
|
||||
Server.checkpoint(state).then(function() {
|
||||
state.isReady = true; // Mark as ready after checkpoint
|
||||
});
|
||||
} else {
|
||||
// No saved state - send initial checkpoint for new player
|
||||
Server.checkpoint(state).then(function() {
|
||||
state.isReady = true; // Mark as ready after checkpoint
|
||||
});
|
||||
}
|
||||
|
||||
UI.update(state, CONFIG);
|
||||
});
|
||||
}
|
||||
|
||||
function updateSpawnIntervals() {
|
||||
state.currentAsteroidSpawnInterval = CONFIG.BASE_ASTEROID_SPAWN_INTERVAL /
|
||||
(1 + state.asteroidUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL);
|
||||
(1 + state.asteroidUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_ASTER);
|
||||
state.currentCometSpawnInterval = CONFIG.BASE_COMET_SPAWN_INTERVAL /
|
||||
(1 + state.cometUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL);
|
||||
(1 + state.cometUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_COMET);
|
||||
state.currentPlanetSpawnInterval = CONFIG.BASE_PLANET_SPAWN_INTERVAL /
|
||||
(1 + state.planetUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL);
|
||||
(1 + state.planetUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_PLANT);
|
||||
state.currentGiantSpawnInterval = CONFIG.BASE_GIANT_SPAWN_INTERVAL /
|
||||
(1 + state.giantUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL);
|
||||
(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);
|
||||
}
|
||||
|
||||
function handleAsteroidUpgrade() {
|
||||
@ -240,9 +271,15 @@ var Game = (function() {
|
||||
state.asteroidUpgradeCost = Math.floor(
|
||||
CONFIG.ASTEROID_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_ASTER, state.asteroidUpgradeLevel)
|
||||
);
|
||||
if (state.asteroidUpgradeLevel >= 20 && !state.cometUnlocked) {
|
||||
state.cometUnlocked = true;
|
||||
showUnlockNotification('comet');
|
||||
asteroids.push(new Asteroid('comet', blackHole, canvas));
|
||||
state.lastCometSpawn = Date.now();
|
||||
}
|
||||
|
||||
updateSpawnIntervals();
|
||||
UI.update(state, CONFIG);
|
||||
Storage.saveGame(state);
|
||||
}
|
||||
}
|
||||
|
||||
@ -251,11 +288,16 @@ var Game = (function() {
|
||||
state.totalMassConsumed -= state.cometUpgradeCost;
|
||||
state.cometUpgradeLevel++;
|
||||
state.cometUpgradeCost = Math.floor(
|
||||
CONFIG.COMET_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER, state.cometUpgradeLevel)
|
||||
CONFIG.COMET_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_COMET, state.cometUpgradeLevel)
|
||||
);
|
||||
if (state.cometUpgradeLevel >= 15 && !state.planetUnlocked) {
|
||||
state.planetUnlocked = true;
|
||||
showUnlockNotification('planet');
|
||||
asteroids.push(new Asteroid('planet', blackHole, canvas));
|
||||
state.lastPlanetSpawn = Date.now();
|
||||
}
|
||||
updateSpawnIntervals();
|
||||
UI.update(state, CONFIG);
|
||||
Storage.saveGame(state);
|
||||
}
|
||||
}
|
||||
|
||||
@ -264,11 +306,16 @@ var Game = (function() {
|
||||
state.totalMassConsumed -= state.planetUpgradeCost;
|
||||
state.planetUpgradeLevel++;
|
||||
state.planetUpgradeCost = Math.floor(
|
||||
CONFIG.PLANET_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER, state.planetUpgradeLevel)
|
||||
CONFIG.PLANET_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_PLANT, state.planetUpgradeLevel)
|
||||
);
|
||||
if (state.planetUpgradeLevel >= 10 && !state.giantUnlocked) {
|
||||
state.giantUnlocked = true;
|
||||
showUnlockNotification('giant');
|
||||
asteroids.push(new Asteroid('giant', blackHole, canvas));
|
||||
state.lastGiantSpawn = Date.now();
|
||||
}
|
||||
updateSpawnIntervals();
|
||||
UI.update(state, CONFIG);
|
||||
Storage.saveGame(state);
|
||||
}
|
||||
}
|
||||
|
||||
@ -277,11 +324,31 @@ var Game = (function() {
|
||||
state.totalMassConsumed -= state.giantUpgradeCost;
|
||||
state.giantUpgradeLevel++;
|
||||
state.giantUpgradeCost = Math.floor(
|
||||
CONFIG.GIANT_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER, state.giantUpgradeLevel)
|
||||
CONFIG.GIANT_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_GIANT, state.giantUpgradeLevel)
|
||||
);
|
||||
|
||||
// Unlock M-Type at giant level 5
|
||||
if (state.giantUpgradeLevel >= 5 && !state.mtypeUnlocked) {
|
||||
state.mtypeUnlocked = true;
|
||||
state.lastMtypeSpawn = Date.now();
|
||||
showUnlockNotification('mtype');
|
||||
asteroids.push(new Asteroid('mtype', blackHole, canvas));
|
||||
}
|
||||
|
||||
updateSpawnIntervals();
|
||||
UI.update(state, CONFIG);
|
||||
}
|
||||
}
|
||||
|
||||
function handleMtypeUpgrade() {
|
||||
if (state.totalMassConsumed >= state.mtypeUpgradeCost) {
|
||||
state.totalMassConsumed -= state.mtypeUpgradeCost;
|
||||
state.mtypeUpgradeLevel++;
|
||||
state.mtypeUpgradeCost = Math.floor(
|
||||
CONFIG.MTYPE_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_MTYPE, state.mtypeUpgradeLevel)
|
||||
);
|
||||
updateSpawnIntervals();
|
||||
UI.update(state, CONFIG);
|
||||
Storage.saveGame(state);
|
||||
}
|
||||
}
|
||||
|
||||
@ -303,24 +370,34 @@ var Game = (function() {
|
||||
asteroids.push(new Asteroid(asteroidType, blackHole, canvas));
|
||||
state.lastAsteroidSpawn = currentTime;
|
||||
}
|
||||
|
||||
if (currentTime - state.lastCometSpawn > state.currentCometSpawnInterval) {
|
||||
asteroids.push(new Asteroid('comet', blackHole, canvas));
|
||||
state.lastCometSpawn = currentTime;
|
||||
//Storage.saveGame(state);
|
||||
|
||||
if (state.cometUnlocked && state.cometUpgradeLevel >= 0) {
|
||||
if (currentTime - state.lastCometSpawn > state.currentCometSpawnInterval) {
|
||||
state.lastCometSpawn = currentTime;
|
||||
asteroids.push(new Asteroid('comet', blackHole, canvas));
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTime - state.lastPlanetSpawn > state.currentPlanetSpawnInterval) {
|
||||
asteroids.push(new Asteroid('planet', blackHole, canvas));
|
||||
state.lastPlanetSpawn = currentTime;
|
||||
//Storage.saveGame(state);
|
||||
if (state.planetUnlocked && state.planetUpgradeLevel >= 0) {
|
||||
if (currentTime - state.lastPlanetSpawn > state.currentPlanetSpawnInterval) {
|
||||
state.lastPlanetSpawn = currentTime;
|
||||
asteroids.push(new Asteroid('planet', blackHole, canvas));
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTime - state.lastGiantSpawn > state.currentGiantSpawnInterval) {
|
||||
asteroids.push(new Asteroid('giant', blackHole, canvas));
|
||||
state.lastGiantSpawn = currentTime;
|
||||
//Storage.saveGame(state);
|
||||
if (state.giantUnlocked && state.giantUpgradeLevel >= 0) {
|
||||
if (currentTime - state.lastGiantSpawn > state.currentGiantSpawnInterval) {
|
||||
state.lastGiantSpawn = currentTime;
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function updateAsteroids() {
|
||||
@ -364,7 +441,7 @@ var Game = (function() {
|
||||
asteroids[i].draw(ctx);
|
||||
}
|
||||
|
||||
blackHole.draw(ctx);
|
||||
blackHole.draw(ctx, state.rateShort || 0);
|
||||
}
|
||||
|
||||
function animate(currentTime) {
|
||||
@ -406,7 +483,6 @@ var Game = (function() {
|
||||
}());
|
||||
|
||||
var Server = {
|
||||
// baseURL removed; now all requests are relative
|
||||
playerId: null,
|
||||
|
||||
init: function() {
|
||||
@ -423,47 +499,81 @@ var Server = {
|
||||
|
||||
checkpoint: async function(gameState) {
|
||||
try {
|
||||
await fetch('/api/checkpoint', { // relative URL
|
||||
// Add savedAt timestamp
|
||||
gameState.savedAt = Date.now();
|
||||
|
||||
const response = await fetch('/api/checkpoint', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
playerId: this.playerId,
|
||||
name: Storage.getCookie('playerName') || 'Anonymous',
|
||||
mass: gameState.blackHoleTotalMass,
|
||||
playTime: gameState.totalPlayTime + (Date.now() - gameState.gameStartTime),
|
||||
levels: {
|
||||
total: gameState.asteroidUpgradeLevel +
|
||||
gameState.cometUpgradeLevel +
|
||||
gameState.planetUpgradeLevel +
|
||||
gameState.giantUpgradeLevel
|
||||
}
|
||||
gameState: gameState
|
||||
})
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (err) {
|
||||
console.error('Checkpoint failed', err);
|
||||
return { success: false };
|
||||
}
|
||||
},
|
||||
|
||||
loadGameState: async function() {
|
||||
try {
|
||||
const resp = await fetch(`/api/player/${encodeURIComponent(this.playerId)}/state`);
|
||||
if (!resp.ok) return null;
|
||||
return await resp.json();
|
||||
} catch (err) {
|
||||
console.error('Failed to load game state', err);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getLeaderboard: async function(sortBy) {
|
||||
try {
|
||||
const resp = await fetch(`/api/leaderboard?sort=${encodeURIComponent(sortBy)}&playerId=${encodeURIComponent(this.playerId)}`); // relative URL
|
||||
const resp = await fetch(`/api/leaderboard?sort=${encodeURIComponent(sortBy)}&playerId=${encodeURIComponent(this.playerId)}`);
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
return data; // should be array of { name, mass, holeAge }
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch leaderboard', err);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
deletePlayer: async function(playerId) {
|
||||
try {
|
||||
const resp = await fetch(`/api/player/${encodeURIComponent(playerId)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
return await resp.json();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete player', err);
|
||||
return { success: false };
|
||||
}
|
||||
},
|
||||
|
||||
startCheckpoints: function() {
|
||||
// Example: autosave every 15s
|
||||
let lastCheckpointData = null;
|
||||
|
||||
setInterval(() => {
|
||||
if (window.Game && Game.state) {
|
||||
this.checkpoint(Game.state);
|
||||
if (window.Game && Game.getState) {
|
||||
const state = Game.getState();
|
||||
|
||||
// Only checkpoint if significant changes
|
||||
const currentData = JSON.stringify({
|
||||
mass: Math.floor(state.blackHoleTotalMass / 1e27), // Round to reduce noise
|
||||
levels: state.asteroidUpgradeLevel + state.cometUpgradeLevel +
|
||||
state.planetUpgradeLevel + state.giantUpgradeLevel
|
||||
});
|
||||
|
||||
if (currentData !== lastCheckpointData) {
|
||||
this.checkpoint(state);
|
||||
lastCheckpointData = currentData;
|
||||
}
|
||||
}
|
||||
}, 15000);
|
||||
}
|
||||
}, 15000); // Check every 15s, but only send if changed
|
||||
},
|
||||
};
|
||||
|
||||
// Start the game when the page loads
|
||||
|
||||
208
js/helpers.js
208
js/helpers.js
@ -49,35 +49,66 @@ function saturateColor(r, g, b, saturationBoost) {
|
||||
function calculateConsumptionRates(state, CONFIG, consumedMassKg) {
|
||||
const now = Date.now();
|
||||
|
||||
// Only update rates once per second
|
||||
if (!state._lastRateUpdate) state._lastRateUpdate = 0;
|
||||
if (now - state._lastRateUpdate < 1000) return; // skip if less than 1s
|
||||
state._lastRateUpdate = now;
|
||||
|
||||
function updateWindow(massKey, timeKey, prevRateKey) {
|
||||
const start = state[timeKey] || now;
|
||||
|
||||
// Add new mass
|
||||
state[massKey] = (state[massKey] || 0) + consumedMassKg;
|
||||
|
||||
const elapsed = now - start;
|
||||
const rate = elapsed > 0 ? state[massKey] / (elapsed / 1000) : 0;
|
||||
|
||||
// Compare to previous rate to get trend
|
||||
if (state[prevRateKey] === undefined) state[prevRateKey] = rate;
|
||||
state[prevRateKey + 'Trend'] =
|
||||
rate > state[prevRateKey] ? 'up' :
|
||||
rate < state[prevRateKey] ? 'down' : 'same';
|
||||
|
||||
// Save current rate for next tick
|
||||
state[prevRateKey] = rate;
|
||||
|
||||
return rate;
|
||||
// Initialize tracking windows if needed
|
||||
if (!state.sM) state.sM = 0;
|
||||
if (!state.sT) state.sT = now;
|
||||
if (!state.mM) state.mM = 0;
|
||||
if (!state.mT) state.mT = now;
|
||||
if (!state.lM) state.lM = 0;
|
||||
if (!state.lT) state.lT = now;
|
||||
|
||||
// Add mass to all windows
|
||||
state.sM += consumedMassKg;
|
||||
state.mM += consumedMassKg;
|
||||
state.lM += consumedMassKg;
|
||||
|
||||
// Update per-second rate (with trend coloring)
|
||||
const elapsedShort = now - state.sT;
|
||||
if (elapsedShort >= 1000) {
|
||||
const newRate = state.sM / (elapsedShort / 1000);
|
||||
|
||||
// Compare to previous rate for trend
|
||||
if (state.rateShort === undefined) {
|
||||
state.rateShort = newRate;
|
||||
state.rateShortTrend = 'same';
|
||||
} else {
|
||||
// Determine trend with 5% threshold to avoid jitter
|
||||
if (newRate > state.rateShort * 1.05) {
|
||||
state.rateShortTrend = 'up';
|
||||
} else if (newRate < state.rateShort * 0.95) {
|
||||
state.rateShortTrend = 'down';
|
||||
} else {
|
||||
state.rateShortTrend = 'same';
|
||||
}
|
||||
state.rateShort = newRate;
|
||||
}
|
||||
|
||||
// Reset short window
|
||||
state.sM = 0;
|
||||
state.sT = now;
|
||||
}
|
||||
|
||||
// Calculate hourly average (rolling 1-hour window)
|
||||
const elapsedMedium = now - state.mT;
|
||||
if (elapsedMedium > 0) {
|
||||
state.rateMedium = state.mM / (elapsedMedium / 1000);
|
||||
}
|
||||
// Reset hourly window after 1 hour
|
||||
if (elapsedMedium >= CONFIG.RATE_WINDOWS.MEDIUM) {
|
||||
state.mM = 0;
|
||||
state.mT = now;
|
||||
}
|
||||
|
||||
// Calculate daily average (rolling 24-hour window)
|
||||
const elapsedLong = now - state.lT;
|
||||
if (elapsedLong > 0) {
|
||||
state.rateLong = state.lM / (elapsedLong / 1000);
|
||||
}
|
||||
// Reset daily window after 24 hours
|
||||
if (elapsedLong >= CONFIG.RATE_WINDOWS.LONG) {
|
||||
state.lM = 0;
|
||||
state.lT = now;
|
||||
}
|
||||
|
||||
state.rateShort = updateWindow("sM", "sT", "prevRateShort");
|
||||
state.rateMedium = updateWindow("mM", "mT", "prevRateMedium");
|
||||
state.rateLong = updateWindow("lM", "lT", "prevRateLong");
|
||||
}
|
||||
|
||||
function formatOfflineTime(ms) {
|
||||
@ -97,29 +128,110 @@ function formatOfflineTime(ms) {
|
||||
}
|
||||
}
|
||||
|
||||
function showOfflineNotification(timeAway, massGained, rateName) {
|
||||
var notification = document.createElement('div');
|
||||
notification.style.position = 'fixed';
|
||||
notification.style.top = '50%';
|
||||
notification.style.left = '50%';
|
||||
notification.style.transform = 'translate(-50%, -50%)';
|
||||
notification.style.background = 'rgba(20, 20, 30, 0.95)';
|
||||
notification.style.border = '2px solid rgba(161, 161, 161, 0.5)';
|
||||
notification.style.padding = '20px 30px';
|
||||
notification.style.borderRadius = '8px';
|
||||
notification.style.color = 'rgba(255, 255, 255, 0.9)';
|
||||
notification.style.fontSize = '12px';
|
||||
notification.style.zIndex = '10000';
|
||||
notification.style.textAlign = 'center';
|
||||
notification.style.lineHeight = '1.6';
|
||||
// Unified notification system
|
||||
function showNotification(options) {
|
||||
// Default options
|
||||
var defaults = {
|
||||
message: '',
|
||||
type: 'info', // 'unlock', 'offline', 'error', 'success', 'info'
|
||||
html: false // if true, message is HTML; if false, it's text
|
||||
};
|
||||
|
||||
notification.innerHTML =
|
||||
// Merge options with defaults
|
||||
var config = {};
|
||||
for (var key in defaults) {
|
||||
config[key] = options[key] !== undefined ? options[key] : defaults[key];
|
||||
}
|
||||
|
||||
// Create notification element
|
||||
var notification = document.createElement('div');
|
||||
notification.className = 'notification ' + config.type;
|
||||
|
||||
// Set content
|
||||
if (config.html) {
|
||||
notification.innerHTML = config.message;
|
||||
} else {
|
||||
notification.textContent = config.message;
|
||||
}
|
||||
|
||||
// Add to DOM
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Dismiss on canvas click
|
||||
var canvas = document.getElementById('space');
|
||||
var dismissHandler = function(e) {
|
||||
// Only dismiss if clicking canvas, not the notification
|
||||
if (e.target === canvas) {
|
||||
notification.remove();
|
||||
canvas.removeEventListener('click', dismissHandler);
|
||||
}
|
||||
};
|
||||
|
||||
// Small delay to prevent immediate dismissal if notification was triggered by a click
|
||||
setTimeout(function() {
|
||||
canvas.addEventListener('click', dismissHandler);
|
||||
}, 100);
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
// Helper functions for specific notification types
|
||||
function showUnlockNotification(type) {
|
||||
var messages = {
|
||||
comet: 'Comets Unlocked',
|
||||
planet: 'Planets Unlocked',
|
||||
giant: 'Gas & Ice Giants Unlocked',
|
||||
mtype: 'M-Type Stars Unlocked'
|
||||
};
|
||||
|
||||
showNotification({
|
||||
message: messages[type] || 'New content unlocked!',
|
||||
type: 'unlock',
|
||||
duration: 4000
|
||||
});
|
||||
}
|
||||
|
||||
function showOfflineNotification(timeAway, massGained) {
|
||||
var message =
|
||||
'<div>Inactive for: <strong>' + timeAway + '</strong></div>' +
|
||||
'<div>Mass gained: <strong>' + massGained + '</strong></div>';
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
document.getElementById('space').addEventListener('click', function() {
|
||||
notification.remove();
|
||||
showNotification({
|
||||
message: message,
|
||||
type: 'offline',
|
||||
duration: 5000,
|
||||
dismissable: true,
|
||||
html: true
|
||||
});
|
||||
}
|
||||
|
||||
// Additional helper for errors (useful for debugging/user feedback)
|
||||
function showErrorNotification(message) {
|
||||
showNotification({
|
||||
message: message,
|
||||
type: 'error',
|
||||
duration: 5000,
|
||||
dismissable: true
|
||||
});
|
||||
}
|
||||
|
||||
// Additional helper for success messages
|
||||
function showSuccessNotification(message) {
|
||||
showNotification({
|
||||
message: message,
|
||||
type: 'success',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
|
||||
function formatTimeSince(ms) {
|
||||
var seconds = Math.floor(ms / 1000);
|
||||
var minutes = Math.floor(seconds / 60);
|
||||
var hours = Math.floor(minutes / 60);
|
||||
var days = Math.floor(hours / 24);
|
||||
|
||||
if (seconds < 60) return 'Just now';
|
||||
if (minutes < 60) return minutes + 'm ago';
|
||||
if (hours < 24) return hours + 'h ago';
|
||||
return days + 'd ago';
|
||||
}
|
||||
|
||||
127
js/server.js
127
js/server.js
@ -7,8 +7,14 @@ app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
const LEADERBOARD_FILE = './leaderboard.json';
|
||||
const INACTIVE_DAYS = 30; // Completely remove from JSON after 30 days
|
||||
const LEADERBOARD_VISIBLE_DAYS = 5; // Only show on leaderboard if active within 5 days
|
||||
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 BOT_USER_AGENTS = [
|
||||
'googlebot', 'bingbot', 'slurp', 'duckduckbot', 'baiduspider',
|
||||
'yandexbot', 'facebookexternalhit', 'twitterbot', 'bot', 'crawler',
|
||||
'spider', 'archive', 'wget', 'curl', 'python-requests'
|
||||
];
|
||||
|
||||
// Load leaderboard from file
|
||||
function loadLeaderboard() {
|
||||
@ -32,7 +38,8 @@ function cleanupInactive(leaderboard) {
|
||||
const active = leaderboard.filter(player => player.lastSeen > thirtyDaysAgo);
|
||||
|
||||
if (active.length < beforeCount) {
|
||||
console.log(`Permanently removed ${beforeCount - active.length} players (>30 days inactive)`);
|
||||
const timestamp = new Date().toLocaleString();
|
||||
console.log(`${timestamp} Permanently removed ${beforeCount - active.length} players (>30 days inactive)`);
|
||||
}
|
||||
|
||||
return active;
|
||||
@ -41,16 +48,40 @@ function cleanupInactive(leaderboard) {
|
||||
// Filter players visible on leaderboard (active within 5 days)
|
||||
function getVisiblePlayers(leaderboard) {
|
||||
const fiveDaysAgo = Date.now() - (LEADERBOARD_VISIBLE_DAYS * 24 * 60 * 60 * 1000);
|
||||
const visible = leaderboard.filter(player => player.lastSeen > fiveDaysAgo);
|
||||
const visible = leaderboard.filter(player =>
|
||||
player.lastSeen > fiveDaysAgo &&
|
||||
!player.id.startsWith('admin_')
|
||||
);
|
||||
return visible;
|
||||
}
|
||||
|
||||
// Submit checkpoint
|
||||
app.post('/api/checkpoint', (req, res) => {
|
||||
const { playerId, name, mass, playTime, levels } = req.body;
|
||||
const userAgent = (req.headers['user-agent'] || '').toLowerCase();
|
||||
|
||||
// Reject known bots
|
||||
if (BOT_USER_AGENTS.some(bot => userAgent.includes(bot))) {
|
||||
return res.status(403).json({ error: 'Bot traffic not allowed' });
|
||||
}
|
||||
|
||||
const { playerId, gameState } = req.body;
|
||||
// Ignore players who haven't consumed any mass
|
||||
if (gameState.totalMassConsumedEver === 0) {
|
||||
return res.json({ success: true }); // Accept but don't save
|
||||
}
|
||||
|
||||
// Only save if they've upgraded at least once
|
||||
const totalLevels = (gameState.asteroidUpgradeLevel || 0) +
|
||||
(gameState.cometUpgradeLevel || 0) +
|
||||
(gameState.planetUpgradeLevel || 0) +
|
||||
(gameState.giantUpgradeLevel || 0);
|
||||
|
||||
if (totalLevels === 0 && gameState.totalMassConsumedEver < 1e6) {
|
||||
return res.json({ success: true }); // Accept but don't save
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if (!playerId || mass < 0 || playTime < 0) {
|
||||
if (!playerId || !gameState) {
|
||||
return res.status(400).json({ error: 'Invalid data' });
|
||||
}
|
||||
|
||||
@ -64,35 +95,43 @@ app.post('/api/checkpoint', (req, res) => {
|
||||
// Find or create player
|
||||
let player = leaderboard.find(p => p.id === playerId);
|
||||
if (player) {
|
||||
// Update existing player
|
||||
player.mass = Math.max(player.mass, mass);
|
||||
player.playTime = Math.max(player.playTime, playTime);
|
||||
player.level = Math.max(player.level, levels?.total || 0);
|
||||
player.name = name || player.name;
|
||||
// Update existing player - store full game state
|
||||
player.gameState = gameState;
|
||||
player.mass = gameState.blackHoleTotalMass || 0;
|
||||
player.level = (gameState.asteroidUpgradeLevel || 0) +
|
||||
(gameState.cometUpgradeLevel || 0) +
|
||||
(gameState.planetUpgradeLevel || 0) +
|
||||
(gameState.giantUpgradeLevel || 0);
|
||||
player.lastSeen = now;
|
||||
|
||||
// Calculate hole age (time since first seen)
|
||||
player.holeAge = now - player.firstSeen;
|
||||
|
||||
} else {
|
||||
// New player
|
||||
leaderboard.push({
|
||||
const timestamp = new Date().toLocaleString();
|
||||
const newPlayer = {
|
||||
id: playerId,
|
||||
name: name || 'Anonymous',
|
||||
mass: mass,
|
||||
playTime: playTime,
|
||||
level: levels?.total || 0,
|
||||
gameState: gameState,
|
||||
mass: gameState.blackHoleTotalMass || 0,
|
||||
level: (gameState.asteroidUpgradeLevel || 0) +
|
||||
(gameState.cometUpgradeLevel || 0) +
|
||||
(gameState.planetUpgradeLevel || 0) +
|
||||
(gameState.giantUpgradeLevel || 0),
|
||||
firstSeen: now,
|
||||
lastSeen: now,
|
||||
holeAge: 0
|
||||
});
|
||||
};
|
||||
|
||||
console.log(`${timestamp} Created new player: `, newPlayer.id);
|
||||
|
||||
leaderboard.push(newPlayer);
|
||||
}
|
||||
|
||||
// Sort by mass and keep top 100 (in JSON file)
|
||||
// Sort by mass and keep top 100
|
||||
leaderboard.sort((a, b) => b.mass - a.mass);
|
||||
leaderboard = leaderboard.slice(0, 100);
|
||||
|
||||
saveLeaderboard(leaderboard);
|
||||
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
@ -168,6 +207,20 @@ app.get('/api/player/:playerId', (req, res) => {
|
||||
res.json(player);
|
||||
});
|
||||
|
||||
// Get player's full game state
|
||||
app.get('/api/player/:playerId/state', (req, res) => {
|
||||
const playerId = req.params.playerId;
|
||||
let leaderboard = loadLeaderboard();
|
||||
|
||||
const player = leaderboard.find(p => p.id === playerId);
|
||||
|
||||
if (!player || !player.gameState) {
|
||||
return res.status(404).json({ error: 'Player not found' });
|
||||
}
|
||||
|
||||
res.json(player.gameState);
|
||||
});
|
||||
|
||||
// Manual cleanup endpoint (optional - for maintenance)
|
||||
app.post('/api/cleanup', (req, res) => {
|
||||
let leaderboard = loadLeaderboard();
|
||||
@ -194,9 +247,35 @@ app.get('/api/stats', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Cleanup bot accounts function
|
||||
function cleanupBotAccounts() {
|
||||
let leaderboard = loadLeaderboard();
|
||||
const beforeCount = leaderboard.length;
|
||||
|
||||
// Remove accounts with zero progress
|
||||
leaderboard = leaderboard.filter(player => {
|
||||
const hasProgress = player.gameState &&
|
||||
(player.gameState.totalMassConsumedEver > 0 ||
|
||||
player.level > 0);
|
||||
return hasProgress;
|
||||
});
|
||||
|
||||
if (leaderboard.length < beforeCount) {
|
||||
console.log(`Cleaned up ${beforeCount - leaderboard.length} bot/inactive accounts`);
|
||||
saveLeaderboard(leaderboard);
|
||||
}
|
||||
}
|
||||
|
||||
// Run cleanup daily (24 hours)
|
||||
setInterval(cleanupBotAccounts, 24 * 60 * 60 * 1000);
|
||||
|
||||
// Run cleanup on startup too
|
||||
cleanupBotAccounts();
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
console.log(`Players hidden from leaderboard after ${LEADERBOARD_VISIBLE_DAYS} days inactive`);
|
||||
console.log(`Players permanently removed after ${INACTIVE_DAYS} days inactive`);
|
||||
const timestamp = new Date().toLocaleString();
|
||||
console.log(`${timestamp} Server running on port ${PORT}`);
|
||||
console.log(`${timestamp} Players hidden from leaderboard after ${LEADERBOARD_VISIBLE_DAYS} days inactive`);
|
||||
console.log(`${timestamp} Players permanently removed after ${INACTIVE_DAYS} days inactive`);
|
||||
});
|
||||
|
||||
@ -24,83 +24,24 @@ var Storage = {
|
||||
document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Strict';
|
||||
},
|
||||
|
||||
// Simple checksum: sum of char codes
|
||||
checksum: function(str) {
|
||||
var sum = 0;
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
sum = (sum + str.charCodeAt(i)) % 65536;
|
||||
}
|
||||
return sum.toString(16).padStart(4, '0');
|
||||
},
|
||||
|
||||
saveGame: function(gameState) {
|
||||
var saveData = {
|
||||
blackHoleTotalMass: gameState.blackHoleTotalMass,
|
||||
totalMassConsumedEver: gameState.totalMassConsumedEver,
|
||||
totalMassConsumed: gameState.totalMassConsumed,
|
||||
asteroidUpgradeLevel: gameState.asteroidUpgradeLevel,
|
||||
asteroidUpgradeCost: gameState.asteroidUpgradeCost,
|
||||
cometUpgradeLevel: gameState.cometUpgradeLevel,
|
||||
cometUpgradeCost: gameState.cometUpgradeCost,
|
||||
planetUpgradeLevel: gameState.planetUpgradeLevel,
|
||||
planetUpgradeCost: gameState.planetUpgradeCost,
|
||||
giantUpgradeLevel: gameState.giantUpgradeLevel,
|
||||
giantUpgradeCost: gameState.giantUpgradeCost,
|
||||
asteroidSpawnCount: gameState.asteroidSpawnCount,
|
||||
lastAsteroidSpawn: gameState.lastAsteroidSpawn,
|
||||
lastCometSpawn: gameState.lastCometSpawn,
|
||||
lastPlanetSpawn: gameState.lastPlanetSpawn,
|
||||
lastGiantSpawn: gameState.lastGiantSpawn,
|
||||
sM: gameState.sM,
|
||||
sT: gameState.sT,
|
||||
mM: gameState.mM,
|
||||
mT: gameState.mT,
|
||||
lM: gameState.lM,
|
||||
lT: gameState.lT,
|
||||
rateShort: gameState.rateShort,
|
||||
rateMedium: gameState.rateMedium,
|
||||
rateLong: gameState.rateLong,
|
||||
savedAt: Date.now()
|
||||
};
|
||||
|
||||
var json = JSON.stringify(saveData);
|
||||
var chksum = this.checksum(json);
|
||||
var packaged = btoa(chksum + json); // prepend checksum, then base64
|
||||
this.setCookie('blackHoleSave', packaged, 365);
|
||||
|
||||
var saveStatus = document.getElementById('save-status');
|
||||
saveStatus.textContent = 'Game saved successfully!';
|
||||
setTimeout(function() { saveStatus.textContent = ''; }, 3000);
|
||||
},
|
||||
|
||||
loadGame: function() {
|
||||
var savedData = this.getCookie('blackHoleSave');
|
||||
if (!savedData) return null;
|
||||
|
||||
try {
|
||||
// try base64 decode
|
||||
var decoded = atob(savedData);
|
||||
|
||||
// new format: first 4 chars = checksum
|
||||
var chksum = decoded.slice(0, 4);
|
||||
var json = decoded.slice(4);
|
||||
|
||||
// verify checksum
|
||||
if (this.checksum(json) !== chksum) {
|
||||
console.warn('Save data integrity check failed.');
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(json);
|
||||
} catch (e) {
|
||||
// old save format (plain JSON)
|
||||
try { return JSON.parse(savedData); }
|
||||
catch { return null; }
|
||||
}
|
||||
},
|
||||
|
||||
resetGame: function() {
|
||||
this.deleteCookie('blackHoleSave');
|
||||
location.reload();
|
||||
if (confirm('Are you sure you want to reset? This will delete all your progress permanently.')) {
|
||||
var playerId = this.getCookie('playerId');
|
||||
|
||||
if (playerId && window.Server) {
|
||||
Server.deletePlayer(playerId).then(function() {
|
||||
Storage.deleteCookie('playerId');
|
||||
|
||||
setTimeout(function() {
|
||||
window.location.href = window.location.href.split('?')[0] + '?nocache=' + Date.now();
|
||||
}, 200);
|
||||
});
|
||||
} else {
|
||||
Storage.deleteCookie('playerId');
|
||||
setTimeout(function() {
|
||||
window.location.reload(true);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
391
js/ui.js
391
js/ui.js
@ -13,10 +13,12 @@ var UI = {
|
||||
cometLevel: document.getElementById('comet-level'),
|
||||
planetLevel: document.getElementById('planet-level'),
|
||||
giantLevel: document.getElementById('giant-level'),
|
||||
mtypeLevel: document.getElementById('mtype-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'),
|
||||
gearIcon: document.getElementById('gear-icon'),
|
||||
settingsMenu: document.getElementById('settings-menu'),
|
||||
resetBtn: document.getElementById('reset-btn'),
|
||||
@ -25,7 +27,8 @@ var UI = {
|
||||
leaderboardPanel: document.getElementById('leaderboardPanel'),
|
||||
leaderboardList: document.getElementById('leaderboardList'),
|
||||
sortMass: document.getElementById('sortMass'),
|
||||
sortAge: document.getElementById('sortAge')
|
||||
sortAge: document.getElementById('sortAge'),
|
||||
massRate: document.getElementById('massRate')
|
||||
};
|
||||
|
||||
// Sorting buttons
|
||||
@ -41,26 +44,34 @@ var UI = {
|
||||
const settingsMenu = self.elements.settingsMenu;
|
||||
const leaderboardToggle = self.elements.leaderboardToggle;
|
||||
const leaderboardPanel = self.elements.leaderboardPanel;
|
||||
|
||||
// ---------- Settings Menu ----------
|
||||
if (gear && settingsMenu) {
|
||||
// Toggle settings menu
|
||||
gear.addEventListener('click', (e) => {
|
||||
e.stopPropagation(); // prevent document click from immediately closing
|
||||
settingsMenu.classList.toggle('open');
|
||||
const resetBtn = self.elements.resetBtn;
|
||||
const holeIcon = document.getElementById('hole-icon');
|
||||
const upgradePanel = document.getElementById('upgrade-panel');
|
||||
|
||||
// ---------- Upgrade Panel ----------
|
||||
if (holeIcon && upgradePanel) {
|
||||
// Toggle upgrade panel
|
||||
holeIcon.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (settingsMenu) settingsMenu.classList.remove('open');
|
||||
if (leaderboardPanel) leaderboardPanel.classList.remove('show');
|
||||
upgradePanel.classList.toggle('open');
|
||||
});
|
||||
|
||||
// Prevent clicks inside menu from closing it
|
||||
settingsMenu.addEventListener('click', (e) => {
|
||||
// Prevent clicks inside panel from closing it
|
||||
upgradePanel.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ---------- Leaderboard ----------
|
||||
if (leaderboardToggle && leaderboardPanel) {
|
||||
// Toggle leaderboard panel
|
||||
leaderboardToggle.addEventListener('click', (e) => {
|
||||
e.stopPropagation(); // prevent document click
|
||||
if (settingsMenu) settingsMenu.classList.remove('open');
|
||||
if (upgradePanel) upgradePanel.classList.remove('open');
|
||||
leaderboardPanel.classList.toggle('show');
|
||||
|
||||
if (leaderboardPanel.classList.contains('show')) {
|
||||
@ -73,6 +84,27 @@ var UI = {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', function() {
|
||||
Storage.resetGame();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Settings Menu ----------
|
||||
if (gear && settingsMenu) {
|
||||
// Toggle settings menu
|
||||
gear.addEventListener('click', (e) => {
|
||||
e.stopPropagation(); // prevent document click from immediately closing
|
||||
if (upgradePanel) upgradePanel.classList.remove('open');
|
||||
if (leaderboardPanel) leaderboardPanel.classList.remove('show');
|
||||
settingsMenu.classList.toggle('open');
|
||||
});
|
||||
|
||||
// Prevent clicks inside menu from closing it
|
||||
settingsMenu.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Click outside to close ----------
|
||||
document.addEventListener('click', () => {
|
||||
@ -81,9 +113,22 @@ var UI = {
|
||||
settingsMenu.classList.remove('open');
|
||||
}
|
||||
|
||||
// Close leaderboard panel
|
||||
if (leaderboardPanel) {
|
||||
leaderboardPanel.classList.remove('show');
|
||||
// Close leaderboard panel
|
||||
if (leaderboardPanel) {
|
||||
leaderboardPanel.classList.remove('show');
|
||||
// Clear refresh intervals when closing
|
||||
if (this._leaderboardRefreshInterval) {
|
||||
clearInterval(this._leaderboardRefreshInterval);
|
||||
this._leaderboardRefreshInterval = null;
|
||||
}
|
||||
if (this._leaderboardIndicatorInterval) {
|
||||
clearInterval(this._leaderboardIndicatorInterval);
|
||||
this._leaderboardIndicatorInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (upgradePanel) {
|
||||
upgradePanel.classList.remove('open');
|
||||
}
|
||||
});
|
||||
},
|
||||
@ -93,6 +138,7 @@ var UI = {
|
||||
this.elements.cometUpgradeBtn.addEventListener('click', handlers.comet);
|
||||
this.elements.planetUpgradeBtn.addEventListener('click', handlers.planet);
|
||||
this.elements.giantUpgradeBtn.addEventListener('click', handlers.giant);
|
||||
this.elements.mtypeUpgradeBtn.addEventListener('click', handlers.mtype);
|
||||
},
|
||||
|
||||
update: function(gameState, config) {
|
||||
@ -101,22 +147,13 @@ var UI = {
|
||||
'Available to Spend: ' + this.formatMass(gameState.totalMassConsumed);
|
||||
}
|
||||
this.updateMassDisplay(gameState, config);
|
||||
this.updateRateDisplay(gameState, config);
|
||||
this.updateUpgradeDisplay(gameState);
|
||||
this.updateTotalLevel(gameState);
|
||||
this.updateActiveObjects();
|
||||
},
|
||||
|
||||
|
||||
updateMassDisplay: function(state, config) {
|
||||
// Format rates
|
||||
const rateShortText = this.formatMass(state.rateShort) + '/s';
|
||||
const rateMediumText = this.formatMass(state.rateMedium * 3600) + '/h';
|
||||
const rateLongText = this.formatMass(state.rateLong * 86400) + '/d';
|
||||
|
||||
// Use span with class based on trend
|
||||
const rateText = `
|
||||
<span class="${'rate-' + (state.prevRateShortTrend || 'same')}">${rateShortText}</span>,
|
||||
<span class="${'rate-' + (state.prevRateMediumTrend || 'same')}">${rateMediumText}</span>,
|
||||
<span class="${'rate-' + (state.prevRateLongTrend || 'same')}">${rateLongText}</span>
|
||||
`;
|
||||
|
||||
const bhMassText = this.formatMass(state.blackHoleTotalMass, { forceSolarMass: true });
|
||||
const consumedText = this.formatMass(state.totalMassConsumedEver);
|
||||
@ -130,17 +167,102 @@ var UI = {
|
||||
Total mass absorbed:<br>
|
||||
${consumedText}
|
||||
</span>
|
||||
</span><br>
|
||||
<span style="font-size:10px; opacity:0.7;">Rate: ${rateText}</span>
|
||||
`;
|
||||
},
|
||||
|
||||
updateRateDisplay: function(state, config) {
|
||||
const now = Date.now();
|
||||
|
||||
// Format per-second rate (always real)
|
||||
const rateShortText = this.formatMass(state.rateShort || 0) + '/s';
|
||||
|
||||
// Check if we have enough data for hourly average
|
||||
const hourlyElapsed = now - (state.mT || now);
|
||||
let rateMediumText;
|
||||
if (hourlyElapsed < 300000) { // Less than 5 minutes of data
|
||||
// Use estimated value from per-second rate
|
||||
rateMediumText = '~' + this.formatMass((state.rateShort || 0) * 3600) + '/h';
|
||||
} else {
|
||||
// Use real average
|
||||
rateMediumText = this.formatMass((state.rateMedium || 0) * 3600) + '/h';
|
||||
}
|
||||
|
||||
// Check if we have enough data for daily average
|
||||
const dailyElapsed = now - (state.lT || now);
|
||||
let rateLongText;
|
||||
if (dailyElapsed < 3600000) { // Less than 1 hour of data
|
||||
// Use estimated value from best available rate
|
||||
const baseRate = (hourlyElapsed >= 300000) ? state.rateMedium : state.rateShort;
|
||||
rateLongText = '~' + this.formatMass((baseRate || 0) * 86400) + '/d';
|
||||
} else {
|
||||
// Use real average
|
||||
rateLongText = this.formatMass((state.rateLong || 0) * 86400) + '/d';
|
||||
}
|
||||
|
||||
// Only color the per-second rate based on trend
|
||||
const rateText = `
|
||||
<span class="${'rate-' + (state.rateShortTrend || 'same')}">${rateShortText}</span>
|
||||
</br><span> ${rateMediumText}</span>
|
||||
</br><span> ${rateLongText}</span>
|
||||
`;
|
||||
|
||||
this.elements.massRate.innerHTML = `
|
||||
<span> Rates: ${rateText}</span>
|
||||
`;
|
||||
},
|
||||
|
||||
updateUpgradeDisplay: function(gameState) {
|
||||
this.updateAsteroidUpgrade(gameState);
|
||||
this.updateCometUpgrade(gameState);
|
||||
this.updatePlanetUpgrade(gameState);
|
||||
this.updateGiantUpgrade(gameState);
|
||||
|
||||
// Only show comet upgrade if unlocked
|
||||
if (gameState.cometUnlocked) {
|
||||
this.updateCometUpgrade(gameState);
|
||||
this.elements.cometLevel.parentElement.style.display = '';
|
||||
} else {
|
||||
this.elements.cometLevel.parentElement.style.display = 'none';
|
||||
}
|
||||
|
||||
// Only show planet upgrade if unlocked
|
||||
if (gameState.planetUnlocked) {
|
||||
this.updatePlanetUpgrade(gameState);
|
||||
this.elements.planetLevel.parentElement.style.display = '';
|
||||
} else {
|
||||
this.elements.planetLevel.parentElement.style.display = 'none';
|
||||
}
|
||||
|
||||
// Only show giant upgrade if unlocked
|
||||
if (gameState.giantUnlocked) {
|
||||
this.updateGiantUpgrade(gameState);
|
||||
this.elements.giantLevel.parentElement.style.display = '';
|
||||
} else {
|
||||
this.elements.giantLevel.parentElement.style.display = 'none';
|
||||
}
|
||||
|
||||
// Only show mtype upgrade if unlocked
|
||||
if (gameState.mtypeUnlocked) {
|
||||
this.updateMtypeUpgrade(gameState);
|
||||
this.elements.mtypeLevel.parentElement.style.display = '';
|
||||
} else {
|
||||
this.elements.mtypeLevel.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);
|
||||
|
||||
var holeIcon = document.getElementById('hole-icon');
|
||||
if (holeIcon) {
|
||||
if (canAffordAny) {
|
||||
holeIcon.classList.add('pulse');
|
||||
} else {
|
||||
holeIcon.classList.remove('pulse');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateTotalLevel: function(gameState) {
|
||||
var el = this.elements.totalLevel;
|
||||
if (!el) return;
|
||||
@ -160,6 +282,36 @@ var UI = {
|
||||
'</span>';
|
||||
},
|
||||
|
||||
updateActiveObjects: function() {
|
||||
var el = document.getElementById('active-objects');
|
||||
if (!el) return;
|
||||
|
||||
var count = window.gameAsteroids ? window.gameAsteroids.length : 0;
|
||||
var now = Date.now();
|
||||
|
||||
// Initialize tracking
|
||||
if (this._lastObjectCount === undefined) {
|
||||
this._lastObjectCount = count;
|
||||
this._objectTrend = 'same';
|
||||
this._lastObjectCheck = now;
|
||||
}
|
||||
|
||||
// Only update trend once per second to avoid flicker
|
||||
if (now - this._lastObjectCheck >= 1000) {
|
||||
if (count > this._lastObjectCount) {
|
||||
this._objectTrend = 'up';
|
||||
} else if (count < this._lastObjectCount) {
|
||||
this._objectTrend = 'down';
|
||||
} else {
|
||||
this._objectTrend = 'same';
|
||||
}
|
||||
this._lastObjectCount = count;
|
||||
this._lastObjectCheck = now;
|
||||
}
|
||||
|
||||
// Apply trend color
|
||||
el.innerHTML = 'Objects: <span class="rate-' + this._objectTrend + '">' + count + '</span>';
|
||||
},
|
||||
|
||||
updateAsteroidUpgrade: function(gameState) {
|
||||
var rate = (1 / gameState.currentAsteroidSpawnInterval * 1000).toFixed(2);
|
||||
@ -224,36 +376,49 @@ var UI = {
|
||||
this.elements.giantUpgradeBtn.disabled =
|
||||
gameState.totalMassConsumed < gameState.giantUpgradeCost;
|
||||
},
|
||||
|
||||
formatMass: function(massKg, options = {}) {
|
||||
// If forcing solar mass display (for black hole)
|
||||
if (options.forceSolarMass) {
|
||||
var solarMasses = massKg / CONFIG.SOLAR_MASS_KG;
|
||||
return solarMasses.toFixed(3) + ' M☉';
|
||||
}
|
||||
|
||||
// Traditional engineering/astronomical units
|
||||
if (massKg >= 1e18) {
|
||||
return (massKg / 1e18).toFixed(2) + ' Et'; // Exatonnes
|
||||
}
|
||||
if (massKg >= 1e15) {
|
||||
return (massKg / 1e15).toFixed(2) + ' Pt'; // Petatonnes
|
||||
}
|
||||
if (massKg >= 1e12) {
|
||||
return (massKg / 1e12).toFixed(2) + ' Tt'; // Teratonnes
|
||||
}
|
||||
if (massKg >= 1e9) {
|
||||
return (massKg / 1e9).toFixed(2) + ' Gt'; // Gigatonnes
|
||||
}
|
||||
if (massKg >= 1e6) {
|
||||
return (massKg / 1e6).toFixed(2) + ' Mt'; // Megatonnes
|
||||
}
|
||||
if (massKg >= 1e3) {
|
||||
return (massKg / 1e3).toFixed(2) + ' tonnes'; // Tonnes
|
||||
}
|
||||
return massKg.toFixed(0) + ' kg'; // Kilograms
|
||||
|
||||
updateMtypeUpgrade: function(gameState) {
|
||||
var rate = (86400000 / gameState.currentMtypeSpawnInterval).toFixed(2);
|
||||
var bonusPercent = (gameState.mtypeUpgradeLevel * 0.5);
|
||||
var tooltipText = 'Spawn Rate: ' + rate + '/day <br>Bonus: ' + bonusPercent + '%';
|
||||
this.elements.mtypeLevel.innerHTML = '<span class="tooltip-trigger">M-Type: Level ' +
|
||||
gameState.mtypeUpgradeLevel +
|
||||
'<span class="tooltip">' + tooltipText + '</span></span>';
|
||||
this.elements.mtypeUpgradeBtn.textContent =
|
||||
'Upgrade (Cost: ' + this.formatMass(gameState.mtypeUpgradeCost) + ')';
|
||||
this.elements.mtypeUpgradeBtn.disabled =
|
||||
gameState.totalMassConsumed < gameState.mtypeUpgradeCost;
|
||||
},
|
||||
|
||||
formatMass: function(massKg, options = {}) {
|
||||
if (massKg == null) return '0'; // fallback
|
||||
const SOLAR_SWITCH_KG = CONFIG.SOLAR_MASS_KG * 0.01; // ~1% solar mass
|
||||
|
||||
// Forced solar mass display (e.g. black holes)
|
||||
if (options.forceSolarMass || massKg >= SOLAR_SWITCH_KG) {
|
||||
var solarMasses = massKg / CONFIG.SOLAR_MASS_KG;
|
||||
|
||||
// Precision scales nicely with magnitude
|
||||
if (solarMasses >= 1000) return solarMasses.toFixed(0) + ' M☉';
|
||||
if (solarMasses >= 1) return solarMasses.toFixed(3) + ' M☉';
|
||||
return solarMasses.toFixed(5) + ' M☉';
|
||||
}
|
||||
|
||||
// SI mass units (tonnes-based)
|
||||
if (massKg >= 1e30) return (massKg / 1e30).toFixed(2) + ' Qt'; // Quettatonnes
|
||||
if (massKg >= 1e27) return (massKg / 1e27).toFixed(2) + ' Rt'; // Ronnatonnes
|
||||
if (massKg >= 1e24) return (massKg / 1e24).toFixed(2) + ' Yt'; // Yottatonnes
|
||||
if (massKg >= 1e21) return (massKg / 1e21).toFixed(2) + ' Zt'; // Zettatonnes
|
||||
if (massKg >= 1e18) return (massKg / 1e18).toFixed(2) + ' Et'; // Exatonnes
|
||||
if (massKg >= 1e15) return (massKg / 1e15).toFixed(2) + ' Pt'; // Petatonnes
|
||||
if (massKg >= 1e12) return (massKg / 1e12).toFixed(2) + ' Tt'; // Teratonnes
|
||||
if (massKg >= 1e9) return (massKg / 1e9).toFixed(2) + ' Gt'; // Gigatonnes
|
||||
if (massKg >= 1e6) return (massKg / 1e6).toFixed(2) + ' Mt'; // Megatonnes
|
||||
if (massKg >= 1e3) return (massKg / 1e3).toFixed(2) + ' tonnes'; // Tonnes
|
||||
|
||||
return massKg.toFixed(0) + ' kg';
|
||||
},
|
||||
|
||||
formatTime: function(ms) {
|
||||
var seconds = Math.floor(ms / 1000);
|
||||
var minutes = Math.floor(seconds / 60);
|
||||
@ -276,9 +441,9 @@ var UI = {
|
||||
var hours = Math.floor((ms % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000));
|
||||
|
||||
if (days > 0) {
|
||||
return days + 'd ' + hours + 'h old';
|
||||
return days + 'd ' + hours + 'h';
|
||||
} else if (hours > 0) {
|
||||
return hours + 'h old';
|
||||
return hours + 'h';
|
||||
} else {
|
||||
return 'New';
|
||||
}
|
||||
@ -287,43 +452,87 @@ var UI = {
|
||||
|
||||
openLeaderboard: async function(sortBy = 'mass') {
|
||||
const container = this.elements.leaderboardList;
|
||||
container.innerHTML = '<div style="text-align:center; padding:20px;">Loading...</div>';
|
||||
|
||||
const localPlayerId = Server.playerId;
|
||||
|
||||
// Show loading only on first load
|
||||
if (!this._cachedLeaderboardData) {
|
||||
container.innerHTML = '<div style="text-align:center; padding:20px;">Loading...</div>';
|
||||
}
|
||||
|
||||
// Fetch leaderboard
|
||||
let data = await Server.getLeaderboard(sortBy);
|
||||
if (!data.length) {
|
||||
container.innerHTML = '<div style="text-align:center; padding:20px;">No entries yet</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort data server-side should already be sorted; else fallback:
|
||||
data.sort((a,b) => sortBy==='mass'?b.mass-a.mass:b.holeAge-a.holeAge);
|
||||
const localPlayerId = Server.playerId; // your local player ID
|
||||
|
||||
// Build entries
|
||||
let html = '';
|
||||
data.forEach((entry, i) => {
|
||||
const rank = i + 1;
|
||||
const medal = rank===1?'🥇':rank===2?'🥈':rank===3?'🥉':rank+'.';
|
||||
const isLocal = entry.id === localPlayerId;
|
||||
|
||||
// Online indicator (green dot if active in last 15 min)
|
||||
const onlineIndicator = entry.isOnline ? '<span class="online-indicator"></span>' : '';
|
||||
const nameSuffix = isLocal ? '💗' : '';
|
||||
|
||||
// Use last 5 chars of playerId if available, otherwise fallback
|
||||
const displayName = entry.id
|
||||
? entry.id.slice(-5)
|
||||
: 'Anon';
|
||||
|
||||
html += `<div class="leaderboard-entry${isLocal ? ' local-player' : ''}">
|
||||
<span class="rank">${medal}</span>
|
||||
<span class="name">${this.escapeHtml(displayName)}${onlineIndicator}${nameSuffix}</span>
|
||||
<span class="value mass${sortBy==='mass'?' sorted':''}">${this.formatMass(entry.mass,{forceSolarMass:true})}</span>
|
||||
<span class="value age${sortBy==='age'?' sorted':''}">${this.formatAge(entry.holeAge)}</span>
|
||||
</div>`;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
|
||||
this._cachedLeaderboardData = data;
|
||||
this._currentSort = sortBy;
|
||||
|
||||
// Render function (can be called independently)
|
||||
const render = () => {
|
||||
const now = Date.now();
|
||||
const fifteenMinutes = 15 * 60 * 1000;
|
||||
|
||||
let html = '';
|
||||
data.forEach((entry, i) => {
|
||||
const rank = i + 1;
|
||||
const medal = rank===1?'🥇':rank===2?'🥈':rank===3?'🥉':rank+'.';
|
||||
const isLocal = entry.id === localPlayerId;
|
||||
|
||||
// Calculate online status in real-time
|
||||
const isOnline = (now - entry.lastSeen) < fifteenMinutes;
|
||||
const onlineIndicator = isOnline ? '<span class="online-indicator"></span>' : '';
|
||||
const nameSuffix = isLocal ? '💗' : '';
|
||||
|
||||
const displayName = entry.id ? entry.id.slice(-5) : 'Anon';
|
||||
|
||||
// Calculate time since last seen
|
||||
const timeSinceLastSeen = now - entry.lastSeen;
|
||||
const lastSeenText = formatTimeSince(timeSinceLastSeen);
|
||||
|
||||
html += `<div class="leaderboard-entry${isLocal ? ' local-player' : ''}">
|
||||
<span class="rank">${medal}</span>
|
||||
<span class="name">${this.escapeHtml(displayName)}${onlineIndicator}${nameSuffix}</span>
|
||||
<span class="value last-seen">${lastSeenText}</span>
|
||||
<span class="value mass${sortBy==='mass'?' sorted':''}">${this.formatMass(entry.mass,{forceSolarMass:true})}</span>
|
||||
<span class="value age${sortBy==='age'?' sorted':''}">${this.formatAge(entry.holeAge)}</span>
|
||||
</div>`;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
};
|
||||
|
||||
render();
|
||||
|
||||
// Auto-refresh data every 30 seconds
|
||||
if (this._leaderboardRefreshInterval) {
|
||||
clearInterval(this._leaderboardRefreshInterval);
|
||||
}
|
||||
|
||||
this._leaderboardRefreshInterval = setInterval(async () => {
|
||||
if (this.elements.leaderboardPanel.classList.contains('show')) {
|
||||
// Re-fetch data
|
||||
data = await Server.getLeaderboard(sortBy);
|
||||
this._cachedLeaderboardData = data;
|
||||
} else {
|
||||
clearInterval(this._leaderboardRefreshInterval);
|
||||
this._leaderboardRefreshInterval = null;
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
// Re-render indicators every 5 seconds (lightweight, no API call)
|
||||
if (this._leaderboardIndicatorInterval) {
|
||||
clearInterval(this._leaderboardIndicatorInterval);
|
||||
}
|
||||
|
||||
this._leaderboardIndicatorInterval = setInterval(() => {
|
||||
if (this.elements.leaderboardPanel.classList.contains('show')) {
|
||||
render(); // Just re-render with updated time calculations
|
||||
} else {
|
||||
clearInterval(this._leaderboardIndicatorInterval);
|
||||
this._leaderboardIndicatorInterval = null;
|
||||
}
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
escapeHtml: function(text) {
|
||||
|
||||
310
style.css
310
style.css
@ -19,28 +19,127 @@ canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#gear-icon {
|
||||
/* STATS */
|
||||
#stats-panel {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 12px;
|
||||
text-shadow:
|
||||
0 0 1px rgba(0, 0, 0, 1),
|
||||
0 0 2px rgba(0, 0, 0, 1),
|
||||
0 0 3px rgba(0, 0, 0, 1),
|
||||
0 0 4px rgba(0, 0, 0, 1),
|
||||
0 0 8px rgba(0, 0, 0, 1),
|
||||
0 0 16px rgba(0, 0, 0, 1);
|
||||
pointer-events: none;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.tooltip-trigger {
|
||||
cursor: help;
|
||||
pointer-events: auto;
|
||||
border-bottom: 1px dotted rgba(255, 255, 255, 0.3);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background: rgba(20, 20, 30, 0.3);
|
||||
backdrop-filter: blur(5px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
width: 200px;
|
||||
z-index: 1000;
|
||||
line-height: 1.4;
|
||||
left: 0;
|
||||
top: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.tooltip-trigger:hover .tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.rate-up {
|
||||
color: rgba(100, 255, 100, 0.9);
|
||||
transition: color 0.5s ease;
|
||||
}
|
||||
|
||||
.rate-down {
|
||||
color: rgba(255, 100, 100, 0.9);
|
||||
transition: color 0.5s ease;
|
||||
}
|
||||
|
||||
.rate-same {
|
||||
color: rgba(200, 200, 200, 0.8);
|
||||
transition: color 0.5s ease;
|
||||
}
|
||||
|
||||
/* ICONS */
|
||||
#gear-icon,
|
||||
.score-icon,
|
||||
#hole-icon {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
opacity: 0.3;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#gear-icon:hover {
|
||||
opacity: 0.6;
|
||||
#gear-icon {
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.score-icon {
|
||||
right: 50px;
|
||||
}
|
||||
|
||||
#hole-icon {
|
||||
top: 16px;
|
||||
right: 80px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
#hole-icon.pulse {
|
||||
animation: holepulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes holepulse {
|
||||
0%, 100% {
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
#hole-icon.pulse:hover {
|
||||
}
|
||||
|
||||
#gear-icon:hover,
|
||||
.score-icon:hover,
|
||||
#hole-icon:hover {
|
||||
opacity: 0.69;
|
||||
}
|
||||
|
||||
|
||||
/* SETTINGS */
|
||||
#settings-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 20px;
|
||||
background: rgba(20, 20, 30, 0.95);
|
||||
background: rgba(20, 20, 30, 0.3);
|
||||
backdrop-filter: blur(2px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
@ -69,7 +168,7 @@ canvas {
|
||||
|
||||
.menu-btn {
|
||||
background: rgba(60, 60, 80, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 0, 0, 0.42);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
padding: 6px 12px;
|
||||
margin-top: 8px;
|
||||
@ -80,69 +179,39 @@ canvas {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.menu-btn:hover {
|
||||
background: rgba(80, 80, 100, 0.6);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.menu-btn.danger:hover {
|
||||
background: rgba(120, 40, 40, 0.6);
|
||||
border-color: rgba(255, 100, 100, 0.3);
|
||||
background: rgba(255, 0, 0, 0.42);
|
||||
border-color: rgba(255, 0, 0, 0.69);
|
||||
}
|
||||
|
||||
#stats-panel {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.tooltip-trigger {
|
||||
cursor: help;
|
||||
pointer-events: auto;
|
||||
border-bottom: 1px dotted rgba(255, 255, 255, 0.3);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background: rgba(20, 20, 30, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
width: 200px;
|
||||
z-index: 1000;
|
||||
line-height: 1.4;
|
||||
left: 0;
|
||||
top: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.tooltip-trigger:hover .tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* UPGRADES*/
|
||||
#upgrade-panel {
|
||||
position: absolute;
|
||||
top: 90px;
|
||||
left: 20px;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-size: 12px;
|
||||
top: 50px;
|
||||
right: 20px;
|
||||
display: none;
|
||||
background: rgba(20, 20, 30, 0.3);
|
||||
backdrop-filter: blur(2px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 11px;
|
||||
width: 280px;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
#upgrade-panel.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.upgrade-row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upgrade-btn {
|
||||
background: rgba(60, 60, 80, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(0, 255, 0, 0.42);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
padding: 4px 8px;
|
||||
margin-top: 4px;
|
||||
@ -157,16 +226,17 @@ canvas {
|
||||
|
||||
.upgrade-btn:disabled {
|
||||
cursor: not-allowed;
|
||||
border: 1px solid rgba(0, 0, 0, 0.69);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* SCORES */
|
||||
.modal {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
top: 50px;
|
||||
right: 20px;
|
||||
display: none;
|
||||
width: 360px;
|
||||
z-index: 5000;
|
||||
z-index: 999;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@ -175,7 +245,8 @@ canvas {
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: rgba(20, 20, 30, 0.95);
|
||||
background: rgba(20, 20, 30, 0.3);
|
||||
backdrop-filter: blur(2px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
@ -184,34 +255,30 @@ canvas {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.leaderboard-title,
|
||||
.leaderboard-entry {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 90px 60px 90px 60px;
|
||||
column-gap: 8px;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
margin-bottom: 4px;
|
||||
background: rgba(10, 10, 10, 0.3);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.leaderboard-title {
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #333;
|
||||
text-transform: uppercase;
|
||||
.leaderboard-entry {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.leaderboard-entry .rank {
|
||||
width: 40px;
|
||||
font-weight: bold;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.leaderboard-entry .name {
|
||||
flex: 1;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -221,8 +288,6 @@ canvas {
|
||||
|
||||
.leaderboard-entry .value {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
min-width: 80px;
|
||||
text-align: right;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@ -261,37 +326,6 @@ canvas {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.rate-up {
|
||||
color: rgba(100, 255, 100, 0.9);
|
||||
transition: color 0.5s ease;
|
||||
}
|
||||
|
||||
.rate-down {
|
||||
color: rgba(255, 100, 100, 0.9);
|
||||
transition: color 0.5s ease;
|
||||
}
|
||||
|
||||
.rate-same {
|
||||
color: rgba(200, 200, 200, 0.8);
|
||||
transition: color 0.5s ease;
|
||||
}
|
||||
|
||||
.score-icon {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.2s;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.score-icon:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.online-indicator {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
@ -314,3 +348,71 @@ canvas {
|
||||
box-shadow: 0 0 4px #00ff00, 0 0 6px #00ff00;
|
||||
}
|
||||
}
|
||||
|
||||
/* Base notification styles */
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 20px 30px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
z-index: 10000;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
animation: notificationFadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Notification types */
|
||||
.notification.unlock {
|
||||
background: rgba(20, 20, 30, 0.95);
|
||||
border: 1px solid rgba(0, 255, 255, 0.8);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
padding: 20px 40px;
|
||||
box-shadow: 0 0 20px rgba(0, 255, 255, 0.42);
|
||||
}
|
||||
|
||||
.notification.offline {
|
||||
background: rgba(20, 20, 30, 0.95);
|
||||
border: 1px solid rgba(255, 255, 0, 0.8);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 0 20px rgba(255, 255, 0, 0.42);
|
||||
}
|
||||
|
||||
.notification.error {
|
||||
background: rgba(20, 20, 30, 0.95);
|
||||
border: 1px solid rgba(255, 0, 0, 0.8);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-transform: uppercase;
|
||||
box-shadow: 0 0 20px rgba(255, 0, 0, 0.42);
|
||||
}
|
||||
|
||||
.notification.success {
|
||||
background: rgba(20, 20, 30, 0.95);
|
||||
border: 1px solid rgba(0, 255, 0, 0.8);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-transform: uppercase;
|
||||
box-shadow: 0 0 20px rgba(0, 255, 0, 0.42);
|
||||
}
|
||||
|
||||
.notification.info {
|
||||
background: rgba(20, 20, 30, 0.95);
|
||||
border: 1px solid rgba(180, 180, 200, 0.8);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Animation */
|
||||
@keyframes notificationFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user