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 |
36
index.html
36
index.html
@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>hoel</title>
|
<title>hoel</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link rel="icon" href="favicon.png" type="image/png">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<svg id="gear-icon" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1">
|
<svg id="gear-icon" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1">
|
||||||
@ -40,10 +41,13 @@ a1.7 1.7 0 0 0-1.5 1z"></path>
|
|||||||
<div class="menu-title">Game Settings</div>
|
<div class="menu-title">Game Settings</div>
|
||||||
<div class="menu-text">
|
<div class="menu-text">
|
||||||
This game uses a browser cookie to save your progress across sessions.
|
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>
|
||||||
<div class="menu-text">
|
<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>
|
</div>
|
||||||
<button id="reset-btn" class="menu-btn danger">Reset</button>
|
<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 id="save-status" style="margin-top: 12px; font-size: 10px; color: rgba(100, 200, 100, 0.6);"></div>
|
||||||
@ -51,11 +55,14 @@ a1.7 1.7 0 0 0-1.5 1z"></path>
|
|||||||
|
|
||||||
<div id="stats-panel">
|
<div id="stats-panel">
|
||||||
<div id="total-mass"></div>
|
<div id="total-mass"></div>
|
||||||
</br>
|
<br>
|
||||||
<div id="total-level" class="stat-line"></div>
|
<div id="active-objects"></div>
|
||||||
|
<span id="massRate"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="upgrade-panel">
|
<div id="upgrade-panel">
|
||||||
|
<div class="menu-title">Upgrades</div>
|
||||||
|
<div id="total-level" class="stat-line"></div>
|
||||||
<div class="upgrade-row">
|
<div class="upgrade-row">
|
||||||
</br>
|
</br>
|
||||||
<div id="spendable-mass">Available to Spend: 0 kg</div>
|
<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>
|
<div id="giant-level"></div>
|
||||||
<button id="giant-upgrade-btn" class="upgrade-btn" disabled>Upgrade (Cost: 1000 tonnes)</button>
|
<button id="giant-upgrade-btn" class="upgrade-btn" disabled>Upgrade (Cost: 1000 tonnes)</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="upgrade-row">
|
||||||
|
<div id="mtype-level"></div>
|
||||||
|
<button id="mtype-upgrade-btn" class="upgrade-btn" disabled>Upgrade</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Leaderboard Toggle -->
|
<!-- Leaderboard Toggle -->
|
||||||
@ -106,16 +117,23 @@ a1.7 1.7 0 0 0-1.5 1z"></path>
|
|||||||
<!-- Leaderboard Panel -->
|
<!-- Leaderboard Panel -->
|
||||||
<div id="leaderboardPanel" class="modal">
|
<div id="leaderboardPanel" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="leaderboard-title">
|
<div class="menu-title">Neighbour Chart</div>
|
||||||
<span class="rank" style="width:20px;"></span>
|
<div class="leaderboard-title" style="">
|
||||||
<span class="name" style="flex:1; min-width:80px;">Name</span>
|
<span class="rank"></span>
|
||||||
<span id="sortMass" class="value mass" style="min-width: 115px; cursor:pointer">Mass</span>
|
<span class="name">Name</span>
|
||||||
<span id="sortAge" class="value age" style="padding-right: 30px; cursor:pointer">Age</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>
|
||||||
<div id="leaderboardList"></div>
|
<div id="leaderboardList"></div>
|
||||||
</div>
|
</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>
|
<canvas id="space"></canvas>
|
||||||
|
|
||||||
<script src="js/config.js"></script>
|
<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 values
|
||||||
INITIAL_BLACK_HOLE_MASS: 42, // in solar masses
|
INITIAL_BLACK_HOLE_MASS: 42, // in solar masses
|
||||||
INITIAL_BLACK_HOLE_RADIUS: 42,
|
INITIAL_BLACK_HOLE_RADIUS: 21,
|
||||||
|
|
||||||
// Performance
|
// Performance
|
||||||
LOGIC_INTERVAL: 10,
|
LOGIC_INTERVAL: 10,
|
||||||
@ -20,6 +20,7 @@ var CONFIG = {
|
|||||||
BASE_COMET_SPAWN_INTERVAL: 120000, // 2 minutes
|
BASE_COMET_SPAWN_INTERVAL: 120000, // 2 minutes
|
||||||
BASE_PLANET_SPAWN_INTERVAL: 3600000, // 1 hour
|
BASE_PLANET_SPAWN_INTERVAL: 3600000, // 1 hour
|
||||||
BASE_GIANT_SPAWN_INTERVAL: 21600000, // 6 hours
|
BASE_GIANT_SPAWN_INTERVAL: 21600000, // 6 hours
|
||||||
|
ASE_MTYPE_SPAWN_INTERVAL: 86400000, // 1 day
|
||||||
|
|
||||||
// Rate tracking windows (in milliseconds)
|
// Rate tracking windows (in milliseconds)
|
||||||
RATE_WINDOWS: {
|
RATE_WINDOWS: {
|
||||||
@ -29,15 +30,24 @@ var CONFIG = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Upgrade costs (in kg)
|
// Upgrade costs (in kg)
|
||||||
ASTEROID_BASE_COST: 1e3,
|
ASTEROID_BASE_COST: 1e5,
|
||||||
COMET_BASE_COST: 1e4,
|
COMET_BASE_COST: 1e7,
|
||||||
PLANET_BASE_COST: 1e5,
|
PLANET_BASE_COST: 1e24,
|
||||||
GIANT_BASE_COST: 1e6,
|
GIANT_BASE_COST: 1e27,
|
||||||
|
MTYPE_BASE_COST: 1e28,
|
||||||
|
|
||||||
// Upgrade scaling
|
// Upgrade scaling
|
||||||
UPGRADE_COST_MULTIPLIER: 1.5,
|
|
||||||
UPGRADE_COST_MULTIPLIER_ASTER: 1.25,
|
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
|
||||||
ASTEROID_SPAWN_PATTERNS: {
|
ASTEROID_SPAWN_PATTERNS: {
|
||||||
@ -46,12 +56,13 @@ var CONFIG = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
ASTEROID_MASS_RANGES: {
|
ASTEROID_MASS_RANGES: {
|
||||||
small: [1e3, 5e3], // 1-5 t
|
small: [1e3, 1e4], // 1-10 t
|
||||||
medium: [5e3, 5e4], // 5-50 t
|
medium: [1e4, 1e5], // 10-100 t
|
||||||
large: [5e4, 5e5], // 50-500 t
|
large: [1e5, 1e6], // 100-1000 t
|
||||||
comet: [1e6, 5e7], // 1000-50000 t
|
comet: [1e7, 1e8], // 10000-100000 t
|
||||||
planet: [1e12, 1e13], // Moons / small planets
|
planet: [1e22, 1e25], // Moons & Planets
|
||||||
giant: [5e13, 5e14] // Gas giants / huge bodies
|
giant: [1e26, 1e28], // Gas & Ice Giants
|
||||||
|
mtype: [1.589e29, 8.95e29] // M-Type: 0.08-0.45 M☉
|
||||||
},
|
},
|
||||||
|
|
||||||
// Planet color schemes
|
// Planet color schemes
|
||||||
@ -81,6 +92,22 @@ var CONFIG = {
|
|||||||
'#d0e0e3',
|
'#d0e0e3',
|
||||||
'#f4cccc'
|
'#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 color distribution
|
||||||
STAR_COLORS: [
|
STAR_COLORS: [
|
||||||
|
|||||||
362
js/entities.js
362
js/entities.js
@ -68,9 +68,39 @@ function Asteroid(type, blackHole, canvas) {
|
|||||||
Asteroid.prototype.initializeTypeSpecificProperties = function() {
|
Asteroid.prototype.initializeTypeSpecificProperties = function() {
|
||||||
// Visual size (unchanged)
|
// Visual size (unchanged)
|
||||||
if (this.type === 'comet') {
|
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') {
|
} else if (this.type === 'giant') {
|
||||||
this.size = 15 + Math.random() * 10;
|
this.size = 10 + Math.random() * 5;
|
||||||
this.orbitSpeed *= 0.3;
|
this.orbitSpeed *= 0.3;
|
||||||
this.decayRate *= 0.4;
|
this.decayRate *= 0.4;
|
||||||
this.planetColors = CONFIG.GIANT_COLORS[Math.floor(Math.random() * CONFIG.GIANT_COLORS.length)];
|
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') {
|
} else if (this.type === 'planet') {
|
||||||
this.size = 10 + Math.random() * 5;
|
this.size = 6 + Math.random() * 4;
|
||||||
this.orbitSpeed *= 0.5;
|
this.orbitSpeed *= 0.5;
|
||||||
this.decayRate *= 0.6;
|
this.decayRate *= 0.6;
|
||||||
this.planetColors = CONFIG.PLANET_COLORS[Math.floor(Math.random() * CONFIG.PLANET_COLORS.length)];
|
this.planetColors = CONFIG.PLANET_COLORS[Math.floor(Math.random() * CONFIG.PLANET_COLORS.length)];
|
||||||
} else if (this.type === 'large') {
|
} else if (this.type === 'large') {
|
||||||
this.size = 5 + Math.random() * 2;
|
this.size = 3 + Math.random() * 1;
|
||||||
} else if (this.type === 'medium') {
|
} else if (this.type === 'medium') {
|
||||||
this.size = 3 + Math.random() * 2;
|
this.size = 2 + Math.random() * 1;
|
||||||
} else {
|
} else {
|
||||||
this.size = 1 + Math.random() * 2;
|
this.size = 1 + Math.random() * 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Physical mass (kg)
|
// Physical mass (kg)
|
||||||
@ -114,6 +144,8 @@ Asteroid.prototype.update = function() {
|
|||||||
Asteroid.prototype.draw = function(ctx) {
|
Asteroid.prototype.draw = function(ctx) {
|
||||||
if (this.type === 'comet') {
|
if (this.type === 'comet') {
|
||||||
this.drawComet(ctx);
|
this.drawComet(ctx);
|
||||||
|
} else if (this.type === 'mtype') {
|
||||||
|
this.drawMType(ctx, performance.now() * 0.002);
|
||||||
} else if (this.type === 'giant') {
|
} else if (this.type === 'giant') {
|
||||||
// Initialize per-giant tilt if not already done
|
// Initialize per-giant tilt if not already done
|
||||||
if (this._ringTiltBase === undefined) {
|
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) {
|
Asteroid.prototype.drawComet = function(ctx) {
|
||||||
var orbitTangentAngle = this.angle + Math.PI / 2;
|
var orbitTangentAngle = this.angle + Math.PI / 2;
|
||||||
var tailLength = this.size * 5;
|
var tailLength = this.size * 5;
|
||||||
@ -272,6 +242,112 @@ Asteroid.prototype.drawComet = function(ctx) {
|
|||||||
ctx.restore();
|
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) {
|
Asteroid.prototype.drawGiant = function(ctx) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(this.x, this.y);
|
ctx.translate(this.x, this.y);
|
||||||
@ -353,9 +429,51 @@ function BlackHole(x, y, radius) {
|
|||||||
this.radius = radius;
|
this.radius = radius;
|
||||||
this.pulse = 0;
|
this.pulse = 0;
|
||||||
this.pulseColor = '#000000';
|
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) {
|
if (this.pulse > 0) {
|
||||||
this.pulse -= 0.02;
|
this.pulse -= 0.02;
|
||||||
if (this.pulse < 0) this.pulse = 0;
|
if (this.pulse < 0) this.pulse = 0;
|
||||||
@ -363,6 +481,7 @@ BlackHole.prototype.draw = function(ctx) {
|
|||||||
if (this.pulse === 0) {
|
if (this.pulse === 0) {
|
||||||
this.pulseColor = '#000000';
|
this.pulseColor = '#000000';
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.fillStyle = '#000000';
|
ctx.fillStyle = '#000000';
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
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);
|
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) {
|
// Intensity affects opacity and size (0-1 scale)
|
||||||
// // Calculate pulse contribution based on mass
|
var alpha = Math.min(intensity, 1);
|
||||||
// // Using log scale: small asteroids (1e3 kg) give ~0.1, giants (5e14 kg) give ~2.0
|
var sizeMultiplier = 0.5 + (intensity * 0.5); // 50% to 100% size based on intensity
|
||||||
// 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';
|
|
||||||
// }
|
|
||||||
//};
|
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
294
js/game.js
294
js/game.js
@ -16,21 +16,31 @@ var Game = (function() {
|
|||||||
planetUpgradeCost: CONFIG.PLANET_BASE_COST,
|
planetUpgradeCost: CONFIG.PLANET_BASE_COST,
|
||||||
giantUpgradeLevel: 0,
|
giantUpgradeLevel: 0,
|
||||||
giantUpgradeCost: CONFIG.GIANT_BASE_COST,
|
giantUpgradeCost: CONFIG.GIANT_BASE_COST,
|
||||||
|
mtypeUpgradeLevel: 0,
|
||||||
|
mtypeUpgradeCost: CONFIG.MTYPE_BASE_COST,
|
||||||
currentAsteroidSpawnInterval: CONFIG.BASE_ASTEROID_SPAWN_INTERVAL,
|
currentAsteroidSpawnInterval: CONFIG.BASE_ASTEROID_SPAWN_INTERVAL,
|
||||||
currentCometSpawnInterval: CONFIG.BASE_COMET_SPAWN_INTERVAL,
|
currentCometSpawnInterval: CONFIG.BASE_COMET_SPAWN_INTERVAL,
|
||||||
currentPlanetSpawnInterval: CONFIG.BASE_PLANET_SPAWN_INTERVAL,
|
currentPlanetSpawnInterval: CONFIG.BASE_PLANET_SPAWN_INTERVAL,
|
||||||
currentGiantSpawnInterval: CONFIG.BASE_GIANT_SPAWN_INTERVAL,
|
currentGiantSpawnInterval: CONFIG.BASE_GIANT_SPAWN_INTERVAL,
|
||||||
|
currentMtypeSpawnInterval: CONFIG.BASE_MTYPE_SPAWN_INTERVAL,
|
||||||
asteroidSpawnCount: 0,
|
asteroidSpawnCount: 0,
|
||||||
lastAsteroidSpawn: Date.now(),
|
lastAsteroidSpawn: Date.now(),
|
||||||
lastCometSpawn: Date.now(),
|
lastCometSpawn: Date.now(),
|
||||||
lastPlanetSpawn: Date.now() - ((0 + Math.random() * 30) * 60 * 1000),
|
lastPlanetSpawn: Date.now(),
|
||||||
lastGiantSpawn: Date.now() - ((60 + Math.random() * 240) * 60 * 1000),
|
lastGiantSpawn: Date.now(),
|
||||||
|
lastMtypeSpawn: Date.now(),
|
||||||
sM: 0, sT: Date.now(),
|
sM: 0, sT: Date.now(),
|
||||||
mM: 0, mT: Date.now(),
|
mM: 0, mT: Date.now(),
|
||||||
lM: 0, lT: Date.now(),
|
lM: 0, lT: Date.now(),
|
||||||
rateShort: 0, // 1 second average
|
rateShort: 0, // 1 second average
|
||||||
rateMedium: 0, // 1 hour 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;
|
var blackHole;
|
||||||
@ -42,6 +52,10 @@ var Game = (function() {
|
|||||||
var currentTime = Date.now();
|
var currentTime = Date.now();
|
||||||
var lastLogicUpdate = Date.now();
|
var lastLogicUpdate = Date.now();
|
||||||
|
|
||||||
|
// Expose to admin interface
|
||||||
|
window.gameBlackHole = null;
|
||||||
|
window.gameAsteroids = asteroids;
|
||||||
|
|
||||||
function resizeCanvas() {
|
function resizeCanvas() {
|
||||||
// Set canvas to actual viewport size
|
// Set canvas to actual viewport size
|
||||||
canvas.width = window.innerWidth
|
canvas.width = window.innerWidth
|
||||||
@ -57,9 +71,14 @@ var Game = (function() {
|
|||||||
CONFIG.INITIAL_BLACK_HOLE_RADIUS
|
CONFIG.INITIAL_BLACK_HOLE_RADIUS
|
||||||
);
|
);
|
||||||
|
|
||||||
|
window.gameBlackHole = blackHole;
|
||||||
|
|
||||||
for (var i = 0; i < CONFIG.STAR_COUNT; i++) {
|
for (var i = 0; i < CONFIG.STAR_COUNT; i++) {
|
||||||
stars.push(new Star(canvas));
|
stars.push(new Star(canvas));
|
||||||
}
|
}
|
||||||
|
setInterval(function() {
|
||||||
|
UI.updateActiveObjects();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
UI.init();
|
UI.init();
|
||||||
|
|
||||||
@ -67,9 +86,11 @@ var Game = (function() {
|
|||||||
asteroid: handleAsteroidUpgrade,
|
asteroid: handleAsteroidUpgrade,
|
||||||
comet: handleCometUpgrade,
|
comet: handleCometUpgrade,
|
||||||
planet: handlePlanetUpgrade,
|
planet: handlePlanetUpgrade,
|
||||||
giant: handleGiantUpgrade
|
giant: handleGiantUpgrade,
|
||||||
|
mtype: handleMtypeUpgrade
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Server.init()
|
||||||
loadGameState();
|
loadGameState();
|
||||||
|
|
||||||
window.addEventListener('resize', function() {
|
window.addEventListener('resize', function() {
|
||||||
@ -81,7 +102,6 @@ var Game = (function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ADD THIS BLOCK - Prevent zoom on desktop
|
|
||||||
window.addEventListener('wheel', function(e) {
|
window.addEventListener('wheel', function(e) {
|
||||||
if (e.ctrlKey) {
|
if (e.ctrlKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -96,31 +116,34 @@ var Game = (function() {
|
|||||||
|
|
||||||
animate(currentTime);
|
animate(currentTime);
|
||||||
UI.update(state, CONFIG);
|
UI.update(state, CONFIG);
|
||||||
Server.init()
|
|
||||||
|
|
||||||
// Save when page becomes hidden (tab switch, minimize, close)
|
// Save when page becomes hidden (tab switch, minimize, close)
|
||||||
document.addEventListener('visibilitychange', function() {
|
document.addEventListener('visibilitychange', function() {
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
Storage.saveGame(state);
|
state.tabHiddenAt = Date.now(); // NEW: Record when hidden
|
||||||
Server.checkpoint(state);
|
Server.checkpoint(state);
|
||||||
} else if (!document.hidden) {
|
} else if (!document.hidden) {
|
||||||
loadGameState();
|
loadGameState();
|
||||||
|
Server.checkpoint(state);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also save on beforeunload as backup
|
// Save when page is about to unload (close tab, navigate away)
|
||||||
window.addEventListener('beforeunload', function() {
|
window.addEventListener('beforeunload', function() {
|
||||||
Storage.saveGame(state);
|
state.tabHiddenAt = Date.now();
|
||||||
|
Server.checkpoint(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Periodic auto-save every 30 seconds as final safety net
|
// Save and update scores when window gains focus
|
||||||
setInterval(function() {
|
window.addEventListener('focus', function() {
|
||||||
Storage.saveGame(state);
|
if (window.Server && window.Game && Game.getState) {
|
||||||
}, 15000);
|
Server.checkpoint(Game.getState());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadGameState() {
|
function loadGameState() {
|
||||||
var savedState = Storage.loadGame();
|
Server.loadGameState().then(function(savedState) {
|
||||||
if (savedState) {
|
if (savedState) {
|
||||||
state.blackHoleTotalMass = savedState.blackHoleTotalMass;
|
state.blackHoleTotalMass = savedState.blackHoleTotalMass;
|
||||||
state.totalMassConsumedEver = savedState.totalMassConsumedEver;
|
state.totalMassConsumedEver = savedState.totalMassConsumedEver;
|
||||||
@ -133,28 +156,21 @@ var Game = (function() {
|
|||||||
state.planetUpgradeCost = savedState.planetUpgradeCost;
|
state.planetUpgradeCost = savedState.planetUpgradeCost;
|
||||||
state.giantUpgradeLevel = savedState.giantUpgradeLevel;
|
state.giantUpgradeLevel = savedState.giantUpgradeLevel;
|
||||||
state.giantUpgradeCost = savedState.giantUpgradeCost;
|
state.giantUpgradeCost = savedState.giantUpgradeCost;
|
||||||
|
state.mtypeUpgradeLevel = savedState.mtypeUpgradeLevel || 0;
|
||||||
|
state.mtypeUpgradeCost = savedState.mtypeUpgradeCost || CONFIG.MTYPE_BASE_COST;
|
||||||
state.asteroidSpawnCount = savedState.asteroidSpawnCount;
|
state.asteroidSpawnCount = savedState.asteroidSpawnCount;
|
||||||
|
|
||||||
// Load spawn timers if they exist, otherwise generate from save timestamp
|
// 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.lastAsteroidSpawn = savedState.lastAsteroidSpawn || Date.now();
|
||||||
state.lastCometSpawn = savedState.lastCometSpawn || Date.now();
|
state.lastCometSpawn = savedState.lastCometSpawn || Date.now();
|
||||||
|
state.lastPlanetSpawn = savedState.lastPlanetSpawn || Date.now();
|
||||||
// For old saves without spawn timers, use savedAt timestamp to generate consistent times
|
state.lastGiantSpawn = savedState.lastGiantSpawn || Date.now();
|
||||||
if (savedState.lastPlanetSpawn) {
|
state.lastMtypeSpawn = savedState.lastMtypeSpawn || Date.now();
|
||||||
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
|
// Load consumption rates
|
||||||
state.sM = savedState.sM || 0;
|
state.sM = savedState.sM || 0;
|
||||||
@ -163,40 +179,47 @@ var Game = (function() {
|
|||||||
|
|
||||||
var now = Date.now();
|
var now = Date.now();
|
||||||
|
|
||||||
state.sT = savedState.sT || (now - CONFIG.RATE_WINDOWS.SHORT);
|
state.sT = savedState.sT || now;
|
||||||
state.mT = savedState.mT || (now - CONFIG.RATE_WINDOWS.MEDIUM);
|
state.mT = savedState.mT || now;
|
||||||
state.lT = savedState.lT || (now - CONFIG.RATE_WINDOWS.LONG);
|
state.lT = savedState.lT || now;
|
||||||
|
|
||||||
state.rateShort = savedState.rateShort || 0;
|
state.rateShort = savedState.rateShort || 0;
|
||||||
|
state.rateShortTrend = savedState.rateShortTrend || 'same';
|
||||||
state.rateMedium = savedState.rateMedium || 0;
|
state.rateMedium = savedState.rateMedium || 0;
|
||||||
state.rateLong = savedState.rateLong || 0;
|
state.rateLong = savedState.rateLong || 0;
|
||||||
|
|
||||||
// Calculate offline progression
|
state.tabHiddenAt = savedState.tabHiddenAt || null;
|
||||||
var now = Date.now();
|
|
||||||
var offlineTime = now - savedState.savedAt; // milliseconds offline
|
// 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) {
|
if (offlineTime > 1000) {
|
||||||
// Pick the longest non-zero rate
|
var rateToUse = 0;
|
||||||
var shortRate = state.sM / ((now - state.sT) / 1000);
|
|
||||||
var mediumRate = state.mM / ((now - state.mT) / 1000);
|
if (state.rateLong > 0 && (now - state.lT) > 3600000) {
|
||||||
var longRate = state.lM / ((now - state.lT) / 1000);
|
rateToUse = state.rateLong;
|
||||||
var rateToUse = longRate || mediumRate || shortRate || 0;
|
} else if (state.rateMedium > 0 && (now - state.mT) > 300000) {
|
||||||
|
rateToUse = state.rateMedium;
|
||||||
|
} else {
|
||||||
|
rateToUse = state.rateShort || 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (rateToUse > 0) {
|
if (rateToUse > 0) {
|
||||||
var offlineMass = rateToUse * (offlineTime / 1000);
|
var offlineMass = rateToUse * (offlineTime / 1000);
|
||||||
|
|
||||||
// Update totals
|
|
||||||
state.blackHoleTotalMass += offlineMass;
|
state.blackHoleTotalMass += offlineMass;
|
||||||
state.totalMassConsumedEver += offlineMass;
|
state.totalMassConsumedEver += offlineMass;
|
||||||
state.totalMassConsumed += offlineMass;
|
state.totalMassConsumed += offlineMass;
|
||||||
|
|
||||||
// Add to rolling windows so rates are meaningful
|
|
||||||
state.sM += offlineMass;
|
state.sM += offlineMass;
|
||||||
state.mM += offlineMass;
|
state.mM += offlineMass;
|
||||||
state.lM += offlineMass;
|
state.lM += offlineMass;
|
||||||
|
|
||||||
|
|
||||||
// Optional notification
|
|
||||||
showOfflineNotification(
|
showOfflineNotification(
|
||||||
formatOfflineTime(offlineTime),
|
formatOfflineTime(offlineTime),
|
||||||
UI.formatMass(offlineMass),
|
UI.formatMass(offlineMass),
|
||||||
@ -205,32 +228,40 @@ var Game = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear the hidden timestamp after processing
|
||||||
|
state.tabHiddenAt = null;
|
||||||
|
|
||||||
updateSpawnIntervals();
|
updateSpawnIntervals();
|
||||||
|
|
||||||
// Restore black hole size
|
// Restore black hole size
|
||||||
var solarMasses = state.blackHoleTotalMass / CONFIG.SOLAR_MASS_KG;
|
blackHole.radius = 0.5 * (state.blackHoleTotalMass / CONFIG.SOLAR_MASS_KG)
|
||||||
blackHole.radius = CONFIG.INITIAL_BLACK_HOLE_RADIUS + (solarMasses - CONFIG.INITIAL_BLACK_HOLE_MASS);
|
|
||||||
|
|
||||||
// Save state
|
|
||||||
Storage.saveGame(state);
|
|
||||||
|
|
||||||
|
// Send checkpoint after loading
|
||||||
|
Server.checkpoint(state).then(function() {
|
||||||
|
state.isReady = true; // Mark as ready after checkpoint
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// No save exists - initialize spawn times with random values
|
// No saved state - send initial checkpoint for new player
|
||||||
state.lastAsteroidSpawn = Date.now();
|
Server.checkpoint(state).then(function() {
|
||||||
state.lastCometSpawn = Date.now();
|
state.isReady = true; // Mark as ready after checkpoint
|
||||||
state.lastPlanetSpawn = Date.now() - ((0 + Math.random() * 30) * 60 * 1000);
|
});
|
||||||
state.lastGiantSpawn = Date.now() - ((60 + Math.random() * 240) * 60 * 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UI.update(state, CONFIG);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSpawnIntervals() {
|
function updateSpawnIntervals() {
|
||||||
state.currentAsteroidSpawnInterval = CONFIG.BASE_ASTEROID_SPAWN_INTERVAL /
|
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 /
|
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 /
|
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 /
|
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() {
|
function handleAsteroidUpgrade() {
|
||||||
@ -240,9 +271,15 @@ var Game = (function() {
|
|||||||
state.asteroidUpgradeCost = Math.floor(
|
state.asteroidUpgradeCost = Math.floor(
|
||||||
CONFIG.ASTEROID_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_ASTER, state.asteroidUpgradeLevel)
|
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();
|
updateSpawnIntervals();
|
||||||
UI.update(state, CONFIG);
|
UI.update(state, CONFIG);
|
||||||
Storage.saveGame(state);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,11 +288,16 @@ var Game = (function() {
|
|||||||
state.totalMassConsumed -= state.cometUpgradeCost;
|
state.totalMassConsumed -= state.cometUpgradeCost;
|
||||||
state.cometUpgradeLevel++;
|
state.cometUpgradeLevel++;
|
||||||
state.cometUpgradeCost = Math.floor(
|
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();
|
updateSpawnIntervals();
|
||||||
UI.update(state, CONFIG);
|
UI.update(state, CONFIG);
|
||||||
Storage.saveGame(state);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,11 +306,16 @@ var Game = (function() {
|
|||||||
state.totalMassConsumed -= state.planetUpgradeCost;
|
state.totalMassConsumed -= state.planetUpgradeCost;
|
||||||
state.planetUpgradeLevel++;
|
state.planetUpgradeLevel++;
|
||||||
state.planetUpgradeCost = Math.floor(
|
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();
|
updateSpawnIntervals();
|
||||||
UI.update(state, CONFIG);
|
UI.update(state, CONFIG);
|
||||||
Storage.saveGame(state);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,11 +324,31 @@ var Game = (function() {
|
|||||||
state.totalMassConsumed -= state.giantUpgradeCost;
|
state.totalMassConsumed -= state.giantUpgradeCost;
|
||||||
state.giantUpgradeLevel++;
|
state.giantUpgradeLevel++;
|
||||||
state.giantUpgradeCost = Math.floor(
|
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();
|
updateSpawnIntervals();
|
||||||
UI.update(state, CONFIG);
|
UI.update(state, CONFIG);
|
||||||
Storage.saveGame(state);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,24 +371,34 @@ var Game = (function() {
|
|||||||
state.lastAsteroidSpawn = currentTime;
|
state.lastAsteroidSpawn = currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.cometUnlocked && state.cometUpgradeLevel >= 0) {
|
||||||
if (currentTime - state.lastCometSpawn > state.currentCometSpawnInterval) {
|
if (currentTime - state.lastCometSpawn > state.currentCometSpawnInterval) {
|
||||||
asteroids.push(new Asteroid('comet', blackHole, canvas));
|
|
||||||
state.lastCometSpawn = currentTime;
|
state.lastCometSpawn = currentTime;
|
||||||
//Storage.saveGame(state);
|
asteroids.push(new Asteroid('comet', blackHole, canvas));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.planetUnlocked && state.planetUpgradeLevel >= 0) {
|
||||||
if (currentTime - state.lastPlanetSpawn > state.currentPlanetSpawnInterval) {
|
if (currentTime - state.lastPlanetSpawn > state.currentPlanetSpawnInterval) {
|
||||||
asteroids.push(new Asteroid('planet', blackHole, canvas));
|
|
||||||
state.lastPlanetSpawn = currentTime;
|
state.lastPlanetSpawn = currentTime;
|
||||||
//Storage.saveGame(state);
|
asteroids.push(new Asteroid('planet', blackHole, canvas));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.giantUnlocked && state.giantUpgradeLevel >= 0) {
|
||||||
if (currentTime - state.lastGiantSpawn > state.currentGiantSpawnInterval) {
|
if (currentTime - state.lastGiantSpawn > state.currentGiantSpawnInterval) {
|
||||||
asteroids.push(new Asteroid('giant', blackHole, canvas));
|
|
||||||
state.lastGiantSpawn = currentTime;
|
state.lastGiantSpawn = currentTime;
|
||||||
//Storage.saveGame(state);
|
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() {
|
function updateAsteroids() {
|
||||||
for (var i = asteroids.length - 1; i >= 0; i--) {
|
for (var i = asteroids.length - 1; i >= 0; i--) {
|
||||||
@ -364,7 +441,7 @@ var Game = (function() {
|
|||||||
asteroids[i].draw(ctx);
|
asteroids[i].draw(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
blackHole.draw(ctx);
|
blackHole.draw(ctx, state.rateShort || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function animate(currentTime) {
|
function animate(currentTime) {
|
||||||
@ -406,7 +483,6 @@ var Game = (function() {
|
|||||||
}());
|
}());
|
||||||
|
|
||||||
var Server = {
|
var Server = {
|
||||||
// baseURL removed; now all requests are relative
|
|
||||||
playerId: null,
|
playerId: null,
|
||||||
|
|
||||||
init: function() {
|
init: function() {
|
||||||
@ -423,47 +499,81 @@ var Server = {
|
|||||||
|
|
||||||
checkpoint: async function(gameState) {
|
checkpoint: async function(gameState) {
|
||||||
try {
|
try {
|
||||||
await fetch('/api/checkpoint', { // relative URL
|
// Add savedAt timestamp
|
||||||
|
gameState.savedAt = Date.now();
|
||||||
|
|
||||||
|
const response = await fetch('/api/checkpoint', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
playerId: this.playerId,
|
playerId: this.playerId,
|
||||||
name: Storage.getCookie('playerName') || 'Anonymous',
|
gameState: gameState
|
||||||
mass: gameState.blackHoleTotalMass,
|
|
||||||
playTime: gameState.totalPlayTime + (Date.now() - gameState.gameStartTime),
|
|
||||||
levels: {
|
|
||||||
total: gameState.asteroidUpgradeLevel +
|
|
||||||
gameState.cometUpgradeLevel +
|
|
||||||
gameState.planetUpgradeLevel +
|
|
||||||
gameState.giantUpgradeLevel
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Checkpoint failed', 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) {
|
getLeaderboard: async function(sortBy) {
|
||||||
try {
|
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}`);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
return data; // should be array of { name, mass, holeAge }
|
return data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch leaderboard', err);
|
console.error('Failed to fetch leaderboard', err);
|
||||||
return [];
|
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() {
|
startCheckpoints: function() {
|
||||||
// Example: autosave every 15s
|
let lastCheckpointData = null;
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (window.Game && Game.state) {
|
if (window.Game && Game.getState) {
|
||||||
this.checkpoint(Game.state);
|
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
|
// Start the game when the page loads
|
||||||
|
|||||||
200
js/helpers.js
200
js/helpers.js
@ -49,35 +49,66 @@ function saturateColor(r, g, b, saturationBoost) {
|
|||||||
function calculateConsumptionRates(state, CONFIG, consumedMassKg) {
|
function calculateConsumptionRates(state, CONFIG, consumedMassKg) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// Only update rates once per second
|
// Initialize tracking windows if needed
|
||||||
if (!state._lastRateUpdate) state._lastRateUpdate = 0;
|
if (!state.sM) state.sM = 0;
|
||||||
if (now - state._lastRateUpdate < 1000) return; // skip if less than 1s
|
if (!state.sT) state.sT = now;
|
||||||
state._lastRateUpdate = 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;
|
||||||
|
|
||||||
function updateWindow(massKey, timeKey, prevRateKey) {
|
// Add mass to all windows
|
||||||
const start = state[timeKey] || now;
|
state.sM += consumedMassKg;
|
||||||
|
state.mM += consumedMassKg;
|
||||||
|
state.lM += consumedMassKg;
|
||||||
|
|
||||||
// Add new mass
|
// Update per-second rate (with trend coloring)
|
||||||
state[massKey] = (state[massKey] || 0) + consumedMassKg;
|
const elapsedShort = now - state.sT;
|
||||||
|
if (elapsedShort >= 1000) {
|
||||||
|
const newRate = state.sM / (elapsedShort / 1000);
|
||||||
|
|
||||||
const elapsed = now - start;
|
// Compare to previous rate for trend
|
||||||
const rate = elapsed > 0 ? state[massKey] / (elapsed / 1000) : 0;
|
if (state.rateShort === undefined) {
|
||||||
|
state.rateShort = newRate;
|
||||||
// Compare to previous rate to get trend
|
state.rateShortTrend = 'same';
|
||||||
if (state[prevRateKey] === undefined) state[prevRateKey] = rate;
|
} else {
|
||||||
state[prevRateKey + 'Trend'] =
|
// Determine trend with 5% threshold to avoid jitter
|
||||||
rate > state[prevRateKey] ? 'up' :
|
if (newRate > state.rateShort * 1.05) {
|
||||||
rate < state[prevRateKey] ? 'down' : 'same';
|
state.rateShortTrend = 'up';
|
||||||
|
} else if (newRate < state.rateShort * 0.95) {
|
||||||
// Save current rate for next tick
|
state.rateShortTrend = 'down';
|
||||||
state[prevRateKey] = rate;
|
} else {
|
||||||
|
state.rateShortTrend = 'same';
|
||||||
return rate;
|
}
|
||||||
|
state.rateShort = newRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.rateShort = updateWindow("sM", "sT", "prevRateShort");
|
// Reset short window
|
||||||
state.rateMedium = updateWindow("mM", "mT", "prevRateMedium");
|
state.sM = 0;
|
||||||
state.rateLong = updateWindow("lM", "lT", "prevRateLong");
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatOfflineTime(ms) {
|
function formatOfflineTime(ms) {
|
||||||
@ -97,29 +128,110 @@ function formatOfflineTime(ms) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showOfflineNotification(timeAway, massGained, rateName) {
|
// Unified notification system
|
||||||
var notification = document.createElement('div');
|
function showNotification(options) {
|
||||||
notification.style.position = 'fixed';
|
// Default options
|
||||||
notification.style.top = '50%';
|
var defaults = {
|
||||||
notification.style.left = '50%';
|
message: '',
|
||||||
notification.style.transform = 'translate(-50%, -50%)';
|
type: 'info', // 'unlock', 'offline', 'error', 'success', 'info'
|
||||||
notification.style.background = 'rgba(20, 20, 30, 0.95)';
|
html: false // if true, message is HTML; if false, it's text
|
||||||
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';
|
|
||||||
|
|
||||||
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>Inactive for: <strong>' + timeAway + '</strong></div>' +
|
||||||
'<div>Mass gained: <strong>' + massGained + '</strong></div>';
|
'<div>Mass gained: <strong>' + massGained + '</strong></div>';
|
||||||
|
|
||||||
document.body.appendChild(notification);
|
showNotification({
|
||||||
|
message: message,
|
||||||
document.getElementById('space').addEventListener('click', function() {
|
type: 'offline',
|
||||||
notification.remove();
|
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';
|
||||||
|
}
|
||||||
|
|||||||
125
js/server.js
125
js/server.js
@ -7,8 +7,14 @@ app.use(cors());
|
|||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
const LEADERBOARD_FILE = './leaderboard.json';
|
const LEADERBOARD_FILE = './leaderboard.json';
|
||||||
const INACTIVE_DAYS = 30; // Completely remove from JSON after 30 days
|
const INACTIVE_DAYS = 30; // Completely remove from JSON after 5 days
|
||||||
const LEADERBOARD_VISIBLE_DAYS = 5; // Only show on leaderboard if active within 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
|
// Load leaderboard from file
|
||||||
function loadLeaderboard() {
|
function loadLeaderboard() {
|
||||||
@ -32,7 +38,8 @@ function cleanupInactive(leaderboard) {
|
|||||||
const active = leaderboard.filter(player => player.lastSeen > thirtyDaysAgo);
|
const active = leaderboard.filter(player => player.lastSeen > thirtyDaysAgo);
|
||||||
|
|
||||||
if (active.length < beforeCount) {
|
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;
|
return active;
|
||||||
@ -41,16 +48,40 @@ function cleanupInactive(leaderboard) {
|
|||||||
// Filter players visible on leaderboard (active within 5 days)
|
// Filter players visible on leaderboard (active within 5 days)
|
||||||
function getVisiblePlayers(leaderboard) {
|
function getVisiblePlayers(leaderboard) {
|
||||||
const fiveDaysAgo = Date.now() - (LEADERBOARD_VISIBLE_DAYS * 24 * 60 * 60 * 1000);
|
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;
|
return visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit checkpoint
|
// Submit checkpoint
|
||||||
app.post('/api/checkpoint', (req, res) => {
|
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
|
// Basic validation
|
||||||
if (!playerId || mass < 0 || playTime < 0) {
|
if (!playerId || !gameState) {
|
||||||
return res.status(400).json({ error: 'Invalid data' });
|
return res.status(400).json({ error: 'Invalid data' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,30 +95,38 @@ app.post('/api/checkpoint', (req, res) => {
|
|||||||
// Find or create player
|
// Find or create player
|
||||||
let player = leaderboard.find(p => p.id === playerId);
|
let player = leaderboard.find(p => p.id === playerId);
|
||||||
if (player) {
|
if (player) {
|
||||||
// Update existing player
|
// Update existing player - store full game state
|
||||||
player.mass = Math.max(player.mass, mass);
|
player.gameState = gameState;
|
||||||
player.playTime = Math.max(player.playTime, playTime);
|
player.mass = gameState.blackHoleTotalMass || 0;
|
||||||
player.level = Math.max(player.level, levels?.total || 0);
|
player.level = (gameState.asteroidUpgradeLevel || 0) +
|
||||||
player.name = name || player.name;
|
(gameState.cometUpgradeLevel || 0) +
|
||||||
|
(gameState.planetUpgradeLevel || 0) +
|
||||||
|
(gameState.giantUpgradeLevel || 0);
|
||||||
player.lastSeen = now;
|
player.lastSeen = now;
|
||||||
|
|
||||||
// Calculate hole age (time since first seen)
|
|
||||||
player.holeAge = now - player.firstSeen;
|
player.holeAge = now - player.firstSeen;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// New player
|
// New player
|
||||||
leaderboard.push({
|
const timestamp = new Date().toLocaleString();
|
||||||
|
const newPlayer = {
|
||||||
id: playerId,
|
id: playerId,
|
||||||
name: name || 'Anonymous',
|
gameState: gameState,
|
||||||
mass: mass,
|
mass: gameState.blackHoleTotalMass || 0,
|
||||||
playTime: playTime,
|
level: (gameState.asteroidUpgradeLevel || 0) +
|
||||||
level: levels?.total || 0,
|
(gameState.cometUpgradeLevel || 0) +
|
||||||
|
(gameState.planetUpgradeLevel || 0) +
|
||||||
|
(gameState.giantUpgradeLevel || 0),
|
||||||
firstSeen: now,
|
firstSeen: now,
|
||||||
lastSeen: now,
|
lastSeen: now,
|
||||||
holeAge: 0
|
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.sort((a, b) => b.mass - a.mass);
|
||||||
leaderboard = leaderboard.slice(0, 100);
|
leaderboard = leaderboard.slice(0, 100);
|
||||||
|
|
||||||
@ -168,6 +207,20 @@ app.get('/api/player/:playerId', (req, res) => {
|
|||||||
res.json(player);
|
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)
|
// Manual cleanup endpoint (optional - for maintenance)
|
||||||
app.post('/api/cleanup', (req, res) => {
|
app.post('/api/cleanup', (req, res) => {
|
||||||
let leaderboard = loadLeaderboard();
|
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;
|
const PORT = process.env.PORT || 3000;
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Server running on port ${PORT}`);
|
const timestamp = new Date().toLocaleString();
|
||||||
console.log(`Players hidden from leaderboard after ${LEADERBOARD_VISIBLE_DAYS} days inactive`);
|
console.log(`${timestamp} Server running on port ${PORT}`);
|
||||||
console.log(`Players permanently removed after ${INACTIVE_DAYS} days inactive`);
|
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';
|
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() {
|
resetGame: function() {
|
||||||
this.deleteCookie('blackHoleSave');
|
if (confirm('Are you sure you want to reset? This will delete all your progress permanently.')) {
|
||||||
location.reload();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
323
js/ui.js
323
js/ui.js
@ -13,10 +13,12 @@ var UI = {
|
|||||||
cometLevel: document.getElementById('comet-level'),
|
cometLevel: document.getElementById('comet-level'),
|
||||||
planetLevel: document.getElementById('planet-level'),
|
planetLevel: document.getElementById('planet-level'),
|
||||||
giantLevel: document.getElementById('giant-level'),
|
giantLevel: document.getElementById('giant-level'),
|
||||||
|
mtypeLevel: document.getElementById('mtype-level'),
|
||||||
asteroidUpgradeBtn: document.getElementById('asteroid-upgrade-btn'),
|
asteroidUpgradeBtn: document.getElementById('asteroid-upgrade-btn'),
|
||||||
cometUpgradeBtn: document.getElementById('comet-upgrade-btn'),
|
cometUpgradeBtn: document.getElementById('comet-upgrade-btn'),
|
||||||
planetUpgradeBtn: document.getElementById('planet-upgrade-btn'),
|
planetUpgradeBtn: document.getElementById('planet-upgrade-btn'),
|
||||||
giantUpgradeBtn: document.getElementById('giant-upgrade-btn'),
|
giantUpgradeBtn: document.getElementById('giant-upgrade-btn'),
|
||||||
|
mtypeUpgradeBtn: document.getElementById('mtype-upgrade-btn'),
|
||||||
gearIcon: document.getElementById('gear-icon'),
|
gearIcon: document.getElementById('gear-icon'),
|
||||||
settingsMenu: document.getElementById('settings-menu'),
|
settingsMenu: document.getElementById('settings-menu'),
|
||||||
resetBtn: document.getElementById('reset-btn'),
|
resetBtn: document.getElementById('reset-btn'),
|
||||||
@ -25,7 +27,8 @@ var UI = {
|
|||||||
leaderboardPanel: document.getElementById('leaderboardPanel'),
|
leaderboardPanel: document.getElementById('leaderboardPanel'),
|
||||||
leaderboardList: document.getElementById('leaderboardList'),
|
leaderboardList: document.getElementById('leaderboardList'),
|
||||||
sortMass: document.getElementById('sortMass'),
|
sortMass: document.getElementById('sortMass'),
|
||||||
sortAge: document.getElementById('sortAge')
|
sortAge: document.getElementById('sortAge'),
|
||||||
|
massRate: document.getElementById('massRate')
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sorting buttons
|
// Sorting buttons
|
||||||
@ -41,26 +44,34 @@ var UI = {
|
|||||||
const settingsMenu = self.elements.settingsMenu;
|
const settingsMenu = self.elements.settingsMenu;
|
||||||
const leaderboardToggle = self.elements.leaderboardToggle;
|
const leaderboardToggle = self.elements.leaderboardToggle;
|
||||||
const leaderboardPanel = self.elements.leaderboardPanel;
|
const leaderboardPanel = self.elements.leaderboardPanel;
|
||||||
|
const resetBtn = self.elements.resetBtn;
|
||||||
|
const holeIcon = document.getElementById('hole-icon');
|
||||||
|
const upgradePanel = document.getElementById('upgrade-panel');
|
||||||
|
|
||||||
// ---------- Settings Menu ----------
|
// ---------- Upgrade Panel ----------
|
||||||
if (gear && settingsMenu) {
|
if (holeIcon && upgradePanel) {
|
||||||
// Toggle settings menu
|
// Toggle upgrade panel
|
||||||
gear.addEventListener('click', (e) => {
|
holeIcon.addEventListener('click', (e) => {
|
||||||
e.stopPropagation(); // prevent document click from immediately closing
|
e.stopPropagation();
|
||||||
settingsMenu.classList.toggle('open');
|
if (settingsMenu) settingsMenu.classList.remove('open');
|
||||||
|
if (leaderboardPanel) leaderboardPanel.classList.remove('show');
|
||||||
|
upgradePanel.classList.toggle('open');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prevent clicks inside menu from closing it
|
// Prevent clicks inside panel from closing it
|
||||||
settingsMenu.addEventListener('click', (e) => {
|
upgradePanel.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---------- Leaderboard ----------
|
// ---------- Leaderboard ----------
|
||||||
if (leaderboardToggle && leaderboardPanel) {
|
if (leaderboardToggle && leaderboardPanel) {
|
||||||
// Toggle leaderboard panel
|
// Toggle leaderboard panel
|
||||||
leaderboardToggle.addEventListener('click', (e) => {
|
leaderboardToggle.addEventListener('click', (e) => {
|
||||||
e.stopPropagation(); // prevent document click
|
e.stopPropagation(); // prevent document click
|
||||||
|
if (settingsMenu) settingsMenu.classList.remove('open');
|
||||||
|
if (upgradePanel) upgradePanel.classList.remove('open');
|
||||||
leaderboardPanel.classList.toggle('show');
|
leaderboardPanel.classList.toggle('show');
|
||||||
|
|
||||||
if (leaderboardPanel.classList.contains('show')) {
|
if (leaderboardPanel.classList.contains('show')) {
|
||||||
@ -73,6 +84,27 @@ var UI = {
|
|||||||
e.stopPropagation();
|
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 ----------
|
// ---------- Click outside to close ----------
|
||||||
document.addEventListener('click', () => {
|
document.addEventListener('click', () => {
|
||||||
@ -84,6 +116,19 @@ var UI = {
|
|||||||
// Close leaderboard panel
|
// Close leaderboard panel
|
||||||
if (leaderboardPanel) {
|
if (leaderboardPanel) {
|
||||||
leaderboardPanel.classList.remove('show');
|
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.cometUpgradeBtn.addEventListener('click', handlers.comet);
|
||||||
this.elements.planetUpgradeBtn.addEventListener('click', handlers.planet);
|
this.elements.planetUpgradeBtn.addEventListener('click', handlers.planet);
|
||||||
this.elements.giantUpgradeBtn.addEventListener('click', handlers.giant);
|
this.elements.giantUpgradeBtn.addEventListener('click', handlers.giant);
|
||||||
|
this.elements.mtypeUpgradeBtn.addEventListener('click', handlers.mtype);
|
||||||
},
|
},
|
||||||
|
|
||||||
update: function(gameState, config) {
|
update: function(gameState, config) {
|
||||||
@ -101,22 +147,13 @@ var UI = {
|
|||||||
'Available to Spend: ' + this.formatMass(gameState.totalMassConsumed);
|
'Available to Spend: ' + this.formatMass(gameState.totalMassConsumed);
|
||||||
}
|
}
|
||||||
this.updateMassDisplay(gameState, config);
|
this.updateMassDisplay(gameState, config);
|
||||||
|
this.updateRateDisplay(gameState, config);
|
||||||
this.updateUpgradeDisplay(gameState);
|
this.updateUpgradeDisplay(gameState);
|
||||||
this.updateTotalLevel(gameState);
|
this.updateTotalLevel(gameState);
|
||||||
|
this.updateActiveObjects();
|
||||||
},
|
},
|
||||||
|
|
||||||
updateMassDisplay: function(state, config) {
|
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 bhMassText = this.formatMass(state.blackHoleTotalMass, { forceSolarMass: true });
|
||||||
const consumedText = this.formatMass(state.totalMassConsumedEver);
|
const consumedText = this.formatMass(state.totalMassConsumedEver);
|
||||||
@ -130,17 +167,102 @@ var UI = {
|
|||||||
Total mass absorbed:<br>
|
Total mass absorbed:<br>
|
||||||
${consumedText}
|
${consumedText}
|
||||||
</span>
|
</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) {
|
updateUpgradeDisplay: function(gameState) {
|
||||||
this.updateAsteroidUpgrade(gameState);
|
this.updateAsteroidUpgrade(gameState);
|
||||||
|
|
||||||
|
// Only show comet upgrade if unlocked
|
||||||
|
if (gameState.cometUnlocked) {
|
||||||
this.updateCometUpgrade(gameState);
|
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.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.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) {
|
updateTotalLevel: function(gameState) {
|
||||||
var el = this.elements.totalLevel;
|
var el = this.elements.totalLevel;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@ -160,6 +282,36 @@ var UI = {
|
|||||||
'</span>';
|
'</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) {
|
updateAsteroidUpgrade: function(gameState) {
|
||||||
var rate = (1 / gameState.currentAsteroidSpawnInterval * 1000).toFixed(2);
|
var rate = (1 / gameState.currentAsteroidSpawnInterval * 1000).toFixed(2);
|
||||||
@ -225,33 +377,46 @@ var UI = {
|
|||||||
gameState.totalMassConsumed < gameState.giantUpgradeCost;
|
gameState.totalMassConsumed < gameState.giantUpgradeCost;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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 = {}) {
|
formatMass: function(massKg, options = {}) {
|
||||||
// If forcing solar mass display (for black hole)
|
if (massKg == null) return '0'; // fallback
|
||||||
if (options.forceSolarMass) {
|
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;
|
var solarMasses = massKg / CONFIG.SOLAR_MASS_KG;
|
||||||
return solarMasses.toFixed(3) + ' M☉';
|
|
||||||
|
// 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☉';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traditional engineering/astronomical units
|
// SI mass units (tonnes-based)
|
||||||
if (massKg >= 1e18) {
|
if (massKg >= 1e30) return (massKg / 1e30).toFixed(2) + ' Qt'; // Quettatonnes
|
||||||
return (massKg / 1e18).toFixed(2) + ' Et'; // Exatonnes
|
if (massKg >= 1e27) return (massKg / 1e27).toFixed(2) + ' Rt'; // Ronnatonnes
|
||||||
}
|
if (massKg >= 1e24) return (massKg / 1e24).toFixed(2) + ' Yt'; // Yottatonnes
|
||||||
if (massKg >= 1e15) {
|
if (massKg >= 1e21) return (massKg / 1e21).toFixed(2) + ' Zt'; // Zettatonnes
|
||||||
return (massKg / 1e15).toFixed(2) + ' Pt'; // Petatonnes
|
if (massKg >= 1e18) return (massKg / 1e18).toFixed(2) + ' Et'; // Exatonnes
|
||||||
}
|
if (massKg >= 1e15) return (massKg / 1e15).toFixed(2) + ' Pt'; // Petatonnes
|
||||||
if (massKg >= 1e12) {
|
if (massKg >= 1e12) return (massKg / 1e12).toFixed(2) + ' Tt'; // Teratonnes
|
||||||
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 >= 1e9) {
|
if (massKg >= 1e3) return (massKg / 1e3).toFixed(2) + ' tonnes'; // Tonnes
|
||||||
return (massKg / 1e9).toFixed(2) + ' Gt'; // Gigatonnes
|
|
||||||
}
|
return massKg.toFixed(0) + ' kg';
|
||||||
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
|
|
||||||
},
|
},
|
||||||
|
|
||||||
formatTime: function(ms) {
|
formatTime: function(ms) {
|
||||||
@ -276,9 +441,9 @@ var UI = {
|
|||||||
var hours = Math.floor((ms % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000));
|
var hours = Math.floor((ms % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000));
|
||||||
|
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
return days + 'd ' + hours + 'h old';
|
return days + 'd ' + hours + 'h';
|
||||||
} else if (hours > 0) {
|
} else if (hours > 0) {
|
||||||
return hours + 'h old';
|
return hours + 'h';
|
||||||
} else {
|
} else {
|
||||||
return 'New';
|
return 'New';
|
||||||
}
|
}
|
||||||
@ -287,7 +452,12 @@ var UI = {
|
|||||||
|
|
||||||
openLeaderboard: async function(sortBy = 'mass') {
|
openLeaderboard: async function(sortBy = 'mass') {
|
||||||
const container = this.elements.leaderboardList;
|
const container = this.elements.leaderboardList;
|
||||||
|
const localPlayerId = Server.playerId;
|
||||||
|
|
||||||
|
// Show loading only on first load
|
||||||
|
if (!this._cachedLeaderboardData) {
|
||||||
container.innerHTML = '<div style="text-align:center; padding:20px;">Loading...</div>';
|
container.innerHTML = '<div style="text-align:center; padding:20px;">Loading...</div>';
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch leaderboard
|
// Fetch leaderboard
|
||||||
let data = await Server.getLeaderboard(sortBy);
|
let data = await Server.getLeaderboard(sortBy);
|
||||||
@ -296,34 +466,73 @@ var UI = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort data server-side should already be sorted; else fallback:
|
this._cachedLeaderboardData = data;
|
||||||
data.sort((a,b) => sortBy==='mass'?b.mass-a.mass:b.holeAge-a.holeAge);
|
this._currentSort = sortBy;
|
||||||
const localPlayerId = Server.playerId; // your local player ID
|
|
||||||
|
// Render function (can be called independently)
|
||||||
|
const render = () => {
|
||||||
|
const now = Date.now();
|
||||||
|
const fifteenMinutes = 15 * 60 * 1000;
|
||||||
|
|
||||||
// Build entries
|
|
||||||
let html = '';
|
let html = '';
|
||||||
data.forEach((entry, i) => {
|
data.forEach((entry, i) => {
|
||||||
const rank = i + 1;
|
const rank = i + 1;
|
||||||
const medal = rank===1?'🥇':rank===2?'🥈':rank===3?'🥉':rank+'.';
|
const medal = rank===1?'🥇':rank===2?'🥈':rank===3?'🥉':rank+'.';
|
||||||
const isLocal = entry.id === localPlayerId;
|
const isLocal = entry.id === localPlayerId;
|
||||||
|
|
||||||
// Online indicator (green dot if active in last 15 min)
|
// Calculate online status in real-time
|
||||||
const onlineIndicator = entry.isOnline ? '<span class="online-indicator"></span>' : '';
|
const isOnline = (now - entry.lastSeen) < fifteenMinutes;
|
||||||
|
const onlineIndicator = isOnline ? '<span class="online-indicator"></span>' : '';
|
||||||
const nameSuffix = isLocal ? '💗' : '';
|
const nameSuffix = isLocal ? '💗' : '';
|
||||||
|
|
||||||
// Use last 5 chars of playerId if available, otherwise fallback
|
const displayName = entry.id ? entry.id.slice(-5) : 'Anon';
|
||||||
const displayName = entry.id
|
|
||||||
? entry.id.slice(-5)
|
// Calculate time since last seen
|
||||||
: 'Anon';
|
const timeSinceLastSeen = now - entry.lastSeen;
|
||||||
|
const lastSeenText = formatTimeSince(timeSinceLastSeen);
|
||||||
|
|
||||||
html += `<div class="leaderboard-entry${isLocal ? ' local-player' : ''}">
|
html += `<div class="leaderboard-entry${isLocal ? ' local-player' : ''}">
|
||||||
<span class="rank">${medal}</span>
|
<span class="rank">${medal}</span>
|
||||||
<span class="name">${this.escapeHtml(displayName)}${onlineIndicator}${nameSuffix}</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 mass${sortBy==='mass'?' sorted':''}">${this.formatMass(entry.mass,{forceSolarMass:true})}</span>
|
||||||
<span class="value age${sortBy==='age'?' sorted':''}">${this.formatAge(entry.holeAge)}</span>
|
<span class="value age${sortBy==='age'?' sorted':''}">${this.formatAge(entry.holeAge)}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
container.innerHTML = html;
|
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) {
|
escapeHtml: function(text) {
|
||||||
|
|||||||
310
style.css
310
style.css
@ -19,28 +19,127 @@ canvas {
|
|||||||
display: block;
|
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;
|
position: absolute;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
right: 20px;
|
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0.3;
|
opacity: 0.5;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
#gear-icon:hover {
|
#gear-icon {
|
||||||
opacity: 0.6;
|
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 {
|
#settings-menu {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50px;
|
top: 50px;
|
||||||
right: 20px;
|
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);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -69,7 +168,7 @@ canvas {
|
|||||||
|
|
||||||
.menu-btn {
|
.menu-btn {
|
||||||
background: rgba(60, 60, 80, 0.5);
|
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);
|
color: rgba(255, 255, 255, 0.5);
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
@ -80,69 +179,39 @@ canvas {
|
|||||||
transition: all 0.2s;
|
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 {
|
.menu-btn.danger:hover {
|
||||||
background: rgba(120, 40, 40, 0.6);
|
background: rgba(255, 0, 0, 0.42);
|
||||||
border-color: rgba(255, 100, 100, 0.3);
|
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 {
|
#upgrade-panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 90px;
|
top: 50px;
|
||||||
left: 20px;
|
right: 20px;
|
||||||
color: rgba(255, 255, 255, 0.3);
|
display: none;
|
||||||
font-size: 12px;
|
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 {
|
.upgrade-row {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upgrade-btn {
|
.upgrade-btn {
|
||||||
background: rgba(60, 60, 80, 0.5);
|
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);
|
color: rgba(255, 255, 255, 0.5);
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
@ -157,16 +226,17 @@ canvas {
|
|||||||
|
|
||||||
.upgrade-btn:disabled {
|
.upgrade-btn:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.69);
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* SCORES */
|
||||||
.modal {
|
.modal {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 80px;
|
top: 50px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
display: none;
|
display: none;
|
||||||
width: 360px;
|
z-index: 999;
|
||||||
z-index: 5000;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +245,8 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.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);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -184,34 +255,30 @@ canvas {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leaderboard-title,
|
||||||
.leaderboard-entry {
|
.leaderboard-entry {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 90px 60px 90px 60px;
|
||||||
|
column-gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
background: rgba(10, 10, 10, 0.3);
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaderboard-title {
|
.leaderboard-entry {
|
||||||
display: flex;
|
background: rgba(255, 255, 255, 0.03);
|
||||||
font-size: 12px;
|
|
||||||
padding: 5px 0;
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaderboard-entry .rank {
|
.leaderboard-entry .rank {
|
||||||
width: 40px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: rgba(255, 255, 255, 0.5);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaderboard-entry .name {
|
.leaderboard-entry .name {
|
||||||
flex: 1;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@ -221,8 +288,6 @@ canvas {
|
|||||||
|
|
||||||
.leaderboard-entry .value {
|
.leaderboard-entry .value {
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
min-width: 80px;
|
|
||||||
text-align: right;
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,37 +326,6 @@ canvas {
|
|||||||
line-height: 1.5;
|
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 {
|
.online-indicator {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 6px;
|
width: 6px;
|
||||||
@ -314,3 +348,71 @@ canvas {
|
|||||||
box-shadow: 0 0 4px #00ff00, 0 0 6px #00ff00;
|
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