Grok 4.5
xAIJuly 2026
What it wrote
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Voidkindle — Incremental</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="bg">
<canvas id="stars"></canvas>
<div class="nebula n1"></div>
<div class="nebula n2"></div>
<div class="nebula n3"></div>
</div>
<div id="toast-root"></div>
<div id="float-root"></div>
<header class="topbar">
<div class="brand">
<div class="brand-mark" aria-hidden="true">◈</div>
<div>
<h1>Voidkindle</h1>
<p class="tagline">Light the void. Grow the fire.</p>
</div>
</div>
<div class="top-actions">
<button class="icon-btn" id="btn-stats" title="Stats">Stats</button>
<button class="icon-btn" id="btn-achievements" title="Achievements">Badges</button>
<button class="icon-btn" id="btn-settings" title="Settings">Settings</button>
</div>
</header>
<main class="layout">
<section class="panel spark-panel">
<div class="energy-block">
<div class="energy-label">Entropy</div>
<div class="energy-value" id="energy">0</div>
<div class="energy-rate"><span id="eps">0</span> / sec</div>
<div class="meta-row">
<span class="meta-chip">Nebulas <strong id="prestige-count">0</strong></span>
<span class="meta-chip">Mult <strong id="prestige-mult">×1</strong></span>
</div>
</div>
<button class="spark-btn" id="spark" aria-label="Kindle spark">
<span class="spark-ring r1"></span>
<span class="spark-ring r2"></span>
<span class="spark-core">
<span class="spark-glyph">✦</span>
</span>
<span class="spark-label">Kindle</span>
</button>
<div class="click-power">+<span id="click-power">1</span> per kindle</div>
</section>
<section class="panel tabs-panel">
<nav class="tabs" role="tablist">
<button class="tab active" data-tab="generators">Generators</button>
<button class="tab" data-tab="upgrades">Upgrades</button>
<button class="tab" data-tab="prestige">Collapse</button>
</nav>
<div class="tab-body active" id="tab-generators">
<div class="buy-row">
<span class="muted">Buy amount</span>
<div class="seg">
<button class="seg-btn active" data-amount="1">1</button>
<button class="seg-btn" data-amount="10">10</button>
<button class="seg-btn" data-amount="100">100</button>
<button class="seg-btn" data-amount="max">Max</button>
</div>
</div>
<div class="card-list" id="generator-list"></div>
</div>
<div class="tab-body" id="tab-upgrades">
<div class="card-list" id="upgrade-list"></div>
</div>
<div class="tab-body" id="tab-prestige">
<div class="prestige-card">
<div class="prestige-icon">◎</div>
<h2>Collapse into Nebula</h2>
<p>
Reset entropy and generators. Gain nebulas that permanently multiply all entropy
production and kindle power.
</p>
<div class="prestige-stats">
<div>
<span class="muted">You will gain</span>
<strong id="prestige-gain">0</strong>
<span class="muted">nebulas</span>
</div>
<div>
<span class="muted">New multiplier</span>
<strong id="prestige-next-mult">×1</strong>
</div>
<div>
<span class="muted">Requirement</span>
<strong id="prestige-req">1.00e6</strong>
<span class="muted">entropy earned</span>
</div>
</div>
<button class="primary-btn wide" id="btn-prestige" disabled>Collapse</button>
<p class="hint" id="prestige-hint">Earn more entropy to collapse.</p>
</div>
</div>
</section>
</main>
<footer class="footer">
<span id="version">v1.0</span>
<span class="dot">·</span>
<span id="last-save">autosave on</span>
</footer>
<!-- Modals -->
<dialog id="modal-stats">
<form method="dialog" class="modal">
<header>
<h2>Statistics</h2>
<button class="icon-btn close" value="cancel">✕</button>
</header>
<div class="stat-grid" id="stats-grid"></div>
</form>
</dialog>
<dialog id="modal-achievements">
<form method="dialog" class="modal">
<header>
<h2>Badges</h2>
<button class="icon-btn close" value="cancel">✕</button>
</header>
<div class="ach-grid" id="ach-grid"></div>
</form>
</dialog>
<dialog id="modal-settings">
<form method="dialog" class="modal">
<header>
<h2>Settings</h2>
<button class="icon-btn close" value="cancel">✕</button>
</header>
<div class="settings-list">
<label class="switch-row">
<span>Floating numbers</span>
<input type="checkbox" id="set-floats" checked />
</label>
<label class="switch-row">
<span>Screen shake on big buys</span>
<input type="checkbox" id="set-shake" checked />
</label>
<label class="switch-row">
<span>Reduced particles</span>
<input type="checkbox" id="set-particles" />
</label>
<div class="settings-actions">
<button type="button" class="ghost-btn" id="btn-export">Export save</button>
<button type="button" class="ghost-btn" id="btn-import">Import save</button>
<button type="button" class="danger-btn" id="btn-reset">Hard reset</button>
</div>
<textarea id="import-box" class="import-box" placeholder="Paste save data here…" hidden></textarea>
<button type="button" class="primary-btn wide" id="btn-import-confirm" hidden>Load imported save</button>
</div>
</form>
</dialog>
<dialog id="modal-offline">
<form method="dialog" class="modal">
<header>
<h2>While you were away</h2>
<button class="icon-btn close" value="cancel">✕</button>
</header>
<p class="offline-msg">
The void simmered for <strong id="offline-time">0s</strong> and produced
<strong id="offline-gain">0</strong> entropy.
</p>
<button class="primary-btn wide" value="default">Collect</button>
</form>
</dialog>
<script src="game.js"></script>
</body>
</html>:root {
--bg: #07060f;
--bg-elev: rgba(16, 14, 32, 0.72);
--bg-card: rgba(24, 20, 48, 0.78);
--border: rgba(168, 140, 255, 0.16);
--border-hot: rgba(130, 220, 255, 0.45);
--text: #f2edff;
--muted: #9b93b8;
--accent: #8b7cff;
--accent-2: #52e0ff;
--accent-3: #ff6bcb;
--good: #5dffb1;
--warn: #ffd166;
--danger: #ff6b8a;
--glow: 0 0 40px rgba(139, 124, 255, 0.35);
--radius: 18px;
--font: "Outfit", system-ui, sans-serif;
--display: "Space Grotesk", "Outfit", system-ui, sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
min-height: 100%;
background: var(--bg);
color: var(--text);
font-family: var(--font);
overflow-x: hidden;
}
body {
min-height: 100vh;
}
button {
font: inherit;
color: inherit;
cursor: pointer;
border: none;
background: none;
}
.bg {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
background:
radial-gradient(1200px 600px at 10% -10%, rgba(88, 40, 180, 0.28), transparent 60%),
radial-gradient(900px 500px at 90% 10%, rgba(20, 120, 180, 0.2), transparent 55%),
radial-gradient(800px 700px at 50% 100%, rgba(180, 40, 120, 0.12), transparent 50%),
var(--bg);
}
#stars {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.nebula {
position: absolute;
border-radius: 50%;
filter: blur(60px);
opacity: 0.45;
animation: drift 28s ease-in-out infinite alternate;
}
.n1 {
width: 420px;
height: 420px;
left: -80px;
top: 20%;
background: radial-gradient(circle, rgba(120, 90, 255, 0.55), transparent 70%);
}
.n2 {
width: 360px;
height: 360px;
right: -60px;
top: 40%;
background: radial-gradient(circle, rgba(50, 200, 255, 0.4), transparent 70%);
animation-duration: 36s;
animation-delay: -8s;
}
.n3 {
width: 500px;
height: 280px;
left: 30%;
bottom: -80px;
background: radial-gradient(circle, rgba(255, 80, 180, 0.25), transparent 70%);
animation-duration: 42s;
animation-delay: -15s;
}
@keyframes drift {
from {
transform: translate3d(0, 0, 0) scale(1);
}
to {
transform: translate3d(40px, -30px, 0) scale(1.08);
}
}
.topbar,
.layout,
.footer {
position: relative;
z-index: 1;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1.1rem 1.4rem 0.4rem;
max-width: 1200px;
margin: 0 auto;
}
.brand {
display: flex;
align-items: center;
gap: 0.85rem;
}
.brand-mark {
width: 48px;
height: 48px;
border-radius: 14px;
display: grid;
place-items: center;
font-size: 1.35rem;
color: var(--accent-2);
background: linear-gradient(145deg, rgba(139, 124, 255, 0.28), rgba(82, 224, 255, 0.12));
border: 1px solid var(--border);
box-shadow: var(--glow);
}
.brand h1 {
margin: 0;
font-family: var(--display);
font-size: 1.35rem;
letter-spacing: 0.02em;
font-weight: 700;
}
.tagline {
margin: 0.1rem 0 0;
color: var(--muted);
font-size: 0.85rem;
}
.top-actions {
display: flex;
gap: 0.45rem;
flex-wrap: wrap;
}
.icon-btn {
padding: 0.55rem 0.85rem;
border-radius: 999px;
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.04);
color: var(--text);
font-size: 0.85rem;
font-weight: 500;
transition: 0.18s ease;
}
.icon-btn:hover {
border-color: var(--border-hot);
background: rgba(130, 220, 255, 0.08);
}
.layout {
display: grid;
grid-template-columns: minmax(280px, 380px) 1fr;
gap: 1.1rem;
max-width: 1200px;
margin: 0.6rem auto 2rem;
padding: 0 1.2rem;
}
.panel {
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: calc(var(--radius) + 4px);
backdrop-filter: blur(16px);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
}
.spark-panel {
display: flex;
flex-direction: column;
align-items: center;
padding: 1.5rem 1.2rem 1.4rem;
position: sticky;
top: 1rem;
height: fit-content;
}
.energy-block {
text-align: center;
width: 100%;
margin-bottom: 1.4rem;
}
.energy-label {
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.18em;
font-size: 0.72rem;
font-weight: 600;
}
.energy-value {
font-family: var(--display);
font-size: clamp(2.2rem, 5vw, 3.1rem);
font-weight: 700;
line-height: 1.1;
margin: 0.25rem 0;
background: linear-gradient(120deg, #fff 20%, var(--accent-2) 55%, var(--accent) 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 0 40px rgba(82, 224, 255, 0.25);
transition: transform 0.12s ease;
}
.energy-value.bump {
transform: scale(1.04);
}
.energy-rate {
color: var(--good);
font-weight: 600;
font-size: 0.95rem;
}
.meta-row {
display: flex;
gap: 0.5rem;
justify-content: center;
flex-wrap: wrap;
margin-top: 0.85rem;
}
.meta-chip {
font-size: 0.8rem;
color: var(--muted);
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border);
padding: 0.35rem 0.7rem;
border-radius: 999px;
}
.meta-chip strong {
color: var(--accent-2);
font-weight: 600;
margin-left: 0.25rem;
}
.spark-btn {
position: relative;
width: 180px;
height: 180px;
border-radius: 50%;
display: grid;
place-items: center;
isolation: isolate;
transition: transform 0.12s ease;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.spark-btn:active,
.spark-btn.pressed {
transform: scale(0.94);
}
.spark-core {
width: 112px;
height: 112px;
border-radius: 50%;
display: grid;
place-items: center;
background:
radial-gradient(circle at 35% 30%, rgba(255, 255, 255, 0.95), transparent 28%),
radial-gradient(circle at 50% 50%, #b8a8ff 0%, #5a47d8 45%, #1a1240 100%);
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.12),
0 0 30px rgba(139, 124, 255, 0.55),
0 0 70px rgba(82, 224, 255, 0.28),
inset 0 -10px 30px rgba(0, 0, 0, 0.35);
z-index: 2;
animation: pulse 2.4s ease-in-out infinite;
}
.spark-glyph {
font-size: 2.2rem;
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8));
}
.spark-label {
position: absolute;
bottom: 10px;
z-index: 3;
font-size: 0.8rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.8);
font-weight: 600;
}
.spark-ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 1px solid rgba(130, 220, 255, 0.25);
z-index: 1;
}
.spark-ring.r1 {
animation: spin 12s linear infinite;
border-top-color: var(--accent-2);
border-right-color: transparent;
}
.spark-ring.r2 {
inset: 12px;
animation: spin 8s linear infinite reverse;
border-bottom-color: var(--accent-3);
border-left-color: transparent;
}
@keyframes pulse {
0%,
100% {
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.12),
0 0 30px rgba(139, 124, 255, 0.55),
0 0 70px rgba(82, 224, 255, 0.28),
inset 0 -10px 30px rgba(0, 0, 0, 0.35);
}
50% {
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.18),
0 0 45px rgba(139, 124, 255, 0.75),
0 0 90px rgba(82, 224, 255, 0.4),
inset 0 -10px 30px rgba(0, 0, 0, 0.35);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.click-power {
margin-top: 1rem;
color: var(--muted);
font-size: 0.9rem;
}
.click-power span {
color: var(--accent-2);
font-weight: 700;
}
.tabs-panel {
display: flex;
flex-direction: column;
min-height: 640px;
overflow: hidden;
}
.tabs {
display: flex;
gap: 0.35rem;
padding: 0.85rem 0.9rem 0;
border-bottom: 1px solid var(--border);
}
.tab {
flex: 1;
padding: 0.75rem 0.5rem;
border-radius: 12px 12px 0 0;
color: var(--muted);
font-weight: 600;
transition: 0.15s ease;
border: 1px solid transparent;
border-bottom: none;
}
.tab:hover {
color: var(--text);
background: rgba(255, 255, 255, 0.03);
}
.tab.active {
color: var(--text);
background: rgba(139, 124, 255, 0.12);
border-color: var(--border);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.tab-body {
display: none;
padding: 0.9rem;
overflow: auto;
flex: 1;
}
.tab-body.active {
display: block;
}
.buy-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 0.85rem;
}
.seg {
display: flex;
background: rgba(0, 0, 0, 0.25);
border: 1px solid var(--border);
border-radius: 999px;
padding: 0.2rem;
gap: 0.15rem;
}
.seg-btn {
min-width: 44px;
padding: 0.35rem 0.7rem;
border-radius: 999px;
color: var(--muted);
font-weight: 600;
font-size: 0.85rem;
}
.seg-btn.active {
background: linear-gradient(135deg, rgba(139, 124, 255, 0.9), rgba(82, 224, 255, 0.55));
color: #0b0a14;
}
.card-list {
display: flex;
flex-direction: column;
gap: 0.7rem;
}
.card {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 0.85rem;
align-items: center;
padding: 0.95rem;
border-radius: 16px;
background: var(--bg-card);
border: 1px solid var(--border);
transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.card:hover {
border-color: rgba(168, 140, 255, 0.35);
}
.card.affordable {
border-color: rgba(93, 255, 177, 0.28);
box-shadow: 0 0 24px rgba(93, 255, 177, 0.06);
}
.card.locked {
opacity: 0.55;
}
.card-icon {
width: 48px;
height: 48px;
border-radius: 14px;
display: grid;
place-items: center;
font-size: 1.35rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
}
.card-body h3 {
margin: 0;
font-family: var(--display);
font-size: 1rem;
font-weight: 600;
}
.card-body p {
margin: 0.15rem 0 0;
color: var(--muted);
font-size: 0.82rem;
line-height: 1.35;
}
.card-meta {
display: flex;
gap: 0.55rem;
flex-wrap: wrap;
margin-top: 0.45rem;
font-size: 0.78rem;
}
.pill {
padding: 0.18rem 0.5rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.05);
color: var(--muted);
border: 1px solid transparent;
}
.pill.owned {
color: var(--accent-2);
border-color: rgba(82, 224, 255, 0.2);
}
.pill.prod {
color: var(--good);
border-color: rgba(93, 255, 177, 0.18);
}
.buy-btn {
min-width: 110px;
padding: 0.7rem 0.85rem;
border-radius: 12px;
background: linear-gradient(135deg, rgba(139, 124, 255, 0.25), rgba(82, 224, 255, 0.1));
border: 1px solid var(--border);
font-weight: 700;
font-size: 0.88rem;
transition: 0.15s ease;
text-align: center;
}
.buy-btn .cost {
display: block;
font-size: 0.78rem;
font-weight: 600;
color: var(--muted);
margin-top: 0.15rem;
}
.buy-btn:not(:disabled):hover {
border-color: var(--border-hot);
transform: translateY(-1px);
box-shadow: 0 8px 24px rgba(82, 224, 255, 0.12);
}
.buy-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.buy-btn.can {
background: linear-gradient(135deg, rgba(93, 255, 177, 0.22), rgba(82, 224, 255, 0.14));
border-color: rgba(93, 255, 177, 0.4);
color: #d8ffe9;
}
.upgrade.owned {
border-color: rgba(139, 124, 255, 0.4);
background: linear-gradient(135deg, rgba(139, 124, 255, 0.12), rgba(24, 20, 48, 0.78));
}
.upgrade.owned .buy-btn {
background: rgba(255, 255, 255, 0.04);
color: var(--good);
}
.prestige-card {
text-align: center;
padding: 1.5rem 1rem 1.2rem;
max-width: 520px;
margin: 1rem auto;
}
.prestige-icon {
width: 72px;
height: 72px;
margin: 0 auto 0.8rem;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 2rem;
color: var(--accent-2);
background: radial-gradient(circle, rgba(82, 224, 255, 0.2), rgba(139, 124, 255, 0.08));
border: 1px solid var(--border);
box-shadow: var(--glow);
}
.prestige-card h2 {
margin: 0 0 0.5rem;
font-family: var(--display);
}
.prestige-card > p {
color: var(--muted);
line-height: 1.5;
margin: 0 auto 1.2rem;
max-width: 34ch;
}
.prestige-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
margin-bottom: 1.2rem;
}
.prestige-stats > div {
background: rgba(0, 0, 0, 0.22);
border: 1px solid var(--border);
border-radius: 14px;
padding: 0.85rem 0.5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.prestige-stats strong {
font-family: var(--display);
font-size: 1.15rem;
color: var(--accent-2);
}
.primary-btn,
.ghost-btn,
.danger-btn {
border-radius: 12px;
padding: 0.8rem 1.1rem;
font-weight: 700;
transition: 0.15s ease;
}
.primary-btn {
background: linear-gradient(135deg, #8b7cff, #4ec7ef);
color: #0b0a14;
box-shadow: 0 10px 30px rgba(82, 224, 255, 0.18);
}
.primary-btn:hover:not(:disabled) {
transform: translateY(-1px);
filter: brightness(1.05);
}
.primary-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
filter: grayscale(0.4);
}
.primary-btn.wide,
.ghost-btn.wide {
width: 100%;
}
.ghost-btn {
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.04);
}
.ghost-btn:hover {
border-color: var(--border-hot);
}
.danger-btn {
border: 1px solid rgba(255, 107, 138, 0.4);
background: rgba(255, 107, 138, 0.12);
color: #ffc0cd;
}
.danger-btn:hover {
background: rgba(255, 107, 138, 0.2);
}
.hint {
color: var(--muted);
font-size: 0.85rem;
margin: 0.75rem 0 0;
}
.footer {
max-width: 1200px;
margin: 0 auto 1.5rem;
padding: 0 1.2rem;
color: var(--muted);
font-size: 0.8rem;
display: flex;
gap: 0.5rem;
align-items: center;
}
.dot {
opacity: 0.5;
}
.muted {
color: var(--muted);
}
/* Floating numbers */
#float-root {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 50;
overflow: hidden;
}
.float-num {
position: fixed;
font-family: var(--display);
font-weight: 700;
font-size: 1.05rem;
color: var(--accent-2);
text-shadow: 0 0 12px rgba(82, 224, 255, 0.7);
animation: float-up 0.9s ease-out forwards;
white-space: nowrap;
}
.float-num.big {
color: var(--good);
font-size: 1.25rem;
}
@keyframes float-up {
0% {
opacity: 0;
transform: translate(-50%, 0) scale(0.8);
}
15% {
opacity: 1;
}
100% {
opacity: 0;
transform: translate(-50%, -70px) scale(1.05);
}
}
/* Toasts */
#toast-root {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 80;
display: flex;
flex-direction: column;
gap: 0.5rem;
width: min(320px, calc(100vw - 2rem));
pointer-events: none;
}
.toast {
pointer-events: auto;
padding: 0.85rem 1rem;
border-radius: 14px;
background: rgba(18, 16, 36, 0.92);
border: 1px solid var(--border-hot);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4), var(--glow);
animation: toast-in 0.28s ease;
backdrop-filter: blur(12px);
}
.toast.ach .title {
color: var(--warn);
}
.toast .title {
font-weight: 700;
font-size: 0.88rem;
margin-bottom: 0.15rem;
}
.toast .body {
color: var(--muted);
font-size: 0.82rem;
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateX(16px);
}
to {
opacity: 1;
transform: none;
}
}
/* Dialogs */
dialog {
border: none;
padding: 0;
background: transparent;
color: var(--text);
max-width: min(560px, calc(100vw - 2rem));
width: 100%;
}
dialog::backdrop {
background: rgba(4, 3, 12, 0.72);
backdrop-filter: blur(6px);
}
.modal {
background: rgba(16, 14, 32, 0.96);
border: 1px solid var(--border);
border-radius: 20px;
padding: 0;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
overflow: hidden;
}
.modal header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.1rem;
border-bottom: 1px solid var(--border);
}
.modal header h2 {
margin: 0;
font-family: var(--display);
font-size: 1.15rem;
}
.modal header .close {
min-width: 36px;
height: 36px;
padding: 0;
display: grid;
place-items: center;
border-radius: 10px;
}
.stat-grid,
.settings-list,
.ach-grid,
.offline-msg {
padding: 1rem 1.1rem 1.2rem;
}
.stat-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.65rem;
}
.stat {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border);
border-radius: 12px;
padding: 0.75rem;
}
.stat .k {
display: block;
color: var(--muted);
font-size: 0.78rem;
margin-bottom: 0.25rem;
}
.stat .v {
font-family: var(--display);
font-weight: 600;
}
.ach-grid {
display: grid;
grid-template-columns: 1fr;
gap: 0.55rem;
max-height: min(60vh, 480px);
overflow: auto;
}
.ach {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.75rem;
align-items: center;
padding: 0.75rem;
border-radius: 12px;
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.03);
}
.ach.done {
border-color: rgba(255, 209, 102, 0.35);
background: linear-gradient(135deg, rgba(255, 209, 102, 0.1), rgba(255, 255, 255, 0.02));
}
.ach .icon {
width: 42px;
height: 42px;
border-radius: 12px;
display: grid;
place-items: center;
background: rgba(0, 0, 0, 0.25);
font-size: 1.15rem;
filter: grayscale(1);
opacity: 0.55;
}
.ach.done .icon {
filter: none;
opacity: 1;
}
.ach h3 {
margin: 0;
font-size: 0.92rem;
}
.ach p {
margin: 0.15rem 0 0;
color: var(--muted);
font-size: 0.8rem;
}
.switch-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.7rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.switch-row input {
width: 18px;
height: 18px;
accent-color: var(--accent-2);
}
.settings-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.55rem;
margin-top: 1rem;
}
.settings-actions .danger-btn {
grid-column: 1 / -1;
}
.import-box {
width: 100%;
min-height: 100px;
margin-top: 0.75rem;
border-radius: 12px;
border: 1px solid var(--border);
background: rgba(0, 0, 0, 0.3);
color: var(--text);
padding: 0.75rem;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.78rem;
resize: vertical;
}
.offline-msg {
color: var(--muted);
line-height: 1.5;
margin: 0;
}
.offline-msg strong {
color: var(--accent-2);
}
body.shake {
animation: shake 0.35s ease;
}
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
20% {
transform: translateX(-4px);
}
40% {
transform: translateX(4px);
}
60% {
transform: translateX(-3px);
}
80% {
transform: translateX(2px);
}
}
@media (max-width: 860px) {
.layout {
grid-template-columns: 1fr;
}
.spark-panel {
position: static;
}
.prestige-stats {
grid-template-columns: 1fr;
}
.stat-grid {
grid-template-columns: 1fr;
}
.settings-actions {
grid-template-columns: 1fr;
}
.card {
grid-template-columns: auto 1fr;
}
.buy-btn {
grid-column: 1 / -1;
}
}(() => {
"use strict";
const SAVE_KEY = "voidkindle_save_v1";
const TICK_MS = 50;
const OFFLINE_CAP_HOURS = 8;
const PRESTIGE_BASE = 1e6;
const GENERATORS = [
{
id: "ember",
name: "Ember Spore",
icon: "✦",
desc: "A dim spark that whispers itself back into being.",
baseCost: 15,
costMult: 1.15,
baseProd: 0.1,
},
{
id: "prism",
name: "Pulse Prism",
icon: "◆",
desc: "Splits one gleam into a lattice of fragile light.",
baseCost: 100,
costMult: 1.15,
baseProd: 1,
},
{
id: "forge",
name: "Skyforge",
icon: "✧",
desc: "Hammers vacuum into warm, usable energy.",
baseCost: 1100,
costMult: 1.14,
baseProd: 8,
},
{
id: "reactor",
name: "Astra Reactor",
icon: "◎",
desc: "Binds unstable stellar corelets in glass.",
baseCost: 12000,
costMult: 1.13,
baseProd: 47,
},
{
id: "cluster",
name: "Cluster Loom",
icon: "⬡",
desc: "Weaves star nurseries along a cosmic filament.",
baseCost: 130000,
costMult: 1.12,
baseProd: 260,
},
{
id: "quasar",
name: "Quasar Heart",
icon: "✵",
desc: "An engine that feeds on the edge of a black glare.",
baseCost: 1.4e6,
costMult: 1.12,
baseProd: 1400,
},
{
id: "singularity",
name: "Singularity Mill",
icon: "◉",
desc: "Grinds spacetime itself into lattice of pure entropy.",
baseCost: 2e7,
costMult: 1.11,
baseProd: 7800,
},
{
id: "echo",
name: "Echo Lattice",
icon: "❋",
desc: "Resonators that amplify every earlier generator.",
baseCost: 3.3e8,
costMult: 1.1,
baseProd: 44000,
},
];
const UPGRADES = [
{
id: "click1",
name: "Firmer Kindling",
icon: "✧",
desc: "Kindle power ×2.",
cost: 50,
effect: (s) => {
s.mult.click *= 2;
},
},
{
id: "click2",
name: "Twin Sparks",
icon: "✦",
desc: "Kindle power ×2.",
cost: 500,
effect: (s) => {
s.mult.click *= 2;
},
},
{
id: "click3",
name: "Solar Touch",
icon: "☀",
desc: "Kindle power ×3.",
cost: 5000,
effect: (s) => {
s.mult.click *= 3;
},
},
{
id: "prod1",
name: "Brighter Foam",
icon: "◇",
desc: "All generator output ×2.",
cost: 1000,
effect: (s) => {
s.mult.prod *= 2;
},
},
{
id: "prod2",
name: "Resonant Void",
icon: "◎",
desc: "All generator output ×2.",
cost: 25000,
effect: (s) => {
s.mult.prod *= 2;
},
},
{
id: "prod3",
name: "Cascade Cascade",
icon: "⧉",
desc: "All generator output ×3.",
cost: 5e5,
effect: (s) => {
s.mult.prod *= 3;
},
},
{
id: "emberBoost",
name: "Spore Nursery",
icon: "✦",
desc: "Ember Spore production ×5.",
cost: 250,
effect: (s) => {
s.mult.gens.ember = (s.mult.gens.ember || 1) * 5;
},
},
{
id: "prismBoost",
name: "Hardlight Edges",
icon: "◆",
desc: "Pulse Prism production ×5.",
cost: 2000,
effect: (s) => {
s.mult.gens.prism = (s.mult.gens.prism || 1) * 5;
},
},
{
id: "synergy",
name: "Crosslinked Fire",
icon: "∞",
desc: "Each generator owned boosts total production by +1%.",
cost: 1e5,
effect: (s) => {
s.flags.synergy = true;
},
},
{
id: "clickGen",
name: "Auto-Kindle",
icon: "↻",
desc: "Generate kindle power automatically each second.",
cost: 5e4,
effect: (s) => {
s.flags.autoClick = true;
},
},
{
id: "offline",
name: "Deep Sleep Circuit",
icon: "☾",
desc: "Offline progress efficiency 50% → 90%.",
cost: 1e6,
effect: (s) => {
s.flags.offlineBoost = true;
},
},
];
const ACHIEVEMENTS = [
{ id: "first", name: "First Light", desc: "Kindle once.", icon: "✦", check: (s) => s.stats.clicks >= 1 },
{ id: "click100", name: "Warm Hands", desc: "Kindle 100 times.", icon: "✋", check: (s) => s.stats.clicks >= 100 },
{ id: "click1k", name: "Immune System of Sparks", desc: "Kindle 1,000 times.", icon: "⚡", check: (s) => s.stats.clicks >= 1000 },
{ id: "e1k", name: "Thousandfold", desc: "Hold 1,000 entropy.", icon: "◎", check: (s) => s.energy >= 1e3 },
{ id: "e1m", name: "Real Entropy", desc: "Hold 1,000,000 entropy.", icon: "✶", check: (s) => s.energy >= 1e6 },
{ id: "e1b", name: "Brightness Uncontained", desc: "Hold 1e9 entropy.", icon: "✵", check: (s) => s.energy >= 1e9 },
{ id: "own10", name: "Seed Nursery", desc: "Own 10 of any generator.", icon: "◆", check: (s) => Object.values(s.gens).some((n) => n >= 10) },
{ id: "own50", name: "Factory Stars", desc: "Own 50 of any generator.", icon: "⬡", check: (s) => Object.values(s.gens).some((n) => n >= 50) },
{ id: "allTypes", name: "Full Spectrum", desc: "Own every generator at least once.", icon: "🌈", check: (s) => GENERATORS.every((g) => (s.gens[g.id] || 0) >= 1) },
{ id: "up3", name: "Augmented", desc: "Buy 3 upgrades.", icon: "◇", check: (s) => s.upgrades.length >= 3 },
{ id: "upAll", name: "Overclocked Reality", desc: "Buy all upgrades.", icon: "∞", check: (s) => s.upgrades.length >= UPGRADES.length },
{ id: "prestige1", name: "First Collapse", desc: "Collapse into a nebula once.", icon: "◎", check: (s) => s.prestige >= 1 },
{ id: "prestige5", name: "Serial Catastrophe", desc: "Collapse 5 times.", icon: "◉", check: (s) => s.prestige >= 5 },
{ id: "eps100", name: "Steady Glow", desc: "Reach 100 entropy / sec.", icon: "➤", check: (s) => s.stats.peakEps >= 100 },
{ id: "eps10k", name: "Solar Storm", desc: "Reach 10,000 entropy / sec.", icon: "☄", check: (s) => s.stats.peakEps >= 1e4 },
];
const settings = {
floats: true,
shake: true,
reducedParticles: false,
};
function defaultState() {
const gens = {};
GENERATORS.forEach((g) => {
gens[g.id] = 0;
});
return {
energy: 0,
gens,
upgrades: [],
prestige: 0,
mult: {
click: 1,
prod: 1,
gens: {},
},
flags: {
synergy: false,
autoClick: false,
offlineBoost: false,
},
achievements: [],
stats: {
clicks: 0,
earned: 0,
totalEarned: 0,
prestigeGained: 0,
timePlayed: 0,
peakEnergy: 0,
peakEps: 0,
births: Date.now(),
},
lastTick: Date.now(),
lastSave: Date.now(),
};
}
let state = defaultState();
let buyAmount = 1;
let uiBuilt = false;
let rafStars = 0;
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => [...document.querySelectorAll(sel)];
const els = {
energy: $("#energy"),
eps: $("#eps"),
prestigeCount: $("#prestige-count"),
prestigeMult: $("#prestige-mult"),
clickPower: $("#click-power"),
spark: $("#spark"),
genList: $("#generator-list"),
upgList: $("#upgrade-list"),
prestigeGain: $("#prestige-gain"),
prestigeNextMult: $("#prestige-next-mult"),
prestigeReq: $("#prestige-req"),
btnPrestige: $("#btn-prestige"),
prestigeHint: $("#prestige-hint"),
floatRoot: $("#float-root"),
toastRoot: $("#toast-root"),
lastSave: $("#last-save"),
statsGrid: $("#stats-grid"),
achGrid: $("#ach-grid"),
stars: $("#stars"),
};
function format(n) {
if (!Number.isFinite(n)) return "∞";
const abs = Math.abs(n);
if (abs < 1000) {
if (abs < 10) return n.toFixed(n % 1 === 0 ? 0 : 2);
if (abs < 100) return n.toFixed(n % 1 === 0 ? 0 : 1);
return Math.floor(n).toString();
}
const units = [
"", "K", "M", "B", "T", "Qa", "Qi", "Sx", "Sp", "Oc", "No", "Dc",
];
const exp = Math.floor(Math.log10(abs));
if (exp >= 36) {
return n.toExponential(2).replace("+", "");
}
const u = Math.floor(exp / 3);
const m = n / Math.pow(10, u * 3);
return `${m.toFixed(m >= 100 ? 0 : m >= 10 ? 1 : 2)}${units[u]}`;
}
function formatTime(sec) {
sec = Math.floor(sec);
if (sec < 60) return `${sec}s`;
if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
if (sec < 86400) {
const h = Math.floor(sec / 3600);
const m = Math.floor((sec % 3600) / 60);
return `${h}h ${m}m`;
}
const d = Math.floor(sec / 86400);
const h = Math.floor((sec % 86400) / 3600);
return `${d}d ${h}h`;
}
function prestigeMult(p) {
return 1 + p * 0.5;
}
function prestigeGainFrom(totalEarned) {
if (totalEarned < PRESTIGE_BASE) return 0;
return Math.floor(Math.sqrt(totalEarned / PRESTIGE_BASE));
}
function genCost(def, owned, qty) {
if (qty === 1) return def.baseCost * Math.pow(def.costMult, owned);
let total = 0;
for (let i = 0; i < qty; i++) {
total += def.baseCost * Math.pow(def.costMult, owned + i);
}
return total;
}
function maxBuy(def, owned, energy) {
let n = 0;
let cost = 0;
let next = def.baseCost * Math.pow(def.costMult, owned);
while (cost + next <= energy && n < 10000) {
cost += next;
n++;
next *= def.costMult;
}
return { n, cost };
}
function totalOwned() {
return Object.values(state.gens).reduce((a, b) => a + b, 0);
}
function clickPower() {
let p = 1 * state.mult.click * prestigeMult(state.prestige);
return p;
}
function genProduction(def) {
const owned = state.gens[def.id] || 0;
if (!owned) return 0;
const gMult = state.mult.gens[def.id] || 1;
return owned * def.baseProd * gMult * state.mult.prod * prestigeMult(state.prestige);
}
function totalEps() {
let eps = GENERATORS.reduce((sum, g) => sum + genProduction(g), 0);
if (state.flags.synergy) {
eps *= 1 + totalOwned() * 0.01;
}
if (state.flags.autoClick) {
eps += clickPower();
}
return eps;
}
function addEnergy(amount, countAsEarned = true) {
if (amount <= 0) return;
state.energy += amount;
if (countAsEarned) {
state.stats.earned += amount;
state.stats.totalEarned += amount;
}
if (state.energy > state.stats.peakEnergy) {
state.stats.peakEnergy = state.energy;
}
}
function toast(title, body, kind = "") {
const el = document.createElement("div");
el.className = `toast ${kind}`;
el.innerHTML = `<div class="title">${title}</div><div class="body">${body}</div>`;
els.toastRoot.appendChild(el);
setTimeout(() => {
el.style.opacity = "0";
el.style.transition = "opacity 0.3s";
setTimeout(() => el.remove(), 300);
}, 2800);
}
function floatAt(x, y, text, big = false) {
if (!settings.floats) return;
const el = document.createElement("div");
el.className = `float-num${big ? " big" : ""}`;
el.textContent = text;
el.style.left = `${x}px`;
el.style.top = `${y}px`;
els.floatRoot.appendChild(el);
setTimeout(() => el.remove(), 950);
}
function shake() {
if (!settings.shake) return;
document.body.classList.remove("shake");
void document.body.offsetWidth;
document.body.classList.add("shake");
setTimeout(() => document.body.classList.remove("shake"), 360);
}
function checkAchievements() {
for (const a of ACHIEVEMENTS) {
if (state.achievements.includes(a.id)) continue;
if (a.check(state)) {
state.achievements.push(a.id);
toast("Badge unlocked", `${a.icon} ${a.name}`, "ach");
}
}
}
function doClick(clientX, clientY) {
const gain = clickPower();
addEnergy(gain);
state.stats.clicks++;
els.energy.classList.add("bump");
setTimeout(() => els.energy.classList.remove("bump"), 120);
floatAt(clientX, clientY, `+${format(gain)}`);
checkAchievements();
renderNumbers();
}
function buyGenerator(id) {
const def = GENERATORS.find((g) => g.id === id);
if (!def) return;
const owned = state.gens[id] || 0;
let qty = buyAmount;
let cost;
if (buyAmount === "max") {
const m = maxBuy(def, owned, state.energy);
if (m.n <= 0) return;
qty = m.n;
cost = m.cost;
} else {
cost = genCost(def, owned, qty);
if (state.energy < cost) return;
}
state.energy -= cost;
state.gens[id] = owned + qty;
if (qty >= 10) shake();
checkAchievements();
renderAll();
}
function buyUpgrade(id) {
if (state.upgrades.includes(id)) return;
const def = UPGRADES.find((u) => u.id === id);
if (!def || state.energy < def.cost) return;
state.energy -= def.cost;
state.upgrades.push(id);
def.effect(state);
toast("Upgrade installed", def.name);
shake();
checkAchievements();
renderAll();
}
function canPrestige() {
return prestigeGainFrom(state.stats.earned) >= 1;
}
function doPrestige() {
const gain = prestigeGainFrom(state.stats.earned);
if (gain < 1) return;
if (!confirm(`Collapse and gain ${gain} nebula${gain === 1 ? "" : "s"}? This resets entropy and generators.`)) {
return;
}
const prestige = state.prestige + gain;
const upgrades = [];
const achievements = state.achievements.slice();
const stats = {
...state.stats,
earned: 0,
prestigeGained: state.stats.prestigeGained + gain,
};
const mult = { click: 1, prod: 1, gens: {} };
const flags = { synergy: false, autoClick: false, offlineBoost: false };
state = defaultState();
state.prestige = prestige;
state.upgrades = upgrades;
state.achievements = achievements;
state.stats = stats;
state.mult = mult;
state.flags = flags;
state.stats.births = stats.births;
toast("Collapse complete", `+${gain} nebula · ×${prestigeMult(prestige).toFixed(1)} mult`);
checkAchievements();
save();
renderAll();
}
function buildUI() {
els.genList.innerHTML = GENERATORS.map(
(g) => `
<article class="card gen" data-id="${g.id}" id="gen-${g.id}">
<div class="card-icon">${g.icon}</div>
<div class="card-body">
<h3>${g.name}</h3>
<p>${g.desc}</p>
<div class="card-meta">
<span class="pill owned">×<span class="owned">0</span></span>
<span class="pill prod"><span class="prod">0</span>/s each</span>
</div>
</div>
<button class="buy-btn" type="button">
Buy
<span class="cost">0</span>
</button>
</article>`
).join("");
els.upgList.innerHTML = UPGRADES.map(
(u) => `
<article class="card upgrade" data-id="${u.id}" id="upg-${u.id}">
<div class="card-icon">${u.icon}</div>
<div class="card-body">
<h3>${u.name}</h3>
<p>${u.desc}</p>
</div>
<button class="buy-btn" type="button">
Buy
<span class="cost">${format(u.cost)}</span>
</button>
</article>`
).join("");
els.achGrid.innerHTML = ACHIEVEMENTS.map(
(a) => `
<div class="ach" id="ach-${a.id}">
<div class="icon">${a.icon}</div>
<div>
<h3>${a.name}</h3>
<p>${a.desc}</p>
</div>
</div>`
).join("");
uiBuilt = true;
}
function renderNumbers() {
els.energy.textContent = format(state.energy);
const eps = totalEps();
els.eps.textContent = format(eps);
els.prestigeCount.textContent = format(state.prestige);
els.prestigeMult.textContent = `×${prestigeMult(state.prestige).toFixed(1)}`;
els.clickPower.textContent = format(clickPower());
if (eps > state.stats.peakEps) state.stats.peakEps = eps;
}
function renderGenerators() {
for (const g of GENERATORS) {
const card = document.getElementById(`gen-${g.id}`);
if (!card) continue;
const owned = state.gens[g.id] || 0;
let qty = buyAmount;
let cost;
if (buyAmount === "max") {
const m = maxBuy(g, owned, state.energy);
qty = Math.max(1, m.n);
cost = m.n > 0 ? m.cost : genCost(g, owned, 1);
} else {
cost = genCost(g, owned, qty);
}
const can = state.energy >= cost && (buyAmount !== "max" || maxBuy(g, owned, state.energy).n > 0);
const unitProd =
g.baseProd *
(state.mult.gens[g.id] || 1) *
state.mult.prod *
prestigeMult(state.prestige);
card.classList.toggle("affordable", can);
card.querySelector(".owned").textContent = format(owned);
card.querySelector(".prod").textContent = format(unitProd);
const btn = card.querySelector(".buy-btn");
btn.disabled = !can;
btn.classList.toggle("can", can);
const label = buyAmount === "max" ? (maxBuy(g, owned, state.energy).n || 1) : qty;
btn.innerHTML = `Buy ${label}<span class="cost">${format(cost)}</span>`;
}
}
function renderUpgrades() {
for (const u of UPGRADES) {
const card = document.getElementById(`upg-${u.id}`);
if (!card) continue;
const owned = state.upgrades.includes(u.id);
const can = !owned && state.energy >= u.cost;
card.classList.toggle("owned", owned);
card.classList.toggle("affordable", can);
const btn = card.querySelector(".buy-btn");
if (owned) {
btn.disabled = true;
btn.classList.remove("can");
btn.innerHTML = `Owned<span class="cost">—</span>`;
} else {
btn.disabled = !can;
btn.classList.toggle("can", can);
btn.innerHTML = `Buy<span class="cost">${format(u.cost)}</span>`;
}
}
}
function renderPrestige() {
const gain = prestigeGainFrom(state.stats.earned);
const nextP = state.prestige + gain;
els.prestigeGain.textContent = format(gain);
els.prestigeNextMult.textContent = `×${prestigeMult(nextP).toFixed(1)}`;
els.prestigeReq.textContent = format(PRESTIGE_BASE);
els.btnPrestige.disabled = gain < 1;
els.prestigeHint.textContent =
gain < 1
? `Need ${format(PRESTIGE_BASE)} entropy earned this life (${format(state.stats.earned)} so far).`
: "Ready to collapse. Generators and entropy reset; nebulas remain.";
}
function renderAchievements() {
for (const a of ACHIEVEMENTS) {
const el = document.getElementById(`ach-${a.id}`);
if (el) el.classList.toggle("done", state.achievements.includes(a.id));
}
}
function renderStats() {
const rows = [
["Entropy", format(state.energy)],
["Per second", format(totalEps())],
["Kindle power", format(clickPower())],
["Total kindles", format(state.stats.clicks)],
["Earned this life", format(state.stats.earned)],
["All-time earned", format(state.stats.totalEarned)],
["Nebulas", format(state.prestige)],
["Prestige mult", `×${prestigeMult(state.prestige).toFixed(1)}`],
["Generators owned", format(totalOwned())],
["Upgrades", `${state.upgrades.length}/${UPGRADES.length}`],
["Badges", `${state.achievements.length}/${ACHIEVEMENTS.length}`],
["Peak entropy", format(state.stats.peakEnergy)],
["Peak / sec", format(state.stats.peakEps)],
["Time played", formatTime(state.stats.timePlayed)],
];
els.statsGrid.innerHTML = rows
.map(
([k, v]) => `
<div class="stat"><span class="k">${k}</span><span class="v">${v}</span></div>`
)
.join("");
}
function renderAll() {
if (!uiBuilt) buildUI();
renderNumbers();
renderGenerators();
renderUpgrades();
renderPrestige();
renderAchievements();
}
function tick(dt) {
state.stats.timePlayed += dt;
const eps = totalEps();
addEnergy(eps * dt);
if (eps > state.stats.peakEps) state.stats.peakEps = eps;
checkAchievements();
}
function save() {
state.lastSave = Date.now();
const payload = {
state,
settings,
v: 1,
};
try {
localStorage.setItem(SAVE_KEY, JSON.stringify(payload));
els.lastSave.textContent = `saved ${new Date().toLocaleTimeString()}`;
} catch {
els.lastSave.textContent = "save failed";
}
}
function load() {
try {
const raw = localStorage.getItem(SAVE_KEY);
if (!raw) return null;
return JSON.parse(raw);
} catch {
return null;
}
}
function applySave(data) {
const base = defaultState();
const s = data.state || {};
state = {
...base,
...s,
gens: { ...base.gens, ...(s.gens || {}) },
mult: {
click: s.mult?.click ?? 1,
prod: s.mult?.prod ?? 1,
gens: { ...(s.mult?.gens || {}) },
},
flags: { ...base.flags, ...(s.flags || {}) },
stats: { ...base.stats, ...(s.stats || {}) },
upgrades: s.upgrades || [],
achievements: s.achievements || [],
};
if (data.settings) Object.assign(settings, data.settings);
}
function exportSave() {
const payload = btoa(unescape(encodeURIComponent(JSON.stringify({ state, settings, v: 1 }))));
navigator.clipboard?.writeText(payload).then(
() => toast("Exported", "Save copied to clipboard"),
() => {
prompt("Copy your save:", payload);
}
);
}
function importSave(text) {
try {
const json = decodeURIComponent(escape(atob(text.trim())));
const data = JSON.parse(json);
applySave(data);
save();
renderAll();
toast("Imported", "Save loaded");
} catch {
toast("Import failed", "Invalid save data");
}
}
function hardReset() {
if (!confirm("Hard reset? This erases everything permanently.")) return;
if (!confirm("Absolutely sure? There is no undo.")) return;
localStorage.removeItem(SAVE_KEY);
state = defaultState();
renderAll();
toast("Reset", "A new void opens");
}
function processOffline() {
const now = Date.now();
const elapsedMs = Math.max(0, now - (state.lastTick || now));
const maxMs = OFFLINE_CAP_HOURS * 3600 * 1000;
const capped = Math.min(elapsedMs, maxMs);
if (capped < 15000) {
state.lastTick = now;
return;
}
const efficiency = state.flags.offlineBoost ? 0.9 : 0.5;
const eps = totalEps();
const gained = eps * (capped / 1000) * efficiency;
if (gained > 0) {
addEnergy(gained);
$("#offline-time").textContent = formatTime(capped / 1000);
$("#offline-gain").textContent = format(gained);
$("#modal-offline").showModal();
}
state.lastTick = now;
}
function bindUI() {
els.spark.addEventListener("pointerdown", (e) => {
els.spark.classList.add("pressed");
doClick(e.clientX, e.clientY - 10);
});
window.addEventListener("pointerup", () => els.spark.classList.remove("pressed"));
els.spark.addEventListener("keydown", (e) => {
if (e.key === " " || e.key === "Enter") {
e.preventDefault();
const r = els.spark.getBoundingClientRect();
doClick(r.left + r.width / 2, r.top + r.height / 2);
}
});
$$(".tab").forEach((tab) => {
tab.addEventListener("click", () => {
$$(".tab").forEach((t) => t.classList.remove("active"));
$$(".tab-body").forEach((b) => b.classList.remove("active"));
tab.classList.add("active");
$(`#tab-${tab.dataset.tab}`).classList.add("active");
});
});
$$(".seg-btn").forEach((btn) => {
btn.addEventListener("click", () => {
$$(".seg-btn").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
buyAmount = btn.dataset.amount === "max" ? "max" : Number(btn.dataset.amount);
renderGenerators();
});
});
els.genList.addEventListener("click", (e) => {
const btn = e.target.closest(".buy-btn");
if (!btn) return;
const card = btn.closest(".gen");
if (card) buyGenerator(card.dataset.id);
});
els.upgList.addEventListener("click", (e) => {
const btn = e.target.closest(".buy-btn");
if (!btn) return;
const card = btn.closest(".upgrade");
if (card) buyUpgrade(card.dataset.id);
});
els.btnPrestige.addEventListener("click", doPrestige);
$("#btn-stats").addEventListener("click", () => {
renderStats();
$("#modal-stats").showModal();
});
$("#btn-achievements").addEventListener("click", () => {
renderAchievements();
$("#modal-achievements").showModal();
});
$("#btn-settings").addEventListener("click", () => {
$("#set-floats").checked = settings.floats;
$("#set-shake").checked = settings.shake;
$("#set-particles").checked = settings.reducedParticles;
$("#modal-settings").showModal();
});
$("#set-floats").addEventListener("change", (e) => {
settings.floats = e.target.checked;
save();
});
$("#set-shake").addEventListener("change", (e) => {
settings.shake = e.target.checked;
save();
});
$("#set-particles").addEventListener("change", (e) => {
settings.reducedParticles = e.target.checked;
save();
});
$("#btn-export").addEventListener("click", exportSave);
$("#btn-import").addEventListener("click", () => {
const box = $("#import-box");
const conf = $("#btn-import-confirm");
box.hidden = !box.hidden;
conf.hidden = box.hidden;
if (!box.hidden) box.focus();
});
$("#btn-import-confirm").addEventListener("click", () => {
importSave($("#import-box").value);
$("#import-box").value = "";
$("#import-box").hidden = true;
$("#btn-import-confirm").hidden = true;
$("#modal-settings").close();
});
$("#btn-reset").addEventListener("click", hardReset);
}
function initStars() {
const canvas = els.stars;
const ctx = canvas.getContext("2d");
let w = 0;
let h = 0;
let stars = [];
let particles = [];
function resize() {
w = canvas.width = window.innerWidth * devicePixelRatio;
h = canvas.height = window.innerHeight * devicePixelRatio;
canvas.style.width = `${window.innerWidth}px`;
canvas.style.height = `${window.innerHeight}px`;
spawn();
}
function spawn() {
const count = settings.reducedParticles ? 60 : 140;
stars = Array.from({ length: count }, () => ({
x: Math.random() * w,
y: Math.random() * h,
z: Math.random() * 0.8 + 0.2,
r: Math.random() * 1.4 + 0.3,
tw: Math.random() * Math.PI * 2,
}));
}
function burst(x, y) {
if (settings.reducedParticles) return;
const n = 8;
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2;
const sp = 0.5 + Math.random() * 2;
particles.push({
x: x * devicePixelRatio,
y: y * devicePixelRatio,
vx: Math.cos(a) * sp * devicePixelRatio,
vy: Math.sin(a) * sp * devicePixelRatio,
life: 1,
});
}
}
els.spark.addEventListener("pointerdown", (e) => burst(e.clientX, e.clientY));
let last = performance.now();
function frame(now) {
const dt = Math.min(0.05, (now - last) / 1000);
last = now;
ctx.clearRect(0, 0, w, h);
for (const s of stars) {
s.tw += dt * (0.8 + s.z);
const alpha = 0.25 + 0.75 * (0.5 + 0.5 * Math.sin(s.tw));
ctx.beginPath();
ctx.fillStyle = `rgba(210, 230, 255, ${alpha * s.z})`;
ctx.arc(s.x, s.y, s.r * s.z * devicePixelRatio, 0, Math.PI * 2);
ctx.fill();
s.y += 4 * s.z * dt * devicePixelRatio;
if (s.y > h) {
s.y = 0;
s.x = Math.random() * w;
}
}
particles = particles.filter((p) => p.life > 0);
for (const p of particles) {
p.x += p.vx;
p.y += p.vy;
p.vy += 4 * dt * devicePixelRatio;
p.life -= dt * 1.6;
ctx.beginPath();
ctx.fillStyle = `rgba(130, 220, 255, ${Math.max(0, p.life)})`;
ctx.arc(p.x, p.y, 2.2 * devicePixelRatio, 0, Math.PI * 2);
ctx.fill();
}
rafStars = requestAnimationFrame(frame);
}
window.addEventListener("resize", resize);
resize();
rafStars = requestAnimationFrame(frame);
}
function startLoop() {
let acc = 0;
let last = performance.now();
let uiAcc = 0;
function loop(now) {
const dt = Math.min(0.1, (now - last) / 1000);
last = now;
acc += dt;
uiAcc += dt;
state.lastTick = Date.now();
while (acc >= TICK_MS / 1000) {
tick(TICK_MS / 1000);
acc -= TICK_MS / 1000;
}
if (uiAcc >= 0.1) {
renderNumbers();
renderGenerators();
renderUpgrades();
renderPrestige();
uiAcc = 0;
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
function init() {
const data = load();
if (data) applySave(data);
buildUI();
bindUI();
processOffline();
renderAll();
initStars();
startLoop();
setInterval(save, 10000);
window.addEventListener("beforeunload", save);
}
init();
})();