OpenAI GPT-5.4 (xhigh)
OpenAIMarch 2026
What it wrote
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Singularity Forge</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<canvas id="starfield" aria-hidden="true"></canvas>
<div class="noise" aria-hidden="true"></div>
<main class="game-shell">
<section class="topbar">
<div class="brand">
<span class="eyebrow">Incremental Reactor Control</span>
<h1>Singularity Forge</h1>
</div>
<div class="topbar-actions">
<button id="saveButton" class="ghost-button" type="button">Save</button>
<button id="resetButton" class="ghost-button danger" type="button">Reset</button>
</div>
</section>
<section class="status-strip">
<article class="status-pill accent-cyan">
<span class="label">Flux</span>
<strong id="fluxDisplay">0</strong>
</article>
<article class="status-pill accent-pink">
<span class="label">Flux / sec</span>
<strong id="fluxPerSecondDisplay">0</strong>
</article>
<article class="status-pill accent-gold">
<span class="label">Tap Power</span>
<strong id="tapPowerDisplay">0</strong>
</article>
<article class="status-pill accent-lime">
<span class="label">Singularity Cores</span>
<strong id="coreDisplay">0</strong>
</article>
<article class="status-pill accent-violet">
<span class="label">Lifetime Flux</span>
<strong id="lifetimeFluxDisplay">0</strong>
</article>
</section>
<section class="main-grid">
<section class="reactor-zone">
<div class="reactor-header">
<div>
<span class="section-kicker">Control Core</span>
<h2>Prime Reactor</h2>
</div>
<button id="overdriveButton" class="feature-button" type="button">
<span>Overdrive</span>
<strong id="overdriveStatus">Ready</strong>
</button>
</div>
<div class="reactor-stage">
<button id="reactorButton" class="reactor-button" type="button" aria-label="Harvest flux">
<span class="reactor-rings" aria-hidden="true"></span>
<span class="reactor-core" aria-hidden="true"></span>
<span class="reactor-label">
<strong>Harvest Flux</strong>
<small>Space</small>
</span>
</button>
<div class="reactor-stats">
<article class="mini-panel">
<span>Critical Tap</span>
<strong id="critDisplay">0%</strong>
</article>
<article class="mini-panel">
<span>Overdrive</span>
<strong id="overdriveBonusDisplay">x1.0</strong>
</article>
<article class="mini-panel">
<span>Event Bonus</span>
<strong id="eventBonusDisplay">x1.0</strong>
</article>
</div>
<div id="anomalyLayer" class="anomaly-layer" aria-live="polite"></div>
</div>
<div class="progress-block">
<div class="progress-copy">
<span class="section-kicker">Prestige</span>
<h3>Collapse Threshold</h3>
<p id="prestigeHint">
Reach 120,000 lifetime Flux to extract your first Singularity Core.
</p>
</div>
<div class="progress-meter" aria-hidden="true">
<div id="prestigeBar" class="progress-fill"></div>
</div>
<button id="prestigeButton" class="prestige-button" type="button">
Collapse For <strong id="prestigeGainDisplay">0</strong> Cores
</button>
</div>
</section>
<section class="systems-zone">
<div class="systems-columns">
<section class="panel-column">
<div class="section-heading">
<span class="section-kicker">Production</span>
<h2>Forge Modules</h2>
</div>
<div id="buildingsList" class="card-list"></div>
</section>
<section class="panel-column">
<div class="section-heading">
<span class="section-kicker">Optimization</span>
<h2>Upgrades</h2>
</div>
<div id="upgradesList" class="card-list"></div>
</section>
</div>
</section>
</section>
<section class="bottom-grid">
<section class="subpanel">
<div class="section-heading">
<span class="section-kicker">Milestones</span>
<h2>Achievements</h2>
</div>
<div id="achievementsList" class="achievement-grid"></div>
</section>
<section class="subpanel">
<div class="section-heading">
<span class="section-kicker">Telemetry</span>
<h2>Forge Feed</h2>
</div>
<div id="eventLog" class="event-log"></div>
</section>
</section>
</main>
<template id="buildingTemplate">
<article class="module-card">
<div class="module-copy">
<div class="module-heading">
<h3 class="module-name"></h3>
<span class="module-count"></span>
</div>
<p class="module-description"></p>
<div class="module-meta">
<span class="module-rate"></span>
<span class="module-cost"></span>
</div>
</div>
<button class="buy-button" type="button">Acquire</button>
</article>
</template>
<template id="upgradeTemplate">
<article class="upgrade-card">
<div class="upgrade-copy">
<div class="module-heading">
<h3 class="upgrade-name"></h3>
<span class="upgrade-tag">Upgrade</span>
</div>
<p class="upgrade-description"></p>
<div class="module-meta">
<span class="upgrade-cost"></span>
</div>
</div>
<button class="buy-button" type="button">Install</button>
</article>
</template>
<template id="achievementTemplate">
<article class="achievement-card">
<div class="achievement-glow" aria-hidden="true"></div>
<div class="achievement-copy">
<h3 class="achievement-name"></h3>
<p class="achievement-description"></p>
</div>
</article>
</template>
<script src="./script.js"></script>
</body>
</html>:root {
color-scheme: dark;
--bg-0: #060816;
--bg-1: #0c1430;
--bg-2: #141c47;
--text: #edf2ff;
--muted: #9aa8d6;
--line: rgba(158, 181, 255, 0.18);
--panel: rgba(9, 16, 38, 0.7);
--panel-strong: rgba(13, 22, 52, 0.88);
--cyan: #4ce6ff;
--pink: #ff62b0;
--gold: #ffd36b;
--lime: #9bf94d;
--violet: #9c7bff;
--red: #ff5a7f;
--shadow: 0 24px 80px rgba(4, 8, 20, 0.55);
}
* {
box-sizing: border-box;
}
html,
body {
min-height: 100%;
}
body {
margin: 0;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
background:
radial-gradient(circle at top left, rgba(76, 230, 255, 0.16), transparent 32%),
radial-gradient(circle at top right, rgba(255, 98, 176, 0.14), transparent 28%),
linear-gradient(180deg, var(--bg-0), var(--bg-1) 45%, #090d1f);
color: var(--text);
overflow-x: hidden;
}
button,
input,
textarea,
select {
font: inherit;
}
#starfield,
.noise {
inset: 0;
pointer-events: none;
position: fixed;
}
#starfield {
z-index: 0;
}
.noise {
z-index: 1;
opacity: 0.07;
background-image:
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
background-size: 5px 5px, 5px 5px;
mix-blend-mode: soft-light;
}
.game-shell {
position: relative;
z-index: 2;
width: min(1440px, calc(100vw - 32px));
margin: 0 auto;
padding: 28px 0 36px;
}
.topbar,
.status-strip,
.main-grid,
.bottom-grid {
width: 100%;
}
.topbar {
display: flex;
align-items: end;
justify-content: space-between;
gap: 16px;
padding-bottom: 20px;
}
.brand h1,
.section-heading h2,
.reactor-header h2,
.progress-copy h3 {
margin: 0;
letter-spacing: 0;
}
.brand h1 {
font-size: clamp(2.2rem, 4vw, 3.6rem);
line-height: 0.96;
}
.eyebrow,
.section-kicker {
display: inline-flex;
margin-bottom: 10px;
color: var(--cyan);
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.topbar-actions {
display: flex;
gap: 10px;
}
.ghost-button,
.feature-button,
.prestige-button,
.buy-button {
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.05);
color: var(--text);
transition:
transform 140ms ease,
border-color 140ms ease,
background 140ms ease,
box-shadow 140ms ease;
}
.ghost-button,
.feature-button,
.prestige-button {
min-height: 48px;
padding: 0 16px;
border-radius: 14px;
}
.ghost-button:hover,
.feature-button:hover,
.prestige-button:hover,
.buy-button:hover {
transform: translateY(-1px);
border-color: rgba(255, 255, 255, 0.35);
}
.ghost-button.danger:hover {
border-color: rgba(255, 90, 127, 0.6);
background: rgba(255, 90, 127, 0.12);
}
.status-strip {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
}
.status-pill,
.subpanel,
.reactor-zone,
.systems-zone {
border: 1px solid var(--line);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.02));
box-shadow: var(--shadow);
backdrop-filter: blur(16px);
}
.status-pill {
min-height: 88px;
padding: 16px 18px;
border-radius: 18px;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
position: relative;
}
.status-pill::after {
content: "";
position: absolute;
inset: auto -12% -30% auto;
width: 94px;
height: 94px;
border-radius: 50%;
opacity: 0.22;
filter: blur(18px);
}
.status-pill strong {
font-size: clamp(1.05rem, 2vw, 1.5rem);
}
.label {
display: block;
margin-bottom: 8px;
color: var(--muted);
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.accent-cyan::after {
background: var(--cyan);
}
.accent-pink::after {
background: var(--pink);
}
.accent-gold::after {
background: var(--gold);
}
.accent-lime::after {
background: var(--lime);
}
.accent-violet::after {
background: var(--violet);
}
.main-grid {
display: grid;
grid-template-columns: minmax(420px, 0.95fr) minmax(500px, 1.15fr);
gap: 18px;
margin-top: 18px;
}
.reactor-zone,
.systems-zone,
.subpanel {
border-radius: 24px;
}
.reactor-zone {
padding: 22px;
display: grid;
grid-template-rows: auto auto auto;
gap: 20px;
}
.reactor-header,
.progress-block,
.section-heading,
.module-heading,
.module-meta,
.systems-columns,
.bottom-grid {
display: flex;
}
.reactor-header {
align-items: start;
justify-content: space-between;
gap: 14px;
}
.feature-button {
display: inline-flex;
flex-direction: column;
align-items: start;
justify-content: center;
min-width: 130px;
background:
linear-gradient(135deg, rgba(76, 230, 255, 0.14), rgba(156, 123, 255, 0.08)),
rgba(255, 255, 255, 0.04);
}
.feature-button strong {
font-size: 0.86rem;
}
.reactor-stage {
position: relative;
min-height: 520px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 24px;
background:
radial-gradient(circle at center, rgba(76, 230, 255, 0.18), transparent 24%),
radial-gradient(circle at center, rgba(156, 123, 255, 0.16), transparent 40%),
linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
overflow: hidden;
display: grid;
place-items: center;
}
.reactor-stage::before {
content: "";
position: absolute;
inset: 12%;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 50%;
}
.reactor-stage::after {
content: "";
position: absolute;
inset: 20%;
border: 1px dashed rgba(255, 255, 255, 0.12);
border-radius: 50%;
}
.reactor-button {
position: relative;
width: min(320px, 60vw);
aspect-ratio: 1;
border: 0;
background: transparent;
cursor: pointer;
display: grid;
place-items: center;
isolation: isolate;
}
.reactor-button:focus-visible {
outline: 2px solid rgba(76, 230, 255, 0.6);
outline-offset: 10px;
}
.reactor-rings,
.reactor-core {
position: absolute;
border-radius: 50%;
}
.reactor-rings {
inset: 0;
background:
radial-gradient(circle, transparent 44%, rgba(255, 255, 255, 0.24) 44.5%, transparent 46%),
radial-gradient(circle, transparent 57%, rgba(255, 98, 176, 0.26) 57.5%, transparent 59%),
radial-gradient(circle, transparent 69%, rgba(76, 230, 255, 0.28) 69.5%, transparent 71%);
filter: drop-shadow(0 0 30px rgba(76, 230, 255, 0.32));
animation: spin 16s linear infinite;
}
.reactor-core {
inset: 23%;
background:
radial-gradient(circle at 35% 35%, rgba(255, 255, 255, 0.95), rgba(76, 230, 255, 0.95) 18%, rgba(124, 116, 255, 0.86) 55%, rgba(255, 98, 176, 0.86) 100%);
box-shadow:
0 0 40px rgba(76, 230, 255, 0.45),
0 0 90px rgba(156, 123, 255, 0.24),
inset 0 0 40px rgba(255, 255, 255, 0.55);
animation: pulse 3.2s ease-in-out infinite;
}
.reactor-label {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
justify-content: center;
text-shadow: 0 2px 18px rgba(0, 0, 0, 0.45);
}
.reactor-label strong {
font-size: clamp(1.1rem, 2vw, 1.45rem);
}
.reactor-label small {
display: inline-flex;
min-width: 52px;
min-height: 28px;
align-items: center;
justify-content: center;
padding: 0 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.12);
font-size: 0.76rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.reactor-stats {
position: absolute;
left: 18px;
right: 18px;
bottom: 18px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.mini-panel {
min-height: 72px;
padding: 12px 14px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.09);
background: rgba(8, 14, 34, 0.72);
display: flex;
flex-direction: column;
justify-content: center;
}
.mini-panel span {
font-size: 0.74rem;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.mini-panel strong {
margin-top: 6px;
font-size: 1rem;
}
.anomaly-layer {
position: absolute;
inset: 0;
}
.anomaly {
position: absolute;
width: 58px;
aspect-ratio: 1;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.24);
background:
radial-gradient(circle at 35% 35%, rgba(255, 255, 255, 0.92), rgba(255, 211, 107, 0.95) 24%, rgba(255, 98, 176, 0.7) 58%, rgba(76, 230, 255, 0.2) 100%);
box-shadow:
0 0 25px rgba(255, 211, 107, 0.5),
inset 0 0 16px rgba(255, 255, 255, 0.55);
cursor: pointer;
animation:
bob 2.6s ease-in-out infinite,
blink 1.8s ease-in-out infinite;
}
.anomaly::after {
content: "";
position: absolute;
inset: -12px;
border-radius: 50%;
border: 1px dashed rgba(255, 211, 107, 0.26);
}
.anomaly:hover {
transform: scale(1.06);
}
.floating-text {
position: absolute;
font-weight: 800;
text-shadow: 0 0 18px rgba(0, 0, 0, 0.5);
animation: floatUp 1s ease-out forwards;
pointer-events: none;
}
.progress-block {
flex-direction: column;
gap: 14px;
padding: 18px;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(8, 14, 34, 0.7);
}
.progress-copy p {
margin: 8px 0 0;
color: var(--muted);
line-height: 1.5;
}
.progress-meter {
width: 100%;
height: 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.07);
overflow: hidden;
}
.progress-fill {
height: 100%;
width: 0;
background: linear-gradient(90deg, var(--pink), var(--gold), var(--cyan));
border-radius: 999px;
box-shadow: 0 0 20px rgba(255, 211, 107, 0.35);
}
.prestige-button {
justify-content: center;
background:
linear-gradient(135deg, rgba(255, 98, 176, 0.14), rgba(255, 211, 107, 0.12)),
rgba(255, 255, 255, 0.04);
}
.prestige-button:disabled,
.buy-button:disabled,
.feature-button:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none;
}
.systems-zone {
padding: 22px;
}
.systems-columns {
align-items: start;
gap: 16px;
}
.panel-column {
flex: 1;
min-width: 0;
}
.section-heading {
align-items: end;
justify-content: space-between;
margin-bottom: 14px;
}
.card-list,
.achievement-grid,
.event-log {
display: grid;
gap: 12px;
}
.card-list {
grid-template-columns: 1fr;
}
.module-card,
.upgrade-card,
.achievement-card {
position: relative;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.09);
background: var(--panel);
border-radius: 18px;
}
.module-card,
.upgrade-card {
padding: 14px;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 14px;
}
.module-card.affordable,
.upgrade-card.affordable {
border-color: rgba(76, 230, 255, 0.42);
box-shadow: inset 0 0 0 1px rgba(76, 230, 255, 0.12);
}
.module-card::before,
.upgrade-card::before {
content: "";
position: absolute;
inset: auto auto -20px -20px;
width: 80px;
height: 80px;
border-radius: 50%;
filter: blur(22px);
opacity: 0.24;
}
.module-card::before {
background: var(--cyan);
}
.upgrade-card::before {
background: var(--violet);
}
.module-heading {
align-items: baseline;
justify-content: space-between;
gap: 10px;
}
.module-name,
.upgrade-name,
.achievement-name {
margin: 0;
}
.module-count,
.upgrade-tag {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--gold);
}
.module-description,
.upgrade-description,
.achievement-description {
margin: 8px 0 0;
color: var(--muted);
line-height: 1.45;
}
.module-meta {
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-top: 10px;
color: #d6ddff;
font-size: 0.88rem;
}
.buy-button {
min-width: 112px;
min-height: 46px;
border-radius: 14px;
padding: 0 14px;
font-weight: 700;
}
.bottom-grid {
gap: 18px;
margin-top: 18px;
}
.subpanel {
flex: 1;
padding: 22px;
min-width: 0;
}
.achievement-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.achievement-card {
min-height: 108px;
padding: 16px;
background: rgba(8, 14, 34, 0.72);
}
.achievement-card.unlocked {
border-color: rgba(155, 249, 77, 0.32);
background:
linear-gradient(135deg, rgba(155, 249, 77, 0.08), rgba(255, 211, 107, 0.05)),
rgba(8, 14, 34, 0.72);
}
.achievement-glow {
position: absolute;
top: -20px;
right: -20px;
width: 86px;
height: 86px;
border-radius: 50%;
background: rgba(155, 249, 77, 0.25);
filter: blur(28px);
opacity: 0;
}
.achievement-card.unlocked .achievement-glow {
opacity: 1;
}
.event-log {
max-height: 360px;
overflow: auto;
padding-right: 6px;
}
.event-log-entry {
padding: 12px 14px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
}
.event-log-entry strong {
display: block;
margin-bottom: 6px;
}
.event-log-entry span {
color: var(--muted);
font-size: 0.92rem;
line-height: 1.45;
}
.event-log-entry em {
display: inline-flex;
margin-top: 8px;
color: rgba(255, 255, 255, 0.5);
font-style: normal;
font-size: 0.74rem;
}
.reactor-button.pressed {
transform: scale(0.985);
}
.reactor-stage.overdrive-active .reactor-core {
box-shadow:
0 0 60px rgba(255, 211, 107, 0.62),
0 0 120px rgba(255, 98, 176, 0.3),
inset 0 0 40px rgba(255, 255, 255, 0.6);
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0%,
100% {
transform: scale(0.98);
}
50% {
transform: scale(1.04);
}
}
@keyframes bob {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
@keyframes blink {
0%,
100% {
filter: brightness(1);
}
50% {
filter: brightness(1.15);
}
}
@keyframes floatUp {
0% {
opacity: 0;
transform: translateY(10px) scale(0.96);
}
20% {
opacity: 1;
}
100% {
opacity: 0;
transform: translateY(-48px) scale(1.04);
}
}
@media (max-width: 1180px) {
.status-strip,
.achievement-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.main-grid,
.bottom-grid,
.systems-columns {
display: grid;
}
.main-grid,
.bottom-grid {
grid-template-columns: 1fr;
}
.systems-columns {
gap: 18px;
}
}
@media (max-width: 780px) {
.game-shell {
width: min(100vw - 18px, 1440px);
padding-top: 18px;
}
.topbar,
.reactor-header,
.section-heading {
align-items: start;
flex-direction: column;
}
.topbar-actions {
width: 100%;
}
.ghost-button,
.feature-button,
.prestige-button {
width: 100%;
}
.status-strip,
.achievement-grid,
.reactor-stats {
grid-template-columns: 1fr;
}
.module-card,
.upgrade-card {
grid-template-columns: 1fr;
}
.buy-button {
width: 100%;
}
.reactor-stage {
min-height: 460px;
}
.reactor-button {
width: min(280px, 72vw);
}
.reactor-stats {
position: static;
width: 100%;
}
.reactor-stage {
display: flex;
flex-direction: column;
justify-content: center;
gap: 16px;
padding: 24px 16px 16px;
}
.topbar-actions > * {
flex: 1;
}
}const STORAGE_KEY = "singularity-forge-save-v1";
const LOG_LIMIT = 12;
const buildingDefinitions = [
{
id: "scavengers",
name: "Shard Scavengers",
description: "Autonomous skimmers drag ambient debris into the reactor intake.",
baseCost: 12,
baseRate: 0.7,
costScale: 1.16
},
{
id: "drifters",
name: "Ion Drifters",
description: "Electrostatic harvesters condense unstable plasma into usable Flux.",
baseCost: 90,
baseRate: 5.2,
costScale: 1.18
},
{
id: "lattices",
name: "Prism Lattices",
description: "Refraction grids multiply every pulse that crosses the chamber.",
baseCost: 760,
baseRate: 34,
costScale: 1.2
},
{
id: "gates",
name: "Void Gates",
description: "Micro-rifts siphon raw energy from places reality should not have edges.",
baseCost: 5200,
baseRate: 190,
costScale: 1.22
},
{
id: "engines",
name: "Chrono Engines",
description: "Temporal recursion loops let the forge profit from moments that have not happened yet.",
baseCost: 38000,
baseRate: 1180,
costScale: 1.24
}
];
const upgradeDefinitions = [
{
id: "tap-focus",
name: "Tap Focus",
description: "Manual harvest output +100%.",
cost: 80,
apply: (state) => {
state.multipliers.tap *= 2;
}
},
{
id: "reactor-fins",
name: "Reactor Fins",
description: "All production +35%.",
cost: 260,
apply: (state) => {
state.multipliers.global *= 1.35;
}
},
{
id: "crit-tuning",
name: "Critical Tuning",
description: "Critical tap chance +8%, critical taps hit harder.",
cost: 680,
apply: (state) => {
state.critChance += 0.08;
state.critMultiplier += 0.8;
}
},
{
id: "scavenger-swarm",
name: "Scavenger Swarm",
description: "Shard Scavengers are 3x stronger.",
cost: 900,
apply: (state) => {
state.buildingBoosts.scavengers *= 3;
}
},
{
id: "drifter-array",
name: "Drifter Array",
description: "Ion Drifters and Prism Lattices are 2x stronger.",
cost: 2400,
apply: (state) => {
state.buildingBoosts.drifters *= 2;
state.buildingBoosts.lattices *= 2;
}
},
{
id: "void-discipline",
name: "Void Discipline",
description: "Overdrive lasts longer and grants a stronger multiplier.",
cost: 12500,
apply: (state) => {
state.overdriveDuration += 6;
state.overdriveMultiplier += 0.8;
}
},
{
id: "reactive-shell",
name: "Reactive Shell",
description: "Manual harvest output scales with 6% of your production.",
cost: 48000,
apply: (state) => {
state.tapFromProductionFactor += 0.06;
}
},
{
id: "echo-mines",
name: "Echo Mines",
description: "All production +90%. Event bonuses last longer.",
cost: 160000,
apply: (state) => {
state.multipliers.global *= 1.9;
state.eventDuration += 8;
}
}
];
const achievementDefinitions = [
{
id: "first-tap",
name: "Cold Start",
description: "Harvest Flux for the first time.",
check: (state) => state.flux > 0
},
{
id: "first-module",
name: "Machine Intent",
description: "Acquire your first production module.",
check: (state) => totalBuildings(state) >= 1
},
{
id: "ten-modules",
name: "System Bloom",
description: "Own 10 production modules.",
check: (state) => totalBuildings(state) >= 10
},
{
id: "first-kilo",
name: "Four Digits",
description: "Store 1,000 Flux.",
check: (state) => state.flux >= 1000
},
{
id: "overdrive",
name: "Redline",
description: "Trigger Overdrive.",
check: (state) => state.timesOverdriven >= 1
},
{
id: "first-core",
name: "Collapse Artist",
description: "Earn a Singularity Core.",
check: (state) => state.cores >= 1
},
{
id: "million-lifetime",
name: "Industrial Gravity",
description: "Generate 1,000,000 lifetime Flux.",
check: (state) => state.lifetimeFlux >= 1_000_000
},
{
id: "anomaly-hunter",
name: "Event Horizon",
description: "Capture 12 anomalies.",
check: (state) => state.anomaliesCaught >= 12
}
];
const ui = {
fluxDisplay: document.getElementById("fluxDisplay"),
fluxPerSecondDisplay: document.getElementById("fluxPerSecondDisplay"),
tapPowerDisplay: document.getElementById("tapPowerDisplay"),
coreDisplay: document.getElementById("coreDisplay"),
lifetimeFluxDisplay: document.getElementById("lifetimeFluxDisplay"),
critDisplay: document.getElementById("critDisplay"),
overdriveBonusDisplay: document.getElementById("overdriveBonusDisplay"),
eventBonusDisplay: document.getElementById("eventBonusDisplay"),
overdriveStatus: document.getElementById("overdriveStatus"),
prestigeHint: document.getElementById("prestigeHint"),
prestigeBar: document.getElementById("prestigeBar"),
prestigeButton: document.getElementById("prestigeButton"),
overdriveButton: document.getElementById("overdriveButton"),
reactorButton: document.getElementById("reactorButton"),
reactorStage: document.querySelector(".reactor-stage"),
anomalyLayer: document.getElementById("anomalyLayer"),
buildingsList: document.getElementById("buildingsList"),
upgradesList: document.getElementById("upgradesList"),
achievementsList: document.getElementById("achievementsList"),
eventLog: document.getElementById("eventLog"),
saveButton: document.getElementById("saveButton"),
resetButton: document.getElementById("resetButton"),
prestigeGainDisplay: document.getElementById("prestigeGainDisplay"),
buildingTemplate: document.getElementById("buildingTemplate"),
upgradeTemplate: document.getElementById("upgradeTemplate"),
achievementTemplate: document.getElementById("achievementTemplate")
};
const state = loadState();
let lastFrame = performance.now();
let lastSecondTick = performance.now();
let lastSave = performance.now();
let lastRender = 0;
let nextAnomalyAt = performance.now() + randomBetween(12000, 22000);
let currentAnomaly = null;
let starfieldState = null;
const buildingEls = new Map();
const upgradeEls = new Map();
const achievementEls = new Map();
renderStaticLists();
applyOfflineProgress();
ensureStartingLog();
render();
requestAnimationFrame(loop);
setupCanvas();
window.addEventListener("resize", setupCanvas);
ui.reactorButton.addEventListener("click", () => harvestFlux());
ui.overdriveButton.addEventListener("click", triggerOverdrive);
ui.prestigeButton.addEventListener("click", triggerPrestige);
ui.saveButton.addEventListener("click", () => {
saveState();
pushLog("Manual Save", "State written to local storage.");
render();
});
ui.resetButton.addEventListener("click", () => {
const confirmed = window.confirm("Reset all Singularity Forge progress?");
if (!confirmed) {
return;
}
localStorage.removeItem(STORAGE_KEY);
window.location.reload();
});
window.addEventListener("keydown", (event) => {
if (event.code === "Space" && !event.repeat) {
event.preventDefault();
harvestFlux();
}
});
function renderStaticLists() {
for (const building of buildingDefinitions) {
const fragment = ui.buildingTemplate.content.cloneNode(true);
const root = fragment.querySelector(".module-card");
const name = fragment.querySelector(".module-name");
const count = fragment.querySelector(".module-count");
const description = fragment.querySelector(".module-description");
const rate = fragment.querySelector(".module-rate");
const cost = fragment.querySelector(".module-cost");
const button = fragment.querySelector(".buy-button");
name.textContent = building.name;
description.textContent = building.description;
button.addEventListener("click", () => buyBuilding(building.id));
buildingEls.set(building.id, { root, count, rate, cost, button });
ui.buildingsList.appendChild(fragment);
}
for (const upgrade of upgradeDefinitions) {
const fragment = ui.upgradeTemplate.content.cloneNode(true);
const root = fragment.querySelector(".upgrade-card");
const name = fragment.querySelector(".upgrade-name");
const description = fragment.querySelector(".upgrade-description");
const cost = fragment.querySelector(".upgrade-cost");
const button = fragment.querySelector(".buy-button");
name.textContent = upgrade.name;
description.textContent = upgrade.description;
button.addEventListener("click", () => buyUpgrade(upgrade.id));
upgradeEls.set(upgrade.id, { root, cost, button });
ui.upgradesList.appendChild(fragment);
}
for (const achievement of achievementDefinitions) {
const fragment = ui.achievementTemplate.content.cloneNode(true);
const root = fragment.querySelector(".achievement-card");
const name = fragment.querySelector(".achievement-name");
const description = fragment.querySelector(".achievement-description");
name.textContent = achievement.name;
description.textContent = achievement.description;
achievementEls.set(achievement.id, { root });
ui.achievementsList.appendChild(fragment);
}
}
function createFreshState() {
const buildingCounts = Object.fromEntries(buildingDefinitions.map((building) => [building.id, 0]));
return {
flux: 0,
lifetimeFlux: 0,
cores: 0,
buildingCounts,
purchasedUpgrades: [],
achievements: [],
eventLog: [],
multipliers: {
tap: 1,
global: 1
},
buildingBoosts: {
scavengers: 1,
drifters: 1,
lattices: 1,
gates: 1,
engines: 1
},
critChance: 0.05,
critMultiplier: 2.4,
overdriveMultiplier: 2,
overdriveDuration: 14,
overdriveRemaining: 0,
overdriveCooldown: 0,
eventMultiplier: 1,
eventRemaining: 0,
eventDuration: 12,
tapFromProductionFactor: 0,
anomaliesCaught: 0,
timesOverdriven: 0,
lastTimestamp: Date.now()
};
}
function loadState() {
const fallback = createFreshState();
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) {
return fallback;
}
const parsed = JSON.parse(raw);
const merged = {
...fallback,
...parsed,
buildingCounts: { ...fallback.buildingCounts, ...parsed.buildingCounts },
multipliers: { ...fallback.multipliers, ...parsed.multipliers },
buildingBoosts: { ...fallback.buildingBoosts, ...parsed.buildingBoosts }
};
merged.purchasedUpgrades = Array.isArray(parsed.purchasedUpgrades) ? parsed.purchasedUpgrades : [];
merged.achievements = Array.isArray(parsed.achievements) ? parsed.achievements : [];
merged.eventLog = Array.isArray(parsed.eventLog) ? parsed.eventLog.slice(0, LOG_LIMIT) : [];
reapplyUpgrades(merged);
return merged;
} catch (error) {
console.error("Failed to load save:", error);
return fallback;
}
}
function reapplyUpgrades(targetState) {
targetState.multipliers = { tap: 1, global: 1 };
targetState.buildingBoosts = {
scavengers: 1,
drifters: 1,
lattices: 1,
gates: 1,
engines: 1
};
targetState.critChance = 0.05;
targetState.critMultiplier = 2.4;
targetState.overdriveMultiplier = 2;
targetState.overdriveDuration = 14;
targetState.eventDuration = 12;
targetState.tapFromProductionFactor = 0;
for (const upgradeId of targetState.purchasedUpgrades) {
const definition = upgradeDefinitions.find((entry) => entry.id === upgradeId);
if (definition) {
definition.apply(targetState);
}
}
}
function applyOfflineProgress() {
const now = Date.now();
const elapsed = Math.max(0, Math.min(8 * 60 * 60, (now - state.lastTimestamp) / 1000));
if (elapsed < 5) {
state.lastTimestamp = now;
return;
}
const offlineGain = getFluxPerSecond() * elapsed * 0.75;
if (offlineGain > 0) {
state.flux += offlineGain;
state.lifetimeFlux += offlineGain;
pushLog("Offline Progress", `The forge accumulated ${formatNumber(offlineGain)} Flux while idle.`);
}
state.lastTimestamp = now;
}
function ensureStartingLog() {
if (state.eventLog.length > 0) {
return;
}
pushLog("Prime Reactor", "Core containment is stable and ready for extraction cycles.");
pushLog("Forge Network", "Fabrication lanes synchronized. Module blueprints are standing by.");
}
function loop(timestamp) {
const delta = Math.min(0.1, (timestamp - lastFrame) / 1000);
lastFrame = timestamp;
tickProduction(delta);
tickTimers(delta);
maybeSpawnAnomaly(timestamp);
maybeTriggerAutoEvents(timestamp);
unlockAchievements();
if (timestamp - lastRender > 50) {
render();
lastRender = timestamp;
}
if (timestamp - lastSave > 12000) {
saveState();
lastSave = timestamp;
}
requestAnimationFrame(loop);
}
function tickProduction(delta) {
const produced = getFluxPerSecond() * delta;
if (produced <= 0) {
return;
}
state.flux += produced;
state.lifetimeFlux += produced;
}
function tickTimers(delta) {
if (state.overdriveRemaining > 0) {
state.overdriveRemaining = Math.max(0, state.overdriveRemaining - delta);
}
if (state.overdriveCooldown > 0) {
state.overdriveCooldown = Math.max(0, state.overdriveCooldown - delta);
}
if (state.eventRemaining > 0) {
state.eventRemaining = Math.max(0, state.eventRemaining - delta);
if (state.eventRemaining === 0) {
state.eventMultiplier = 1;
pushLog("Event Window Closed", "Anomaly resonance returned to baseline.");
}
}
}
function maybeSpawnAnomaly(timestamp) {
if (currentAnomaly || timestamp < nextAnomalyAt) {
return;
}
const node = document.createElement("button");
node.className = "anomaly";
node.type = "button";
node.style.left = `${randomBetween(10, 82)}%`;
node.style.top = `${randomBetween(12, 68)}%`;
node.title = "Capture anomaly";
const anomaly = {
node,
expiresAt: timestamp + 8500
};
node.addEventListener("click", () => captureAnomaly(anomaly));
currentAnomaly = anomaly;
ui.anomalyLayer.appendChild(node);
}
function maybeTriggerAutoEvents(timestamp) {
if (timestamp - lastSecondTick < 1000) {
if (currentAnomaly && timestamp > currentAnomaly.expiresAt) {
clearAnomaly();
nextAnomalyAt = timestamp + randomBetween(9000, 17000);
}
return;
}
lastSecondTick = timestamp;
if (currentAnomaly && timestamp > currentAnomaly.expiresAt) {
clearAnomaly();
nextAnomalyAt = timestamp + randomBetween(9000, 17000);
}
if (Math.random() < 0.1) {
const messages = [
"Vacuum tension stabilized across the outer rings.",
"Chrono Engines shaved latency from the harvest loop.",
"Void Gates reported a temporary spike in clean intake.",
"The reactor shell refracted a pulse into auxiliary storage."
];
pushLog("Telemetry", messages[Math.floor(Math.random() * messages.length)]);
}
}
function captureAnomaly(anomaly) {
const burst = Math.max(getTapPower() * 5, getFluxPerSecond() * 18, 60);
state.flux += burst;
state.lifetimeFlux += burst;
state.eventMultiplier = Math.min(4.5, state.eventMultiplier + 0.5);
state.eventRemaining = state.eventDuration;
state.anomaliesCaught += 1;
spawnFloatingText(`+${formatNumber(burst)}`, anomaly.node.offsetLeft + 20, anomaly.node.offsetTop);
pushLog(
"Anomaly Captured",
`Resonance burst delivered ${formatNumber(burst)} Flux. Production surged to ${formatMultiplier(state.eventMultiplier)} for ${Math.round(state.eventRemaining)}s.`
);
clearAnomaly();
nextAnomalyAt = performance.now() + randomBetween(11000, 22000);
}
function clearAnomaly() {
if (!currentAnomaly) {
return;
}
currentAnomaly.node.remove();
currentAnomaly = null;
}
function harvestFlux() {
const tapPower = getTapPower();
const isCrit = Math.random() < state.critChance;
const amount = isCrit ? tapPower * state.critMultiplier : tapPower;
state.flux += amount;
state.lifetimeFlux += amount;
ui.reactorButton.classList.add("pressed");
window.clearTimeout(harvestFlux._timeoutId);
harvestFlux._timeoutId = window.setTimeout(() => {
ui.reactorButton.classList.remove("pressed");
}, 120);
const reactorBounds = ui.reactorStage.getBoundingClientRect();
spawnFloatingText(
`${isCrit ? "CRIT " : ""}+${formatNumber(amount)}`,
reactorBounds.width * 0.5,
reactorBounds.height * 0.45,
isCrit ? "var(--gold)" : "var(--cyan)"
);
if (isCrit) {
pushLog("Critical Tap", `Manual harvest spiked to ${formatNumber(amount)} Flux.`);
}
}
function triggerOverdrive() {
if (state.overdriveCooldown > 0 || state.overdriveRemaining > 0) {
return;
}
state.overdriveRemaining = state.overdriveDuration;
state.overdriveCooldown = 50;
state.timesOverdriven += 1;
pushLog(
"Overdrive Active",
`Prime Reactor output amplified to ${formatMultiplier(1 + state.overdriveMultiplier)} for ${Math.round(state.overdriveDuration)}s.`
);
render();
}
function triggerPrestige() {
const gain = getPrestigeGain();
if (gain <= 0) {
return;
}
const confirmed = window.confirm(`Collapse the forge and gain ${gain} Singularity Core${gain === 1 ? "" : "s"}?`);
if (!confirmed) {
return;
}
const retainedCores = state.cores + gain;
const retainedAchievements = [...state.achievements];
const newState = createFreshState();
newState.cores = retainedCores;
newState.achievements = retainedAchievements;
newState.eventLog = [];
Object.assign(state, newState);
pushLog(
"Singularity Collapse",
`The forge folded inward and re-opened with ${retainedCores} permanent core ${retainedCores === 1 ? "charge" : "charges"}.`
);
saveState();
render();
}
function buyBuilding(buildingId) {
const definition = buildingDefinitions.find((building) => building.id === buildingId);
if (!definition) {
return;
}
const cost = getBuildingCost(buildingId);
if (state.flux < cost) {
return;
}
state.flux -= cost;
state.buildingCounts[buildingId] += 1;
pushLog("Module Acquired", `${definition.name} online. Production climbing.`);
render();
}
function buyUpgrade(upgradeId) {
if (state.purchasedUpgrades.includes(upgradeId)) {
return;
}
const definition = upgradeDefinitions.find((upgrade) => upgrade.id === upgradeId);
if (!definition || state.flux < definition.cost) {
return;
}
state.flux -= definition.cost;
state.purchasedUpgrades.push(upgradeId);
definition.apply(state);
pushLog("Upgrade Installed", `${definition.name} integrated into the control stack.`);
render();
}
function unlockAchievements() {
for (const achievement of achievementDefinitions) {
if (state.achievements.includes(achievement.id)) {
continue;
}
if (!achievement.check(state)) {
continue;
}
state.achievements.push(achievement.id);
pushLog("Achievement Unlocked", achievement.name);
}
}
function getBuildingCost(buildingId) {
const definition = buildingDefinitions.find((building) => building.id === buildingId);
const owned = state.buildingCounts[buildingId] ?? 0;
return definition.baseCost * Math.pow(definition.costScale, owned);
}
function getBuildingRate(buildingId) {
const definition = buildingDefinitions.find((building) => building.id === buildingId);
const owned = state.buildingCounts[buildingId] ?? 0;
const buildingBoost = state.buildingBoosts[buildingId] ?? 1;
return owned * definition.baseRate * buildingBoost;
}
function getCoreMultiplier() {
return 1 + state.cores * 0.18;
}
function getOverdriveMultiplier() {
return state.overdriveRemaining > 0 ? 1 + state.overdriveMultiplier : 1;
}
function getFluxPerSecond() {
const baseProduction = buildingDefinitions.reduce((total, building) => total + getBuildingRate(building.id), 0);
return baseProduction * state.multipliers.global * getCoreMultiplier() * getOverdriveMultiplier() * state.eventMultiplier;
}
function getTapPower() {
const baseTap = 1.2 + state.cores * 0.45 + getFluxPerSecond() * state.tapFromProductionFactor;
return baseTap * state.multipliers.tap * state.eventMultiplier;
}
function getPrestigeRequirement() {
const ownedCores = state.cores;
return 120000 * Math.pow(2.35, ownedCores);
}
function getPrestigeGain() {
const requirement = getPrestigeRequirement();
if (state.lifetimeFlux < requirement) {
return 0;
}
const ratio = state.lifetimeFlux / requirement;
return Math.max(1, Math.floor(Math.pow(ratio, 0.62)));
}
function totalBuildings(targetState) {
return Object.values(targetState.buildingCounts).reduce((sum, count) => sum + count, 0);
}
function pushLog(title, message) {
const stamp = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
state.eventLog.unshift({ title, message, stamp });
state.eventLog = state.eventLog.slice(0, LOG_LIMIT);
}
function saveState() {
state.lastTimestamp = Date.now();
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
function render() {
const fluxPerSecond = getFluxPerSecond();
const tapPower = getTapPower();
const prestigeRequirement = getPrestigeRequirement();
const prestigeGain = getPrestigeGain();
const progress = Math.min(1, state.lifetimeFlux / prestigeRequirement);
ui.fluxDisplay.textContent = formatNumber(state.flux);
ui.fluxPerSecondDisplay.textContent = formatNumber(fluxPerSecond);
ui.tapPowerDisplay.textContent = formatNumber(tapPower);
ui.coreDisplay.textContent = formatNumber(state.cores);
ui.lifetimeFluxDisplay.textContent = formatNumber(state.lifetimeFlux);
ui.critDisplay.textContent = `${(state.critChance * 100).toFixed(0)}%`;
ui.overdriveBonusDisplay.textContent = formatMultiplier(getOverdriveMultiplier());
ui.eventBonusDisplay.textContent = formatMultiplier(state.eventMultiplier);
ui.prestigeGainDisplay.textContent = formatNumber(prestigeGain);
ui.prestigeBar.style.width = `${progress * 100}%`;
ui.prestigeButton.disabled = prestigeGain <= 0;
ui.prestigeHint.textContent =
prestigeGain > 0
? `Collapse now for ${prestigeGain} core${prestigeGain === 1 ? "" : "s"}, or push further to increase the payout.`
: `Reach ${formatNumber(prestigeRequirement)} lifetime Flux for the next collapse.`;
if (state.overdriveRemaining > 0) {
ui.overdriveStatus.textContent = `${state.overdriveRemaining.toFixed(1)}s`;
ui.reactorStage.classList.add("overdrive-active");
} else if (state.overdriveCooldown > 0) {
ui.overdriveStatus.textContent = `${state.overdriveCooldown.toFixed(1)}s`;
ui.reactorStage.classList.remove("overdrive-active");
} else {
ui.overdriveStatus.textContent = "Ready";
ui.reactorStage.classList.remove("overdrive-active");
}
ui.overdriveButton.disabled = state.overdriveCooldown > 0 || state.overdriveRemaining > 0;
for (const building of buildingDefinitions) {
const els = buildingEls.get(building.id);
const count = state.buildingCounts[building.id] ?? 0;
const cost = getBuildingCost(building.id);
const perModule = building.baseRate * (state.buildingBoosts[building.id] ?? 1) * state.multipliers.global * getCoreMultiplier();
els.count.textContent = `${count} online`;
els.rate.textContent = `${formatNumber(perModule)} Flux/s each`;
els.cost.textContent = `${formatNumber(cost)} Flux`;
els.button.disabled = state.flux < cost;
els.root.classList.toggle("affordable", state.flux >= cost);
}
for (const upgrade of upgradeDefinitions) {
const els = upgradeEls.get(upgrade.id);
const purchased = state.purchasedUpgrades.includes(upgrade.id);
els.cost.textContent = purchased ? "Installed" : `${formatNumber(upgrade.cost)} Flux`;
els.button.textContent = purchased ? "Online" : "Install";
els.button.disabled = purchased || state.flux < upgrade.cost;
els.root.classList.toggle("affordable", !purchased && state.flux >= upgrade.cost);
els.root.style.display = purchased ? "none" : "grid";
}
for (const achievement of achievementDefinitions) {
const els = achievementEls.get(achievement.id);
els.root.classList.toggle("unlocked", state.achievements.includes(achievement.id));
}
ui.eventLog.innerHTML = "";
for (const entry of state.eventLog) {
const node = document.createElement("article");
node.className = "event-log-entry";
node.innerHTML = `<strong>${entry.title}</strong><span>${entry.message}</span><em>${entry.stamp}</em>`;
ui.eventLog.appendChild(node);
}
}
function spawnFloatingText(text, x, y, color = "var(--cyan)") {
const node = document.createElement("div");
node.className = "floating-text";
node.textContent = text;
node.style.left = `${x}px`;
node.style.top = `${y}px`;
node.style.color = color;
ui.reactorStage.appendChild(node);
window.setTimeout(() => node.remove(), 1000);
}
function setupCanvas() {
const canvas = document.getElementById("starfield");
const context = canvas.getContext("2d");
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const width = window.innerWidth;
const height = window.innerHeight;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
context.setTransform(dpr, 0, 0, dpr, 0, 0);
const stars = Array.from({ length: Math.floor((width * height) / 14000) }, () => ({
x: Math.random() * width,
y: Math.random() * height,
radius: Math.random() * 1.7 + 0.2,
speed: Math.random() * 0.22 + 0.04,
alpha: Math.random() * 0.7 + 0.15
}));
starfieldState = {
context,
width,
height,
stars
};
if (!setupCanvas._running) {
setupCanvas._running = true;
requestAnimationFrame(drawStarfield);
}
}
function drawStarfield() {
if (!starfieldState) {
requestAnimationFrame(drawStarfield);
return;
}
const { context, width, height, stars } = starfieldState;
context.clearRect(0, 0, width, height);
for (const star of stars) {
star.y += star.speed;
if (star.y > height + 6) {
star.y = -6;
star.x = Math.random() * width;
}
context.beginPath();
context.fillStyle = `rgba(255,255,255,${star.alpha})`;
context.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
context.fill();
}
requestAnimationFrame(drawStarfield);
}
function formatNumber(value) {
if (!Number.isFinite(value)) {
return "0";
}
if (Math.abs(value) < 1000) {
return value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2).replace(/\.0+$/, "").replace(/(\.\d*[1-9])0+$/, "$1");
}
const units = ["K", "M", "B", "T", "Qa", "Qi", "Sx", "Sp"];
let unitIndex = -1;
let scaled = value;
while (scaled >= 1000 && unitIndex < units.length - 1) {
scaled /= 1000;
unitIndex += 1;
}
return `${scaled.toFixed(scaled >= 100 ? 0 : scaled >= 10 ? 1 : 2).replace(/\.0+$/, "").replace(/(\.\d*[1-9])0+$/, "$1")}${units[unitIndex]}`;
}
function formatMultiplier(value) {
return `x${value.toFixed(value >= 10 ? 0 : 1)}`;
}
function randomBetween(min, max) {
return Math.random() * (max - min) + min;
}