OpenAI GPT-5.5 Pro
OpenAIApril 2026
What it wrote
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Nebula Forge: a flashy incremental game made with HTML, CSS, and JavaScript only." />
<title>Nebula Forge</title>
<link rel="stylesheet" href="styles.css" />
<script src="script.js" defer></script>
</head>
<body>
<canvas id="starfield" aria-hidden="true"></canvas>
<div class="aurora aurora-one" aria-hidden="true"></div>
<div class="aurora aurora-two" aria-hidden="true"></div>
<div class="app-shell">
<header class="hero">
<div class="brand-block">
<div class="logo-mark" aria-hidden="true">✦</div>
<div>
<p class="eyebrow">Cosmic incremental engine</p>
<h1>Nebula Forge</h1>
<p class="tagline">Tap the stellar core, automate the cosmos, collapse reality, repeat stronger.</p>
</div>
</div>
<div class="top-actions" aria-label="Save controls">
<button class="ghost-button" id="saveBtn" type="button">Save</button>
<button class="ghost-button" id="exportBtn" type="button">Export</button>
<button class="ghost-button" id="importBtn" type="button">Import</button>
<button class="danger-button" id="resetBtn" type="button">Reset</button>
</div>
</header>
<section class="resource-grid" aria-label="Core resources">
<article class="resource-card primary-resource">
<span>Stardust</span>
<strong id="stardustValue">0</strong>
</article>
<article class="resource-card">
<span>Production</span>
<strong><span id="ppsValue">0</span>/s</strong>
</article>
<article class="resource-card">
<span>Tap power</span>
<strong id="tapValue">1</strong>
</article>
<article class="resource-card">
<span>Quasar shards</span>
<strong id="shardValue">0</strong>
</article>
<article class="resource-card">
<span>Total multiplier</span>
<strong id="multiplierValue">1×</strong>
</article>
</section>
<main class="game-layout">
<section class="left-column" aria-label="Core controls">
<article class="panel core-panel">
<div class="panel-heading">
<div>
<p class="eyebrow">Active power</p>
<h2>Stellar Core</h2>
</div>
<span class="status-pill" id="coreStatus">Stable</span>
</div>
<div class="core-stage">
<button id="coreButton" class="core-button" type="button" aria-label="Tap the stellar core for stardust">
<span class="core-ring core-ring-one"></span>
<span class="core-ring core-ring-two"></span>
<span class="core-ring core-ring-three"></span>
<span class="core-glow"></span>
<span class="core-text">Forge</span>
</button>
<div id="floaterLayer" class="floater-layer" aria-hidden="true"></div>
</div>
<div class="stat-stack">
<div>
<span>Run stardust</span>
<strong id="runValue">0</strong>
</div>
<div>
<span>Fleet size</span>
<strong id="fleetValue">0</strong>
</div>
<div>
<span>Achievements</span>
<strong id="achievementValue">0/0</strong>
</div>
</div>
</article>
<article class="panel prestige-panel">
<div class="panel-heading">
<div>
<p class="eyebrow">Prestige layer</p>
<h2>Collapse Protocol</h2>
</div>
<span class="status-pill status-hot" id="prestigeGain">+0</span>
</div>
<p class="muted" id="prestigeHint">Reach 1,000,000 stardust in this run to form your first quasar shard.</p>
<div class="progress-bar" aria-hidden="true">
<span id="prestigeProgress"></span>
</div>
<button class="primary-button" id="prestigeBtn" type="button">Collapse star</button>
</article>
<article class="panel log-panel">
<div class="panel-heading">
<div>
<p class="eyebrow">Transmission feed</p>
<h2>Mission Control</h2>
</div>
</div>
<ul id="eventLog" class="event-log" aria-live="polite"></ul>
</article>
</section>
<section class="right-column" aria-label="Shops and progress">
<article class="panel shop-panel">
<div class="panel-heading shop-heading">
<div>
<p class="eyebrow">Automation market</p>
<h2>Orbit Foundry</h2>
</div>
<div class="buy-mode" aria-label="Purchase quantity">
<button class="mode-button active" type="button" data-mode="1">1</button>
<button class="mode-button" type="button" data-mode="10">10</button>
<button class="mode-button" type="button" data-mode="100">100</button>
<button class="mode-button" type="button" data-mode="max">Max</button>
</div>
</div>
<nav class="tabs" aria-label="Game tabs">
<button class="tab-button active" type="button" data-tab="generatorsPanel">Generators</button>
<button class="tab-button" type="button" data-tab="upgradesPanel">Upgrades</button>
<button class="tab-button" type="button" data-tab="achievementsPanel">Achievements</button>
</nav>
<section id="generatorsPanel" class="tab-panel">
<div id="generatorsList" class="shop-list"></div>
</section>
<section id="upgradesPanel" class="tab-panel" hidden>
<div id="upgradesList" class="upgrade-grid"></div>
</section>
<section id="achievementsPanel" class="tab-panel" hidden>
<div id="achievementsList" class="achievement-grid"></div>
</section>
</article>
</section>
</main>
<footer class="footer-note">
<span>Hotkeys: <kbd>Space</kbd> forge, <kbd>1</kbd>/<kbd>2</kbd>/<kbd>3</kbd>/<kbd>M</kbd> buy mode, <kbd>S</kbd> save.</span>
<span id="saveState">Autosave armed.</span>
</footer>
</div>
</body>
</html>:root {
--bg: #070816;
--bg-soft: #101127;
--panel: rgba(14, 18, 45, 0.72);
--panel-strong: rgba(21, 25, 62, 0.88);
--line: rgba(255, 255, 255, 0.14);
--line-bright: rgba(134, 240, 255, 0.42);
--text: #f6f7ff;
--muted: #aab3d9;
--muted-2: #737ba3;
--cyan: #75f7ff;
--blue: #7aa7ff;
--pink: #ff74d4;
--violet: #9d7bff;
--gold: #ffd36e;
--green: #85ffb5;
--danger: #ff6c87;
--shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
--radius-xl: 28px;
--radius-lg: 22px;
--radius-md: 16px;
--radius-sm: 12px;
--transition: 180ms ease;
}
* {
box-sizing: border-box;
}
html {
min-height: 100%;
background: var(--bg);
}
body {
min-height: 100vh;
margin: 0;
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background:
radial-gradient(circle at top left, rgba(117, 247, 255, 0.18), transparent 34rem),
radial-gradient(circle at 80% 10%, rgba(255, 116, 212, 0.16), transparent 35rem),
radial-gradient(circle at 50% 100%, rgba(157, 123, 255, 0.16), transparent 32rem),
var(--bg);
overflow-x: hidden;
}
button,
input,
textarea {
font: inherit;
}
button {
color: inherit;
}
button:focus-visible {
outline: 3px solid rgba(117, 247, 255, 0.78);
outline-offset: 3px;
}
#starfield {
position: fixed;
inset: 0;
z-index: -3;
width: 100%;
height: 100%;
}
.aurora {
position: fixed;
z-index: -2;
width: 34rem;
height: 34rem;
border-radius: 999px;
filter: blur(80px);
opacity: 0.35;
pointer-events: none;
animation: drift 16s ease-in-out infinite alternate;
}
.aurora-one {
left: -12rem;
top: 18rem;
background: var(--cyan);
}
.aurora-two {
right: -14rem;
top: 4rem;
background: var(--pink);
animation-delay: -7s;
}
@keyframes drift {
from {
transform: translate3d(0, 0, 0) scale(0.92);
}
to {
transform: translate3d(4rem, -3rem, 0) scale(1.12);
}
}
.app-shell {
width: min(1440px, calc(100% - 32px));
margin: 0 auto;
padding: 28px 0 36px;
}
.hero {
display: flex;
justify-content: space-between;
gap: 24px;
align-items: center;
padding: 28px;
border: 1px solid var(--line);
border-radius: var(--radius-xl);
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.035));
box-shadow: var(--shadow);
backdrop-filter: blur(22px);
}
.brand-block {
display: flex;
align-items: center;
gap: 20px;
}
.logo-mark {
display: grid;
place-items: center;
flex: 0 0 auto;
width: 68px;
height: 68px;
border-radius: 24px;
font-size: 2.2rem;
color: #0b1024;
background:
radial-gradient(circle at 30% 20%, #fff8c8, transparent 34%),
linear-gradient(135deg, var(--cyan), var(--pink));
box-shadow: 0 0 40px rgba(117, 247, 255, 0.4), inset 0 0 18px rgba(255, 255, 255, 0.55);
}
.eyebrow {
margin: 0 0 6px;
color: var(--cyan);
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.16em;
text-transform: uppercase;
}
h1,
h2,
h3,
p {
margin-top: 0;
}
h1 {
margin-bottom: 4px;
font-size: clamp(2.1rem, 6vw, 4.8rem);
line-height: 0.92;
letter-spacing: -0.07em;
}
h2 {
margin-bottom: 0;
font-size: clamp(1.25rem, 2vw, 1.8rem);
letter-spacing: -0.04em;
}
h3 {
margin-bottom: 7px;
font-size: 1.06rem;
}
.tagline,
.muted {
margin-bottom: 0;
color: var(--muted);
line-height: 1.55;
}
.top-actions,
.buy-mode,
.tabs {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
button {
border: 0;
cursor: pointer;
}
.ghost-button,
.danger-button,
.mode-button,
.tab-button,
.primary-button,
.buy-button,
.upgrade-button {
border-radius: 999px;
transition: transform var(--transition), border-color var(--transition), background var(--transition), opacity var(--transition), box-shadow var(--transition);
}
.ghost-button,
.danger-button,
.mode-button,
.tab-button {
padding: 10px 14px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.06);
color: var(--text);
}
.ghost-button:hover,
.mode-button:hover,
.tab-button:hover {
transform: translateY(-2px);
border-color: var(--line-bright);
background: rgba(117, 247, 255, 0.11);
}
.danger-button {
border-color: rgba(255, 108, 135, 0.42);
color: #ffdfe7;
}
.danger-button:hover {
transform: translateY(-2px);
background: rgba(255, 108, 135, 0.14);
}
.resource-grid {
display: grid;
grid-template-columns: 1.55fr repeat(4, 1fr);
gap: 14px;
margin: 18px 0;
}
.resource-card {
min-width: 0;
padding: 18px;
border: 1px solid var(--line);
border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.055);
backdrop-filter: blur(18px);
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.28);
}
.resource-card span {
display: block;
margin-bottom: 8px;
color: var(--muted);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.resource-card strong {
display: block;
overflow: hidden;
text-overflow: ellipsis;
color: var(--text);
font-size: clamp(1.15rem, 2.5vw, 2rem);
letter-spacing: -0.04em;
white-space: nowrap;
}
.primary-resource {
border-color: rgba(117, 247, 255, 0.36);
background:
radial-gradient(circle at top right, rgba(117, 247, 255, 0.18), transparent 58%),
rgba(255, 255, 255, 0.07);
}
.game-layout {
display: grid;
grid-template-columns: minmax(320px, 0.9fr) minmax(520px, 1.45fr);
gap: 18px;
align-items: start;
}
.left-column,
.right-column {
display: grid;
gap: 18px;
}
.panel {
position: relative;
overflow: hidden;
padding: 22px;
border: 1px solid var(--line);
border-radius: var(--radius-xl);
background: var(--panel);
box-shadow: var(--shadow);
backdrop-filter: blur(22px);
}
.panel::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.12), transparent 32%, rgba(117, 247, 255, 0.08));
mask: linear-gradient(#000, transparent 58%);
}
.panel-heading {
position: relative;
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-start;
margin-bottom: 18px;
}
.status-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 72px;
padding: 8px 12px;
border: 1px solid rgba(117, 247, 255, 0.34);
border-radius: 999px;
color: var(--cyan);
background: rgba(117, 247, 255, 0.08);
font-weight: 800;
font-size: 0.78rem;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.status-hot {
border-color: rgba(255, 211, 110, 0.42);
color: var(--gold);
background: rgba(255, 211, 110, 0.1);
}
.core-stage {
position: relative;
display: grid;
place-items: center;
min-height: 330px;
isolation: isolate;
}
.core-button {
position: relative;
display: grid;
place-items: center;
width: min(70vw, 280px);
height: min(70vw, 280px);
border-radius: 999px;
background: transparent;
transform: translateZ(0);
transition: transform 120ms ease, filter 120ms ease;
}
.core-button:hover {
filter: saturate(1.18) brightness(1.08);
}
.core-button:active,
.core-button.pressed {
transform: scale(0.96);
}
.core-ring,
.core-glow,
.core-text {
position: absolute;
border-radius: 999px;
}
.core-ring-one {
inset: 0;
border: 2px solid rgba(117, 247, 255, 0.35);
box-shadow: 0 0 24px rgba(117, 247, 255, 0.35), inset 0 0 38px rgba(117, 247, 255, 0.18);
animation: spin 12s linear infinite;
}
.core-ring-two {
inset: 24px;
border: 2px dashed rgba(255, 116, 212, 0.44);
animation: spin 16s linear infinite reverse;
}
.core-ring-three {
inset: 48px;
border: 1px solid rgba(255, 211, 110, 0.5);
box-shadow: inset 0 0 28px rgba(255, 211, 110, 0.16);
animation: breathe 2.2s ease-in-out infinite;
}
.core-glow {
inset: 70px;
background:
radial-gradient(circle at 35% 30%, #fff9ca, transparent 18%),
radial-gradient(circle at 45% 45%, var(--cyan), transparent 35%),
radial-gradient(circle at 55% 60%, var(--pink), transparent 56%),
radial-gradient(circle, var(--violet), #25275b 70%);
box-shadow:
0 0 42px rgba(117, 247, 255, 0.78),
0 0 88px rgba(255, 116, 212, 0.42),
inset 0 0 30px rgba(255, 255, 255, 0.46);
animation: pulse 1.9s ease-in-out infinite;
}
.core-text {
inset: 0;
display: grid;
place-items: center;
z-index: 3;
color: white;
font-size: 1.35rem;
font-weight: 1000;
letter-spacing: 0.16em;
text-transform: uppercase;
text-shadow: 0 0 14px rgba(7, 8, 22, 0.9), 0 0 24px white;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes breathe {
50% {
transform: scale(1.05);
opacity: 0.72;
}
}
@keyframes pulse {
50% {
transform: scale(1.035);
box-shadow:
0 0 55px rgba(117, 247, 255, 0.86),
0 0 112px rgba(255, 116, 212, 0.5),
inset 0 0 38px rgba(255, 255, 255, 0.55);
}
}
.floater-layer {
position: absolute;
inset: 0;
pointer-events: none;
overflow: visible;
}
.floater {
position: absolute;
z-index: 10;
left: 50%;
top: 50%;
padding: 6px 10px;
border: 1px solid rgba(255, 255, 255, 0.28);
border-radius: 999px;
color: #061021;
background: linear-gradient(135deg, var(--cyan), var(--gold));
box-shadow: 0 10px 30px rgba(117, 247, 255, 0.32);
font-weight: 1000;
transform: translate(-50%, -50%);
animation: floatUp 900ms ease-out forwards;
white-space: nowrap;
}
.floater.critical {
background: linear-gradient(135deg, var(--gold), var(--pink));
transform: translate(-50%, -50%) scale(1.18);
}
@keyframes floatUp {
to {
opacity: 0;
transform: translate(var(--x), -170px) scale(0.82);
}
}
.stat-stack {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.stat-stack div {
padding: 14px;
border: 1px solid var(--line);
border-radius: var(--radius-md);
background: rgba(255, 255, 255, 0.055);
}
.stat-stack span {
display: block;
color: var(--muted);
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.stat-stack strong {
display: block;
margin-top: 6px;
font-size: 1.1rem;
letter-spacing: -0.03em;
}
.progress-bar {
height: 12px;
margin: 18px 0;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(0, 0, 0, 0.28);
}
.progress-bar span,
.card-progress span {
display: block;
width: 0%;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--cyan), var(--pink), var(--gold));
box-shadow: 0 0 18px rgba(117, 247, 255, 0.4);
transition: width 240ms ease;
}
.primary-button,
.buy-button,
.upgrade-button {
width: 100%;
padding: 13px 16px;
color: #071024;
background: linear-gradient(135deg, var(--cyan), var(--pink));
box-shadow: 0 14px 38px rgba(117, 247, 255, 0.18);
font-weight: 1000;
}
.primary-button:hover,
.buy-button:hover,
.upgrade-button:hover {
transform: translateY(-2px);
box-shadow: 0 18px 46px rgba(255, 116, 212, 0.25);
}
.primary-button:disabled,
.buy-button:disabled,
.upgrade-button:disabled,
button:disabled {
cursor: not-allowed;
opacity: 0.45;
transform: none;
box-shadow: none;
}
.event-log {
display: grid;
gap: 9px;
min-height: 144px;
margin: 0;
padding: 0;
list-style: none;
}
.event-log li {
padding: 11px 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--radius-sm);
color: var(--muted);
background: rgba(255, 255, 255, 0.045);
line-height: 1.4;
}
.shop-panel {
min-height: 700px;
}
.shop-heading {
align-items: center;
}
.mode-button.active,
.tab-button.active {
border-color: rgba(117, 247, 255, 0.64);
color: var(--cyan);
background: rgba(117, 247, 255, 0.14);
box-shadow: 0 0 24px rgba(117, 247, 255, 0.12);
}
.tabs {
position: relative;
margin-bottom: 18px;
padding: 7px;
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(0, 0, 0, 0.18);
}
.tab-button {
flex: 1 1 140px;
}
.tab-panel[hidden] {
display: none;
}
.shop-list,
.upgrade-grid,
.achievement-grid {
display: grid;
gap: 12px;
}
.shop-card,
.upgrade-card,
.achievement-card {
position: relative;
overflow: hidden;
padding: 16px;
border: 1px solid var(--line);
border-radius: var(--radius-lg);
background:
radial-gradient(circle at top right, rgba(117, 247, 255, 0.08), transparent 46%),
rgba(255, 255, 255, 0.055);
transition: transform var(--transition), border-color var(--transition), background var(--transition);
}
.shop-card:hover,
.upgrade-card:hover,
.achievement-card:hover {
transform: translateY(-2px);
border-color: rgba(117, 247, 255, 0.34);
}
.shop-card.locked,
.upgrade-card.locked {
filter: saturate(0.65);
}
.shop-card.purchasable,
.upgrade-card.purchasable {
border-color: rgba(133, 255, 181, 0.36);
background:
radial-gradient(circle at top right, rgba(133, 255, 181, 0.1), transparent 48%),
rgba(255, 255, 255, 0.065);
}
.shop-card-header {
display: grid;
grid-template-columns: auto 1fr;
gap: 14px;
align-items: start;
}
.shop-icon,
.upgrade-icon,
.achievement-icon {
display: grid;
place-items: center;
width: 48px;
height: 48px;
border: 1px solid var(--line);
border-radius: 16px;
background: rgba(255, 255, 255, 0.09);
font-size: 1.4rem;
box-shadow: inset 0 0 18px rgba(255, 255, 255, 0.05);
}
.card-description {
margin-bottom: 12px;
color: var(--muted);
line-height: 1.46;
}
.card-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 12px;
}
.card-stats div {
min-width: 0;
padding: 10px;
border: 1px solid rgba(255, 255, 255, 0.09);
border-radius: var(--radius-sm);
background: rgba(0, 0, 0, 0.16);
}
.card-stats span,
.upgrade-meta span,
.achievement-card span {
display: block;
color: var(--muted-2);
font-size: 0.68rem;
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.card-stats strong,
.upgrade-meta strong {
display: block;
overflow: hidden;
margin-top: 5px;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-progress {
height: 8px;
margin-top: 10px;
overflow: hidden;
border-radius: 999px;
background: rgba(0, 0, 0, 0.28);
}
.upgrade-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.upgrade-card.installed {
border-color: rgba(255, 211, 110, 0.45);
background:
radial-gradient(circle at top right, rgba(255, 211, 110, 0.16), transparent 48%),
rgba(255, 255, 255, 0.065);
}
.upgrade-top,
.achievement-top {
display: grid;
grid-template-columns: auto 1fr;
gap: 12px;
align-items: start;
margin-bottom: 12px;
}
.upgrade-meta {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 12px;
}
.upgrade-meta div {
padding: 10px;
border: 1px solid rgba(255, 255, 255, 0.09);
border-radius: var(--radius-sm);
background: rgba(0, 0, 0, 0.16);
}
.achievement-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.achievement-card {
min-height: 150px;
}
.achievement-card.earned {
border-color: rgba(255, 211, 110, 0.5);
background:
radial-gradient(circle at top right, rgba(255, 211, 110, 0.16), transparent 48%),
rgba(255, 255, 255, 0.065);
}
.achievement-card.locked-achievement {
opacity: 0.72;
}
.achievement-icon {
color: var(--gold);
}
.achievement-card p {
margin-bottom: 0;
color: var(--muted);
line-height: 1.45;
}
.footer-note {
display: flex;
justify-content: space-between;
gap: 14px;
flex-wrap: wrap;
margin-top: 18px;
padding: 18px 22px;
border: 1px solid var(--line);
border-radius: var(--radius-lg);
color: var(--muted);
background: rgba(255, 255, 255, 0.045);
backdrop-filter: blur(18px);
}
kbd {
display: inline-block;
min-width: 1.7em;
padding: 2px 6px;
border: 1px solid var(--line);
border-bottom-width: 3px;
border-radius: 7px;
color: var(--text);
background: rgba(255, 255, 255, 0.08);
font-size: 0.82em;
font-weight: 800;
text-align: center;
}
.shimmer {
animation: shimmer 700ms ease;
}
@keyframes shimmer {
0% {
box-shadow: 0 0 0 rgba(117, 247, 255, 0);
}
42% {
box-shadow: 0 0 40px rgba(117, 247, 255, 0.34);
}
100% {
box-shadow: 0 0 0 rgba(117, 247, 255, 0);
}
}
@media (max-width: 1120px) {
.resource-grid {
grid-template-columns: repeat(2, 1fr);
}
.primary-resource {
grid-column: 1 / -1;
}
.game-layout {
grid-template-columns: 1fr;
}
.shop-panel {
min-height: auto;
}
}
@media (max-width: 760px) {
.app-shell {
width: min(100% - 20px, 1440px);
padding-top: 10px;
}
.hero,
.brand-block,
.panel-heading,
.shop-heading {
align-items: stretch;
flex-direction: column;
}
.brand-block {
gap: 14px;
}
.top-actions,
.buy-mode {
width: 100%;
}
.top-actions button,
.buy-mode button {
flex: 1 1 auto;
}
.resource-grid,
.stat-stack,
.card-stats,
.upgrade-grid,
.achievement-grid {
grid-template-columns: 1fr;
}
.core-stage {
min-height: 280px;
}
.core-button {
width: 250px;
height: 250px;
}
.footer-note {
flex-direction: column;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
}
}(() => {
'use strict';
const SAVE_KEY = 'nebulaForge.save.v1';
const SAVE_EVERY_MS = 5000;
const FIRST_SHARD_REQUIREMENT = 1_000_000;
const GENERATORS = [
{
id: 'collector',
name: 'Spark Collector',
icon: '🛰️',
description: 'Tiny orbital siphons that scoop loose sparks from the edge of the core.',
baseCost: 15,
growth: 1.15,
baseProduction: 0.14,
unlockAt: 0
},
{
id: 'drone',
name: 'Magnet Drone',
icon: '🤖',
description: 'Self-guided drones that herd stardust through neon magnetic rails.',
baseCost: 120,
growth: 1.16,
baseProduction: 1.1,
unlockAt: 60
},
{
id: 'prism',
name: 'Quantum Prism',
icon: '🔷',
description: 'Splits every photon into several profitable, legally questionable photons.',
baseCost: 900,
growth: 1.17,
baseProduction: 7.8,
unlockAt: 500
},
{
id: 'array',
name: 'Solar Array',
icon: '☀️',
description: 'A glittering fan of panels that drinks sunlight from three dimensions at once.',
baseCost: 7_500,
growth: 1.18,
baseProduction: 52,
unlockAt: 4_000
},
{
id: 'harvester',
name: 'Star Harvester',
icon: '🌌',
description: 'Industrial-scale stellar farming equipment. Looks amazing. Sounds terrifying.',
baseCost: 65_000,
growth: 1.19,
baseProduction: 310,
unlockAt: 35_000
},
{
id: 'singularity',
name: 'Singularity Pump',
icon: '⚫',
description: 'Compresses dust until physics starts filing complaints.',
baseCost: 700_000,
growth: 1.20,
baseProduction: 1_900,
unlockAt: 250_000
},
{
id: 'wormhole',
name: 'Wormhole Loom',
icon: '🌀',
description: 'Threads production lines through shortcuts in space-time.',
baseCost: 8_000_000,
growth: 1.21,
baseProduction: 13_500,
unlockAt: 2_000_000
},
{
id: 'compiler',
name: 'Celestial Compiler',
icon: '💠',
description: 'Turns equations into galaxies, then invoices the galaxies.',
baseCost: 120_000_000,
growth: 1.22,
baseProduction: 98_000,
unlockAt: 25_000_000
}
];
const UPGRADES = [
{
id: 'click_ion',
name: 'Ionised Fingers',
icon: '⚡',
category: 'Tap',
cost: 40,
unlockAt: 20,
description: 'Your taps bite deeper into the stellar core.',
effect: 'Tap power ×2'
},
{
id: 'collector_magnet',
name: 'Magnetic Funnels',
icon: '🧲',
category: 'Generator',
cost: 120,
unlockAt: 70,
description: 'Spark Collectors stop missing the obvious shiny bits.',
effect: 'Collectors ×3'
},
{
id: 'click_plasma',
name: 'Plasma Gauntlet',
icon: '🧤',
category: 'Tap',
cost: 450,
unlockAt: 220,
description: 'A dangerous glove for safe, sensible core-tapping.',
effect: 'Tap power ×4'
},
{
id: 'drone_ai',
name: 'Drone Hive Mind',
icon: '🧠',
category: 'Generator',
cost: 1_300,
unlockAt: 800,
description: 'Drones finally agree on which way is forwards.',
effect: 'Drones ×3'
},
{
id: 'prism_choir',
name: 'Prism Choir',
icon: '🎶',
category: 'Generator',
cost: 9_500,
unlockAt: 5_000,
description: 'Prisms resonate in suspiciously catchy harmonies.',
effect: 'Prisms ×3'
},
{
id: 'click_crit',
name: 'Critical Cascade',
icon: '💥',
category: 'Tap',
cost: 5_000,
unlockAt: 2_500,
description: 'Some taps trigger a beautiful overreaction.',
effect: '5% crit chance, crits ×12'
},
{
id: 'auto_tap',
name: 'Auto-Forge Servo',
icon: '🔁',
category: 'Utility',
cost: 20_000,
unlockAt: 10_000,
description: 'A tiny mechanical hand taps the core once every second.',
effect: 'Passive taps +1/s'
},
{
id: 'array_lens',
name: 'Heliolens Matrix',
icon: '🔆',
category: 'Generator',
cost: 80_000,
unlockAt: 40_000,
description: 'Solar Arrays become dramatic, focused, and several times brighter.',
effect: 'Solar Arrays ×3'
},
{
id: 'global_cartography',
name: 'Solar Cartography',
icon: '🗺️',
category: 'Global',
cost: 250_000,
unlockAt: 125_000,
description: 'Better star maps make every machine less lost.',
effect: 'All generators ×2'
},
{
id: 'discount_coupon',
name: 'Quantum Coupons',
icon: '🎟️',
category: 'Utility',
cost: 400_000,
unlockAt: 150_000,
description: 'Bulk-buying across parallel universes finally pays off.',
effect: 'Generator costs −10%'
},
{
id: 'harvester_teeth',
name: 'Comet Teeth',
icon: '☄️',
category: 'Generator',
cost: 900_000,
unlockAt: 300_000,
description: 'Star Harvesters are fitted with comet-forged cutting heads.',
effect: 'Harvesters ×3'
},
{
id: 'global_chrono',
name: 'Chrono Bearings',
icon: '⏱️',
category: 'Global',
cost: 1_500_000,
unlockAt: 750_000,
description: 'Every moving part borrows a few extra seconds from tomorrow.',
effect: 'All generators ×3'
},
{
id: 'offline_cap',
name: 'Dark Matter Ledger',
icon: '📒',
category: 'Utility',
cost: 2_000_000,
unlockAt: 900_000,
description: 'Your accountants learn how to track production while reality is closed.',
effect: 'Offline cap: 8 hours'
},
{
id: 'singularity_stabiliser',
name: 'Event Horizon Brakes',
icon: '🛑',
category: 'Generator',
cost: 3_200_000,
unlockAt: 1_200_000,
description: 'Singularity Pumps become less explosive and much more profitable.',
effect: 'Singularity Pumps ×3'
},
{
id: 'shard_resonance',
name: 'Shard Resonance',
icon: '💎',
category: 'Prestige',
cost: 5_000_000,
unlockAt: 2_000_000,
description: 'Quasar shards sing louder during this run.',
effect: 'Shard bonus +50%'
},
{
id: 'click_storm',
name: 'Thunder Palm',
icon: '🌩️',
category: 'Tap',
cost: 12_000_000,
unlockAt: 5_000_000,
description: 'Your hand is now considered a weather system.',
effect: 'Tap ×8, +7% crit chance'
},
{
id: 'wormhole_knot',
name: 'Wormhole Knotwork',
icon: '🪢',
category: 'Generator',
cost: 18_000_000,
unlockAt: 7_500_000,
description: 'Wormholes stop fraying at inconvenient moments.',
effect: 'Wormhole Looms ×3'
},
{
id: 'quantum_bloom',
name: 'Quantum Bloom',
icon: '🌺',
category: 'Global',
cost: 35_000_000,
unlockAt: 15_000_000,
description: 'Production blossoms in impossible directions.',
effect: 'All production ×5, tap ×5'
},
{
id: 'compiler_oracle',
name: 'Oracle Subroutines',
icon: '🔮',
category: 'Generator',
cost: 140_000_000,
unlockAt: 50_000_000,
description: 'Celestial Compilers predict profitable universes before compiling them.',
effect: 'Compilers ×3'
}
];
const ACHIEVEMENTS = [
{
id: 'first_spark',
name: 'Ignition',
icon: '✨',
description: 'Forge stardust with your first manual tap.',
check: () => state.stats.manualClicks >= 1
},
{
id: 'hundred_dust',
name: 'Pocket Moon',
icon: '🌙',
description: 'Create 100 lifetime stardust.',
check: () => state.lifetimeStardust >= 100
},
{
id: 'thousand_dust',
name: 'Shiny Problem',
icon: '💫',
description: 'Create 1,000 lifetime stardust.',
check: () => state.lifetimeStardust >= 1_000
},
{
id: 'first_machine',
name: 'Orbital Intern',
icon: '🛰️',
description: 'Buy your first generator.',
check: () => totalOwned() >= 1
},
{
id: 'fleet_25',
name: 'Tiny Fleet',
icon: '🚀',
description: 'Own 25 total generators.',
check: () => totalOwned() >= 25
},
{
id: 'fleet_100',
name: 'Crowded Orbit',
icon: '🛸',
description: 'Own 100 total generators.',
check: () => totalOwned() >= 100
},
{
id: 'upgrades_5',
name: 'Patent Pending',
icon: '📜',
description: 'Buy 5 upgrades across all runs.',
check: () => state.stats.upgradesBought >= 5
},
{
id: 'crit_20',
name: 'Explosive Touch',
icon: '💥',
description: 'Land 20 critical taps.',
check: () => state.stats.critHits >= 20
},
{
id: 'million_run',
name: 'Starbreaker',
icon: '🌟',
description: 'Make 1,000,000 stardust in a single run.',
check: () => state.runStardust >= 1_000_000
},
{
id: 'first_collapse',
name: 'Beautiful Disaster',
icon: '💎',
description: 'Collapse a star for quasar shards.',
check: () => state.ascensions >= 1
},
{
id: 'ten_shards',
name: 'Shard Collector',
icon: '🏆',
description: 'Hold 10 quasar shards.',
check: () => state.shards >= 10
},
{
id: 'billion_lifetime',
name: 'Galaxy Invoice',
icon: '🧾',
description: 'Create 1,000,000,000 lifetime stardust.',
check: () => state.lifetimeStardust >= 1_000_000_000
}
];
const GENERATOR_UPGRADE_MULTIPLIERS = {
collector: { collector_magnet: 3 },
drone: { drone_ai: 3 },
prism: { prism_choir: 3 },
array: { array_lens: 3 },
harvester: { harvester_teeth: 3 },
singularity: { singularity_stabiliser: 3 },
wormhole: { wormhole_knot: 3 },
compiler: { compiler_oracle: 3 }
};
const els = {
canvas: document.querySelector('#starfield'),
stardustValue: document.querySelector('#stardustValue'),
ppsValue: document.querySelector('#ppsValue'),
tapValue: document.querySelector('#tapValue'),
shardValue: document.querySelector('#shardValue'),
multiplierValue: document.querySelector('#multiplierValue'),
runValue: document.querySelector('#runValue'),
fleetValue: document.querySelector('#fleetValue'),
achievementValue: document.querySelector('#achievementValue'),
coreButton: document.querySelector('#coreButton'),
coreStatus: document.querySelector('#coreStatus'),
floaterLayer: document.querySelector('#floaterLayer'),
prestigeGain: document.querySelector('#prestigeGain'),
prestigeHint: document.querySelector('#prestigeHint'),
prestigeProgress: document.querySelector('#prestigeProgress'),
prestigeBtn: document.querySelector('#prestigeBtn'),
generatorsList: document.querySelector('#generatorsList'),
upgradesList: document.querySelector('#upgradesList'),
achievementsList: document.querySelector('#achievementsList'),
eventLog: document.querySelector('#eventLog'),
saveBtn: document.querySelector('#saveBtn'),
exportBtn: document.querySelector('#exportBtn'),
importBtn: document.querySelector('#importBtn'),
resetBtn: document.querySelector('#resetBtn'),
saveState: document.querySelector('#saveState')
};
let state = loadState();
let logMessages = [];
let lastFrame = performance.now();
let renderClock = 0;
let saveClock = 0;
let lastWholeStardust = Math.floor(state.stardust);
const starfield = createStarfield(els.canvas);
init();
function init() {
buildGeneratorCards();
buildUpgradeCards();
buildAchievementCards();
bindEvents();
applyOfflineProgress();
checkAchievements();
updateModeButtons();
addLog('Core online. The forge awaits your first spark.');
render();
requestAnimationFrame(gameLoop);
}
function createInitialState() {
return {
version: 1,
stardust: 0,
runStardust: 0,
lifetimeStardust: 0,
shards: 0,
ascensions: 0,
generators: Object.fromEntries(GENERATORS.map((generator) => [generator.id, 0])),
upgrades: {},
achievements: {},
stats: {
manualClicks: 0,
totalClicks: 0,
totalBought: 0,
upgradesBought: 0,
critHits: 0
},
options: {
buyMode: '1'
},
lastTick: Date.now()
};
}
function loadState() {
const fresh = createInitialState();
const raw = localStorage.getItem(SAVE_KEY);
if (!raw) {
return fresh;
}
try {
const parsed = JSON.parse(raw);
const hydrated = {
...fresh,
...parsed,
generators: {
...fresh.generators,
...(parsed.generators || {})
},
upgrades: {
...(parsed.upgrades || {})
},
achievements: {
...(parsed.achievements || {})
},
stats: {
...fresh.stats,
...(parsed.stats || {})
},
options: {
...fresh.options,
...(parsed.options || {})
}
};
for (const generator of GENERATORS) {
hydrated.generators[generator.id] = Math.max(0, Number(hydrated.generators[generator.id]) || 0);
}
hydrated.stardust = sanitiseNumber(hydrated.stardust);
hydrated.runStardust = sanitiseNumber(hydrated.runStardust);
hydrated.lifetimeStardust = sanitiseNumber(hydrated.lifetimeStardust);
hydrated.shards = Math.max(0, Math.floor(Number(hydrated.shards) || 0));
hydrated.ascensions = Math.max(0, Math.floor(Number(hydrated.ascensions) || 0));
hydrated.lastTick = Number(hydrated.lastTick) || Date.now();
return hydrated;
} catch (error) {
console.warn('Could not load Nebula Forge save:', error);
return fresh;
}
}
function sanitiseNumber(value) {
const number = Number(value);
return Number.isFinite(number) && number > 0 ? number : 0;
}
function bindEvents() {
els.coreButton.addEventListener('click', () => handleCoreTap());
els.prestigeBtn.addEventListener('click', () => collapseStar());
els.saveBtn.addEventListener('click', () => saveState(true));
els.exportBtn.addEventListener('click', () => exportSave());
els.importBtn.addEventListener('click', () => importSave());
els.resetBtn.addEventListener('click', () => resetSave());
document.querySelectorAll('[data-mode]').forEach((button) => {
button.addEventListener('click', () => setBuyMode(button.dataset.mode));
});
document.querySelectorAll('[data-tab]').forEach((button) => {
button.addEventListener('click', () => switchTab(button.dataset.tab));
});
els.generatorsList.addEventListener('click', (event) => {
const button = event.target.closest('[data-buy-generator]');
if (!button) return;
buyGenerator(button.dataset.buyGenerator);
});
els.upgradesList.addEventListener('click', (event) => {
const button = event.target.closest('[data-buy-upgrade]');
if (!button) return;
buyUpgrade(button.dataset.buyUpgrade);
});
document.addEventListener('keydown', (event) => {
const target = event.target;
const isTyping = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement;
if (isTyping) return;
if (event.code === 'Space') {
event.preventDefault();
handleCoreTap();
}
if (event.key === '1') setBuyMode('1');
if (event.key === '2') setBuyMode('10');
if (event.key === '3') setBuyMode('100');
if (event.key.toLowerCase() === 'm') setBuyMode('max');
if (event.key.toLowerCase() === 's') {
event.preventDefault();
saveState(true);
}
});
window.addEventListener('beforeunload', () => saveState(false));
}
function gameLoop(timestamp) {
const delta = Math.min((timestamp - lastFrame) / 1000, 0.5);
lastFrame = timestamp;
const passive = getPassivePerSecond();
const servo = hasUpgrade('auto_tap') ? getClickPower() : 0;
addStardust((passive + servo) * delta);
renderClock += delta;
saveClock += delta;
if (renderClock >= 0.1) {
checkAchievements();
render();
renderClock = 0;
}
if (saveClock >= SAVE_EVERY_MS / 1000) {
saveState(false);
saveClock = 0;
}
starfield.draw(timestamp, getCoreHeat());
requestAnimationFrame(gameLoop);
}
function handleCoreTap() {
let amount = getClickPower();
const critical = Math.random() < getCritChance();
if (critical) {
amount *= getCritMultiplier();
state.stats.critHits += 1;
}
state.stats.manualClicks += 1;
state.stats.totalClicks += 1;
addStardust(amount);
showFloater(amount, critical);
pulseCore();
checkAchievements();
render();
}
function addStardust(amount) {
if (!Number.isFinite(amount) || amount <= 0) return;
state.stardust += amount;
state.runStardust += amount;
state.lifetimeStardust += amount;
const whole = Math.floor(state.stardust);
if (whole > lastWholeStardust) {
lastWholeStardust = whole;
}
}
function buyGenerator(generatorId) {
const generator = GENERATORS.find((item) => item.id === generatorId);
if (!generator || !isGeneratorUnlocked(generator)) return;
const mode = state.options.buyMode;
const purchase = mode === 'max'
? getMaxAffordable(generator)
: { count: Number(mode), cost: getBulkCost(generator, Number(mode)) };
if (!purchase.count || purchase.count < 1 || state.stardust < purchase.cost) {
return;
}
state.stardust -= purchase.cost;
state.generators[generator.id] += purchase.count;
state.stats.totalBought += purchase.count;
addLog(`Bought ${formatInteger(purchase.count)} ${generator.name}${purchase.count === 1 ? '' : 's'}.`);
flashCard(`[data-generator-card="${generator.id}"]`);
checkAchievements();
render();
}
function buyUpgrade(upgradeId) {
const upgrade = UPGRADES.find((item) => item.id === upgradeId);
if (!upgrade || hasUpgrade(upgrade.id) || !isUpgradeUnlocked(upgrade) || state.stardust < upgrade.cost) {
return;
}
state.stardust -= upgrade.cost;
state.upgrades[upgrade.id] = true;
state.stats.upgradesBought += 1;
addLog(`Installed ${upgrade.name}. ${upgrade.effect}.`);
flashCard(`[data-upgrade-card="${upgrade.id}"]`);
checkAchievements();
render();
}
function collapseStar() {
const gain = getPrestigeGain();
if (gain < 1) {
addLog('Collapse denied: the star needs at least 1,000,000 run stardust.');
return;
}
const confirmed = window.confirm(
`Collapse this run for ${formatInteger(gain)} quasar shard${gain === 1 ? '' : 's'}? Your machines and upgrades reset, but shards and achievements remain.`
);
if (!confirmed) return;
const achievements = { ...state.achievements };
const previousShards = state.shards;
const previousAscensions = state.ascensions;
const previousOptions = { ...state.options };
const lifetimeStardust = state.lifetimeStardust;
const totalClicks = state.stats.totalClicks;
const manualClicks = state.stats.manualClicks;
const totalBought = state.stats.totalBought;
const upgradesBought = state.stats.upgradesBought;
const critHits = state.stats.critHits;
state = createInitialState();
state.shards = previousShards + gain;
state.ascensions = previousAscensions + 1;
state.options = previousOptions;
state.lifetimeStardust = lifetimeStardust;
state.achievements = achievements;
state.stats.totalClicks = totalClicks;
state.stats.manualClicks = manualClicks;
state.stats.totalBought = totalBought;
state.stats.upgradesBought = upgradesBought;
state.stats.critHits = critHits;
state.lastTick = Date.now();
addLog(`Reality collapsed beautifully. Gained ${formatInteger(gain)} quasar shard${gain === 1 ? '' : 's'}.`);
checkAchievements();
saveState(false);
updateModeButtons();
render();
}
function getPrestigeGain() {
return Math.floor(Math.sqrt(state.runStardust / FIRST_SHARD_REQUIREMENT));
}
function getPrestigeProgress() {
const nextShard = Math.max(1, getPrestigeGain() + 1);
const requirement = FIRST_SHARD_REQUIREMENT * nextShard * nextShard;
return {
nextShard,
requirement,
percent: clamp((state.runStardust / requirement) * 100, 0, 100)
};
}
function applyOfflineProgress() {
const now = Date.now();
const elapsed = Math.max(0, (now - state.lastTick) / 1000);
const cap = hasUpgrade('offline_cap') ? 8 * 60 * 60 : 2 * 60 * 60;
const counted = Math.min(elapsed, cap);
if (counted >= 30) {
const earned = getPassivePerSecond() * counted;
if (earned >= 1) {
addStardust(earned);
addLog(`Offline systems generated ${format(earned)} stardust over ${formatDuration(counted)}.`);
}
}
state.lastTick = now;
}
function getClickPower() {
let power = 1;
if (hasUpgrade('click_ion')) power *= 2;
if (hasUpgrade('click_plasma')) power *= 4;
if (hasUpgrade('click_storm')) power *= 8;
if (hasUpgrade('quantum_bloom')) power *= 5;
power *= getShardMultiplier();
power *= getAchievementMultiplier();
return power;
}
function getCritChance() {
let chance = 0;
if (hasUpgrade('click_crit')) chance += 0.05;
if (hasUpgrade('click_storm')) chance += 0.07;
if (hasUpgrade('quantum_bloom')) chance += 0.03;
return clamp(chance, 0, 0.5);
}
function getCritMultiplier() {
return hasUpgrade('quantum_bloom') ? 16 : 12;
}
function getPassivePerSecond() {
return GENERATORS.reduce((total, generator) => {
const owned = state.generators[generator.id] || 0;
return total + owned * generator.baseProduction * getGeneratorMultiplier(generator);
}, 0);
}
function getGeneratorMultiplier(generator) {
let multiplier = getShardMultiplier() * getAchievementMultiplier();
if (hasUpgrade('global_cartography')) multiplier *= 2;
if (hasUpgrade('global_chrono')) multiplier *= 3;
if (hasUpgrade('quantum_bloom')) multiplier *= 5;
const specific = GENERATOR_UPGRADE_MULTIPLIERS[generator.id] || {};
for (const [upgradeId, value] of Object.entries(specific)) {
if (hasUpgrade(upgradeId)) multiplier *= value;
}
return multiplier;
}
function getShardMultiplier() {
const shardStrength = hasUpgrade('shard_resonance') ? 0.3 : 0.2;
return 1 + state.shards * shardStrength;
}
function getAchievementMultiplier() {
return 1 + earnedAchievementCount() * 0.04;
}
function getTotalMultiplier() {
let multiplier = getShardMultiplier() * getAchievementMultiplier();
if (hasUpgrade('global_cartography')) multiplier *= 2;
if (hasUpgrade('global_chrono')) multiplier *= 3;
if (hasUpgrade('quantum_bloom')) multiplier *= 5;
return multiplier;
}
function getGeneratorCost(generator, owned = state.generators[generator.id] || 0) {
const raw = generator.baseCost * Math.pow(generator.growth, owned) * getCostMultiplier();
return Math.max(1, Math.ceil(raw));
}
function getBulkCost(generator, count, owned = state.generators[generator.id] || 0) {
if (!Number.isFinite(count) || count <= 0) return 0;
const growth = generator.growth;
const start = generator.baseCost * Math.pow(growth, owned) * getCostMultiplier();
const total = start * ((Math.pow(growth, count) - 1) / (growth - 1));
if (!Number.isFinite(total)) return Number.POSITIVE_INFINITY;
return Math.max(1, Math.ceil(total));
}
function getMaxAffordable(generator) {
const owned = state.generators[generator.id] || 0;
const start = generator.baseCost * Math.pow(generator.growth, owned) * getCostMultiplier();
if (!Number.isFinite(start) || state.stardust < Math.ceil(start)) {
return { count: 0, cost: 0 };
}
const ratio = (state.stardust * (generator.growth - 1)) / start + 1;
let count = Math.floor(Math.log(ratio) / Math.log(generator.growth));
count = clamp(Math.floor(count), 0, 100_000);
let cost = getBulkCost(generator, count, owned);
while (cost > state.stardust && count > 0) {
count -= 1;
cost = getBulkCost(generator, count, owned);
}
while (count < 100_000) {
const nextCost = getBulkCost(generator, count + 1, owned);
if (nextCost > state.stardust) break;
count += 1;
cost = nextCost;
}
return { count, cost };
}
function getCostMultiplier() {
return hasUpgrade('discount_coupon') ? 0.9 : 1;
}
function totalOwned() {
return GENERATORS.reduce((total, generator) => total + (state.generators[generator.id] || 0), 0);
}
function hasUpgrade(id) {
return Boolean(state.upgrades[id]);
}
function isGeneratorUnlocked(generator) {
return generator.unlockAt <= 0
|| state.runStardust >= generator.unlockAt
|| state.lifetimeStardust >= generator.unlockAt
|| (state.generators[generator.id] || 0) > 0;
}
function isUpgradeUnlocked(upgrade) {
return state.runStardust >= upgrade.unlockAt
|| state.lifetimeStardust >= upgrade.unlockAt
|| hasUpgrade(upgrade.id);
}
function earnedAchievementCount() {
return Object.keys(state.achievements || {}).length;
}
function checkAchievements() {
const newlyEarned = [];
for (const achievement of ACHIEVEMENTS) {
if (!state.achievements[achievement.id] && achievement.check()) {
state.achievements[achievement.id] = Date.now();
newlyEarned.push(achievement);
}
}
for (const achievement of newlyEarned) {
addLog(`Achievement unlocked: ${achievement.name}. Permanent multiplier increased.`);
flashCard(`[data-achievement-card="${achievement.id}"]`);
}
}
function buildGeneratorCards() {
els.generatorsList.innerHTML = GENERATORS.map((generator) => `
<article class="shop-card" data-generator-card="${generator.id}">
<div class="shop-card-header">
<div class="shop-icon" aria-hidden="true">${generator.icon}</div>
<div>
<h3>${generator.name}</h3>
<p class="card-description">${generator.description}</p>
</div>
</div>
<div class="card-stats">
<div><span>Owned</span><strong data-generator-owned="${generator.id}">0</strong></div>
<div><span>Each</span><strong data-generator-each="${generator.id}">0/s</strong></div>
<div><span>Total</span><strong data-generator-total="${generator.id}">0/s</strong></div>
</div>
<button class="buy-button" type="button" data-buy-generator="${generator.id}">Buy</button>
<div class="card-progress" aria-hidden="true"><span data-generator-progress="${generator.id}"></span></div>
</article>
`).join('');
}
function buildUpgradeCards() {
els.upgradesList.innerHTML = UPGRADES.map((upgrade) => `
<article class="upgrade-card" data-upgrade-card="${upgrade.id}">
<div class="upgrade-top">
<div class="upgrade-icon" aria-hidden="true">${upgrade.icon}</div>
<div>
<h3>${upgrade.name}</h3>
<p class="card-description">${upgrade.description}</p>
</div>
</div>
<div class="upgrade-meta">
<div><span>Class</span><strong>${upgrade.category}</strong></div>
<div><span>Effect</span><strong>${upgrade.effect}</strong></div>
</div>
<button class="upgrade-button" type="button" data-buy-upgrade="${upgrade.id}">Install</button>
</article>
`).join('');
}
function buildAchievementCards() {
els.achievementsList.innerHTML = ACHIEVEMENTS.map((achievement) => `
<article class="achievement-card" data-achievement-card="${achievement.id}">
<div class="achievement-top">
<div class="achievement-icon" aria-hidden="true">${achievement.icon}</div>
<div>
<span data-achievement-status="${achievement.id}">Locked</span>
<h3>${achievement.name}</h3>
</div>
</div>
<p>${achievement.description}</p>
</article>
`).join('');
}
function render() {
const passive = getPassivePerSecond();
const tap = getClickPower();
const achievements = earnedAchievementCount();
const gain = getPrestigeGain();
const prestige = getPrestigeProgress();
els.stardustValue.textContent = format(state.stardust);
els.ppsValue.textContent = format(passive + (hasUpgrade('auto_tap') ? tap : 0));
els.tapValue.textContent = format(tap);
els.shardValue.textContent = formatInteger(state.shards);
els.multiplierValue.textContent = formatMultiplier(getTotalMultiplier());
els.runValue.textContent = format(state.runStardust);
els.fleetValue.textContent = formatInteger(totalOwned());
els.achievementValue.textContent = `${achievements}/${ACHIEVEMENTS.length}`;
els.coreStatus.textContent = getCoreStatus(passive);
els.prestigeGain.textContent = `+${formatInteger(gain)}`;
els.prestigeBtn.disabled = gain < 1;
els.prestigeProgress.style.width = `${prestige.percent}%`;
els.prestigeHint.textContent = gain >= 1
? `Collapse now for ${formatInteger(gain)} quasar shard${gain === 1 ? '' : 's'}. Next shard at ${format(prestige.requirement)} run stardust.`
: `Reach ${format(FIRST_SHARD_REQUIREMENT)} stardust in this run to form your first quasar shard.`;
updateGeneratorCards();
updateUpgradeCards();
updateAchievementCards();
}
function updateGeneratorCards() {
for (const generator of GENERATORS) {
const card = document.querySelector(`[data-generator-card="${generator.id}"]`);
const owned = state.generators[generator.id] || 0;
const unlocked = isGeneratorUnlocked(generator);
const each = generator.baseProduction * getGeneratorMultiplier(generator);
const total = owned * each;
const nextCost = getGeneratorCost(generator);
const progress = clamp((state.stardust / nextCost) * 100, 0, 100);
const button = card.querySelector(`[data-buy-generator="${generator.id}"]`);
const purchaseInfo = getGeneratorPurchaseInfo(generator);
card.classList.toggle('locked', !unlocked);
card.classList.toggle('purchasable', unlocked && purchaseInfo.canBuy);
document.querySelector(`[data-generator-owned="${generator.id}"]`).textContent = formatInteger(owned);
document.querySelector(`[data-generator-each="${generator.id}"]`).textContent = `${format(each)}/s`;
document.querySelector(`[data-generator-total="${generator.id}"]`).textContent = `${format(total)}/s`;
document.querySelector(`[data-generator-progress="${generator.id}"]`).style.width = `${progress}%`;
if (!unlocked) {
button.disabled = true;
button.textContent = `Unlocks at ${format(generator.unlockAt)}`;
} else {
button.disabled = !purchaseInfo.canBuy;
button.textContent = purchaseInfo.label;
}
}
}
function getGeneratorPurchaseInfo(generator) {
const mode = state.options.buyMode;
if (mode === 'max') {
const max = getMaxAffordable(generator);
if (max.count < 1) {
return {
canBuy: false,
label: `Need ${format(getGeneratorCost(generator))}`
};
}
return {
canBuy: true,
label: `Buy Max (${formatInteger(max.count)}) · ${format(max.cost)}`
};
}
const count = Number(mode);
const cost = getBulkCost(generator, count);
return {
canBuy: state.stardust >= cost,
label: `Buy ${formatInteger(count)} · ${format(cost)}`
};
}
function updateUpgradeCards() {
for (const upgrade of UPGRADES) {
const card = document.querySelector(`[data-upgrade-card="${upgrade.id}"]`);
const button = card.querySelector(`[data-buy-upgrade="${upgrade.id}"]`);
const installed = hasUpgrade(upgrade.id);
const unlocked = isUpgradeUnlocked(upgrade);
const canBuy = unlocked && !installed && state.stardust >= upgrade.cost;
card.classList.toggle('locked', !unlocked);
card.classList.toggle('installed', installed);
card.classList.toggle('purchasable', canBuy);
if (installed) {
button.disabled = true;
button.textContent = 'Installed';
} else if (!unlocked) {
button.disabled = true;
button.textContent = `Locked · ${format(upgrade.unlockAt)}`;
} else {
button.disabled = !canBuy;
button.textContent = `Install · ${format(upgrade.cost)}`;
}
}
}
function updateAchievementCards() {
for (const achievement of ACHIEVEMENTS) {
const card = document.querySelector(`[data-achievement-card="${achievement.id}"]`);
const status = document.querySelector(`[data-achievement-status="${achievement.id}"]`);
const earned = Boolean(state.achievements[achievement.id]);
card.classList.toggle('earned', earned);
card.classList.toggle('locked-achievement', !earned);
status.textContent = earned ? 'Earned · +4%' : 'Locked · +4%';
}
}
function switchTab(panelId) {
document.querySelectorAll('.tab-button').forEach((button) => {
button.classList.toggle('active', button.dataset.tab === panelId);
});
document.querySelectorAll('.tab-panel').forEach((panel) => {
panel.hidden = panel.id !== panelId;
});
}
function setBuyMode(mode) {
if (!['1', '10', '100', 'max'].includes(mode)) return;
state.options.buyMode = mode;
updateModeButtons();
render();
}
function updateModeButtons() {
document.querySelectorAll('[data-mode]').forEach((button) => {
button.classList.toggle('active', button.dataset.mode === state.options.buyMode);
});
}
function saveState(manual) {
state.lastTick = Date.now();
localStorage.setItem(SAVE_KEY, JSON.stringify(state));
const stamp = new Date().toLocaleTimeString('en-GB', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
els.saveState.textContent = `Saved ${stamp}.`;
if (manual) {
addLog('Save written to local storage.');
}
}
async function exportSave() {
saveState(false);
const code = encodeSave(state);
try {
await navigator.clipboard.writeText(code);
addLog('Export code copied to clipboard.');
} catch (error) {
window.prompt('Copy your Nebula Forge save code:', code);
addLog('Export code prepared.');
}
}
function importSave() {
const code = window.prompt('Paste your Nebula Forge save code:');
if (!code) return;
try {
const imported = decodeSave(code);
state = hydrateImportedState(imported);
state.lastTick = Date.now();
saveState(false);
updateModeButtons();
checkAchievements();
render();
addLog('Imported save successfully.');
} catch (error) {
console.warn('Import failed:', error);
addLog('Import failed. The save code was not recognised.');
}
}
function hydrateImportedState(imported) {
const fresh = createInitialState();
localStorage.setItem(SAVE_KEY, JSON.stringify(imported));
const previous = state;
state = {
...fresh,
...imported,
generators: {
...fresh.generators,
...(imported.generators || {})
},
upgrades: {
...(imported.upgrades || {})
},
achievements: {
...(imported.achievements || {})
},
stats: {
...fresh.stats,
...(imported.stats || {})
},
options: {
...fresh.options,
...(imported.options || {})
}
};
const hydrated = loadState();
state = previous;
return hydrated;
}
function encodeSave(payload) {
const json = JSON.stringify(payload);
const bytes = new TextEncoder().encode(json);
let binary = '';
bytes.forEach((byte) => {
binary += String.fromCharCode(byte);
});
return btoa(binary);
}
function decodeSave(code) {
const binary = atob(code.trim());
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
return JSON.parse(new TextDecoder().decode(bytes));
}
function resetSave() {
const confirmed = window.confirm('Delete your Nebula Forge save and restart from zero?');
if (!confirmed) return;
localStorage.removeItem(SAVE_KEY);
state = createInitialState();
updateModeButtons();
render();
addLog('Reality reset. Fresh core installed.');
saveState(false);
}
function addLog(message) {
const stamp = new Date().toLocaleTimeString('en-GB', {
hour: '2-digit',
minute: '2-digit'
});
logMessages.unshift({ stamp, message });
logMessages = logMessages.slice(0, 6);
renderLog();
}
function renderLog() {
els.eventLog.replaceChildren();
for (const item of logMessages) {
const li = document.createElement('li');
const strong = document.createElement('strong');
strong.textContent = `${item.stamp} `;
const text = document.createTextNode(item.message);
li.append(strong, text);
els.eventLog.append(li);
}
}
function showFloater(amount, critical) {
if (els.floaterLayer.children.length > 36) {
els.floaterLayer.firstElementChild?.remove();
}
const floater = document.createElement('span');
floater.className = `floater${critical ? ' critical' : ''}`;
floater.textContent = `${critical ? 'CRIT ' : ''}+${format(amount)}`;
floater.style.setProperty('--x', `${random(-90, 90)}px`);
floater.style.left = `${random(35, 65)}%`;
floater.style.top = `${random(38, 62)}%`;
els.floaterLayer.append(floater);
window.setTimeout(() => floater.remove(), 950);
}
function pulseCore() {
els.coreButton.classList.add('pressed');
window.setTimeout(() => els.coreButton.classList.remove('pressed'), 110);
}
function flashCard(selector) {
const card = document.querySelector(selector);
if (!card) return;
card.classList.remove('shimmer');
void card.offsetWidth;
card.classList.add('shimmer');
}
function getCoreStatus(passive) {
const heat = passive + getClickPower();
if (heat >= 10_000_000) return 'Mythic';
if (heat >= 1_000_000) return 'Ablaze';
if (heat >= 100_000) return 'Radiant';
if (heat >= 10_000) return 'Roaring';
if (heat >= 1_000) return 'Charged';
if (heat >= 100) return 'Warming';
return 'Stable';
}
function getCoreHeat() {
return clamp(Math.log10(getPassivePerSecond() + getClickPower() + 1) / 7, 0, 1);
}
function format(value) {
if (!Number.isFinite(value)) return '∞';
if (value < 0) return `-${format(Math.abs(value))}`;
if (value < 10) return value.toFixed(2).replace(/\.00$/, '');
if (value < 100) return value.toFixed(1).replace(/\.0$/, '');
if (value < 1_000) return Math.floor(value).toLocaleString('en-GB');
const suffixes = ['', 'K', 'M', 'B', 'T', 'Qa', 'Qi', 'Sx', 'Sp', 'Oc', 'No', 'Dc'];
const tier = Math.floor(Math.log10(value) / 3);
if (tier >= suffixes.length) {
return value.toExponential(2).replace('+', '');
}
const scaled = value / Math.pow(1000, tier);
const decimals = scaled >= 100 ? 0 : scaled >= 10 ? 1 : 2;
return `${scaled.toFixed(decimals)}${suffixes[tier]}`;
}
function formatInteger(value) {
if (!Number.isFinite(value)) return '∞';
if (value < 1000) return Math.floor(value).toLocaleString('en-GB');
return format(Math.floor(value));
}
function formatMultiplier(value) {
if (value < 1000) {
return `${value.toFixed(2).replace(/\.00$/, '')}×`;
}
return `${format(value)}×`;
}
function formatDuration(seconds) {
const whole = Math.floor(seconds);
const hours = Math.floor(whole / 3600);
const minutes = Math.floor((whole % 3600) / 60);
const secs = whole % 60;
if (hours > 0) return `${hours}h ${minutes}m`;
if (minutes > 0) return `${minutes}m ${secs}s`;
return `${secs}s`;
}
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
function random(min, max) {
return Math.random() * (max - min) + min;
}
function createStarfield(canvas) {
const ctx = canvas.getContext('2d');
const stars = [];
let width = 0;
let height = 0;
let dpr = 1;
function resize() {
dpr = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
width = window.innerWidth;
height = window.innerHeight;
canvas.width = Math.floor(width * dpr);
canvas.height = Math.floor(height * dpr);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
stars.length = 0;
const count = Math.floor(clamp((width * height) / 9500, 90, 210));
for (let index = 0; index < count; index += 1) {
stars.push(makeStar(true));
}
}
function makeStar(anywhere = false) {
return {
x: Math.random() * width,
y: anywhere ? Math.random() * height : -10,
radius: random(0.45, 1.9),
speed: random(8, 34),
phase: random(0, Math.PI * 2),
hue: Math.random() > 0.72 ? 'cyan' : 'white'
};
}
function draw(timestamp, heat) {
if (!ctx) return;
ctx.clearRect(0, 0, width, height);
const glow = 0.2 + heat * 0.38;
for (let index = 0; index < stars.length; index += 1) {
const star = stars[index];
star.y += (star.speed + heat * 46) / 60;
if (star.y > height + 12) {
stars[index] = makeStar(false);
continue;
}
const twinkle = 0.45 + Math.sin(timestamp / 700 + star.phase) * 0.25 + glow;
const alpha = clamp(twinkle, 0.25, 0.95);
ctx.beginPath();
ctx.fillStyle = star.hue === 'cyan'
? `rgba(117, 247, 255, ${alpha})`
: `rgba(255, 255, 255, ${alpha})`;
ctx.arc(star.x, star.y, star.radius + heat * 0.8, 0, Math.PI * 2);
ctx.fill();
}
}
window.addEventListener('resize', resize);
resize();
return { draw };
}
})();