OpenAI GPT-5.6 Sol Ultra
OpenAIJuly 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" />
<meta
name="description"
content="Singularity Forge — a cosmic incremental game about waking a dead star."
/>
<meta name="theme-color" content="#070a0e" />
<title>Singularity Forge</title>
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><rect width='64' height='64' rx='16' fill='%23070a0e'/><circle cx='32' cy='32' r='17' fill='%23ff6c42'/><circle cx='27' cy='26' r='7' fill='%23fff0a8'/><path d='M8 32h10M46 32h10M32 8v10M32 46v10' stroke='%23d9ff72' stroke-width='3'/></svg>"
/>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="cosmos" aria-hidden="true">
<div class="nebula nebula-one"></div>
<div class="nebula nebula-two"></div>
<div class="starfield" id="starfield"></div>
<div class="horizon-grid"></div>
<div class="scanlines"></div>
</div>
<header class="command-bar">
<div class="brand-block">
<div class="brand-sigil" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</div>
<div>
<p class="eyebrow">DEEP SPACE ENGINE // 09</p>
<h1>SINGULARITY <em>FORGE</em></h1>
</div>
</div>
<div class="top-readouts" aria-label="Resource summary">
<div class="top-readout primary-readout">
<span>AVAILABLE FLUX</span>
<strong id="header-flux">0</strong>
</div>
<div class="top-readout">
<span>OUTPUT / SEC</span>
<strong id="header-rate">0</strong>
</div>
<div class="top-readout stardust-readout">
<span>VOID CORES</span>
<strong id="header-stardust">0</strong>
</div>
</div>
<div class="header-actions">
<div class="save-state" id="save-state">
<span class="save-dot"></span>
<span id="save-label">LOCAL LINK</span>
</div>
<button class="icon-button" id="sound-toggle" type="button" aria-label="Toggle sound">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 9v6h4l5 4V5L8 9H4Zm12.5 3a4.5 4.5 0 0 0-2.1-3.8v7.6a4.5 4.5 0 0 0 2.1-3.8Z" />
<path class="sound-wave" d="M17.7 4.8a9 9 0 0 1 0 14.4l-1.2-1.5a7 7 0 0 0 0-11.4l1.2-1.5Z" />
</svg>
</button>
<button class="text-button danger-button" id="reset-button" type="button">RESET</button>
</div>
</header>
<main class="game-shell">
<aside class="panel mission-panel">
<div class="panel-corner" aria-hidden="true"></div>
<div class="panel-heading">
<div>
<p class="eyebrow">MISSION CONTROL</p>
<h2>Wake the dark</h2>
</div>
<span class="live-badge"><i></i> LIVE</span>
</div>
<div class="transmission-card">
<p class="transmission-label">CURRENT TRANSMISSION</p>
<p id="transmission-text">The universe is cold. Give it a reason to burn.</p>
<div class="signal-bars" aria-hidden="true">
<i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i>
</div>
</div>
<section class="mission-section" aria-labelledby="milestone-title">
<div class="section-title-row">
<h3 id="milestone-title">FORGE SEQUENCE</h3>
<span id="milestone-count">0 / 7</span>
</div>
<div class="milestone-list" id="milestone-list"></div>
</section>
<section class="mission-section achievement-section" aria-labelledby="achievement-title">
<div class="section-title-row">
<h3 id="achievement-title">STAR RECORDS</h3>
<span id="achievement-count">0 / 6</span>
</div>
<div class="achievement-list" id="achievement-list"></div>
<p class="record-boost">Each record adds <strong>+3%</strong> to all output.</p>
</section>
<section class="collapse-card locked" id="collapse-card" aria-labelledby="collapse-title">
<div class="collapse-orbit" aria-hidden="true"></div>
<div>
<p class="eyebrow">PROTOCOL Ω</p>
<h3 id="collapse-title">COLLAPSE THE STAR</h3>
<p id="collapse-description">Reach 1M run Flux to tear open the next universe.</p>
</div>
<div class="collapse-progress" aria-label="Collapse progress">
<span id="collapse-progress-bar"></span>
</div>
<button class="collapse-button" id="collapse-button" type="button" disabled>
<span id="collapse-button-label">LOCKED // 0%</span>
<small id="collapse-gain">+0 VOID CORES</small>
</button>
</section>
</aside>
<section class="reactor-stage" aria-labelledby="reactor-title">
<div class="stage-meta stage-meta-left" aria-hidden="true">
<span>HELIOCENTRIC NODE</span>
<strong>α–771</strong>
</div>
<div class="stage-meta stage-meta-right" aria-hidden="true">
<span>CORE TEMP</span>
<strong id="temperature-readout">0.00 K</strong>
</div>
<div class="reactor-title-block">
<p class="eyebrow">DORMANT STAR // MANUAL IGNITION</p>
<h2 id="reactor-title">Feed the core</h2>
</div>
<div class="reactor-wrap" id="reactor-wrap">
<div class="reactor-halo halo-outer" aria-hidden="true"></div>
<div class="reactor-halo halo-mid" aria-hidden="true"></div>
<div class="orbit orbit-one" aria-hidden="true"><i></i><i></i></div>
<div class="orbit orbit-two" aria-hidden="true"><i></i></div>
<button class="core-button" id="harvest-button" type="button" aria-label="Harvest Flux">
<span class="core-corona"></span>
<span class="core-surface"></span>
<span class="core-glint"></span>
<span class="core-label">
<small>PRESS TO</small>
<strong>IGNITE</strong>
<em id="click-value">+1 FLUX</em>
</span>
</button>
<div class="reactor-reticle" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
<button class="anomaly" id="anomaly-button" type="button" hidden>
<span>✦</span>
<strong>ANOMALY</strong>
</button>
<div class="particle-layer" id="particle-layer" aria-hidden="true"></div>
</div>
<div class="main-counter">
<strong id="main-flux">0</strong>
<span>FLUX IN CONTAINMENT</span>
</div>
<div class="resonance-row">
<div class="resonance-stat">
<span>RESONANCE</span>
<strong id="combo-readout">×1.00</strong>
</div>
<div class="mini-divider"></div>
<div class="resonance-stat right">
<span>AUTOMATED YIELD</span>
<strong id="rate-readout">+0 / SEC</strong>
</div>
</div>
<div class="overdrive-module" id="overdrive-module">
<div class="overdrive-copy">
<div class="overdrive-icon" aria-hidden="true">↯</div>
<div>
<span>CORE OVERDRIVE</span>
<strong id="overdrive-status">CHARGE WITH MANUAL INPUT</strong>
</div>
</div>
<div class="overdrive-track" aria-label="Overdrive charge">
<span id="overdrive-bar"></span>
<div class="track-segments" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
</div>
<button class="overdrive-button" id="overdrive-button" type="button" disabled>
IGNITE ×5
</button>
</div>
<div class="ticker" aria-live="polite">
<span>FORGE LOG</span>
<p id="ticker-text">SYSTEM QUIET // Awaiting first ignition.</p>
</div>
</section>
<aside class="panel fabricator-panel">
<div class="panel-corner" aria-hidden="true"></div>
<div class="panel-heading fabrication-heading">
<div>
<p class="eyebrow">ORBITAL FABRICATOR</p>
<h2>Build the impossible</h2>
</div>
<span class="fabricator-index">02</span>
</div>
<div class="tab-row" role="tablist" aria-label="Fabricator views">
<button class="tab-button active" id="units-tab" type="button" role="tab" aria-selected="true" aria-controls="units-panel">
GENERATORS
</button>
<button class="tab-button" id="research-tab" type="button" role="tab" aria-selected="false" aria-controls="research-panel">
RESEARCH <span id="research-badge">0</span>
</button>
</div>
<div class="buy-controls" id="buy-controls">
<span>ASSEMBLE</span>
<div>
<button class="buy-mode active" type="button" data-buy="1">×1</button>
<button class="buy-mode" type="button" data-buy="10">×10</button>
<button class="buy-mode" type="button" data-buy="max">MAX</button>
</div>
</div>
<div class="shop-panel active" id="units-panel" role="tabpanel" aria-labelledby="units-tab">
<div class="generator-list" id="generator-list"></div>
</div>
<div class="shop-panel" id="research-panel" role="tabpanel" aria-labelledby="research-tab" hidden>
<div class="research-intro">
<span>ONE-TIME AUGMENTATIONS</span>
<p>Rewrite the forge. Keep the consequences.</p>
</div>
<div class="research-list" id="research-list"></div>
</div>
<div class="fabricator-footer">
<span>TOTAL ASSEMBLIES</span>
<strong id="total-generators">0</strong>
<span class="footer-divider"></span>
<span>OUTPUT BONUS</span>
<strong id="output-bonus">×1.00</strong>
</div>
</aside>
</main>
<footer class="status-footer">
<span>SFG.OS <strong>v1.0.0</strong></span>
<span class="footer-line"></span>
<span>SPACE = IGNITE</span>
<span class="footer-line"></span>
<span>AUTOSAVE ACTIVE</span>
<span class="footer-coordinate">41°24′12″ N 02°10′26″ E</span>
</footer>
<div class="toast-stack" id="toast-stack" aria-live="polite" aria-atomic="true"></div>
<div class="modal-backdrop" id="modal-backdrop" hidden>
<section class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-describedby="modal-description">
<div class="modal-symbol" id="modal-symbol">Ω</div>
<p class="eyebrow" id="modal-eyebrow">IRREVERSIBLE PROTOCOL</p>
<h2 id="modal-title">Collapse this star?</h2>
<p id="modal-description">The forge will be rebuilt in a stronger universe.</p>
<div class="modal-reward" id="modal-reward"></div>
<div class="modal-actions">
<button class="text-button" id="modal-cancel" type="button">CANCEL</button>
<button class="confirm-button" id="modal-confirm" type="button">CONFIRM COLLAPSE</button>
</div>
</section>
</div>
<script src="script.js"></script>
</body>
</html>:root {
color-scheme: dark;
--ink: #070a0e;
--ink-soft: #0c1117;
--panel: rgba(12, 17, 22, 0.9);
--panel-solid: #0c1116;
--line: rgba(218, 234, 223, 0.13);
--line-bright: rgba(218, 234, 223, 0.3);
--text: #e8ede8;
--muted: #7f8b87;
--acid: #d9ff72;
--acid-soft: #a9c94f;
--ember: #ff6c42;
--ember-hot: #ffb15a;
--cyan: #6ce5e8;
--void: #a98dff;
--danger: #ff5f64;
--mono: "SFMono-Regular", "Roboto Mono", "Cascadia Code", "Liberation Mono", monospace;
--display: "Arial Narrow", "Avenir Next Condensed", "Helvetica Neue", sans-serif;
--body: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
html {
min-width: 320px;
background: var(--ink);
}
body {
min-height: 100vh;
margin: 0;
overflow-x: hidden;
color: var(--text);
background: var(--ink);
font-family: var(--body);
-webkit-font-smoothing: antialiased;
}
button {
color: inherit;
font: inherit;
}
button:focus-visible {
outline: 2px solid var(--acid);
outline-offset: 3px;
}
[hidden] {
display: none !important;
}
.cosmos {
position: fixed;
z-index: -2;
inset: 0;
overflow: hidden;
background:
radial-gradient(circle at 50% 40%, rgba(47, 39, 25, 0.23), transparent 33%),
linear-gradient(180deg, #070a0e 0%, #080c10 58%, #0c0f12 100%);
}
.cosmos::before {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.012) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.012) 1px, transparent 1px);
background-size: 80px 80px;
content: "";
mask-image: linear-gradient(to bottom, black, transparent 82%);
}
.nebula {
position: absolute;
width: 46vw;
height: 46vw;
border-radius: 50%;
filter: blur(100px);
opacity: 0.08;
}
.nebula-one {
top: -20%;
left: -12%;
background: var(--cyan);
}
.nebula-two {
right: -18%;
bottom: -24%;
background: var(--ember);
}
.starfield,
.starfield span {
position: absolute;
inset: 0;
}
.starfield span {
width: var(--size);
height: var(--size);
left: var(--x);
top: var(--y);
border-radius: 50%;
background: rgba(235, 242, 237, var(--opacity));
box-shadow: 0 0 calc(var(--size) * 3) rgba(235, 242, 237, 0.45);
animation: twinkle var(--duration) ease-in-out infinite alternate;
animation-delay: var(--delay);
}
.horizon-grid {
position: absolute;
right: -20%;
bottom: -56%;
left: -20%;
height: 82%;
opacity: 0.12;
background-image:
linear-gradient(rgba(217, 255, 114, 0.28) 1px, transparent 1px),
linear-gradient(90deg, rgba(217, 255, 114, 0.25) 1px, transparent 1px);
background-size: 54px 54px;
transform: perspective(420px) rotateX(62deg);
transform-origin: top;
mask-image: linear-gradient(to bottom, black, transparent 74%);
}
.scanlines {
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.025;
background: repeating-linear-gradient(
to bottom,
transparent 0,
transparent 2px,
#fff 3px,
transparent 4px
);
}
.command-bar {
position: relative;
z-index: 10;
display: grid;
grid-template-columns: minmax(270px, 1fr) minmax(400px, 1.35fr) minmax(230px, 1fr);
min-height: 88px;
padding: 0 28px;
border-bottom: 1px solid var(--line);
background: rgba(7, 10, 14, 0.88);
backdrop-filter: blur(18px);
}
.command-bar::after {
position: absolute;
right: 0;
bottom: -1px;
left: 0;
height: 1px;
background: linear-gradient(90deg, transparent, var(--acid) 48%, transparent 70%);
opacity: 0.28;
content: "";
}
.brand-block,
.top-readouts,
.header-actions {
display: flex;
align-items: center;
}
.brand-block {
gap: 14px;
}
.brand-block h1 {
margin: 3px 0 0;
font-family: var(--display);
font-size: clamp(1rem, 1.55vw, 1.45rem);
font-stretch: condensed;
font-weight: 800;
letter-spacing: 0.08em;
line-height: 1;
}
.brand-block h1 em {
color: var(--acid);
font-style: normal;
}
.eyebrow {
margin: 0;
color: var(--muted);
font-family: var(--mono);
font-size: 0.62rem;
font-weight: 700;
letter-spacing: 0.18em;
line-height: 1.25;
}
.brand-sigil {
position: relative;
width: 40px;
height: 40px;
flex: none;
border: 1px solid rgba(217, 255, 114, 0.35);
border-radius: 50%;
animation: sigil-pulse 4s ease-in-out infinite;
}
.brand-sigil::before,
.brand-sigil::after,
.brand-sigil span {
position: absolute;
border-radius: 50%;
content: "";
}
.brand-sigil::before {
inset: 7px;
border: 1px dashed rgba(217, 255, 114, 0.5);
}
.brand-sigil::after {
inset: 15px;
background: var(--acid);
box-shadow: 0 0 12px var(--acid);
}
.brand-sigil span:nth-child(1) {
width: 4px;
height: 4px;
top: 5px;
left: 17px;
background: var(--ember);
}
.brand-sigil span:nth-child(2) {
width: 2px;
height: 2px;
right: 4px;
bottom: 10px;
background: var(--cyan);
}
.top-readouts {
align-self: stretch;
justify-content: center;
border-right: 1px solid var(--line);
border-left: 1px solid var(--line);
}
.top-readout {
display: flex;
min-width: 130px;
height: 100%;
padding: 0 22px;
flex-direction: column;
justify-content: center;
border-right: 1px solid var(--line);
}
.top-readout:last-child {
border-right: 0;
}
.top-readout span {
margin-bottom: 5px;
color: var(--muted);
font-family: var(--mono);
font-size: 0.57rem;
font-weight: 700;
letter-spacing: 0.13em;
}
.top-readout strong {
overflow: hidden;
font-family: var(--mono);
font-size: 1rem;
font-variant-numeric: tabular-nums;
text-overflow: ellipsis;
white-space: nowrap;
}
.primary-readout strong {
color: var(--acid);
}
.stardust-readout strong {
color: var(--void);
}
.header-actions {
justify-content: flex-end;
gap: 10px;
}
.save-state {
display: flex;
margin-right: 8px;
align-items: center;
gap: 7px;
color: var(--muted);
font-family: var(--mono);
font-size: 0.55rem;
font-weight: 700;
letter-spacing: 0.12em;
}
.save-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--acid);
box-shadow: 0 0 7px var(--acid);
}
.save-state.saving .save-dot {
animation: saving 0.55s ease infinite alternate;
}
.icon-button,
.text-button {
height: 34px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.025);
cursor: pointer;
transition: 160ms ease;
}
.icon-button:hover,
.text-button:hover {
border-color: var(--line-bright);
background: rgba(255, 255, 255, 0.06);
}
.icon-button {
display: grid;
width: 34px;
padding: 8px;
place-items: center;
}
.icon-button svg {
width: 100%;
fill: var(--muted);
}
.icon-button.muted .sound-wave {
opacity: 0.15;
}
.text-button {
padding: 0 13px;
color: var(--muted);
font-family: var(--mono);
font-size: 0.58rem;
font-weight: 800;
letter-spacing: 0.12em;
}
.danger-button:hover {
border-color: rgba(255, 95, 100, 0.5);
color: var(--danger);
}
.game-shell {
display: grid;
grid-template-columns: minmax(270px, 0.83fr) minmax(440px, 1.45fr) minmax(330px, 1fr);
min-height: calc(100vh - 124px);
padding: 18px;
gap: 18px;
}
.panel {
position: relative;
min-width: 0;
overflow: hidden;
border: 1px solid var(--line);
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.018), transparent 30%),
var(--panel);
box-shadow: 0 28px 60px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
}
.panel::before {
position: absolute;
top: 0;
right: 58px;
left: 0;
height: 2px;
background: linear-gradient(90deg, var(--acid), transparent 78%);
opacity: 0.55;
content: "";
}
.panel-corner {
position: absolute;
top: 0;
right: 0;
width: 38px;
height: 38px;
border-bottom: 1px solid var(--line);
border-left: 1px solid var(--line);
background:
linear-gradient(135deg, transparent 47%, var(--line) 48%, var(--line) 50%, transparent 51%);
}
.mission-panel {
max-height: calc(100vh - 124px);
overflow-y: auto;
scrollbar-color: rgba(217, 255, 114, 0.25) transparent;
scrollbar-width: thin;
}
.panel-heading {
display: flex;
min-height: 84px;
padding: 20px 20px 16px;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--line);
}
.panel-heading h2 {
margin: 6px 0 0;
font-family: var(--display);
font-size: 1.15rem;
font-weight: 700;
letter-spacing: 0.02em;
}
.live-badge {
display: flex;
align-items: center;
gap: 7px;
color: var(--acid);
font-family: var(--mono);
font-size: 0.56rem;
font-weight: 800;
letter-spacing: 0.1em;
}
.live-badge i {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--acid);
box-shadow: 0 0 8px var(--acid);
animation: live-pulse 1.5s ease infinite;
}
.transmission-card {
position: relative;
min-height: 114px;
padding: 18px 20px 24px;
overflow: hidden;
border-bottom: 1px solid var(--line);
background: rgba(217, 255, 114, 0.025);
}
.transmission-label {
margin: 0 0 11px;
color: var(--acid-soft);
font-family: var(--mono);
font-size: 0.54rem;
font-weight: 800;
letter-spacing: 0.13em;
}
#transmission-text {
max-width: 95%;
margin: 0;
color: #bbc4be;
font-family: var(--mono);
font-size: 0.68rem;
line-height: 1.65;
}
.signal-bars {
position: absolute;
right: 17px;
bottom: 12px;
left: 20px;
display: flex;
height: 10px;
align-items: end;
gap: 3px;
opacity: 0.24;
}
.signal-bars i {
width: 3px;
height: 35%;
background: var(--acid);
animation: signal 1.7s ease-in-out infinite alternate;
}
.signal-bars i:nth-child(2n) { height: 80%; animation-delay: -0.4s; }
.signal-bars i:nth-child(3n) { height: 50%; animation-delay: -0.9s; }
.signal-bars i:nth-child(4n) { height: 100%; animation-delay: -1.2s; }
.mission-section {
padding: 18px 20px 14px;
border-bottom: 1px solid var(--line);
}
.section-title-row {
display: flex;
margin-bottom: 12px;
align-items: center;
justify-content: space-between;
}
.section-title-row h3,
.section-title-row span {
margin: 0;
color: var(--muted);
font-family: var(--mono);
font-size: 0.57rem;
font-weight: 800;
letter-spacing: 0.14em;
}
.milestone-list {
display: grid;
gap: 7px;
}
.milestone {
position: relative;
display: grid;
grid-template-columns: 26px minmax(0, 1fr) auto;
min-height: 42px;
padding: 8px 9px;
align-items: center;
gap: 8px;
overflow: hidden;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.015);
}
.milestone::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 2px;
background: rgba(255, 255, 255, 0.07);
content: "";
}
.milestone::before {
position: absolute;
z-index: 1;
bottom: 0;
left: 0;
width: var(--progress);
height: 2px;
background: var(--acid);
box-shadow: 0 0 8px var(--acid);
content: "";
transition: width 250ms ease;
}
.milestone.complete {
border-color: rgba(217, 255, 114, 0.2);
background: rgba(217, 255, 114, 0.035);
}
.milestone-index {
display: grid;
width: 22px;
height: 22px;
place-items: center;
border: 1px solid var(--line);
color: var(--muted);
font-family: var(--mono);
font-size: 0.5rem;
}
.milestone.complete .milestone-index {
border-color: var(--acid-soft);
color: var(--acid);
}
.milestone-copy {
min-width: 0;
}
.milestone-copy strong,
.milestone-copy span {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.milestone-copy strong {
color: #cbd2cd;
font-size: 0.64rem;
font-weight: 650;
}
.milestone-copy span {
margin-top: 2px;
color: #5e6965;
font-family: var(--mono);
font-size: 0.49rem;
}
.milestone-value {
color: var(--muted);
font-family: var(--mono);
font-size: 0.5rem;
}
.milestone.complete .milestone-value {
color: var(--acid-soft);
}
.achievement-section {
padding-bottom: 18px;
}
.achievement-list {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 5px;
}
.achievement-chip {
display: grid;
aspect-ratio: 1;
place-items: center;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.015);
color: #47514e;
cursor: help;
font-family: var(--mono);
font-size: 0.65rem;
transition: 180ms ease;
}
.achievement-chip.earned {
border-color: rgba(108, 229, 232, 0.35);
background: rgba(108, 229, 232, 0.06);
color: var(--cyan);
box-shadow: inset 0 0 12px rgba(108, 229, 232, 0.04);
}
.record-boost {
margin: 9px 0 0;
color: #5f6a66;
font-family: var(--mono);
font-size: 0.49rem;
}
.record-boost strong {
color: var(--cyan);
}
.collapse-card {
position: relative;
margin: 16px 20px 20px;
padding: 17px;
overflow: hidden;
border: 1px solid rgba(169, 141, 255, 0.32);
background:
radial-gradient(circle at 100% 0%, rgba(169, 141, 255, 0.15), transparent 45%),
rgba(169, 141, 255, 0.035);
}
.collapse-card.locked {
filter: saturate(0.3);
opacity: 0.64;
}
.collapse-card h3 {
position: relative;
margin: 5px 0 6px;
font-family: var(--display);
font-size: 0.9rem;
letter-spacing: 0.06em;
}
.collapse-card p:not(.eyebrow) {
position: relative;
margin: 0;
color: #7d758f;
font-size: 0.58rem;
line-height: 1.45;
}
.collapse-orbit {
position: absolute;
width: 100px;
height: 100px;
top: -53px;
right: -35px;
border: 1px solid rgba(169, 141, 255, 0.24);
border-radius: 50%;
box-shadow: inset 0 0 20px rgba(169, 141, 255, 0.08);
}
.collapse-progress {
height: 3px;
margin: 14px 0 11px;
overflow: hidden;
background: rgba(169, 141, 255, 0.12);
}
.collapse-progress span {
display: block;
width: 0;
height: 100%;
background: var(--void);
box-shadow: 0 0 10px var(--void);
transition: width 300ms ease;
}
.collapse-button {
display: flex;
width: 100%;
min-height: 42px;
padding: 8px 11px;
align-items: center;
justify-content: space-between;
border: 1px solid rgba(169, 141, 255, 0.35);
background: rgba(169, 141, 255, 0.09);
color: #d5c9ff;
cursor: pointer;
font-family: var(--mono);
font-size: 0.55rem;
font-weight: 800;
letter-spacing: 0.08em;
}
.collapse-button small {
color: var(--void);
font-size: 0.47rem;
}
.collapse-button:disabled {
border-color: var(--line);
color: var(--muted);
cursor: not-allowed;
}
.collapse-button:not(:disabled):hover {
background: rgba(169, 141, 255, 0.18);
box-shadow: 0 0 20px rgba(169, 141, 255, 0.12);
}
.reactor-stage {
position: relative;
display: flex;
min-width: 0;
padding: 21px 32px 16px;
overflow: hidden;
align-items: center;
flex-direction: column;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
background:
radial-gradient(circle at 50% 35%, rgba(255, 108, 66, 0.075), transparent 34%),
linear-gradient(180deg, rgba(255, 255, 255, 0.012), transparent 60%);
}
.reactor-stage::before,
.reactor-stage::after {
position: absolute;
width: 90px;
height: 90px;
content: "";
}
.reactor-stage::before {
top: 0;
left: 0;
border-top: 1px solid var(--line-bright);
border-left: 1px solid var(--line-bright);
}
.reactor-stage::after {
right: 0;
bottom: 0;
border-right: 1px solid var(--line-bright);
border-bottom: 1px solid var(--line-bright);
}
.stage-meta {
position: absolute;
top: 19px;
display: flex;
flex-direction: column;
color: var(--muted);
font-family: var(--mono);
font-size: 0.47rem;
letter-spacing: 0.11em;
line-height: 1.55;
}
.stage-meta strong {
color: #adb7b1;
font-size: 0.55rem;
}
.stage-meta-left { left: 22px; }
.stage-meta-right { right: 22px; text-align: right; }
.reactor-title-block {
position: relative;
z-index: 2;
text-align: center;
}
.reactor-title-block h2 {
margin: 6px 0 0;
font-family: var(--display);
font-size: clamp(1.2rem, 2vw, 1.65rem);
font-weight: 500;
letter-spacing: 0.025em;
}
.reactor-wrap {
--core-scale: 1;
position: relative;
width: min(41vh, 380px);
min-width: 270px;
aspect-ratio: 1;
margin: 4px 0 -6px;
flex: none;
isolation: isolate;
}
.reactor-halo,
.orbit,
.reactor-reticle,
.core-button {
position: absolute;
top: 50%;
left: 50%;
border-radius: 50%;
transform: translate(-50%, -50%);
}
.reactor-halo {
pointer-events: none;
}
.halo-outer {
width: 91%;
height: 91%;
border: 1px solid rgba(255, 255, 255, 0.045);
box-shadow:
inset 0 0 40px rgba(255, 108, 66, 0.02),
0 0 45px rgba(255, 108, 66, 0.025);
animation: breathe 4s ease-in-out infinite;
}
.halo-outer::before,
.halo-outer::after {
position: absolute;
border-radius: 50%;
content: "";
}
.halo-outer::before {
inset: 12%;
border: 1px dashed rgba(255, 177, 90, 0.13);
animation: spin 32s linear infinite;
}
.halo-outer::after {
inset: 25%;
background: radial-gradient(circle, rgba(255, 108, 66, 0.13), transparent 67%);
filter: blur(11px);
}
.halo-mid {
width: 71%;
height: 71%;
border: 1px solid rgba(255, 177, 90, 0.1);
box-shadow: 0 0 35px rgba(255, 108, 66, 0.06);
animation: spin-reverse 20s linear infinite;
}
.halo-mid::before,
.halo-mid::after {
position: absolute;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--ember-hot);
box-shadow: 0 0 8px var(--ember-hot);
content: "";
}
.halo-mid::before { top: -3px; left: 50%; }
.halo-mid::after { right: 8%; bottom: 14%; width: 3px; height: 3px; }
.orbit {
z-index: 3;
border: 1px solid rgba(232, 237, 232, 0.13);
pointer-events: none;
}
.orbit-one {
width: 76%;
height: 43%;
transform: translate(-50%, -50%) rotate(-16deg);
animation: orbit-wobble 10s ease-in-out infinite alternate;
}
.orbit-two {
width: 54%;
height: 83%;
transform: translate(-50%, -50%) rotate(60deg);
opacity: 0.7;
}
.orbit i {
position: absolute;
width: 6px;
height: 6px;
border: 1px solid var(--cyan);
border-radius: 50%;
background: var(--ink);
box-shadow: 0 0 9px var(--cyan);
}
.orbit-one i:first-child { top: 11%; left: 10%; }
.orbit-one i:last-child { right: 8%; bottom: 12%; width: 3px; height: 3px; }
.orbit-two i { right: 3%; bottom: 20%; width: 4px; height: 4px; border-color: var(--acid); box-shadow: 0 0 8px var(--acid); }
.core-button {
z-index: 5;
display: block;
width: 46%;
height: 46%;
padding: 0;
border: 0;
outline: none;
background: transparent;
cursor: pointer;
transform: translate(-50%, -50%) scale(var(--core-scale));
transition: transform 80ms ease, filter 180ms ease;
-webkit-tap-highlight-color: transparent;
}
.core-button:hover {
--core-scale: 1.035;
filter: brightness(1.08);
}
.core-button:active,
.core-button.pressed {
--core-scale: 0.965;
}
.core-button:focus-visible {
outline: 2px solid var(--acid);
outline-offset: 12px;
}
.core-corona,
.core-surface,
.core-glint {
position: absolute;
border-radius: 50%;
pointer-events: none;
}
.core-corona {
inset: -38%;
background:
conic-gradient(
from 20deg,
transparent 0 8%,
rgba(255, 108, 66, 0.28) 10%,
transparent 14% 30%,
rgba(255, 177, 90, 0.2) 34%,
transparent 41% 67%,
rgba(255, 108, 66, 0.24) 70%,
transparent 76%
);
filter: blur(10px);
opacity: 0.67;
animation: spin 15s linear infinite;
}
.core-corona::before {
position: absolute;
inset: 22%;
border-radius: 50%;
background: rgba(255, 108, 66, 0.42);
filter: blur(19px);
content: "";
animation: core-pulse 2.2s ease-in-out infinite alternate;
}
.core-surface {
inset: 0;
overflow: hidden;
border: 1px solid rgba(255, 229, 177, 0.8);
background:
radial-gradient(circle at 38% 31%, #fff8c6 0 3%, transparent 12%),
radial-gradient(circle at 68% 67%, rgba(169, 41, 13, 0.42), transparent 18%),
radial-gradient(circle at 38% 72%, rgba(255, 210, 95, 0.65), transparent 22%),
radial-gradient(circle at 50% 50%, #ffda72 0, #ff963e 38%, #ed4d21 68%, #8d1a12 100%);
box-shadow:
inset -18px -21px 30px rgba(112, 13, 5, 0.6),
inset 13px 12px 25px rgba(255, 244, 174, 0.46),
0 0 12px #ffbf55,
0 0 34px rgba(255, 104, 45, 0.8),
0 0 78px rgba(255, 88, 38, 0.35);
}
.core-surface::before {
position: absolute;
inset: -30%;
border-radius: 44%;
background:
repeating-radial-gradient(ellipse at 30% 40%, transparent 0 7px, rgba(255, 244, 194, 0.11) 8px 9px),
conic-gradient(transparent, rgba(255, 232, 160, 0.32), transparent 30%);
mix-blend-mode: screen;
content: "";
animation: spin-reverse 19s linear infinite;
}
.core-glint {
width: 22%;
height: 9%;
top: 16%;
left: 24%;
background: rgba(255, 255, 222, 0.8);
filter: blur(5px);
transform: rotate(-29deg);
}
.core-label {
position: absolute;
z-index: 2;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
border-radius: 50%;
color: #270c07;
text-align: center;
text-shadow: 0 1px rgba(255, 233, 172, 0.3);
}
.core-label small {
font-family: var(--mono);
font-size: clamp(0.38rem, 0.6vw, 0.52rem);
font-weight: 900;
letter-spacing: 0.16em;
}
.core-label strong {
margin: 2px 0;
font-family: var(--display);
font-size: clamp(1rem, 2.2vw, 1.6rem);
font-weight: 900;
letter-spacing: -0.04em;
line-height: 1;
}
.core-label em {
padding: 3px 6px;
border: 1px solid rgba(64, 16, 5, 0.22);
font-family: var(--mono);
font-size: clamp(0.36rem, 0.55vw, 0.48rem);
font-style: normal;
font-weight: 900;
}
.reactor-reticle {
z-index: 6;
width: 57%;
height: 57%;
border: 1px dashed rgba(255, 222, 150, 0.23);
pointer-events: none;
animation: spin 44s linear infinite;
}
.reactor-reticle i {
position: absolute;
width: 15px;
height: 1px;
background: rgba(255, 232, 177, 0.4);
}
.reactor-reticle i:nth-child(1) { top: -1px; left: 50%; transform: translateX(-50%) rotate(90deg); }
.reactor-reticle i:nth-child(2) { top: 50%; right: -8px; }
.reactor-reticle i:nth-child(3) { bottom: -1px; left: 50%; transform: translateX(-50%) rotate(90deg); }
.reactor-reticle i:nth-child(4) { top: 50%; left: -8px; }
.reactor-wrap.overdrive .core-surface {
background:
radial-gradient(circle at 35% 30%, #fff 0 4%, transparent 13%),
radial-gradient(circle at 50% 50%, #e7fbff 0, #78edf0 35%, #4ca7ff 65%, #4051c9 100%);
box-shadow:
inset -15px -20px 32px rgba(35, 33, 126, 0.65),
0 0 15px #d7ffff,
0 0 46px rgba(108, 229, 232, 0.92),
0 0 105px rgba(80, 130, 255, 0.46);
}
.reactor-wrap.overdrive .core-corona {
filter: hue-rotate(115deg) blur(9px);
animation-duration: 2.5s;
}
.reactor-wrap.overdrive .reactor-reticle,
.reactor-wrap.overdrive .halo-mid {
border-color: rgba(108, 229, 232, 0.55);
animation-duration: 5s;
}
.reactor-wrap.stage-hot .core-button { --core-scale: 1.04; }
.reactor-wrap.stage-critical .halo-outer { border-color: rgba(169, 141, 255, 0.25); }
.particle-layer {
position: absolute;
z-index: 20;
inset: 0;
overflow: visible;
pointer-events: none;
}
.click-particle {
position: absolute;
width: var(--particle-size);
height: var(--particle-size);
left: var(--particle-x);
top: var(--particle-y);
border-radius: 50%;
background: var(--particle-color);
box-shadow: 0 0 7px var(--particle-color);
animation: particle-fly 700ms cubic-bezier(0.15, 0.7, 0.35, 1) forwards;
}
.float-number {
position: absolute;
z-index: 21;
left: var(--float-x);
top: var(--float-y);
color: var(--acid);
font-family: var(--mono);
font-size: 0.7rem;
font-weight: 900;
text-shadow: 0 0 8px rgba(217, 255, 114, 0.6);
white-space: nowrap;
animation: float-away 850ms ease-out forwards;
}
.float-number.critical {
color: var(--cyan);
font-size: 0.85rem;
text-shadow: 0 0 12px var(--cyan);
}
.anomaly {
position: absolute;
z-index: 30;
top: 12%;
right: 7%;
display: flex;
padding: 8px 10px;
align-items: center;
gap: 6px;
border: 1px solid var(--cyan);
background: rgba(6, 20, 24, 0.9);
color: var(--cyan);
cursor: pointer;
font-family: var(--mono);
font-size: 0.48rem;
letter-spacing: 0.1em;
box-shadow: 0 0 25px rgba(108, 229, 232, 0.18);
animation: anomaly-float 2s ease-in-out infinite alternate;
}
.anomaly span {
font-size: 1rem;
animation: spin 2s linear infinite;
}
.main-counter {
position: relative;
z-index: 2;
display: flex;
margin-top: -4px;
align-items: center;
flex-direction: column;
}
.main-counter strong {
max-width: 100%;
overflow: hidden;
color: var(--acid);
font-family: var(--mono);
font-size: clamp(2.1rem, 4vw, 3.25rem);
font-weight: 500;
font-variant-numeric: tabular-nums;
letter-spacing: -0.065em;
line-height: 1;
text-overflow: ellipsis;
text-shadow: 0 0 26px rgba(217, 255, 114, 0.12);
}
.main-counter span {
margin-top: 6px;
color: var(--muted);
font-family: var(--mono);
font-size: 0.55rem;
font-weight: 700;
letter-spacing: 0.18em;
}
.resonance-row {
display: flex;
width: min(100%, 450px);
margin: 13px 0 12px;
align-items: center;
justify-content: center;
}
.resonance-stat {
display: flex;
min-width: 128px;
align-items: center;
flex-direction: column;
}
.resonance-stat span {
color: var(--muted);
font-family: var(--mono);
font-size: 0.48rem;
letter-spacing: 0.12em;
}
.resonance-stat strong {
margin-top: 3px;
color: #d3dad5;
font-family: var(--mono);
font-size: 0.63rem;
font-variant-numeric: tabular-nums;
}
.mini-divider {
width: 1px;
height: 22px;
margin: 0 20px;
background: var(--line);
}
.overdrive-module {
display: grid;
grid-template-columns: auto minmax(90px, 1fr) auto;
width: min(100%, 580px);
min-height: 62px;
padding: 10px;
align-items: center;
gap: 12px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.022);
}
.overdrive-module.active {
border-color: rgba(108, 229, 232, 0.5);
background: rgba(108, 229, 232, 0.06);
box-shadow: 0 0 26px rgba(108, 229, 232, 0.07);
}
.overdrive-copy {
display: flex;
align-items: center;
gap: 9px;
}
.overdrive-icon {
display: grid;
width: 32px;
height: 32px;
place-items: center;
border: 1px solid rgba(255, 177, 90, 0.35);
color: var(--ember-hot);
font-size: 1rem;
}
.overdrive-copy span,
.overdrive-copy strong {
display: block;
font-family: var(--mono);
}
.overdrive-copy span {
color: #b5bdb8;
font-size: 0.55rem;
font-weight: 800;
letter-spacing: 0.08em;
}
.overdrive-copy strong {
margin-top: 3px;
color: var(--muted);
font-size: 0.43rem;
font-weight: 600;
letter-spacing: 0.04em;
}
.overdrive-track {
position: relative;
height: 9px;
overflow: hidden;
border: 1px solid var(--line);
background: #080b0e;
}
.overdrive-track > span {
display: block;
width: 0;
height: 100%;
background: linear-gradient(90deg, var(--ember), var(--ember-hot));
box-shadow: 0 0 10px rgba(255, 108, 66, 0.6);
transition: width 100ms linear;
}
.active .overdrive-track > span {
background: linear-gradient(90deg, #478db4, var(--cyan));
}
.track-segments {
position: absolute;
inset: 0;
display: flex;
justify-content: space-evenly;
}
.track-segments i {
width: 1px;
height: 100%;
background: rgba(7, 10, 14, 0.68);
}
.overdrive-button {
min-width: 84px;
height: 36px;
padding: 0 10px;
border: 1px solid var(--ember);
background: rgba(255, 108, 66, 0.09);
color: var(--ember-hot);
cursor: pointer;
font-family: var(--mono);
font-size: 0.53rem;
font-weight: 900;
letter-spacing: 0.08em;
}
.overdrive-button:disabled {
border-color: var(--line);
color: #4d5653;
cursor: not-allowed;
}
.overdrive-button:not(:disabled):hover {
background: var(--ember);
color: #160804;
box-shadow: 0 0 16px rgba(255, 108, 66, 0.28);
}
.ticker {
display: flex;
width: min(100%, 580px);
min-height: 28px;
margin-top: 10px;
align-items: center;
gap: 10px;
overflow: hidden;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
}
.ticker span {
align-self: stretch;
display: flex;
padding: 0 8px;
align-items: center;
flex: none;
background: var(--acid);
color: #11150b;
font-family: var(--mono);
font-size: 0.48rem;
font-weight: 900;
letter-spacing: 0.08em;
}
.ticker p {
margin: 0;
overflow: hidden;
color: #707b77;
font-family: var(--mono);
font-size: 0.49rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.fabricator-panel {
display: flex;
max-height: calc(100vh - 124px);
flex-direction: column;
}
.fabrication-heading {
flex: none;
}
.fabricator-index {
margin-right: 22px;
color: #222a27;
font-family: var(--mono);
font-size: 2rem;
font-weight: 900;
}
.tab-row {
display: grid;
grid-template-columns: 1fr 1fr;
flex: none;
border-bottom: 1px solid var(--line);
}
.tab-button {
position: relative;
height: 44px;
border: 0;
border-right: 1px solid var(--line);
background: transparent;
color: var(--muted);
cursor: pointer;
font-family: var(--mono);
font-size: 0.56rem;
font-weight: 800;
letter-spacing: 0.09em;
}
.tab-button:last-child {
border-right: 0;
}
.tab-button.active {
background: rgba(217, 255, 114, 0.035);
color: var(--acid);
}
.tab-button.active::after {
position: absolute;
right: 18%;
bottom: -1px;
left: 18%;
height: 2px;
background: var(--acid);
box-shadow: 0 0 7px var(--acid);
content: "";
}
.tab-button span {
display: inline-grid;
width: 16px;
height: 16px;
margin-left: 4px;
place-items: center;
border-radius: 50%;
background: var(--ember);
color: #170904;
font-size: 0.46rem;
}
.buy-controls {
display: flex;
min-height: 44px;
padding: 6px 14px;
flex: none;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--line);
background: rgba(255, 255, 255, 0.013);
}
.buy-controls > span {
color: var(--muted);
font-family: var(--mono);
font-size: 0.5rem;
font-weight: 800;
letter-spacing: 0.13em;
}
.buy-controls > div {
display: flex;
gap: 4px;
}
.buy-mode {
min-width: 40px;
height: 25px;
border: 1px solid var(--line);
background: transparent;
color: var(--muted);
cursor: pointer;
font-family: var(--mono);
font-size: 0.5rem;
font-weight: 800;
}
.buy-mode:hover,
.buy-mode.active {
border-color: rgba(217, 255, 114, 0.3);
background: rgba(217, 255, 114, 0.06);
color: var(--acid);
}
.shop-panel {
min-height: 0;
overflow-y: auto;
scrollbar-color: rgba(217, 255, 114, 0.25) transparent;
scrollbar-width: thin;
}
.shop-panel.active {
flex: 1;
}
.generator-list,
.research-list {
padding: 10px;
}
.generator-card {
--generator-color: var(--acid);
position: relative;
display: grid;
grid-template-columns: 47px minmax(0, 1fr) auto;
min-height: 86px;
margin-bottom: 7px;
padding: 11px 10px;
align-items: center;
gap: 10px;
overflow: hidden;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.016);
cursor: pointer;
transition: border-color 150ms ease, background 150ms ease, transform 150ms ease;
}
.generator-card::before {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 2px;
background: var(--generator-color);
opacity: 0.6;
content: "";
}
.generator-card:hover:not(.locked) {
border-color: color-mix(in srgb, var(--generator-color) 33%, transparent);
background: rgba(255, 255, 255, 0.035);
transform: translateX(-2px);
}
.generator-card.affordable {
border-color: color-mix(in srgb, var(--generator-color) 25%, transparent);
}
.generator-card.locked {
min-height: 63px;
filter: saturate(0);
opacity: 0.42;
cursor: not-allowed;
}
.generator-glyph {
position: relative;
display: grid;
width: 42px;
height: 42px;
place-items: center;
border: 1px solid color-mix(in srgb, var(--generator-color) 40%, transparent);
background: color-mix(in srgb, var(--generator-color) 6%, transparent);
color: var(--generator-color);
font-family: var(--mono);
font-size: 1.05rem;
}
.generator-glyph::after {
position: absolute;
right: 3px;
bottom: 3px;
width: 3px;
height: 3px;
border-radius: 50%;
background: var(--generator-color);
box-shadow: 0 0 6px var(--generator-color);
content: "";
}
.generator-info {
min-width: 0;
}
.generator-name-row {
display: flex;
align-items: center;
gap: 6px;
}
.generator-name-row strong {
overflow: hidden;
font-family: var(--display);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.025em;
text-overflow: ellipsis;
white-space: nowrap;
}
.owned-pill {
display: inline-grid;
min-width: 18px;
height: 16px;
padding: 0 4px;
place-items: center;
border: 1px solid var(--line);
color: var(--generator-color);
font-family: var(--mono);
font-size: 0.48rem;
}
.generator-info p {
margin: 4px 0 7px;
overflow: hidden;
color: #66716d;
font-size: 0.52rem;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.generator-output {
color: var(--muted);
font-family: var(--mono);
font-size: 0.48rem;
}
.generator-output b {
color: #abb5af;
font-weight: 700;
}
.generator-purchase {
min-width: 73px;
text-align: right;
}
.generator-purchase small,
.generator-purchase strong {
display: block;
font-family: var(--mono);
}
.generator-purchase small {
margin-bottom: 4px;
color: var(--muted);
font-size: 0.43rem;
letter-spacing: 0.08em;
}
.generator-purchase strong {
color: var(--generator-color);
font-size: 0.6rem;
font-variant-numeric: tabular-nums;
}
.generator-card:not(.affordable) .generator-purchase strong {
color: #59615e;
}
.generator-card.locked .generator-purchase strong {
font-size: 0.46rem;
}
.research-intro {
padding: 13px 14px 3px;
}
.research-intro span {
color: var(--acid-soft);
font-family: var(--mono);
font-size: 0.5rem;
font-weight: 800;
letter-spacing: 0.1em;
}
.research-intro p {
margin: 4px 0 0;
color: var(--muted);
font-size: 0.56rem;
}
.research-card {
position: relative;
min-height: 92px;
margin-bottom: 7px;
padding: 12px 12px 11px 48px;
overflow: hidden;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.017);
cursor: pointer;
transition: 150ms ease;
}
.research-card:hover:not(.locked):not(.purchased),
.research-card.affordable {
border-color: rgba(108, 229, 232, 0.27);
background: rgba(108, 229, 232, 0.032);
}
.research-card.locked {
filter: saturate(0);
opacity: 0.38;
cursor: not-allowed;
}
.research-card.purchased {
border-color: rgba(217, 255, 114, 0.2);
background: rgba(217, 255, 114, 0.028);
cursor: default;
}
.research-number {
position: absolute;
top: 12px;
left: 11px;
display: grid;
width: 26px;
height: 26px;
place-items: center;
border: 1px solid rgba(108, 229, 232, 0.3);
color: var(--cyan);
font-family: var(--mono);
font-size: 0.5rem;
}
.research-card.purchased .research-number {
border-color: rgba(217, 255, 114, 0.4);
color: var(--acid);
}
.research-card h3 {
margin: 0;
font-family: var(--display);
font-size: 0.73rem;
letter-spacing: 0.03em;
}
.research-card p {
margin: 4px 0 8px;
color: #68736f;
font-size: 0.52rem;
line-height: 1.35;
}
.research-cost {
display: flex;
align-items: center;
justify-content: space-between;
color: var(--cyan);
font-family: var(--mono);
font-size: 0.49rem;
font-weight: 800;
}
.research-cost span:last-child {
color: var(--muted);
font-size: 0.43rem;
}
.research-card.purchased .research-cost {
color: var(--acid);
}
.fabricator-footer {
display: grid;
grid-template-columns: auto auto 1px auto auto;
min-height: 47px;
padding: 0 14px;
flex: none;
align-items: center;
justify-content: space-between;
gap: 8px;
border-top: 1px solid var(--line);
background: rgba(0, 0, 0, 0.15);
font-family: var(--mono);
font-size: 0.46rem;
}
.fabricator-footer span {
color: var(--muted);
}
.fabricator-footer strong {
color: var(--acid);
font-size: 0.55rem;
}
.footer-divider {
width: 1px;
height: 18px;
background: var(--line);
}
.status-footer {
display: flex;
min-height: 36px;
padding: 0 28px;
align-items: center;
gap: 12px;
border-top: 1px solid var(--line);
background: rgba(7, 10, 14, 0.9);
color: #4d5753;
font-family: var(--mono);
font-size: 0.48rem;
font-weight: 700;
letter-spacing: 0.08em;
}
.status-footer strong {
color: var(--muted);
}
.footer-line {
width: 24px;
height: 1px;
background: var(--line);
}
.footer-coordinate {
margin-left: auto;
}
.toast-stack {
position: fixed;
z-index: 100;
right: 20px;
bottom: 52px;
display: flex;
width: min(340px, calc(100vw - 40px));
flex-direction: column;
gap: 9px;
pointer-events: none;
}
.toast {
position: relative;
display: grid;
grid-template-columns: 34px 1fr;
min-height: 67px;
padding: 12px;
align-items: center;
gap: 11px;
overflow: hidden;
border: 1px solid rgba(217, 255, 114, 0.32);
background: rgba(12, 17, 22, 0.96);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(15px);
animation: toast-in 280ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.toast::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 2px;
background: var(--acid);
animation: toast-timer 4s linear forwards;
content: "";
}
.toast.exit {
animation: toast-out 230ms ease forwards;
}
.toast-icon {
display: grid;
width: 34px;
height: 34px;
place-items: center;
border: 1px solid rgba(217, 255, 114, 0.4);
color: var(--acid);
font-family: var(--mono);
font-size: 0.8rem;
}
.toast-copy strong,
.toast-copy span {
display: block;
}
.toast-copy strong {
margin-bottom: 3px;
font-family: var(--mono);
font-size: 0.58rem;
letter-spacing: 0.08em;
}
.toast-copy span {
color: var(--muted);
font-size: 0.55rem;
line-height: 1.35;
}
.modal-backdrop {
position: fixed;
z-index: 200;
inset: 0;
display: grid;
padding: 20px;
place-items: center;
background: rgba(3, 5, 7, 0.83);
backdrop-filter: blur(10px);
}
.modal {
position: relative;
width: min(430px, 100%);
padding: 32px;
overflow: hidden;
border: 1px solid rgba(169, 141, 255, 0.4);
background:
radial-gradient(circle at 100% 0%, rgba(169, 141, 255, 0.16), transparent 45%),
#0b0f14;
box-shadow: 0 40px 100px rgba(0, 0, 0, 0.5);
animation: modal-in 220ms ease-out;
}
.modal::before {
position: absolute;
top: 0;
right: 25%;
left: 0;
height: 2px;
background: var(--void);
content: "";
}
.modal-symbol {
display: grid;
width: 56px;
height: 56px;
margin-bottom: 22px;
place-items: center;
border: 1px solid rgba(169, 141, 255, 0.45);
border-radius: 50%;
color: var(--void);
font-family: var(--mono);
font-size: 1.3rem;
box-shadow: inset 0 0 20px rgba(169, 141, 255, 0.09);
}
.modal h2 {
margin: 7px 0 10px;
font-family: var(--display);
font-size: 1.55rem;
font-weight: 600;
}
.modal > p:not(.eyebrow) {
margin: 0;
color: var(--muted);
font-size: 0.72rem;
line-height: 1.55;
}
.modal-reward {
margin: 19px 0;
padding: 13px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.02);
color: var(--void);
font-family: var(--mono);
font-size: 0.65rem;
text-align: center;
}
.modal-actions {
display: grid;
grid-template-columns: 1fr 1.6fr;
gap: 8px;
}
.confirm-button {
min-height: 42px;
border: 1px solid var(--void);
background: rgba(169, 141, 255, 0.14);
color: #ded5ff;
cursor: pointer;
font-family: var(--mono);
font-size: 0.55rem;
font-weight: 900;
letter-spacing: 0.07em;
}
.confirm-button:hover {
background: var(--void);
color: #100b1c;
}
body.collapse-flash::after {
position: fixed;
z-index: 300;
inset: 0;
background: #fff;
pointer-events: none;
content: "";
animation: collapse-flash 1.1s ease-out forwards;
}
body.screen-shake {
animation: screen-shake 180ms linear;
}
@keyframes twinkle { to { opacity: 0.2; transform: scale(0.65); } }
@keyframes sigil-pulse { 50% { box-shadow: 0 0 15px rgba(217, 255, 114, 0.13); } }
@keyframes live-pulse { 50% { opacity: 0.3; } }
@keyframes saving { to { opacity: 0.2; } }
@keyframes signal { to { transform: scaleY(0.25); opacity: 0.35; } }
@keyframes breathe { 50% { opacity: 0.65; transform: translate(-50%, -50%) scale(1.03); } }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes spin-reverse { to { transform: rotate(-360deg); } }
@keyframes core-pulse { to { opacity: 0.5; transform: scale(1.14); } }
@keyframes orbit-wobble { to { transform: translate(-50%, -50%) rotate(-11deg) scale(1.04); } }
@keyframes particle-fly {
from { opacity: 1; transform: translate(-50%, -50%) scale(1); }
to { opacity: 0; transform: translate(calc(-50% + var(--particle-dx)), calc(-50% + var(--particle-dy))) scale(0.1); }
}
@keyframes float-away {
from { opacity: 0; transform: translate(-50%, 0) scale(0.75); }
15% { opacity: 1; }
to { opacity: 0; transform: translate(-50%, -55px) scale(1.06); }
}
@keyframes anomaly-float { to { transform: translateY(-8px); box-shadow: 0 0 35px rgba(108, 229, 232, 0.3); } }
@keyframes toast-in { from { opacity: 0; transform: translateX(35px); } }
@keyframes toast-out { to { opacity: 0; transform: translateX(35px); } }
@keyframes toast-timer { to { transform: translateX(100%); } }
@keyframes modal-in { from { opacity: 0; transform: scale(0.96) translateY(8px); } }
@keyframes collapse-flash { 0% { opacity: 1; } 100% { opacity: 0; } }
@keyframes screen-shake {
0%, 100% { transform: translate(0); }
25% { transform: translate(-3px, 2px); }
50% { transform: translate(3px, -2px); }
75% { transform: translate(-2px, -1px); }
}
@media (max-width: 1220px) {
.command-bar {
grid-template-columns: minmax(260px, 1fr) 1.2fr auto;
}
.top-readout {
min-width: 105px;
padding: 0 13px;
}
.save-state {
display: none;
}
.game-shell {
grid-template-columns: minmax(235px, 0.78fr) minmax(390px, 1.3fr) minmax(300px, 1fr);
gap: 10px;
padding: 10px;
}
.reactor-stage {
padding-right: 20px;
padding-left: 20px;
}
.panel-heading,
.mission-section {
padding-right: 14px;
padding-left: 14px;
}
.transmission-card {
padding-right: 14px;
padding-left: 14px;
}
.collapse-card {
margin-right: 14px;
margin-left: 14px;
}
}
@media (max-width: 960px) {
.command-bar {
grid-template-columns: 1fr auto;
padding: 0 18px;
backdrop-filter: none;
}
.top-readouts {
position: fixed;
z-index: 20;
right: 0;
bottom: 0;
left: 0;
height: 54px;
border-top: 1px solid var(--line);
border-right: 0;
border-left: 0;
background: rgba(7, 10, 14, 0.96);
backdrop-filter: blur(16px);
}
.top-readout {
width: 33.333%;
height: 100%;
min-width: 0;
padding: 0 14px;
}
.top-readout span { font-size: 0.48rem; }
.top-readout strong { font-size: 0.8rem; }
.game-shell {
grid-template-columns: 1fr 1fr;
min-height: 0;
padding: 12px 12px 70px;
}
.reactor-stage {
grid-column: 1 / -1;
grid-row: 1;
min-height: 700px;
}
.mission-panel,
.fabricator-panel {
max-height: none;
}
.shop-panel.active {
max-height: 650px;
}
.status-footer {
display: none;
}
}
@media (max-width: 640px) {
.command-bar {
min-height: 70px;
padding: 0 12px;
}
.brand-sigil {
width: 34px;
height: 34px;
}
.brand-block h1 { font-size: 0.95rem; }
.brand-block .eyebrow { font-size: 0.46rem; }
.header-actions { gap: 5px; }
.header-actions .danger-button {
display: block;
padding: 0 8px;
font-size: 0.48rem;
}
.game-shell {
display: flex;
padding: 8px 8px 66px;
flex-direction: column;
}
.reactor-stage {
min-height: 650px;
padding: 18px 12px 14px;
order: 1;
}
.fabricator-panel { order: 2; }
.mission-panel { order: 3; }
.stage-meta { display: none; }
.reactor-wrap {
width: min(88vw, 350px);
min-width: 0;
margin-top: 7px;
}
.main-counter strong {
font-size: 2.45rem;
}
.overdrive-module {
grid-template-columns: 1fr auto;
}
.overdrive-track {
grid-column: 1 / -1;
grid-row: 2;
}
.overdrive-copy strong { max-width: 180px; }
.generator-list,
.research-list { padding: 8px; }
.generator-card { min-height: 82px; }
.footer-coordinate { display: none; }
}
@media (max-width: 360px) {
.brand-block { gap: 8px; }
.brand-block .eyebrow { display: none; }
.brand-block h1 { font-size: 0.82rem; }
.brand-sigil { width: 31px; height: 31px; }
}
@media (max-height: 760px) and (min-width: 961px) {
.command-bar { min-height: 76px; }
.game-shell { min-height: calc(100vh - 112px); }
.fabricator-panel { max-height: calc(100vh - 112px); }
.mission-panel { max-height: calc(100vh - 112px); }
.reactor-stage { padding-top: 14px; }
.reactor-wrap { width: min(36vh, 315px); }
.transmission-card { min-height: 98px; padding-top: 13px; }
.mission-section { padding-top: 12px; padding-bottom: 10px; }
.milestone { min-height: 36px; padding-top: 5px; padding-bottom: 5px; }
.collapse-card { margin-top: 10px; padding: 12px; }
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}"use strict";
const SAVE_KEY = "singularity-forge-save-v1";
const COST_GROWTH = 1.16;
const COLLAPSE_THRESHOLD = 1_000_000;
const OVERDRIVE_DURATION = 12_000;
const OFFLINE_CAP_SECONDS = 8 * 60 * 60;
const GENERATORS = [
{
id: "coil",
name: "Flux Capacitor",
description: "A closed loop that remembers the first spark.",
glyph: "⌁",
baseCost: 15,
rate: 0.5,
unlockAt: 0,
color: "#d9ff72",
},
{
id: "drone",
name: "Drone Ring",
description: "Tiny machines teach the darkness to orbit.",
glyph: "◌",
baseCost: 100,
rate: 4,
unlockAt: 50,
color: "#6ce5e8",
},
{
id: "spire",
name: "Fusion Spire",
description: "A cathedral built from pressure and bad intentions.",
glyph: "△",
baseCost: 1_100,
rate: 28,
unlockAt: 2_500,
color: "#ffb15a",
},
{
id: "petal",
name: "Dyson Petal",
description: "One golden leaf in a machine-made sun flower.",
glyph: "✣",
baseCost: 12_000,
rate: 180,
unlockAt: 25_000,
color: "#ff6c42",
},
{
id: "loom",
name: "Reality Loom",
description: "Weave spare dimensions into usable current.",
glyph: "⌘",
baseCost: 150_000,
rate: 1_200,
unlockAt: 250_000,
color: "#a98dff",
},
{
id: "singularity",
name: "Pocket Singularity",
description: "A hungry little ending, perfectly contained.",
glyph: "●",
baseCost: 2_200_000,
rate: 8_500,
unlockAt: 1_000_000,
color: "#f38fff",
},
];
const RESEARCH = [
{
id: "resonant-touch",
name: "Resonant Touch",
description: "Manual ignition yields twice as much Flux.",
cost: 75,
unlockAt: 20,
effect: "MANUAL OUTPUT ×2",
},
{
id: "self-winding",
name: "Self-Winding Coils",
description: "Flux Capacitors loop through their own exhaust.",
cost: 600,
unlockAt: 250,
effect: "CAPACITORS ×3",
},
{
id: "kinetic-recursion",
name: "Kinetic Recursion",
description: "Each ignition also captures 1% of automated yield.",
cost: 3_000,
unlockAt: 1_500,
effect: "+1% / SEC TO CLICKS",
},
{
id: "solar-geometry",
name: "Solar Geometry",
description: "Realign every machine along impossible angles.",
cost: 10_000,
unlockAt: 5_000,
effect: "ALL OUTPUT ×2",
},
{
id: "critical-pulse",
name: "Critical Pulse",
description: "Manual ignition gains a 10% chance to strike ×10.",
cost: 40_000,
unlockAt: 25_000,
effect: "10% CRITICAL CHANCE",
},
{
id: "dark-matter-lacing",
name: "Dark Matter Lacing",
description: "Thread the whole forge with mass that refuses light.",
cost: 250_000,
unlockAt: 100_000,
effect: "ALL OUTPUT ×3",
},
];
const MILESTONES = [
{ name: "FIRST LIGHT", detail: "Ignite the dormant core", target: 1 },
{ name: "CLOSED CIRCUIT", detail: "Stabilize basic automation", target: 100 },
{ name: "ORBITAL SWARM", detail: "Wake the drone foundries", target: 2_500 },
{ name: "FUSION AGE", detail: "Raise a stellar cathedral", target: 25_000 },
{ name: "DYSON BLOOM", detail: "Teach the star to flower", target: 250_000 },
{ name: "EVENT HORIZON", detail: "Unlock collapse protocol", target: 1_000_000 },
{ name: "REALITY ENGINE", detail: "Outgrow a single universe", target: 10_000_000 },
];
const ACHIEVEMENTS = [
{ id: "first-light", symbol: "I", name: "First Light", detail: "Ignite the core once.", test: () => state.clicks >= 1 },
{ id: "persistent", symbol: "C", name: "Persistent Signal", detail: "Ignite the core 100 times.", test: () => state.clicks >= 100 },
{ id: "architect", symbol: "A", name: "Orbital Architect", detail: "Own 10 generators.", test: () => totalGeneratorCount() >= 10 },
{ id: "unstoppable", symbol: "U", name: "Unstoppable", detail: "Reach 100 Flux per second.", test: () => getProduction() >= 100 },
{ id: "million", symbol: "M", name: "One in a Million", detail: "Generate 1M lifetime Flux.", test: () => state.lifetimeFlux >= 1_000_000 },
{ id: "reborn", symbol: "Ω", name: "Born Again", detail: "Collapse a star.", test: () => state.collapses >= 1 },
];
const TRANSMISSIONS = [
"The universe is cold. Give it a reason to burn.",
"A signal answers from inside the star. It sounds like your name.",
"Automation is stable. The machine dreams while you are away.",
"New orbital signatures detected. They are arranging themselves.",
"The core is no longer a star. It is an argument against entropy.",
"Dyson geometry online. Night has become an engineering problem.",
"Protocol Ω is listening. Build enough light to survive the dark.",
"This universe is getting too small for the forge.",
];
function createDefaultState() {
return {
version: 1,
flux: 0,
runFlux: 0,
lifetimeFlux: 0,
stardust: 0,
collapses: 0,
generators: Object.fromEntries(GENERATORS.map((generator) => [generator.id, 0])),
upgrades: [],
achievements: [],
clicks: 0,
sound: true,
buyMode: "1",
overdrive: 0,
overdriveUntil: 0,
playedSeconds: 0,
lastSave: Date.now(),
};
}
function sanitizeNumber(value, fallback = 0) {
const numeric = Number(value);
return Number.isFinite(numeric) && numeric >= 0 ? numeric : fallback;
}
function loadState() {
const fresh = createDefaultState();
try {
const raw = localStorage.getItem(SAVE_KEY);
if (!raw) return { state: fresh, offlineSeconds: 0 };
const parsed = JSON.parse(raw);
const merged = { ...fresh, ...parsed };
merged.flux = sanitizeNumber(parsed.flux);
merged.runFlux = sanitizeNumber(parsed.runFlux);
merged.lifetimeFlux = sanitizeNumber(parsed.lifetimeFlux);
merged.stardust = sanitizeNumber(parsed.stardust);
merged.collapses = Math.floor(sanitizeNumber(parsed.collapses));
merged.clicks = Math.floor(sanitizeNumber(parsed.clicks));
merged.overdrive = Math.min(100, sanitizeNumber(parsed.overdrive));
merged.overdriveUntil = sanitizeNumber(parsed.overdriveUntil);
merged.playedSeconds = sanitizeNumber(parsed.playedSeconds);
merged.lastSave = sanitizeNumber(parsed.lastSave, Date.now());
merged.sound = parsed.sound !== false;
merged.buyMode = ["1", "10", "max"].includes(String(parsed.buyMode)) ? String(parsed.buyMode) : "1";
merged.upgrades = Array.isArray(parsed.upgrades)
? parsed.upgrades.filter((id) => RESEARCH.some((research) => research.id === id))
: [];
merged.achievements = Array.isArray(parsed.achievements)
? parsed.achievements.filter((id) => ACHIEVEMENTS.some((achievement) => achievement.id === id))
: [];
merged.generators = { ...fresh.generators };
for (const generator of GENERATORS) {
merged.generators[generator.id] = Math.floor(sanitizeNumber(parsed.generators?.[generator.id]));
}
const offlineSeconds = Math.min(
OFFLINE_CAP_SECONDS,
Math.max(0, (Date.now() - merged.lastSave) / 1000),
);
return { state: merged, offlineSeconds };
} catch (error) {
console.warn("Could not load forge save:", error);
return { state: fresh, offlineSeconds: 0 };
}
}
const loaded = loadState();
let state = loaded.state;
let combo = 0;
let lastClickAt = 0;
let lastTickAt = performance.now();
let lastUiRenderAt = 0;
let activeModal = null;
let modalReturnFocus = null;
let runtimeMilestoneCount = completedMilestoneCount();
let audioContext = null;
let anomalyTimer = null;
let anomalyExpiry = null;
let shopRenderQueued = false;
const dom = {
starfield: document.querySelector("#starfield"),
headerFlux: document.querySelector("#header-flux"),
headerRate: document.querySelector("#header-rate"),
headerStardust: document.querySelector("#header-stardust"),
mainFlux: document.querySelector("#main-flux"),
rateReadout: document.querySelector("#rate-readout"),
clickValue: document.querySelector("#click-value"),
comboReadout: document.querySelector("#combo-readout"),
temperature: document.querySelector("#temperature-readout"),
transmission: document.querySelector("#transmission-text"),
ticker: document.querySelector("#ticker-text"),
harvestButton: document.querySelector("#harvest-button"),
reactorWrap: document.querySelector("#reactor-wrap"),
particleLayer: document.querySelector("#particle-layer"),
overdriveModule: document.querySelector("#overdrive-module"),
overdriveBar: document.querySelector("#overdrive-bar"),
overdriveButton: document.querySelector("#overdrive-button"),
overdriveStatus: document.querySelector("#overdrive-status"),
anomalyButton: document.querySelector("#anomaly-button"),
milestoneList: document.querySelector("#milestone-list"),
milestoneCount: document.querySelector("#milestone-count"),
achievementList: document.querySelector("#achievement-list"),
achievementCount: document.querySelector("#achievement-count"),
collapseCard: document.querySelector("#collapse-card"),
collapseProgress: document.querySelector("#collapse-progress-bar"),
collapseButton: document.querySelector("#collapse-button"),
collapseButtonLabel: document.querySelector("#collapse-button-label"),
collapseGain: document.querySelector("#collapse-gain"),
collapseDescription: document.querySelector("#collapse-description"),
generatorList: document.querySelector("#generator-list"),
researchList: document.querySelector("#research-list"),
researchBadge: document.querySelector("#research-badge"),
totalGenerators: document.querySelector("#total-generators"),
outputBonus: document.querySelector("#output-bonus"),
buyControls: document.querySelector("#buy-controls"),
unitsTab: document.querySelector("#units-tab"),
researchTab: document.querySelector("#research-tab"),
unitsPanel: document.querySelector("#units-panel"),
researchPanel: document.querySelector("#research-panel"),
soundToggle: document.querySelector("#sound-toggle"),
resetButton: document.querySelector("#reset-button"),
saveState: document.querySelector("#save-state"),
saveLabel: document.querySelector("#save-label"),
toastStack: document.querySelector("#toast-stack"),
modalBackdrop: document.querySelector("#modal-backdrop"),
modalSymbol: document.querySelector("#modal-symbol"),
modalEyebrow: document.querySelector("#modal-eyebrow"),
modalTitle: document.querySelector("#modal-title"),
modalDescription: document.querySelector("#modal-description"),
modalReward: document.querySelector("#modal-reward"),
modalCancel: document.querySelector("#modal-cancel"),
modalConfirm: document.querySelector("#modal-confirm"),
};
function hasUpgrade(id) {
return state.upgrades.includes(id);
}
function totalGeneratorCount() {
return GENERATORS.reduce((total, generator) => total + (state.generators[generator.id] || 0), 0);
}
function getGlobalMultiplier() {
let multiplier = 1;
if (hasUpgrade("solar-geometry")) multiplier *= 2;
if (hasUpgrade("dark-matter-lacing")) multiplier *= 3;
multiplier *= 1 + state.stardust * 0.2;
multiplier *= 1 + state.achievements.length * 0.03;
return multiplier;
}
function getOverdriveMultiplier() {
return Date.now() < state.overdriveUntil ? 5 : 1;
}
function getGeneratorRawOutput(generator) {
const count = state.generators[generator.id] || 0;
let rate = generator.rate * count;
if (generator.id === "coil" && hasUpgrade("self-winding")) rate *= 3;
if (generator.id === "drone" && hasUpgrade("swarm-intelligence")) {
rate *= 1 + (state.generators.coil || 0) * 0.05;
}
return rate;
}
function getBaseProduction() {
const raw = GENERATORS.reduce((total, generator) => total + getGeneratorRawOutput(generator), 0);
return raw * getGlobalMultiplier();
}
function getProduction() {
return getBaseProduction() * getOverdriveMultiplier();
}
function getClickBase() {
let rawClick = hasUpgrade("resonant-touch") ? 2 : 1;
rawClick *= getGlobalMultiplier();
if (hasUpgrade("kinetic-recursion")) rawClick += getBaseProduction() * 0.01;
return rawClick * getOverdriveMultiplier();
}
function getComboMultiplier() {
return 1 + combo * 0.1;
}
function getGeneratorUnitCost(generator, ownedOffset = 0) {
const owned = state.generators[generator.id] || 0;
return generator.baseCost * COST_GROWTH ** (owned + ownedOffset);
}
function getGeneratorCost(generator, amount) {
if (amount <= 0) return 0;
const firstCost = getGeneratorUnitCost(generator);
return firstCost * ((COST_GROWTH ** amount - 1) / (COST_GROWTH - 1));
}
function getMaxBuyable(generator) {
const firstCost = getGeneratorUnitCost(generator);
if (state.flux + 1e-9 < firstCost) return 0;
const amount = Math.floor(
Math.log(1 + (state.flux * (COST_GROWTH - 1)) / firstCost) / Math.log(COST_GROWTH),
);
return Math.max(0, Math.min(10_000, amount));
}
function desiredBuyAmount(generator) {
if (state.buyMode === "max") return getMaxBuyable(generator);
return Number(state.buyMode);
}
function addFlux(amount) {
if (!Number.isFinite(amount) || amount <= 0) return;
state.flux += amount;
state.runFlux += amount;
state.lifetimeFlux += amount;
}
function completedMilestoneCount() {
return MILESTONES.filter((milestone) => state.lifetimeFlux >= milestone.target).length;
}
function getCollapseGain() {
return Math.floor(Math.sqrt(state.runFlux / COLLAPSE_THRESHOLD));
}
function formatNumber(value, forceInteger = false) {
const number = Number.isFinite(value) ? Math.max(0, value) : 0;
const suffixes = ["", "K", "M", "B", "T", "Qa", "Qi", "Sx", "Sp", "Oc", "No", "Dc"];
if (number < 1_000) {
if (forceInteger) return Math.floor(number).toLocaleString("en-US");
if (number < 10 && Math.abs(number - Math.round(number)) > 0.001) return number.toFixed(2);
if (number < 100 && Math.abs(number - Math.round(number)) > 0.01) return number.toFixed(1);
return Math.floor(number).toLocaleString("en-US");
}
const tier = Math.min(suffixes.length - 1, Math.floor(Math.log10(number) / 3));
const scaled = number / 1000 ** tier;
const decimals = scaled >= 100 ? 0 : scaled >= 10 ? 1 : 2;
return `${scaled.toFixed(decimals)}${suffixes[tier]}`;
}
function formatDuration(seconds) {
if (seconds < 60) return `${Math.floor(seconds)} SEC`;
if (seconds < 3_600) return `${Math.floor(seconds / 60)} MIN`;
const hours = Math.floor(seconds / 3_600);
const minutes = Math.floor((seconds % 3_600) / 60);
return `${hours}H ${minutes}M`;
}
function createStarfield() {
const fragment = document.createDocumentFragment();
for (let index = 0; index < 105; index += 1) {
const star = document.createElement("span");
const size = Math.random() < 0.84 ? Math.random() * 1.25 + 0.35 : Math.random() * 2 + 1;
star.style.setProperty("--size", `${size}px`);
star.style.setProperty("--x", `${Math.random() * 100}%`);
star.style.setProperty("--y", `${Math.random() * 90}%`);
star.style.setProperty("--opacity", (Math.random() * 0.55 + 0.18).toFixed(2));
star.style.setProperty("--duration", `${Math.random() * 3 + 2}s`);
star.style.setProperty("--delay", `${Math.random() * -4}s`);
fragment.appendChild(star);
}
dom.starfield.appendChild(fragment);
}
function makeParticles(clientX, clientY, amount, critical = false) {
const bounds = dom.reactorWrap.getBoundingClientRect();
const localX = clientX == null ? bounds.width / 2 : clientX - bounds.left;
const localY = clientY == null ? bounds.height / 2 : clientY - bounds.top;
const color = critical ? "#6ce5e8" : Math.random() > 0.4 ? "#ffb15a" : "#d9ff72";
const float = document.createElement("span");
float.className = `float-number${critical ? " critical" : ""}`;
float.textContent = `${critical ? "CRITICAL // " : "+"}${formatNumber(amount)}`;
float.style.setProperty("--float-x", `${localX}px`);
float.style.setProperty("--float-y", `${localY}px`);
dom.particleLayer.appendChild(float);
window.setTimeout(() => float.remove(), 900);
for (let index = 0; index < (critical ? 16 : 8); index += 1) {
const particle = document.createElement("i");
const angle = Math.random() * Math.PI * 2;
const distance = Math.random() * 65 + 25;
particle.className = "click-particle";
particle.style.setProperty("--particle-x", `${localX}px`);
particle.style.setProperty("--particle-y", `${localY}px`);
particle.style.setProperty("--particle-size", `${Math.random() * 3 + 1}px`);
particle.style.setProperty("--particle-color", color);
particle.style.setProperty("--particle-dx", `${Math.cos(angle) * distance}px`);
particle.style.setProperty("--particle-dy", `${Math.sin(angle) * distance}px`);
dom.particleLayer.appendChild(particle);
window.setTimeout(() => particle.remove(), 750);
}
}
function harvest(event) {
const now = performance.now();
combo = now - lastClickAt < 650 ? Math.min(20, combo + 1) : 0;
lastClickAt = now;
const critical = hasUpgrade("critical-pulse") && Math.random() < 0.1;
const amount = getClickBase() * getComboMultiplier() * (critical ? 10 : 1);
addFlux(amount);
state.clicks += 1;
if (Date.now() >= state.overdriveUntil) {
state.overdrive = Math.min(100, state.overdrive + (critical ? 14 : 5));
}
const bounds = dom.harvestButton.getBoundingClientRect();
const x = event?.clientX ?? bounds.left + bounds.width / 2;
const y = event?.clientY ?? bounds.top + bounds.height / 2;
makeParticles(x, y, amount, critical);
dom.harvestButton.classList.add("pressed");
window.setTimeout(() => dom.harvestButton.classList.remove("pressed"), 90);
if (critical) {
document.body.classList.remove("screen-shake");
void document.body.offsetWidth;
document.body.classList.add("screen-shake");
setTicker("CRITICAL PULSE // The core answered louder than physics allows.");
playTone("critical");
} else {
playTone("click");
}
checkAchievements();
checkMilestones();
renderStats();
queueShopRender();
}
function triggerOverdrive() {
if (state.overdrive < 100 || Date.now() < state.overdriveUntil) return;
state.overdrive = 0;
state.overdriveUntil = Date.now() + OVERDRIVE_DURATION;
setTicker("OVERDRIVE ENGAGED // Every forge system is operating far beyond warranty.");
showToast("CORE OVERDRIVE", "All output multiplied by five for 12 seconds.", "↯");
playTone("overdrive");
renderStats();
queueShopRender();
}
function buyGenerator(id) {
const generator = GENERATORS.find((item) => item.id === id);
if (!generator || state.lifetimeFlux < generator.unlockAt) return;
const amount = desiredBuyAmount(generator);
if (amount <= 0) return;
const cost = getGeneratorCost(generator, amount);
if (state.flux + 1e-7 < cost) return;
state.flux = Math.max(0, state.flux - cost);
state.generators[id] += amount;
setTicker(`ASSEMBLY COMPLETE // ${amount} ${generator.name}${amount === 1 ? "" : "s"} joined the orbit.`);
playTone("purchase");
checkAchievements();
renderShop();
renderStats();
}
function buyResearch(id) {
const research = RESEARCH.find((item) => item.id === id);
if (!research || hasUpgrade(id) || state.lifetimeFlux < research.unlockAt || state.flux < research.cost) return;
state.flux -= research.cost;
state.upgrades.push(id);
setTransmission(`${research.name} installed. ${research.description}`);
setTicker(`RESEARCH ONLINE // ${research.effect}.`);
showToast("RESEARCH COMPLETE", `${research.name}: ${research.effect}`, "◇");
playTone("research");
renderShop();
renderStats();
}
function renderGenerators() {
dom.generatorList.innerHTML = GENERATORS.map((generator) => {
const unlocked = state.lifetimeFlux >= generator.unlockAt;
const owned = state.generators[generator.id] || 0;
const amount = desiredBuyAmount(generator);
const displayAmount = state.buyMode === "max" ? amount : Number(state.buyMode);
const priceAmount = amount > 0 ? amount : 1;
const cost = getGeneratorCost(generator, priceAmount);
const affordable = unlocked && amount > 0 && state.flux + 1e-7 >= getGeneratorCost(generator, amount);
const output = getGeneratorRawOutput(generator) * getGlobalMultiplier() * getOverdriveMultiplier();
const unlockLabel = `SIGNAL ${formatNumber(generator.unlockAt)}`;
return `
<button
class="generator-card${unlocked ? "" : " locked"}${affordable ? " affordable" : ""}"
type="button"
data-generator="${generator.id}"
style="--generator-color: ${generator.color}"
${unlocked ? "" : "disabled"}
>
<span class="generator-glyph" aria-hidden="true">${unlocked ? generator.glyph : "?"}</span>
<span class="generator-info">
<span class="generator-name-row">
<strong>${unlocked ? generator.name : "UNKNOWN ASSEMBLY"}</strong>
${unlocked ? `<span class="owned-pill">${owned}</span>` : ""}
</span>
<p>${unlocked ? generator.description : `Requires ${formatNumber(generator.unlockAt)} lifetime Flux`}</p>
<span class="generator-output">${unlocked ? `YIELD <b>+${formatNumber(output)} / SEC</b>` : "BLUEPRINT ENCRYPTED"}</span>
</span>
<span class="generator-purchase">
<small>${unlocked ? `BUILD ×${displayAmount}` : "LOCKED"}</small>
<strong>${unlocked ? `${formatNumber(cost)} F` : unlockLabel}</strong>
</span>
</button>`;
}).join("");
}
function renderResearch() {
let affordableCount = 0;
dom.researchList.innerHTML = RESEARCH.map((research, index) => {
const unlocked = state.lifetimeFlux >= research.unlockAt;
const purchased = hasUpgrade(research.id);
const affordable = unlocked && !purchased && state.flux >= research.cost;
if (affordable) affordableCount += 1;
return `
<button
class="research-card${unlocked ? "" : " locked"}${purchased ? " purchased" : ""}${affordable ? " affordable" : ""}"
type="button"
data-research="${research.id}"
${unlocked && !purchased ? "" : "disabled"}
>
<span class="research-number">${purchased ? "✓" : String(index + 1).padStart(2, "0")}</span>
<h3>${unlocked ? research.name : "CLASSIFIED AUGMENT"}</h3>
<p>${unlocked ? research.description : `Decode at ${formatNumber(research.unlockAt)} lifetime Flux.`}</p>
<span class="research-cost">
<span>${purchased ? "INSTALLED" : unlocked ? `${formatNumber(research.cost)} FLUX` : "ENCRYPTED"}</span>
<span>${unlocked ? research.effect : `SIGNAL ${formatNumber(research.unlockAt)}`}</span>
</span>
</button>`;
}).join("");
dom.researchBadge.textContent = String(affordableCount);
dom.researchBadge.hidden = affordableCount === 0;
}
function renderShop() {
const focusedGenerator = document.activeElement?.dataset?.generator;
const focusedResearch = document.activeElement?.dataset?.research;
renderGenerators();
renderResearch();
const replacement = focusedGenerator
? dom.generatorList.querySelector(`[data-generator="${focusedGenerator}"]`)
: focusedResearch
? dom.researchList.querySelector(`[data-research="${focusedResearch}"]`)
: null;
if (replacement && !replacement.disabled) {
replacement.focus({ preventScroll: true });
} else if (focusedResearch) {
dom.researchTab.focus({ preventScroll: true });
}
shopRenderQueued = false;
}
function queueShopRender() {
if (shopRenderQueued) return;
shopRenderQueued = true;
window.setTimeout(() => {
const shopControlFocused = dom.generatorList.contains(document.activeElement)
|| dom.researchList.contains(document.activeElement);
if (shopControlFocused) {
shopRenderQueued = false;
return;
}
renderShop();
}, 120);
}
function renderMilestones() {
dom.milestoneList.innerHTML = MILESTONES.map((milestone, index) => {
const complete = state.lifetimeFlux >= milestone.target;
const previousTarget = index === 0 ? 0 : MILESTONES[index - 1].target;
const segmentProgress = complete
? 100
: Math.max(0, Math.min(100, ((state.lifetimeFlux - previousTarget) / (milestone.target - previousTarget)) * 100));
return `
<div class="milestone${complete ? " complete" : ""}" style="--progress: ${segmentProgress}%">
<span class="milestone-index">${complete ? "✓" : String(index + 1).padStart(2, "0")}</span>
<span class="milestone-copy">
<strong>${milestone.name}</strong>
<span>${milestone.detail}</span>
</span>
<span class="milestone-value">${complete ? "DONE" : formatNumber(milestone.target)}</span>
</div>`;
}).join("");
dom.milestoneCount.textContent = `${completedMilestoneCount()} / ${MILESTONES.length}`;
}
function renderAchievements() {
dom.achievementList.innerHTML = ACHIEVEMENTS.map((achievement) => {
const earned = state.achievements.includes(achievement.id);
return `<span class="achievement-chip${earned ? " earned" : ""}" title="${achievement.name} — ${achievement.detail}">${achievement.symbol}</span>`;
}).join("");
dom.achievementCount.textContent = `${state.achievements.length} / ${ACHIEVEMENTS.length}`;
}
function renderStats() {
const production = getProduction();
const clickValue = getClickBase() * getComboMultiplier();
const activeOverdrive = Date.now() < state.overdriveUntil;
const overdriveRemaining = Math.max(0, (state.overdriveUntil - Date.now()) / 1000);
const collapseGain = getCollapseGain();
const collapseProgress = Math.min(100, (state.runFlux / COLLAPSE_THRESHOLD) * 100);
const temperature = 2.73 + Math.log10(state.runFlux + 1) ** 3 * 12_500;
dom.headerFlux.textContent = formatNumber(state.flux);
dom.headerRate.textContent = formatNumber(production);
dom.headerStardust.textContent = formatNumber(state.stardust, true);
dom.mainFlux.textContent = formatNumber(state.flux);
dom.rateReadout.textContent = `+${formatNumber(production)} / SEC`;
dom.clickValue.textContent = `+${formatNumber(clickValue)} FLUX`;
dom.comboReadout.textContent = `×${getComboMultiplier().toFixed(2)}`;
dom.temperature.textContent = `${formatNumber(temperature)} K`;
dom.overdriveBar.style.width = `${activeOverdrive ? (overdriveRemaining / (OVERDRIVE_DURATION / 1000)) * 100 : state.overdrive}%`;
dom.overdriveButton.disabled = state.overdrive < 100 || activeOverdrive;
dom.overdriveButton.textContent = activeOverdrive ? `${overdriveRemaining.toFixed(1)} SEC` : state.overdrive >= 100 ? "IGNITE ×5" : `${Math.floor(state.overdrive)}%`;
dom.overdriveStatus.textContent = activeOverdrive
? `OUTPUT ×5 // ${overdriveRemaining.toFixed(1)} SECONDS`
: state.overdrive >= 100
? "CHARGE READY // RELEASE THE CORE"
: "CHARGE WITH MANUAL INPUT";
dom.overdriveModule.classList.toggle("active", activeOverdrive);
dom.reactorWrap.classList.toggle("overdrive", activeOverdrive);
dom.reactorWrap.classList.toggle("stage-hot", state.runFlux >= 25_000);
dom.reactorWrap.classList.toggle("stage-critical", state.runFlux >= COLLAPSE_THRESHOLD);
dom.collapseProgress.style.width = `${collapseProgress}%`;
dom.collapseGain.textContent = `+${collapseGain} VOID CORE${collapseGain === 1 ? "" : "S"}`;
dom.collapseButton.disabled = collapseGain < 1;
dom.collapseButtonLabel.textContent = collapseGain >= 1 ? "INITIATE COLLAPSE" : `LOCKED // ${Math.floor(collapseProgress)}%`;
dom.collapseCard.classList.toggle("locked", collapseGain < 1);
dom.collapseDescription.textContent = collapseGain >= 1
? "A stronger universe is waiting on the other side."
: `Reach 1M run Flux to tear open the next universe. ${formatNumber(state.runFlux)} / 1.00M`;
dom.totalGenerators.textContent = formatNumber(totalGeneratorCount(), true);
dom.outputBonus.textContent = `×${getGlobalMultiplier().toFixed(2)}`;
dom.soundToggle.classList.toggle("muted", !state.sound);
dom.soundToggle.setAttribute("aria-label", state.sound ? "Mute sound" : "Enable sound");
}
function checkAchievements() {
let changed = false;
for (const achievement of ACHIEVEMENTS) {
if (!state.achievements.includes(achievement.id) && achievement.test()) {
state.achievements.push(achievement.id);
showToast(`RECORD // ${achievement.name.toUpperCase()}`, `${achievement.detail} Permanent output +3%.`, achievement.symbol);
playTone("achievement");
changed = true;
}
}
if (changed) {
renderAchievements();
renderStats();
queueShopRender();
}
}
function checkMilestones() {
const completed = completedMilestoneCount();
if (completed > runtimeMilestoneCount) {
for (let index = runtimeMilestoneCount; index < completed; index += 1) {
const milestone = MILESTONES[index];
showToast(`SEQUENCE ${String(index + 1).padStart(2, "0")}`, milestone.name, "◆");
setTransmission(TRANSMISSIONS[Math.min(index + 1, TRANSMISSIONS.length - 1)]);
setTicker(`MILESTONE // ${milestone.name} // ${milestone.detail}.`);
}
runtimeMilestoneCount = completed;
renderMilestones();
queueShopRender();
}
}
function setTransmission(message) {
dom.transmission.textContent = message;
}
function setTicker(message) {
dom.ticker.textContent = message;
}
function showToast(title, message, symbol = "◆") {
const toast = document.createElement("div");
toast.className = "toast";
toast.innerHTML = `
<span class="toast-icon">${symbol}</span>
<span class="toast-copy"><strong>${title}</strong><span>${message}</span></span>`;
dom.toastStack.appendChild(toast);
window.setTimeout(() => {
toast.classList.add("exit");
window.setTimeout(() => toast.remove(), 250);
}, 4_000);
}
function switchTab(tab) {
const researchOpen = tab === "research";
dom.unitsTab.classList.toggle("active", !researchOpen);
dom.researchTab.classList.toggle("active", researchOpen);
dom.unitsTab.setAttribute("aria-selected", String(!researchOpen));
dom.researchTab.setAttribute("aria-selected", String(researchOpen));
dom.unitsPanel.hidden = researchOpen;
dom.researchPanel.hidden = !researchOpen;
dom.unitsPanel.classList.toggle("active", !researchOpen);
dom.researchPanel.classList.toggle("active", researchOpen);
dom.buyControls.hidden = researchOpen;
}
function showModal(type) {
activeModal = type;
if (type === "collapse") {
const gain = getCollapseGain();
if (gain < 1) {
activeModal = null;
return;
}
dom.modalSymbol.textContent = "Ω";
dom.modalEyebrow.textContent = "IRREVERSIBLE PROTOCOL";
dom.modalTitle.textContent = "Collapse this star?";
dom.modalDescription.textContent = "Flux, generators, research, and this run's progress will reset. Records and Void Cores survive.";
dom.modalReward.textContent = `REWARD // +${gain} VOID CORE${gain === 1 ? "" : "S"} // PERMANENT OUTPUT +${gain * 20}%`;
dom.modalConfirm.textContent = "CONFIRM COLLAPSE";
} else {
dom.modalSymbol.textContent = "×";
dom.modalEyebrow.textContent = "LOCAL DATA PURGE";
dom.modalTitle.textContent = "Erase the forge?";
dom.modalDescription.textContent = "This deletes all Flux, generators, research, records, and Void Cores from this browser.";
dom.modalReward.textContent = "THIS ACTION CANNOT BE UNDONE";
dom.modalConfirm.textContent = "ERASE EVERYTHING";
}
modalReturnFocus = document.activeElement;
[document.querySelector(".command-bar"), document.querySelector(".game-shell"), document.querySelector(".status-footer")]
.filter(Boolean)
.forEach((element) => { element.inert = true; });
dom.modalBackdrop.hidden = false;
window.setTimeout(() => dom.modalCancel.focus(), 0);
}
function closeModal() {
dom.modalBackdrop.hidden = true;
[document.querySelector(".command-bar"), document.querySelector(".game-shell"), document.querySelector(".status-footer")]
.filter(Boolean)
.forEach((element) => { element.inert = false; });
activeModal = null;
const returnTarget = modalReturnFocus;
modalReturnFocus = null;
returnTarget?.focus({ preventScroll: true });
}
function performCollapse() {
const gain = getCollapseGain();
if (gain < 1) return;
state.stardust += gain;
state.collapses += 1;
state.flux = state.stardust * 25;
state.runFlux = 0;
state.generators = Object.fromEntries(GENERATORS.map((generator) => [generator.id, 0]));
state.upgrades = [];
state.overdrive = 0;
state.overdriveUntil = 0;
combo = 0;
closeModal();
document.body.classList.add("collapse-flash");
window.setTimeout(() => document.body.classList.remove("collapse-flash"), 1_150);
setTransmission("The universe ended beautifully. The forge remembers everything that mattered.");
setTicker(`REBIRTH COMPLETE // ${gain} Void Core${gain === 1 ? "" : "s"} recovered. New universe seeded with ${formatNumber(state.flux)} Flux.`);
showToast("UNIVERSE REBORN", `Void Cores now multiply all output by ×${(1 + state.stardust * 0.2).toFixed(2)}.`, "Ω");
playTone("collapse");
checkAchievements();
renderAll();
saveGame();
}
function performFullReset() {
const sound = state.sound;
state = createDefaultState();
state.sound = sound;
combo = 0;
lastClickAt = 0;
runtimeMilestoneCount = 0;
try {
localStorage.removeItem(SAVE_KEY);
} catch (error) {
console.warn("Could not remove forge save:", error);
}
closeModal();
setTransmission(TRANSMISSIONS[0]);
setTicker("SYSTEM QUIET // Awaiting first ignition.");
renderAll();
showToast("FORGE ERASED", "The dark is waiting again.", "×");
}
function saveGame(showIndicator = true) {
try {
state.lastSave = Date.now();
localStorage.setItem(SAVE_KEY, JSON.stringify(state));
if (showIndicator) {
dom.saveState.classList.add("saving");
dom.saveLabel.textContent = "SYNCING";
window.setTimeout(() => {
dom.saveState.classList.remove("saving");
dom.saveLabel.textContent = "LOCAL LINK";
}, 550);
}
} catch (error) {
dom.saveLabel.textContent = "SAVE ERROR";
console.warn("Could not save forge:", error);
}
}
function initAudio() {
if (!state.sound) return null;
if (!audioContext) {
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (!AudioContextClass) return null;
audioContext = new AudioContextClass();
}
if (audioContext.state === "suspended") audioContext.resume();
return audioContext;
}
function playTone(kind) {
if (!state.sound) return;
const context = initAudio();
if (!context) return;
const settings = {
click: { start: 170 + combo * 5, end: 90, duration: 0.07, volume: 0.012, type: "sine" },
purchase: { start: 240, end: 420, duration: 0.12, volume: 0.022, type: "triangle" },
research: { start: 330, end: 760, duration: 0.25, volume: 0.03, type: "sine" },
critical: { start: 820, end: 180, duration: 0.22, volume: 0.035, type: "sawtooth" },
achievement: { start: 440, end: 880, duration: 0.3, volume: 0.028, type: "triangle" },
overdrive: { start: 120, end: 680, duration: 0.42, volume: 0.04, type: "sawtooth" },
collapse: { start: 520, end: 42, duration: 0.9, volume: 0.05, type: "sine" },
}[kind] || { start: 200, end: 100, duration: 0.1, volume: 0.02, type: "sine" };
const oscillator = context.createOscillator();
const gain = context.createGain();
oscillator.type = settings.type;
oscillator.frequency.setValueAtTime(settings.start, context.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(Math.max(1, settings.end), context.currentTime + settings.duration);
gain.gain.setValueAtTime(settings.volume, context.currentTime);
gain.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + settings.duration);
oscillator.connect(gain);
gain.connect(context.destination);
oscillator.start();
oscillator.stop(context.currentTime + settings.duration);
}
function scheduleAnomaly() {
window.clearTimeout(anomalyTimer);
anomalyTimer = window.setTimeout(showAnomaly, 24_000 + Math.random() * 20_000);
}
function showAnomaly() {
if (!dom.anomalyButton.hidden) return;
dom.anomalyButton.hidden = false;
dom.anomalyButton.style.top = `${10 + Math.random() * 58}%`;
dom.anomalyButton.style.right = `${3 + Math.random() * 18}%`;
setTicker("UNIDENTIFIED SIGNAL // A loose packet of energy has entered the forge.");
anomalyExpiry = window.setTimeout(() => {
dom.anomalyButton.hidden = true;
setTicker("SIGNAL LOST // The anomaly slipped beyond containment.");
scheduleAnomaly();
}, 9_000);
}
function collectAnomaly() {
window.clearTimeout(anomalyExpiry);
dom.anomalyButton.hidden = true;
const reward = Math.max(25, getProduction() * 25, getClickBase() * 35);
addFlux(reward);
showToast("ANOMALY CONTAINED", `Recovered ${formatNumber(reward)} Flux from the signal.`, "✦");
setTicker(`ANOMALY CONTAINED // +${formatNumber(reward)} Flux recovered.`);
playTone("achievement");
checkMilestones();
checkAchievements();
renderStats();
queueShopRender();
scheduleAnomaly();
}
function tick(now) {
const deltaSeconds = Math.min(60, Math.max(0, (now - lastTickAt) / 1000));
lastTickAt = now;
state.playedSeconds += deltaSeconds;
const production = getProduction();
if (production > 0) addFlux(production * deltaSeconds);
if (now - lastClickAt > 700 && combo > 0) {
combo = Math.max(0, combo - deltaSeconds * 3.2);
}
if (
Date.now() >= state.overdriveUntil
&& now - lastClickAt > 650
&& state.overdrive > 0
&& state.overdrive < 100
) {
state.overdrive = Math.max(0, state.overdrive - deltaSeconds * 2.1);
}
if (now - lastUiRenderAt >= 100) {
checkMilestones();
checkAchievements();
renderStats();
lastUiRenderAt = now;
}
window.requestAnimationFrame(tick);
}
function renderAll() {
document.querySelectorAll(".buy-mode").forEach((button) => {
button.classList.toggle("active", button.dataset.buy === state.buyMode);
});
renderMilestones();
renderAchievements();
renderShop();
renderStats();
}
function bindEvents() {
dom.harvestButton.addEventListener("click", harvest);
dom.harvestButton.addEventListener("keydown", (event) => {
if (event.code === "Space" || event.key === "Enter") {
event.preventDefault();
harvest();
}
});
dom.overdriveButton.addEventListener("click", triggerOverdrive);
dom.anomalyButton.addEventListener("click", collectAnomaly);
dom.generatorList.addEventListener("click", (event) => {
const card = event.target.closest("[data-generator]");
if (card) buyGenerator(card.dataset.generator);
});
dom.researchList.addEventListener("click", (event) => {
const card = event.target.closest("[data-research]");
if (card) buyResearch(card.dataset.research);
});
document.querySelectorAll(".buy-mode").forEach((button) => {
button.addEventListener("click", () => {
state.buyMode = button.dataset.buy;
document.querySelectorAll(".buy-mode").forEach((item) => item.classList.toggle("active", item === button));
renderGenerators();
});
});
dom.unitsTab.addEventListener("click", () => switchTab("units"));
dom.researchTab.addEventListener("click", () => switchTab("research"));
dom.collapseButton.addEventListener("click", () => showModal("collapse"));
dom.resetButton.addEventListener("click", () => showModal("reset"));
dom.modalCancel.addEventListener("click", closeModal);
dom.modalConfirm.addEventListener("click", () => {
if (activeModal === "collapse") performCollapse();
if (activeModal === "reset") performFullReset();
});
dom.modalBackdrop.addEventListener("pointerdown", (event) => {
if (event.target === dom.modalBackdrop) closeModal();
});
dom.soundToggle.addEventListener("click", () => {
state.sound = !state.sound;
if (state.sound) playTone("purchase");
renderStats();
saveGame(false);
});
window.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !dom.modalBackdrop.hidden) {
closeModal();
return;
}
if (event.key === "Tab" && !dom.modalBackdrop.hidden) {
const focusable = [dom.modalCancel, dom.modalConfirm];
const currentIndex = focusable.indexOf(document.activeElement);
if (event.shiftKey && currentIndex <= 0) {
event.preventDefault();
dom.modalConfirm.focus();
} else if (!event.shiftKey && currentIndex === focusable.length - 1) {
event.preventDefault();
dom.modalCancel.focus();
}
return;
}
const tagName = document.activeElement?.tagName;
const isInteractive = ["BUTTON", "INPUT", "TEXTAREA", "SELECT", "A"].includes(tagName);
if (event.code === "Space" && !event.repeat && !isInteractive && dom.modalBackdrop.hidden) {
event.preventDefault();
harvest();
}
});
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
saveGame(false);
} else {
const awaySeconds = Math.min(
OFFLINE_CAP_SECONDS,
Math.max(0, (Date.now() - state.lastSave) / 1000),
);
state.lastSave = Date.now();
applyOfflineProgress(awaySeconds);
checkMilestones();
checkAchievements();
renderAll();
}
lastTickAt = performance.now();
});
window.addEventListener("beforeunload", () => saveGame(false));
}
function applyOfflineProgress(seconds) {
if (seconds < 5) return;
const offlineRate = getProduction();
const earned = offlineRate * seconds;
if (earned > 0) {
addFlux(earned);
window.setTimeout(() => {
showToast("OFFLINE HARVEST", `${formatDuration(seconds)} away // +${formatNumber(earned)} Flux recovered.`, "◷");
setTicker(`OFFLINE LINK // Recovered ${formatNumber(earned)} Flux after ${formatDuration(seconds)}.`);
}, 500);
}
}
function initialize() {
createStarfield();
bindEvents();
applyOfflineProgress(loaded.offlineSeconds);
runtimeMilestoneCount = completedMilestoneCount();
setTransmission(TRANSMISSIONS[Math.min(runtimeMilestoneCount, TRANSMISSIONS.length - 1)]);
if (state.lifetimeFlux > 0) {
setTicker(`FORGE LINK RESTORED // ${formatNumber(state.lifetimeFlux)} lifetime Flux recorded.`);
}
checkAchievements();
renderAll();
scheduleAnomaly();
window.setInterval(() => {
if (!document.hidden) saveGame(true);
}, 10_000);
window.setInterval(() => {
renderMilestones();
const shopControlFocused = dom.generatorList.contains(document.activeElement)
|| dom.researchList.contains(document.activeElement);
if (!shopControlFocused) renderShop();
}, 1_000);
window.requestAnimationFrame(tick);
}
initialize();