OpenAI GPT-5.6 Sol
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="theme-color" content="#07080d" />
<meta
name="description"
content="Parallax is a cosmic incremental game about growing a tiny universe."
/>
<title>PARALLAX — A Pocket Universe</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<canvas id="starfield" aria-hidden="true"></canvas>
<div class="noise" aria-hidden="true"></div>
<div class="intro" id="intro">
<div class="intro__flare"></div>
<p class="eyebrow">EXPERIMENT 7—A</p>
<h1>Something is waiting<br />in the dark.</h1>
<p class="intro__copy">
Feed the anomaly. Automate the impossible.<br />Grow a universe from almost nothing.
</p>
<button class="primary-button" id="beginButton">
<span>INITIATE EXPERIMENT</span>
<span aria-hidden="true">↗</span>
</button>
<button class="text-button intro__continue hidden" id="continueButton">CONTINUE SAVED EXPERIMENT</button>
</div>
<div class="game-shell is-hidden" id="gameShell">
<header class="topbar">
<a class="brand" href="#" aria-label="Parallax home">
<span class="brand__mark"><i></i></span>
<span>PARALLAX</span>
<small>POCKET UNIVERSE LAB</small>
</a>
<div class="resource-readout" aria-live="polite">
<span class="resource-readout__icon">✦</span>
<div>
<span id="dustValue">0</span>
<small>STARDUST</small>
</div>
<div class="rate-pill"><span id="rateValue">+0</span>/SEC</div>
</div>
<div class="topbar__actions">
<button class="icon-button" id="soundButton" title="Toggle sound" aria-label="Toggle sound">
<span class="sound-on">◖))</span><span class="sound-off hidden">◖×</span>
</button>
<button class="icon-button" id="settingsButton" title="Open lab controls" aria-label="Open lab controls">⌁</button>
</div>
</header>
<main class="game-grid">
<aside class="panel mission-panel">
<div class="panel__heading">
<span>MISSION</span><span class="status-dot">LIVE</span>
</div>
<section class="mission-card">
<span class="mission-card__number" id="phaseNumber">01</span>
<p class="eyebrow" id="phaseEyebrow">PHASE ONE</p>
<h2 id="phaseTitle">Wake the dark</h2>
<p id="phaseCopy">Tap the anomaly and recover your first traces of stardust.</p>
<div class="progress-track"><i id="phaseProgress"></i></div>
<div class="progress-meta">
<span id="phaseCurrent">0 gathered</span>
<span id="phaseGoal">100 target</span>
</div>
</section>
<section class="stat-list">
<div><span>LIFETIME OUTPUT</span><strong id="lifetimeValue">0</strong></div>
<div><span>CORE POWER</span><strong id="clickPowerValue">1 / TAP</strong></div>
<div><span>RESONANCE</span><strong id="multiplierValue">×1.00</strong></div>
<div><span>RUN TIME</span><strong id="runTimeValue">00:00:00</strong></div>
</section>
<section class="milestones">
<div class="section-label"><span>THRESHOLDS</span><span id="milestoneCount">0 / 5</span></div>
<div class="milestone-grid" id="milestoneGrid"></div>
</section>
<blockquote class="field-note" id="fieldNote">
“The anomaly reacts to attention. This is either very good—or extremely bad.”
</blockquote>
</aside>
<section class="reactor-stage" id="reactorStage">
<div class="stage-label"><span>ANOMALY A—01</span><span id="stabilityLabel">STABILITY 100%</span></div>
<div class="orbit orbit--outer" aria-hidden="true"><i></i><i></i><i></i></div>
<div class="orbit orbit--inner" aria-hidden="true"><i></i><i></i></div>
<div class="pulse-ring" aria-hidden="true"></div>
<button class="core" id="coreButton" aria-label="Harvest stardust from the anomaly">
<span class="core__halo"></span>
<span class="core__surface"></span>
<span class="core__shine"></span>
<span class="core__content">
<small>HARVEST</small>
<strong>✦</strong>
<em id="tapValue">+1</em>
</span>
</button>
<div class="core-instruction">
<span class="mouse-icon"><i></i></span>
<div><strong>CLICK THE CORE</strong><small>or press space</small></div>
</div>
<div class="event-ticker">
<span class="event-ticker__label">FIELD LOG</span>
<p id="eventText">The instruments are quiet. The core is listening.</p>
</div>
</section>
<aside class="panel upgrades-panel">
<div class="panel__heading">
<span>CONSTRUCT</span>
<div class="buy-modes" aria-label="Purchase amount">
<button class="active" data-buy-mode="1">1</button>
<button data-buy-mode="10">10</button>
<button data-buy-mode="max">MAX</button>
</div>
</div>
<div class="generator-list" id="generatorList"></div>
<div class="panel__heading upgrade-heading">
<span>BREAKTHROUGHS</span><span id="upgradeCount">0 / 6</span>
</div>
<div class="upgrade-list" id="upgradeList"></div>
<button class="prestige-card locked" id="prestigeButton">
<span class="prestige-card__icon">◈</span>
<span><small>COLLAPSE THE UNIVERSE</small><strong id="prestigeLabel">Requires 50K lifetime stardust</strong></span>
<i>→</i>
</button>
</aside>
</main>
<footer class="footer">
<span>EXPERIMENT <strong>#<span id="experimentNumber">0001</span></strong></span>
<span class="footer__hint">SPACE TO HARVEST · NUMBERS 1–4 TO BUY</span>
<span>LOCAL SAVE <strong id="saveStatus">READY</strong></span>
</footer>
</div>
<aside class="drawer" id="settingsDrawer" aria-hidden="true">
<div class="drawer__backdrop" data-close-drawer></div>
<div class="drawer__panel">
<div class="drawer__header"><div><p class="eyebrow">SYSTEM</p><h2>Lab controls</h2></div><button class="icon-button" data-close-drawer aria-label="Close controls">×</button></div>
<div class="control-row"><div><strong>Sound synthesis</strong><small>Reactive tones and interface sounds</small></div><button class="toggle is-on" id="soundToggle" aria-label="Toggle sound"><i></i></button></div>
<div class="control-row"><div><strong>Reduced motion</strong><small>Calm the anomaly’s visual effects</small></div><button class="toggle" id="motionToggle" aria-label="Toggle reduced motion"><i></i></button></div>
<div class="drawer__actions">
<button class="secondary-button" id="exportButton">EXPORT SAVE</button>
<button class="secondary-button" id="importButton">IMPORT SAVE</button>
<button class="danger-button" id="resetButton">RESET EXPERIMENT</button>
</div>
<p class="drawer__version">PARALLAX FIELD SYSTEM · BUILD 1.0.0</p>
</div>
</aside>
<div class="modal" id="modal" aria-hidden="true">
<div class="modal__backdrop"></div>
<div class="modal__card">
<div class="modal__icon" id="modalIcon">◈</div>
<p class="eyebrow" id="modalEyebrow">SINGULARITY EVENT</p>
<h2 id="modalTitle">Collapse this universe?</h2>
<p id="modalCopy"></p>
<div class="modal__actions">
<button class="secondary-button" id="modalCancel">NOT YET</button>
<button class="primary-button" id="modalConfirm">COLLAPSE</button>
</div>
</div>
</div>
<div class="toast-stack" id="toastStack" aria-live="polite"></div>
<script src="game.js"></script>
</body>
</html>@import url("https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Manrope:wght@400;500;600;700;800&display=swap");
:root {
--bg: #07080d;
--panel: rgba(16, 18, 26, 0.78);
--panel-solid: #10121a;
--line: rgba(255, 255, 255, 0.105);
--line-bright: rgba(255, 255, 255, 0.2);
--text: #f5f4ef;
--muted: #8e909b;
--faint: #5e606c;
--acid: #c7ff4a;
--acid-soft: rgba(199, 255, 74, 0.15);
--violet: #9f86ff;
--cyan: #67e8f9;
--danger: #ff6b76;
--mono: "DM Mono", monospace;
--sans: "Manrope", sans-serif;
}
* { box-sizing: border-box; }
html, body { min-height: 100%; }
body {
margin: 0;
color: var(--text);
background:
radial-gradient(circle at 50% 42%, rgba(88, 73, 160, 0.12), transparent 34%),
var(--bg);
font-family: var(--sans);
overflow-x: hidden;
}
button, input { font: inherit; }
button { color: inherit; }
#starfield {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
z-index: -2;
opacity: 0.7;
}
.noise {
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0.035;
z-index: 20;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 160 160' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E");
}
.hidden { display: none !important; }
.is-hidden { opacity: 0; pointer-events: none; }
.eyebrow {
margin: 0;
color: var(--acid);
font: 500 0.68rem/1.2 var(--mono);
letter-spacing: 0.18em;
}
.intro {
position: fixed;
inset: 0;
z-index: 30;
display: grid;
place-content: center;
justify-items: center;
text-align: center;
overflow: hidden;
background: radial-gradient(circle at 50% 54%, rgba(151, 121, 255, .16), transparent 24%);
transition: opacity .9s ease, visibility .9s;
}
.intro::before,
.intro::after {
content: "";
position: absolute;
width: min(54vw, 700px);
aspect-ratio: 1;
border: 1px solid rgba(255,255,255,.07);
border-radius: 50%;
animation: intro-orbit 20s linear infinite;
}
.intro::after { width: min(38vw, 490px); animation-direction: reverse; animation-duration: 14s; }
.intro.is-exiting { opacity: 0; visibility: hidden; }
.intro__flare {
position: absolute;
top: 50%; left: 50%;
width: 8px; height: 8px;
border-radius: 50%;
background: white;
box-shadow: 0 0 40px 20px rgba(199,255,74,.13), 0 0 160px 70px rgba(159,134,255,.1);
animation: breathe 3.5s ease-in-out infinite;
}
.intro h1 {
z-index: 1;
margin: 22px 0 20px;
font-size: clamp(3rem, 7vw, 6.7rem);
line-height: .92;
letter-spacing: -.065em;
font-weight: 600;
}
.intro__copy {
z-index: 1;
color: #aaaab3;
font-size: clamp(.9rem, 1.4vw, 1.08rem);
line-height: 1.75;
margin: 0 0 38px;
}
.primary-button, .secondary-button, .danger-button {
border: 0;
cursor: pointer;
font: 600 .7rem var(--mono);
letter-spacing: .12em;
transition: transform .2s ease, filter .2s ease, border-color .2s ease;
}
.primary-button {
z-index: 1;
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 34px;
min-width: 245px;
padding: 17px 19px;
background: var(--acid);
color: #10130b;
box-shadow: 0 12px 40px rgba(199, 255, 74, .12);
}
.primary-button:hover, .secondary-button:hover, .danger-button:hover { transform: translateY(-2px); filter: brightness(1.08); }
.primary-button:active, .secondary-button:active, .danger-button:active { transform: translateY(0); }
.text-button {
z-index: 1;
margin-top: 20px;
border: 0;
background: none;
color: var(--muted);
cursor: pointer;
font: .62rem var(--mono);
letter-spacing: .12em;
}
.text-button:hover { color: var(--text); }
.game-shell {
min-height: 100vh;
display: flex;
flex-direction: column;
transition: opacity .8s ease .2s;
}
.topbar {
height: 76px;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: 0 28px;
border-bottom: 1px solid var(--line);
background: rgba(7,8,13,.68);
backdrop-filter: blur(18px);
position: relative;
z-index: 10;
}
.brand {
display: flex;
align-items: center;
gap: 11px;
color: var(--text);
text-decoration: none;
font-size: .9rem;
font-weight: 800;
letter-spacing: .16em;
}
.brand small { color: var(--faint); font: .54rem var(--mono); letter-spacing: .11em; margin-left: 5px; }
.brand__mark { width: 24px; height: 24px; border: 1px solid var(--acid); border-radius: 50%; display: grid; place-items: center; }
.brand__mark i { width: 6px; height: 6px; border-radius: 50%; background: var(--acid); box-shadow: 0 0 10px var(--acid); }
.resource-readout { display: flex; align-items: center; gap: 12px; }
.resource-readout__icon { color: var(--acid); font-size: 1.3rem; filter: drop-shadow(0 0 8px rgba(199,255,74,.65)); }
.resource-readout > div:not(.rate-pill) { display: flex; flex-direction: column; min-width: 96px; }
.resource-readout #dustValue { font: 500 1.25rem var(--mono); letter-spacing: -.04em; }
.resource-readout small { color: var(--faint); font: .55rem var(--mono); letter-spacing: .16em; }
.rate-pill { padding: 7px 9px; border: 1px solid rgba(199,255,74,.2); background: rgba(199,255,74,.06); color: var(--muted); font: .56rem var(--mono); }
.rate-pill span { color: var(--acid); }
.topbar__actions { justify-self: end; display: flex; gap: 8px; }
.icon-button { width: 36px; height: 36px; border: 1px solid var(--line); background: rgba(255,255,255,.025); cursor: pointer; color: var(--muted); }
.icon-button:hover { border-color: var(--line-bright); color: var(--text); }
.sound-on, .sound-off { font: .7rem var(--mono); }
.game-grid {
flex: 1;
min-height: 700px;
display: grid;
grid-template-columns: minmax(250px, 310px) minmax(410px, 1fr) minmax(310px, 375px);
}
.panel {
position: relative;
z-index: 4;
background: var(--panel);
backdrop-filter: blur(18px);
padding: 24px;
}
.mission-panel { border-right: 1px solid var(--line); }
.upgrades-panel { border-left: 1px solid var(--line); overflow-y: auto; max-height: calc(100vh - 112px); }
.panel__heading, .section-label {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--muted);
font: 500 .59rem var(--mono);
letter-spacing: .14em;
}
.status-dot { color: var(--acid); }
.status-dot::before { content: ""; display: inline-block; width: 5px; height: 5px; margin-right: 6px; border-radius: 50%; background: var(--acid); box-shadow: 0 0 8px var(--acid); vertical-align: 1px; animation: blink 1.5s infinite; }
.mission-card { position: relative; margin-top: 23px; padding: 23px 0 25px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.mission-card__number { position: absolute; top: 18px; right: 0; color: rgba(255,255,255,.05); font: 300 3.9rem var(--mono); }
.mission-card h2 { margin: 9px 0 10px; font-size: 1.5rem; letter-spacing: -.04em; }
.mission-card > p:not(.eyebrow) { max-width: 240px; margin: 0 0 22px; color: var(--muted); font-size: .76rem; line-height: 1.65; }
.progress-track { height: 3px; background: rgba(255,255,255,.07); overflow: hidden; }
.progress-track i { display: block; width: 0; height: 100%; background: var(--acid); box-shadow: 0 0 8px var(--acid); transition: width .4s ease; }
.progress-meta { display: flex; justify-content: space-between; margin-top: 8px; color: var(--faint); font: .52rem var(--mono); }
.stat-list { padding: 16px 0; border-bottom: 1px solid var(--line); }
.stat-list div { display: flex; justify-content: space-between; padding: 7px 0; }
.stat-list span { color: var(--faint); font: .54rem var(--mono); letter-spacing: .08em; }
.stat-list strong { font: 500 .64rem var(--mono); }
.milestones { padding-top: 19px; }
.milestone-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 5px; margin-top: 12px; }
.milestone {
aspect-ratio: 1;
border: 1px solid var(--line);
display: grid;
place-items: center;
color: var(--faint);
font: .66rem var(--mono);
cursor: help;
position: relative;
}
.milestone.earned { color: #0d1107; border-color: var(--acid); background: var(--acid); box-shadow: 0 0 18px rgba(199,255,74,.1); }
.milestone::after { content: attr(data-tip); position: absolute; bottom: calc(100% + 8px); left: 50%; width: max-content; max-width: 180px; transform: translate(-50%, 4px); padding: 7px 9px; background: #22242e; color: var(--text); font: .54rem/1.4 var(--mono); opacity: 0; pointer-events: none; transition: .2s; z-index: 20; }
.milestone:hover::after { opacity: 1; transform: translate(-50%, 0); }
.field-note { margin: 20px 0 0; padding: 0 0 0 13px; border-left: 1px solid var(--violet); color: #737581; font: italic .68rem/1.65 var(--sans); }
.reactor-stage {
position: relative;
min-height: 640px;
display: grid;
place-items: center;
overflow: hidden;
background:
linear-gradient(rgba(255,255,255,.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.025) 1px, transparent 1px);
background-size: 72px 72px;
}
.reactor-stage::before {
content: "";
position: absolute;
width: min(68vw, 780px);
aspect-ratio: 1;
border-radius: 50%;
background: radial-gradient(circle, rgba(151,122,255,.14), rgba(67,50,130,.035) 35%, transparent 67%);
animation: breathe 4s ease-in-out infinite;
}
.stage-label { position: absolute; top: 24px; left: 28px; right: 28px; display: flex; justify-content: space-between; color: var(--faint); font: .54rem var(--mono); letter-spacing: .12em; }
.stage-label span:last-child { color: var(--acid); }
.orbit { position: absolute; border: 1px solid rgba(255,255,255,.08); border-radius: 50%; pointer-events: none; animation: spin 24s linear infinite; }
.orbit--outer { width: min(40vw, 530px); aspect-ratio: 1; border-style: dashed; }
.orbit--inner { width: min(30vw, 390px); aspect-ratio: 1; animation-direction: reverse; animation-duration: 15s; }
.orbit i { position: absolute; top: 50%; left: -4px; width: 7px; height: 7px; border-radius: 50%; background: var(--acid); box-shadow: 0 0 12px var(--acid); }
.orbit i:nth-child(2) { top: 12%; left: 75%; background: var(--violet); box-shadow: 0 0 12px var(--violet); }
.orbit i:nth-child(3) { top: 82%; left: 73%; background: var(--cyan); box-shadow: 0 0 12px var(--cyan); }
.pulse-ring { position: absolute; width: min(22vw, 275px); aspect-ratio: 1; border-radius: 50%; border: 1px solid rgba(199,255,74,.2); opacity: 0; pointer-events: none; }
.pulse-ring.fire { animation: ring-out .55s ease-out; }
.core {
position: relative;
width: min(20vw, 240px);
min-width: 180px;
aspect-ratio: 1;
padding: 0;
border: 0;
border-radius: 50%;
background: transparent;
cursor: pointer;
z-index: 2;
-webkit-tap-highlight-color: transparent;
filter: drop-shadow(0 30px 45px rgba(0,0,0,.5));
}
.core__halo { position: absolute; inset: -18%; border-radius: 50%; background: radial-gradient(circle, rgba(199,255,74,.1), transparent 65%); animation: breathe 2.8s ease-in-out infinite; }
.core__surface { position: absolute; inset: 0; border-radius: 50%; overflow: hidden; background: radial-gradient(circle at 35% 28%, #e9ffae 0, #b7e83e 10%, #829e30 35%, #303718 67%, #0f120c 100%); box-shadow: inset -25px -30px 40px rgba(0,0,0,.62), inset 15px 10px 35px rgba(255,255,255,.15), 0 0 0 1px rgba(199,255,74,.35), 0 0 50px rgba(199,255,74,.14); transition: transform .12s ease; }
.core__surface::before, .core__surface::after { content:""; position:absolute; inset: -35%; border-radius: 42%; background: repeating-radial-gradient(ellipse at 40% 50%, transparent 0 13px, rgba(8,10,6,.22) 14px 17px); animation: spin 17s linear infinite; mix-blend-mode: soft-light; }
.core__surface::after { inset: -10%; background: radial-gradient(ellipse at 70% 25%, rgba(255,255,255,.25), transparent 20%), radial-gradient(ellipse at 28% 71%, rgba(0,0,0,.4), transparent 25%); animation-direction: reverse; animation-duration: 12s; }
.core__shine { position: absolute; inset: 3%; border-radius: 50%; box-shadow: inset 7px 7px 12px rgba(255,255,255,.17); }
.core__content { position: absolute; inset: 0; display: grid; place-content: center; justify-items: center; text-shadow: 0 2px 12px rgba(0,0,0,.7); }
.core__content small { font: 500 .56rem var(--mono); letter-spacing: .2em; }
.core__content strong { margin: 4px; font-size: 2.15rem; line-height: 1; }
.core__content em { padding: 4px 8px; border-radius: 20px; background: rgba(0,0,0,.3); font: normal 500 .65rem var(--mono); }
.core:hover .core__surface { transform: scale(1.035); filter: brightness(1.1); }
.core:active .core__surface, .core.is-tapped .core__surface { transform: scale(.94); filter: brightness(1.3); }
.core:focus-visible { outline: 2px solid white; outline-offset: 12px; }
.float-number { position: absolute; z-index: 6; color: var(--acid); font: 600 .75rem var(--mono); pointer-events: none; text-shadow: 0 0 10px rgba(199,255,74,.7); animation: float-away .9s ease-out forwards; }
.spark { position: absolute; z-index: 5; width: 3px; height: 3px; border-radius: 50%; background: var(--acid); pointer-events: none; animation: spark-away .6s ease-out forwards; }
.core-instruction { position: absolute; top: calc(50% + min(15vw, 175px)); display: flex; align-items: center; gap: 10px; color: var(--muted); }
.core-instruction div { display: flex; flex-direction: column; }
.core-instruction strong { font: 500 .59rem var(--mono); letter-spacing: .11em; }
.core-instruction small { margin-top: 3px; color: var(--faint); font: .54rem var(--mono); }
.mouse-icon { width: 19px; height: 27px; border: 1px solid var(--muted); border-radius: 10px; position: relative; }
.mouse-icon i { position: absolute; width: 2px; height: 6px; top: 4px; left: 8px; background: var(--acid); animation: mouse-scroll 1.5s infinite; }
.event-ticker { position: absolute; bottom: 25px; left: 28px; right: 28px; display: flex; align-items: center; gap: 15px; padding-top: 14px; border-top: 1px solid var(--line); }
.event-ticker__label { flex: 0 0 auto; padding: 5px 7px; color: #0e100a; background: var(--acid); font: 600 .5rem var(--mono); }
.event-ticker p { margin: 0; color: var(--muted); font: .58rem/1.5 var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.buy-modes { display: flex; gap: 3px; }
.buy-modes button { border: 1px solid var(--line); padding: 3px 7px; background: none; color: var(--faint); cursor: pointer; font: .5rem var(--mono); }
.buy-modes button.active { color: #0d1107; background: var(--acid); border-color: var(--acid); }
.generator-list { margin: 16px 0 26px; display: grid; gap: 8px; }
.generator {
position: relative;
width: 100%;
min-height: 77px;
display: grid;
grid-template-columns: 40px 1fr auto;
align-items: center;
gap: 12px;
padding: 12px;
border: 1px solid var(--line);
background: rgba(255,255,255,.018);
text-align: left;
cursor: pointer;
overflow: hidden;
transition: border-color .2s ease, background .2s ease, transform .2s ease;
}
.generator::after { content:""; position:absolute; inset: auto 0 0; height: 2px; transform-origin: left; transform: scaleX(var(--afford, 0)); background: rgba(199,255,74,.35); }
.generator:hover:not(:disabled) { border-color: rgba(199,255,74,.4); background: rgba(199,255,74,.04); transform: translateX(-2px); }
.generator:disabled { cursor: not-allowed; opacity: .5; }
.generator.is-locked { opacity: .28; filter: grayscale(1); }
.generator__icon { width: 39px; height: 39px; display: grid; place-items: center; border: 1px solid var(--line); color: var(--acid); font-size: 1rem; }
.generator__main { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.generator__main strong { font-size: .73rem; }
.generator__main small { color: var(--faint); font: .52rem var(--mono); }
.generator__meta { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
.generator__level { color: var(--muted); font: .55rem var(--mono); }
.generator__cost { color: var(--acid); font: .58rem var(--mono); }
.upgrade-heading { padding-top: 20px; border-top: 1px solid var(--line); }
.upgrade-list { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; margin-top: 15px; }
.upgrade {
min-height: 90px;
padding: 11px;
border: 1px solid var(--line);
background: rgba(255,255,255,.018);
text-align: left;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.upgrade:hover:not(:disabled) { border-color: rgba(159,134,255,.5); background: rgba(159,134,255,.05); }
.upgrade:disabled { opacity: .35; cursor: not-allowed; }
.upgrade.is-bought { opacity: .45; border-color: rgba(199,255,74,.25); }
.upgrade strong { font-size: .64rem; }
.upgrade small { color: var(--faint); font: .48rem/1.35 var(--mono); }
.upgrade span { color: var(--violet); font: .55rem var(--mono); }
.prestige-card { width: 100%; margin-top: 18px; padding: 13px; border: 1px solid rgba(159,134,255,.3); background: linear-gradient(100deg, rgba(159,134,255,.12), rgba(159,134,255,.025)); display: grid; grid-template-columns: auto 1fr auto; gap: 12px; align-items: center; text-align: left; cursor: pointer; }
.prestige-card.locked { opacity: .42; cursor: not-allowed; filter: grayscale(.4); }
.prestige-card__icon { font-size: 1.3rem; color: var(--violet); }
.prestige-card span:nth-child(2) { display: flex; flex-direction: column; gap: 3px; }
.prestige-card small { color: var(--violet); font: .51rem var(--mono); letter-spacing: .06em; }
.prestige-card strong { color: var(--muted); font: 500 .5rem var(--mono); }
.prestige-card i { font-style: normal; }
.footer { height: 36px; flex: 0 0 36px; padding: 0 28px; display: flex; justify-content: space-between; align-items: center; border-top: 1px solid var(--line); color: var(--faint); font: .5rem var(--mono); letter-spacing: .1em; }
.footer strong { color: var(--muted); font-weight: 400; }
.drawer { position: fixed; inset: 0; z-index: 40; display: none; visibility: hidden; overflow: hidden; pointer-events: none; }
.drawer.is-open { display: block; visibility: visible; pointer-events: auto; }
.drawer__backdrop { position: absolute; inset: 0; background: rgba(0,0,0,.58); opacity: 0; transition: opacity .3s; }
.drawer__panel { position: absolute; top: 0; right: 0; height: 100%; width: min(92vw, 390px); padding: 30px; background: #11131b; border-left: 1px solid var(--line); transform: translateX(100%); transition: transform .35s cubic-bezier(.2,.8,.2,1); }
.drawer.is-open .drawer__backdrop { opacity: 1; }
.drawer.is-open .drawer__backdrop { animation: backdrop-in .3s ease both; }
.drawer.is-open .drawer__panel { transform: translateX(0); animation: drawer-in .35s cubic-bezier(.2,.8,.2,1) both; }
.drawer__header { display: flex; justify-content: space-between; align-items: flex-start; padding-bottom: 25px; border-bottom: 1px solid var(--line); }
.drawer__header h2 { margin: 7px 0 0; font-size: 1.6rem; }
.control-row { display: flex; align-items: center; justify-content: space-between; padding: 20px 0; border-bottom: 1px solid var(--line); }
.control-row div { display: flex; flex-direction: column; gap: 5px; }
.control-row strong { font-size: .76rem; }
.control-row small { color: var(--faint); font: .52rem var(--mono); }
.toggle { width: 38px; height: 21px; padding: 2px; border: 1px solid var(--line); border-radius: 20px; background: #252832; cursor: pointer; }
.toggle i { display: block; width: 15px; height: 15px; border-radius: 50%; background: var(--muted); transition: .2s; }
.toggle.is-on { background: var(--acid-soft); border-color: rgba(199,255,74,.4); }
.toggle.is-on i { transform: translateX(17px); background: var(--acid); }
.drawer__actions { margin-top: 30px; display: grid; gap: 9px; }
.secondary-button, .danger-button { padding: 13px 15px; border: 1px solid var(--line); background: rgba(255,255,255,.03); }
.danger-button { color: var(--danger); border-color: rgba(255,107,118,.2); }
.drawer__version { position: absolute; bottom: 20px; color: var(--faint); font: .48rem var(--mono); }
.modal { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; visibility: hidden; }
.modal.is-open { visibility: visible; }
.modal__backdrop { position: absolute; inset: 0; background: rgba(3,3,7,.84); backdrop-filter: blur(8px); opacity: 0; transition: opacity .25s; }
.modal.is-open .modal__backdrop { opacity: 1; }
.modal__card { position: relative; width: min(90vw, 470px); padding: 38px; border: 1px solid var(--line-bright); background: radial-gradient(circle at 50% 0, rgba(159,134,255,.14), transparent 42%), #10121a; text-align: center; transform: scale(.94); opacity: 0; transition: .25s; }
.modal.is-open .modal__card { opacity: 1; transform: scale(1); }
.modal__icon { margin-bottom: 18px; color: var(--violet); font-size: 2.5rem; text-shadow: 0 0 30px var(--violet); }
.modal__card h2 { margin: 9px 0 13px; font-size: 1.8rem; }
.modal__card > p:not(.eyebrow) { color: var(--muted); font-size: .78rem; line-height: 1.7; }
.modal__actions { margin-top: 27px; display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }
.modal__actions .primary-button { min-width: 0; justify-content: center; padding: 13px; }
.toast-stack { position: fixed; z-index: 60; right: 20px; bottom: 51px; display: grid; gap: 8px; pointer-events: none; }
.toast { width: min(360px, calc(100vw - 40px)); display: grid; grid-template-columns: auto 1fr; gap: 12px; padding: 14px; background: #181a23; border: 1px solid var(--line-bright); box-shadow: 0 10px 40px rgba(0,0,0,.4); animation: toast-in .35s ease both; }
.toast.is-leaving { animation: toast-out .3s ease both; }
.toast__icon { width: 30px; height: 30px; display: grid; place-items: center; background: var(--acid); color: #0b0e07; }
.toast strong { display: block; font-size: .68rem; }
.toast p { margin: 3px 0 0; color: var(--muted); font: .52rem/1.5 var(--mono); }
body.reduced-motion *, body.reduced-motion *::before, body.reduced-motion *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes breathe { 50% { transform: scale(1.08); opacity: .75; } }
@keyframes blink { 50% { opacity: .3; } }
@keyframes intro-orbit { to { transform: rotate(360deg); } }
@keyframes ring-out { from { transform: scale(.65); opacity: 1; } to { transform: scale(1.65); opacity: 0; } }
@keyframes float-away { from { transform: translate(-50%, 0) scale(.85); opacity: 0; } 20% { opacity: 1; } to { transform: translate(-50%, -80px) scale(1.1); opacity: 0; } }
@keyframes spark-away { to { transform: translate(var(--dx), var(--dy)) scale(0); opacity: 0; } }
@keyframes mouse-scroll { 50% { transform: translateY(5px); opacity: .2; } }
@keyframes toast-in { from { opacity: 0; transform: translateX(20px); } }
@keyframes toast-out { to { opacity: 0; transform: translateX(20px); } }
@keyframes backdrop-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes drawer-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
@media (max-width: 1080px) {
.game-grid { grid-template-columns: 235px minmax(370px, 1fr) 310px; }
.panel { padding: 18px; }
.brand small, .footer__hint { display: none; }
.upgrade-list { grid-template-columns: 1fr; }
}
@media (max-width: 820px) {
.topbar { grid-template-columns: auto 1fr auto; padding: 0 15px; }
.brand > span:not(.brand__mark), .brand small { display: none; }
.resource-readout { justify-self: center; }
.game-grid { display: flex; flex-direction: column; }
.reactor-stage { order: 1; min-height: 570px; }
.mission-panel { order: 2; border: 0; border-top: 1px solid var(--line); }
.upgrades-panel { order: 3; border-left: 0; border-top: 1px solid var(--line); max-height: none; }
.orbit--outer { width: min(85vw, 480px); }
.orbit--inner { width: min(64vw, 350px); }
.core { width: min(45vw, 220px); }
.core-instruction { top: calc(50% + 145px); }
.upgrade-list { grid-template-columns: 1fr 1fr; }
.mission-card > p:not(.eyebrow) { max-width: 80%; }
.milestones { overflow: hidden; }
.footer { padding: 0 15px; }
}
@media (max-width: 500px) {
.topbar { height: 68px; }
.resource-readout__icon { display: none; }
.resource-readout > div:not(.rate-pill) { min-width: 78px; }
.resource-readout #dustValue { font-size: 1rem; }
.rate-pill { padding: 6px; }
.reactor-stage { min-height: 520px; }
.stage-label { left: 16px; right: 16px; }
.event-ticker { left: 16px; right: 16px; }
.core-instruction { top: calc(50% + 128px); }
.upgrade-list { grid-template-columns: 1fr; }
.intro { padding: 25px; }
.intro h1 { font-size: 3.15rem; }
.intro::before { width: 125vw; }
.intro::after { width: 85vw; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; }
}(() => {
"use strict";
const SAVE_KEY = "parallax-pocket-universe-v1";
const VERSION = 1;
const generators = [
{ id: "siphon", name: "Ion Siphon", icon: "⌁", baseCost: 15, rate: 0.6, unlock: 0, copy: "Draws charge from the field" },
{ id: "lattice", name: "Quantum Lattice", icon: "◇", baseCost: 120, rate: 5, unlock: 50, copy: "Multiplies unstable particles" },
{ id: "foundry", name: "Gravity Foundry", icon: "◎", baseCost: 950, rate: 36, unlock: 500, copy: "Folds matter into existence" },
{ id: "heart", name: "Stellar Heart", icon: "✺", baseCost: 7500, rate: 250, unlock: 5000, copy: "A captive star, mostly obedient" },
];
const upgrades = [
{ id: "gloves", name: "Conductive Gloves", copy: "Tap power ×2", cost: 75, unlock: 25 },
{ id: "overflow", name: "Vacuum Overflow", copy: "All output ×1.5", cost: 400, unlock: 150 },
{ id: "needle", name: "Causal Needle", copy: "Tap power ×5", cost: 1500, unlock: 700 },
{ id: "choir", name: "Siphon Choir", copy: "Ion Siphons ×3", cost: 3500, unlock: 1800 },
{ id: "blueprint", name: "Impossible Blueprint", copy: "All output ×2", cost: 12000, unlock: 7000 },
{ id: "afterimage", name: "Temporal Afterimage", copy: "Taps echo 10 sec/sec", cost: 35000, unlock: 22000 },
];
const phases = [
{ goal: 100, eyebrow: "PHASE ONE", title: "Wake the dark", copy: "Tap the anomaly and recover your first traces of stardust.", note: "The anomaly reacts to attention. This is either very good—or extremely bad." },
{ goal: 1000, eyebrow: "PHASE TWO", title: "Teach it hunger", copy: "Construct an autonomous system and make the field feed itself.", note: "It no longer waits for us to touch it. Progress, according to the forms." },
{ goal: 10000, eyebrow: "PHASE THREE", title: "Bend the constants", copy: "Scale the reaction beyond anything the lab was built to contain.", note: "Local gravity now considers our safety guidelines to be optional." },
{ goal: 50000, eyebrow: "PHASE FOUR", title: "Approach the edge", copy: "Push this universe toward collapse—and whatever waits beyond it.", note: "We found an echo of this experiment. The handwriting is mine." },
{ goal: Infinity, eyebrow: "PHASE ∞", title: "Outgrow reality", copy: "Collapse, return, and build faster with every impossible echo.", note: "There was never one universe. There was only practice." },
];
const milestones = [
{ amount: 10, icon: "I", title: "First Light · 10 lifetime" },
{ amount: 100, icon: "II", title: "A Small Hum · 100 lifetime" },
{ amount: 1000, icon: "III", title: "Self-Sustaining · 1K lifetime" },
{ amount: 10000, icon: "IV", title: "Cosmic Industry · 10K lifetime" },
{ amount: 50000, icon: "V", title: "The Event Horizon · 50K lifetime" },
];
const defaultState = () => ({
version: VERSION,
dust: 0,
lifetime: 0,
totalLifetime: 0,
echoes: 0,
experiments: 1,
buildings: Object.fromEntries(generators.map((g) => [g.id, 0])),
upgrades: [],
achievements: [],
runSeconds: 0,
lastSave: Date.now(),
sound: true,
reducedMotion: window.matchMedia("(prefers-reduced-motion: reduce)").matches,
});
let state = defaultState();
let buyMode = 1;
let running = false;
let lastFrame = performance.now();
let lastRender = 0;
let saveTimer = 0;
let audioContext = null;
let modalAction = null;
const $ = (selector) => document.querySelector(selector);
const els = {
intro: $("#intro"), gameShell: $("#gameShell"), beginButton: $("#beginButton"), continueButton: $("#continueButton"),
dust: $("#dustValue"), rate: $("#rateValue"), lifetime: $("#lifetimeValue"), clickPower: $("#clickPowerValue"),
multiplier: $("#multiplierValue"), runTime: $("#runTimeValue"), tapValue: $("#tapValue"), phaseNumber: $("#phaseNumber"),
phaseEyebrow: $("#phaseEyebrow"), phaseTitle: $("#phaseTitle"), phaseCopy: $("#phaseCopy"), phaseProgress: $("#phaseProgress"),
phaseCurrent: $("#phaseCurrent"), phaseGoal: $("#phaseGoal"), fieldNote: $("#fieldNote"), generatorList: $("#generatorList"),
upgradeList: $("#upgradeList"), upgradeCount: $("#upgradeCount"), milestoneGrid: $("#milestoneGrid"), milestoneCount: $("#milestoneCount"),
core: $("#coreButton"), stage: $("#reactorStage"), pulseRing: $(".pulse-ring"), eventText: $("#eventText"),
stability: $("#stabilityLabel"), prestige: $("#prestigeButton"), prestigeLabel: $("#prestigeLabel"), experiment: $("#experimentNumber"),
saveStatus: $("#saveStatus"), soundButton: $("#soundButton"), settingsButton: $("#settingsButton"), drawer: $("#settingsDrawer"),
soundToggle: $("#soundToggle"), motionToggle: $("#motionToggle"), modal: $("#modal"), modalIcon: $("#modalIcon"), modalEyebrow: $("#modalEyebrow"),
modalTitle: $("#modalTitle"), modalCopy: $("#modalCopy"), modalConfirm: $("#modalConfirm"), modalCancel: $("#modalCancel"),
toastStack: $("#toastStack"), starfield: $("#starfield"),
};
function format(value, decimals = 2) {
if (!Number.isFinite(value)) return "∞";
if (value < 1000) {
if (value >= 100) return Math.floor(value).toLocaleString("en-US");
if (value >= 10) return value.toFixed(value % 1 && decimals ? 1 : 0);
return value.toFixed(value % 1 && decimals ? 2 : 0);
}
const units = ["K", "M", "B", "T", "Qa", "Qi", "Sx", "Sp", "Oc"];
const tier = Math.min(Math.floor(Math.log10(value) / 3), units.length);
if (tier > units.length) return value.toExponential(2);
const scaled = value / Math.pow(1000, tier);
return `${scaled >= 100 ? scaled.toFixed(0) : scaled >= 10 ? scaled.toFixed(1) : scaled.toFixed(2)}${units[tier - 1]}`;
}
function formatTime(total) {
const seconds = Math.floor(total % 60).toString().padStart(2, "0");
const minutes = Math.floor((total / 60) % 60).toString().padStart(2, "0");
const hours = Math.floor(total / 3600).toString().padStart(2, "0");
return `${hours}:${minutes}:${seconds}`;
}
function multiplier() {
let value = 1 + state.echoes * 0.15;
if (state.upgrades.includes("overflow")) value *= 1.5;
if (state.upgrades.includes("blueprint")) value *= 2;
return value;
}
function clickPower() {
let value = 1;
if (state.upgrades.includes("gloves")) value *= 2;
if (state.upgrades.includes("needle")) value *= 5;
return value * multiplier();
}
function generatorRate(generator) {
let value = generator.rate * state.buildings[generator.id];
if (generator.id === "siphon" && state.upgrades.includes("choir")) value *= 3;
return value * multiplier();
}
function productionRate() {
const generatorsPerSecond = generators.reduce((sum, generator) => sum + generatorRate(generator), 0);
const tapEcho = state.upgrades.includes("afterimage") ? clickPower() * 10 : 0;
return generatorsPerSecond + tapEcho;
}
function generatorCost(generator, amount = 1) {
const owned = state.buildings[generator.id];
const ratio = 1.15;
return generator.baseCost * Math.pow(ratio, owned) * (Math.pow(ratio, amount) - 1) / (ratio - 1);
}
function maxAffordable(generator) {
const ownedCost = generator.baseCost * Math.pow(1.15, state.buildings[generator.id]);
return Math.max(0, Math.floor(Math.log(1 + state.dust * 0.15 / ownedCost) / Math.log(1.15)));
}
function purchaseAmount(generator) {
return buyMode === "max" ? maxAffordable(generator) : buyMode;
}
function addDust(amount) {
if (!Number.isFinite(amount) || amount <= 0) return;
state.dust += amount;
state.lifetime += amount;
state.totalLifetime += amount;
checkMilestones();
}
function tap(event) {
if (!running) return;
const amount = clickPower();
addDust(amount);
els.core.classList.add("is-tapped");
setTimeout(() => els.core.classList.remove("is-tapped"), 90);
els.pulseRing.classList.remove("fire");
void els.pulseRing.offsetWidth;
els.pulseRing.classList.add("fire");
createTapEffects(event, amount);
tone(155 + Math.min(380, Math.log10(state.lifetime + 1) * 68), .035, "sine", .025);
render();
}
function createTapEffects(event, amount) {
const rect = els.stage.getBoundingClientRect();
const coreRect = els.core.getBoundingClientRect();
const x = event?.clientX ? event.clientX - rect.left : coreRect.left + coreRect.width / 2 - rect.left;
const y = event?.clientY ? event.clientY - rect.top : coreRect.top + coreRect.height / 2 - rect.top;
const number = document.createElement("span");
number.className = "float-number";
number.textContent = `+${format(amount)}`;
number.style.left = `${x}px`;
number.style.top = `${y}px`;
els.stage.append(number);
setTimeout(() => number.remove(), 900);
if (!state.reducedMotion) {
for (let i = 0; i < 7; i++) {
const spark = document.createElement("i");
spark.className = "spark";
const angle = Math.random() * Math.PI * 2;
const distance = 35 + Math.random() * 70;
spark.style.left = `${x}px`;
spark.style.top = `${y}px`;
spark.style.setProperty("--dx", `${Math.cos(angle) * distance}px`);
spark.style.setProperty("--dy", `${Math.sin(angle) * distance}px`);
els.stage.append(spark);
setTimeout(() => spark.remove(), 650);
}
}
}
function buyGenerator(id) {
const generator = generators.find((item) => item.id === id);
if (!generator || state.lifetime < generator.unlock) return;
const amount = purchaseAmount(generator);
if (amount < 1) return tone(80, .07, "sawtooth", .025);
const cost = generatorCost(generator, amount);
if (state.dust + 1e-9 < cost) return tone(80, .07, "sawtooth", .025);
state.dust -= cost;
state.buildings[id] += amount;
tone(230 + generators.indexOf(generator) * 65, .08, "triangle", .035);
logEvent(`${amount} × ${generator.name} brought online. Field output increased.`);
render();
}
function buyUpgrade(id) {
const upgrade = upgrades.find((item) => item.id === id);
if (!upgrade || state.upgrades.includes(id) || state.lifetime < upgrade.unlock || state.dust < upgrade.cost) return;
state.dust -= upgrade.cost;
state.upgrades.push(id);
tone(440, .18, "sine", .04);
setTimeout(() => tone(660, .2, "sine", .03), 90);
toast("BREAKTHROUGH ACQUIRED", `${upgrade.name} · ${upgrade.copy}`, "⌬");
logEvent(`${upgrade.name} verified. The laws of this universe have been amended.`);
render();
saveGame();
}
function checkMilestones() {
milestones.forEach((milestone, index) => {
if (state.lifetime >= milestone.amount && !state.achievements.includes(index)) {
state.achievements.push(index);
toast("THRESHOLD REACHED", milestone.title, milestone.icon);
tone(520 + index * 60, .12, "sine", .035);
}
});
}
function prestigeGain() {
return Math.max(0, Math.floor(Math.sqrt(state.lifetime / 50000)));
}
function requestPrestige() {
const gain = prestigeGain();
if (gain < 1) return;
openModal({
icon: "◈", eyebrow: "SINGULARITY EVENT", title: "Collapse this universe?",
copy: `Everything in this experiment will be consumed. You will carry ${gain} ${gain === 1 ? "Echo" : "Echoes"} forward, permanently increasing all future output by ${gain * 15}%.`,
confirm: "COLLAPSE", action: () => doPrestige(gain),
});
}
function doPrestige(gain) {
const persistent = { echoes: state.echoes + gain, experiments: state.experiments + 1, totalLifetime: state.totalLifetime, sound: state.sound, reducedMotion: state.reducedMotion };
state = { ...defaultState(), ...persistent };
closeModal();
toast("UNIVERSE REBORN", `${gain} ${gain === 1 ? "Echo" : "Echoes"} carried through the singularity.`, "◈");
logEvent("The instruments insist this is the first experiment. They are lying.");
tone(110, .5, "sine", .05);
saveGame();
render();
}
function logEvent(message) {
els.eventText.animate([{ opacity: 0, transform: "translateY(4px)" }, { opacity: 1, transform: "translateY(0)" }], { duration: 300 });
els.eventText.textContent = message;
}
function renderGenerators() {
els.generatorList.innerHTML = generators.map((generator, index) => {
const unlocked = state.lifetime >= generator.unlock;
const amount = purchaseAmount(generator);
const cost = amount > 0 ? generatorCost(generator, amount) : generatorCost(generator, 1);
const affordable = unlocked && amount > 0 && state.dust + 1e-9 >= cost;
const nextCost = generatorCost(generator, 1);
const afford = Math.min(1, state.dust / nextCost);
const price = amount > 0 ? cost : nextCost;
const rate = generatorRate(generator);
const subtitle = unlocked ? `${format(rate)}/sec total · +${format(generator.rate * multiplier())} each` : `Unlocks at ${format(generator.unlock)} lifetime`;
return `<button class="generator ${unlocked ? "" : "is-locked"}" data-generator="${generator.id}" ${affordable ? "" : "disabled"} style="--afford:${afford}">
<span class="generator__icon">${generator.icon}</span>
<span class="generator__main"><strong>${String(index + 1).padStart(2, "0")} · ${generator.name}</strong><small>${subtitle}</small></span>
<span class="generator__meta"><span class="generator__level">LV ${state.buildings[generator.id]}</span><span class="generator__cost">✦ ${format(price)}${amount > 1 ? ` ×${amount}` : ""}</span></span>
</button>`;
}).join("");
}
function renderUpgrades() {
els.upgradeList.innerHTML = upgrades.map((upgrade) => {
const bought = state.upgrades.includes(upgrade.id);
const unlocked = state.lifetime >= upgrade.unlock;
const disabled = bought || !unlocked || state.dust < upgrade.cost;
return `<button class="upgrade ${bought ? "is-bought" : ""}" data-upgrade="${upgrade.id}" ${disabled ? "disabled" : ""}>
<strong>${bought ? "✓ " : ""}${upgrade.name}</strong>
<small>${unlocked ? upgrade.copy : `Reveal at ${format(upgrade.unlock)} lifetime`}</small>
<span>${bought ? "INSTALLED" : `✦ ${format(upgrade.cost)}`}</span>
</button>`;
}).join("");
els.upgradeCount.textContent = `${state.upgrades.length} / ${upgrades.length}`;
}
function renderMilestones() {
els.milestoneGrid.innerHTML = milestones.map((milestone, index) => `<div class="milestone ${state.achievements.includes(index) ? "earned" : ""}" data-tip="${milestone.title}">${state.achievements.includes(index) ? "✓" : milestone.icon}</div>`).join("");
els.milestoneCount.textContent = `${state.achievements.length} / ${milestones.length}`;
}
function render() {
const rate = productionRate();
const click = clickPower();
els.dust.textContent = format(state.dust);
els.rate.textContent = `+${format(rate)}`;
els.lifetime.textContent = format(state.lifetime);
els.clickPower.textContent = `${format(click)} / TAP`;
els.multiplier.textContent = `×${multiplier().toFixed(2)}`;
els.runTime.textContent = formatTime(state.runSeconds);
els.tapValue.textContent = `+${format(click)}`;
els.experiment.textContent = String(state.experiments).padStart(4, "0");
const phaseIndex = phases.findIndex((phase) => state.lifetime < phase.goal);
const index = phaseIndex === -1 ? phases.length - 1 : phaseIndex;
const phase = phases[index];
const previousGoal = index === 0 ? 0 : phases[index - 1].goal;
const progress = phase.goal === Infinity ? 100 : Math.min(100, (state.lifetime - previousGoal) / (phase.goal - previousGoal) * 100);
els.phaseNumber.textContent = index === phases.length - 1 ? "∞" : String(index + 1).padStart(2, "0");
els.phaseEyebrow.textContent = phase.eyebrow;
els.phaseTitle.textContent = phase.title;
els.phaseCopy.textContent = phase.copy;
els.fieldNote.textContent = `“${phase.note}”`;
els.phaseProgress.style.width = `${progress}%`;
els.phaseCurrent.textContent = `${format(state.lifetime)} gathered`;
els.phaseGoal.textContent = phase.goal === Infinity ? "no known limit" : `${format(phase.goal)} target`;
const stability = Math.max(3, 100 - Math.log10(state.lifetime + 1) * 19);
els.stability.textContent = `STABILITY ${stability.toFixed(0)}%`;
els.stability.style.color = stability < 25 ? "var(--danger)" : "var(--acid)";
const gain = prestigeGain();
els.prestige.classList.toggle("locked", gain < 1);
els.prestige.disabled = gain < 1;
els.prestigeLabel.textContent = gain ? `Reset for +${gain} ${gain === 1 ? "Echo" : "Echoes"} · ${gain * 15}% permanent output` : `Requires ${format(Math.max(0, 50000 - state.lifetime))} more lifetime stardust`;
document.body.classList.toggle("reduced-motion", state.reducedMotion);
els.soundToggle.classList.toggle("is-on", state.sound);
els.motionToggle.classList.toggle("is-on", state.reducedMotion);
$(".sound-on").classList.toggle("hidden", !state.sound);
$(".sound-off").classList.toggle("hidden", state.sound);
renderGenerators();
renderUpgrades();
renderMilestones();
}
function loop(now) {
if (!running) return;
const delta = Math.min((now - lastFrame) / 1000, 1);
lastFrame = now;
addDust(productionRate() * delta);
state.runSeconds += delta;
saveTimer += delta;
if (now - lastRender > 120) { render(); lastRender = now; }
if (saveTimer >= 10) { saveGame(); saveTimer = 0; }
requestAnimationFrame(loop);
}
function saveGame(showStatus = true) {
try {
state.lastSave = Date.now();
localStorage.setItem(SAVE_KEY, JSON.stringify(state));
if (showStatus) {
els.saveStatus.textContent = "SAVED";
setTimeout(() => { els.saveStatus.textContent = "READY"; }, 1200);
}
} catch (error) { console.warn("Unable to save Parallax", error); }
}
function loadGame() {
const raw = localStorage.getItem(SAVE_KEY);
if (!raw) return false;
try {
const saved = JSON.parse(raw);
const fresh = defaultState();
state = {
...fresh, ...saved,
buildings: { ...fresh.buildings, ...(saved.buildings || {}) },
upgrades: Array.isArray(saved.upgrades) ? saved.upgrades : [],
achievements: Array.isArray(saved.achievements) ? saved.achievements : [],
};
return true;
} catch (error) { console.warn("Unable to load Parallax", error); return false; }
}
function applyOfflineProgress() {
const elapsed = Math.min(8 * 3600, Math.max(0, (Date.now() - state.lastSave) / 1000));
const amount = productionRate() * elapsed;
if (elapsed > 30 && amount > 0) {
addDust(amount);
setTimeout(() => toast("FIELD ACTIVE WHILE AWAY", `${format(amount)} stardust recovered over ${formatTime(elapsed)}.`, "⌁"), 600);
}
}
function startGame(useSave) {
if (useSave) { loadGame(); applyOfflineProgress(); }
els.intro.classList.add("is-exiting");
els.gameShell.classList.remove("is-hidden");
running = true;
lastFrame = performance.now();
render();
saveGame(false);
requestAnimationFrame(loop);
setTimeout(() => { els.intro.style.display = "none"; }, 950);
tone(110, .4, "sine", .035);
}
function openDrawer() { els.drawer.classList.add("is-open"); els.drawer.setAttribute("aria-hidden", "false"); }
function closeDrawer() { els.drawer.classList.remove("is-open"); els.drawer.setAttribute("aria-hidden", "true"); }
function openModal({ icon, eyebrow, title, copy, confirm, action }) {
els.modalIcon.textContent = icon;
els.modalEyebrow.textContent = eyebrow;
els.modalTitle.textContent = title;
els.modalCopy.textContent = copy;
els.modalConfirm.textContent = confirm;
modalAction = action;
els.modal.classList.add("is-open");
els.modal.setAttribute("aria-hidden", "false");
}
function closeModal() { els.modal.classList.remove("is-open"); els.modal.setAttribute("aria-hidden", "true"); modalAction = null; }
function toast(title, copy, icon = "✦") {
const item = document.createElement("div");
item.className = "toast";
item.innerHTML = `<span class="toast__icon">${icon}</span><div><strong>${title}</strong><p>${copy}</p></div>`;
els.toastStack.append(item);
setTimeout(() => { item.classList.add("is-leaving"); setTimeout(() => item.remove(), 320); }, 4200);
}
function tone(frequency, duration, type = "sine", volume = .03) {
if (!state.sound) return;
try {
audioContext ||= new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gain = audioContext.createGain();
oscillator.type = type;
oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime);
gain.gain.setValueAtTime(0.0001, audioContext.currentTime);
gain.gain.exponentialRampToValueAtTime(volume, audioContext.currentTime + .01);
gain.gain.exponentialRampToValueAtTime(0.0001, audioContext.currentTime + duration);
oscillator.connect(gain).connect(audioContext.destination);
oscillator.start(); oscillator.stop(audioContext.currentTime + duration + .02);
} catch (_) { /* Audio is a non-essential enhancement. */ }
}
function exportSave() {
const data = btoa(unescape(encodeURIComponent(JSON.stringify(state))));
navigator.clipboard.writeText(data).then(() => toast("SAVE COPIED", "Your experiment code is on the clipboard.", "⧉"));
}
function importSave() {
const data = window.prompt("Paste your Parallax experiment code:");
if (!data) return;
try {
const parsed = JSON.parse(decodeURIComponent(escape(atob(data.trim()))));
if (!parsed || typeof parsed.dust !== "number") throw new Error("Invalid save");
localStorage.setItem(SAVE_KEY, JSON.stringify(parsed));
loadGame(); closeDrawer(); render(); saveGame(); toast("EXPERIMENT RESTORED", "The field has returned to its recorded state.", "↺");
} catch (_) { toast("INVALID EXPERIMENT CODE", "That save could not be decoded.", "!"); }
}
function resetGame() {
openModal({ icon: "×", eyebrow: "DESTRUCTIVE ACTION", title: "Erase the experiment?", copy: "This clears all stardust, buildings, upgrades, echoes, and progress. This action cannot be undone.", confirm: "ERASE EVERYTHING", action: () => {
localStorage.removeItem(SAVE_KEY); state = defaultState(); closeModal(); closeDrawer(); render(); saveGame(); logEvent("Experiment initialized. No prior field data found.");
}});
}
function setupStarfield() {
const canvas = els.starfield;
const ctx = canvas.getContext("2d");
let stars = [];
const resize = () => {
const ratio = Math.min(devicePixelRatio || 1, 2);
canvas.width = innerWidth * ratio; canvas.height = innerHeight * ratio;
canvas.style.width = `${innerWidth}px`; canvas.style.height = `${innerHeight}px`;
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
stars = Array.from({ length: Math.floor(innerWidth * innerHeight / 11000) }, () => ({ x: Math.random() * innerWidth, y: Math.random() * innerHeight, r: Math.random() * 1.1 + .15, a: Math.random() * .65 + .12, s: Math.random() * .06 + .01 }));
};
const draw = (time) => {
ctx.clearRect(0, 0, innerWidth, innerHeight);
stars.forEach((star, i) => {
ctx.globalAlpha = star.a * (.72 + Math.sin(time * .0006 + i) * .28);
ctx.fillStyle = i % 11 === 0 ? "#c7ff4a" : i % 7 === 0 ? "#9f86ff" : "#ffffff";
ctx.beginPath(); ctx.arc(star.x, star.y, star.r, 0, Math.PI * 2); ctx.fill();
star.y += state.reducedMotion ? 0 : star.s;
if (star.y > innerHeight) star.y = 0;
});
requestAnimationFrame(draw);
};
resize(); addEventListener("resize", resize); requestAnimationFrame(draw);
}
function bindEvents() {
els.beginButton.addEventListener("click", () => startGame(false));
els.continueButton.addEventListener("click", () => startGame(true));
els.core.addEventListener("click", tap);
els.generatorList.addEventListener("click", (event) => { const button = event.target.closest("[data-generator]"); if (button) buyGenerator(button.dataset.generator); });
els.upgradeList.addEventListener("click", (event) => { const button = event.target.closest("[data-upgrade]"); if (button) buyUpgrade(button.dataset.upgrade); });
document.querySelectorAll("[data-buy-mode]").forEach((button) => button.addEventListener("click", () => {
buyMode = button.dataset.buyMode === "max" ? "max" : Number(button.dataset.buyMode);
document.querySelectorAll("[data-buy-mode]").forEach((item) => item.classList.toggle("active", item === button)); render();
}));
els.prestige.addEventListener("click", requestPrestige);
els.settingsButton.addEventListener("click", openDrawer);
document.querySelectorAll("[data-close-drawer]").forEach((element) => element.addEventListener("click", closeDrawer));
els.soundButton.addEventListener("click", () => { state.sound = !state.sound; tone(280, .08); render(); saveGame(); });
els.soundToggle.addEventListener("click", () => { state.sound = !state.sound; tone(280, .08); render(); saveGame(); });
els.motionToggle.addEventListener("click", () => { state.reducedMotion = !state.reducedMotion; render(); saveGame(); });
$("#exportButton").addEventListener("click", exportSave);
$("#importButton").addEventListener("click", importSave);
$("#resetButton").addEventListener("click", resetGame);
els.modalCancel.addEventListener("click", closeModal);
$(".modal__backdrop").addEventListener("click", closeModal);
els.modalConfirm.addEventListener("click", () => { if (modalAction) modalAction(); });
document.addEventListener("keydown", (event) => {
if (event.code === "Space" && running && !els.modal.classList.contains("is-open") && !els.drawer.classList.contains("is-open")) { event.preventDefault(); tap(); }
if (/^Digit[1-4]$/.test(event.code) && running) buyGenerator(generators[Number(event.code.slice(-1)) - 1].id);
if (event.code === "Escape") { closeDrawer(); closeModal(); }
});
addEventListener("beforeunload", () => saveGame(false));
document.addEventListener("visibilitychange", () => { if (document.hidden) saveGame(false); });
}
const hasSave = Boolean(localStorage.getItem(SAVE_KEY));
els.continueButton.classList.toggle("hidden", !hasSave);
if (hasSave) els.beginButton.querySelector("span:first-child").textContent = "START NEW EXPERIMENT";
setupStarfield();
bindEvents();
})();