Cursor Composer 2.5
CursorMay 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>VOID FORGE — Incremental Reality Engine</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;800&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<canvas id="starfield" aria-hidden="true"></canvas>
<div class="scanlines" aria-hidden="true"></div>
<div class="grain" aria-hidden="true"></div>
<header class="hud">
<div class="hud-brand">
<span class="brand-glyph">◈</span>
<div>
<h1>VOID FORGE</h1>
<p class="tagline">Harvest the fabric of reality</p>
</div>
</div>
<div class="hud-stats">
<div class="stat-block primary">
<span class="stat-label">Void Shards</span>
<span class="stat-value" id="shard-count">0</span>
<span class="stat-rate" id="shard-rate">+0 / sec</span>
</div>
<div class="stat-block">
<span class="stat-label">Paradox</span>
<span class="stat-value paradox" id="paradox-count">0</span>
<span class="stat-rate" id="paradox-bonus">×1.00 global</span>
</div>
</div>
<div class="hud-actions">
<button class="btn ghost" id="save-btn" title="Save game">Save</button>
<button class="btn ghost" id="export-btn" title="Export save">Export</button>
<button class="btn ghost danger" id="reset-btn" title="Hard reset">Reset</button>
</div>
</header>
<main class="layout">
<section class="forge-panel">
<div class="forge-ring ring-outer"></div>
<div class="forge-ring ring-mid"></div>
<div class="forge-ring ring-inner"></div>
<button class="forge-core" id="forge-btn" aria-label="Harvest void shards">
<span class="core-icon">⬡</span>
<span class="core-label">HARVEST</span>
<span class="core-power" id="click-power">+1</span>
</button>
<div class="forge-particles" id="click-particles" aria-hidden="true"></div>
<div class="milestone-track" id="milestone-track"></div>
</section>
<section class="shop-panel">
<nav class="tabs" role="tablist">
<button class="tab active" data-tab="generators" role="tab" aria-selected="true">Generators</button>
<button class="tab" data-tab="upgrades" role="tab">Upgrades</button>
<button class="tab" data-tab="ascension" role="tab">Ascension</button>
</nav>
<div class="tab-content active" id="tab-generators" role="tabpanel">
<p class="panel-desc">Reality tunnels that bleed void energy into your forge.</p>
<div class="item-list" id="generator-list"></div>
</div>
<div class="tab-content" id="tab-upgrades" role="tabpanel">
<p class="panel-desc">Warp the laws of physics. Each upgrade stacks forever.</p>
<div class="item-list" id="upgrade-list"></div>
</div>
<div class="tab-content" id="tab-ascension" role="tabpanel">
<div class="ascension-hero">
<h2>Collapse Reality</h2>
<p>Reset your forge to earn <strong>Paradox Points</strong> — permanent multipliers that transcend time.</p>
<div class="ascension-preview">
<div class="preview-row">
<span>Shards this run</span>
<span id="asc-shards">0</span>
</div>
<div class="preview-row highlight">
<span>Paradox on collapse</span>
<span id="asc-gain">+0</span>
</div>
<div class="preview-row">
<span>Total paradox after</span>
<span id="asc-total">0</span>
</div>
</div>
<button class="btn ascension" id="ascend-btn" disabled>Collapse Reality</button>
<div class="paradox-shop">
<h3>Paradox Upgrades</h3>
<div class="item-list" id="paradox-list"></div>
</div>
</div>
</div>
</section>
</main>
<aside class="event-log" id="event-log" aria-live="polite">
<div class="log-header">Signal Feed</div>
<ul id="log-entries"></ul>
</aside>
<div class="toast-zone" id="toast-zone" aria-live="assertive"></div>
<script src="game.js"></script>
</body>
</html>/* VOID FORGE — retro-futuristic incremental aesthetic */
:root {
--void: #050508;
--void-deep: #0a0a12;
--surface: #12121c;
--surface-raised: #1a1a28;
--border: #2a2a3e;
--border-glow: #3d3d5c;
--shard: #00f0d4;
--shard-dim: #00a896;
--shard-glow: rgba(0, 240, 212, 0.35);
--paradox: #ff6b9d;
--paradox-glow: rgba(255, 107, 157, 0.35);
--amber: #ffb347;
--text: #e8e6f0;
--text-dim: #8a8799;
--font-display: 'Syne', sans-serif;
--font-mono: 'IBM Plex Mono', monospace;
--radius: 4px;
--transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
scroll-behavior: smooth;
}
body {
min-height: 100vh;
font-family: var(--font-mono);
background: var(--void);
color: var(--text);
overflow-x: hidden;
cursor: crosshair;
}
#starfield {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
}
.scanlines {
position: fixed;
inset: 0;
z-index: 1;
pointer-events: none;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 0, 0, 0.08) 2px,
rgba(0, 0, 0, 0.08) 4px
);
opacity: 0.4;
}
.grain {
position: fixed;
inset: 0;
z-index: 2;
pointer-events: none;
opacity: 0.04;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* ── HUD ── */
.hud {
position: relative;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
padding: 1rem 1.5rem;
background: linear-gradient(180deg, rgba(10, 10, 18, 0.95) 0%, rgba(5, 5, 8, 0.85) 100%);
border-bottom: 1px solid var(--border);
backdrop-filter: blur(12px);
flex-wrap: wrap;
}
.hud-brand {
display: flex;
align-items: center;
gap: 0.75rem;
}
.brand-glyph {
font-size: 2rem;
color: var(--shard);
text-shadow: 0 0 20px var(--shard-glow);
animation: pulse-glyph 3s ease-in-out infinite;
}
@keyframes pulse-glyph {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.05); }
}
.hud-brand h1 {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.25rem;
letter-spacing: 0.15em;
line-height: 1.1;
}
.tagline {
font-size: 0.65rem;
color: var(--text-dim);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hud-stats {
display: flex;
gap: 1.5rem;
flex: 1;
justify-content: center;
}
.stat-block {
text-align: center;
padding: 0.5rem 1.25rem;
border: 1px solid var(--border);
background: var(--surface);
min-width: 140px;
}
.stat-block.primary {
border-color: var(--shard-dim);
box-shadow: inset 0 0 30px rgba(0, 240, 212, 0.05);
}
.stat-label {
display: block;
font-size: 0.6rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-dim);
margin-bottom: 0.25rem;
}
.stat-value {
display: block;
font-family: var(--font-display);
font-size: 1.5rem;
font-weight: 800;
color: var(--shard);
text-shadow: 0 0 15px var(--shard-glow);
font-variant-numeric: tabular-nums;
}
.stat-value.paradox {
color: var(--paradox);
text-shadow: 0 0 15px var(--paradox-glow);
}
.stat-rate {
display: block;
font-size: 0.65rem;
color: var(--text-dim);
margin-top: 0.15rem;
}
.hud-actions {
display: flex;
gap: 0.5rem;
}
/* ── Layout ── */
.layout {
position: relative;
z-index: 10;
display: grid;
grid-template-columns: 1fr 420px;
gap: 0;
min-height: calc(100vh - 80px);
}
@media (max-width: 900px) {
.layout {
grid-template-columns: 1fr;
}
}
/* ── Forge Panel ── */
.forge-panel {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 500px;
padding: 2rem;
border-right: 1px solid var(--border);
overflow: hidden;
}
@media (max-width: 900px) {
.forge-panel {
border-right: none;
border-bottom: 1px solid var(--border);
min-height: 380px;
}
}
.forge-ring {
position: absolute;
border-radius: 50%;
border: 1px solid transparent;
pointer-events: none;
}
.ring-outer {
width: 340px;
height: 340px;
border-color: rgba(0, 240, 212, 0.08);
animation: spin-slow 40s linear infinite;
}
.ring-mid {
width: 260px;
height: 260px;
border-color: rgba(255, 107, 157, 0.12);
border-style: dashed;
animation: spin-slow 25s linear infinite reverse;
}
.ring-inner {
width: 180px;
height: 180px;
border-color: rgba(255, 179, 71, 0.15);
animation: spin-slow 15s linear infinite;
}
@keyframes spin-slow {
to { transform: rotate(360deg); }
}
.forge-core {
position: relative;
z-index: 5;
width: 140px;
height: 140px;
border-radius: 50%;
border: 2px solid var(--shard);
background: radial-gradient(circle at 35% 35%, #1a2830 0%, var(--void-deep) 60%, var(--void) 100%);
color: var(--shard);
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.15rem;
transition: transform var(--transition), box-shadow var(--transition), border-color var(--transition);
box-shadow:
0 0 40px var(--shard-glow),
inset 0 0 30px rgba(0, 240, 212, 0.08);
font-family: var(--font-display);
}
.forge-core:hover {
transform: scale(1.05);
box-shadow:
0 0 60px var(--shard-glow),
0 0 100px rgba(0, 240, 212, 0.15),
inset 0 0 40px rgba(0, 240, 212, 0.12);
}
.forge-core:active {
transform: scale(0.92);
border-color: var(--amber);
box-shadow:
0 0 80px rgba(255, 179, 71, 0.4),
inset 0 0 50px rgba(255, 179, 71, 0.15);
}
.forge-core.clicked {
animation: core-flash 0.15s ease-out;
}
@keyframes core-flash {
0% { filter: brightness(2); }
100% { filter: brightness(1); }
}
.core-icon {
font-size: 2rem;
line-height: 1;
}
.core-label {
font-size: 0.55rem;
font-weight: 800;
letter-spacing: 0.2em;
}
.core-power {
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--amber);
opacity: 0.9;
}
.forge-particles {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
}
.float-num {
position: absolute;
font-family: var(--font-display);
font-weight: 800;
font-size: 1.1rem;
color: var(--shard);
text-shadow: 0 0 10px var(--shard-glow);
pointer-events: none;
animation: float-up 0.9s ease-out forwards;
z-index: 20;
}
.float-num.crit {
color: var(--amber);
font-size: 1.4rem;
text-shadow: 0 0 15px rgba(255, 179, 71, 0.6);
}
@keyframes float-up {
0% {
opacity: 1;
transform: translateY(0) scale(1);
}
100% {
opacity: 0;
transform: translateY(-80px) scale(0.6);
}
}
.milestone-track {
position: absolute;
bottom: 1.5rem;
left: 1.5rem;
right: 1.5rem;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: center;
}
.milestone {
font-size: 0.55rem;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 0.25rem 0.5rem;
border: 1px solid var(--border);
color: var(--text-dim);
background: var(--surface);
transition: all var(--transition);
}
.milestone.unlocked {
border-color: var(--shard-dim);
color: var(--shard);
box-shadow: 0 0 10px var(--shard-glow);
}
/* ── Shop Panel ── */
.shop-panel {
display: flex;
flex-direction: column;
background: var(--void-deep);
max-height: calc(100vh - 80px);
overflow: hidden;
}
.tabs {
display: flex;
border-bottom: 1px solid var(--border);
background: var(--surface);
}
.tab {
flex: 1;
padding: 0.85rem 0.5rem;
border: none;
background: transparent;
color: var(--text-dim);
font-family: var(--font-display);
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
cursor: pointer;
transition: all var(--transition);
border-bottom: 2px solid transparent;
}
.tab:hover {
color: var(--text);
background: rgba(255, 255, 255, 0.03);
}
.tab.active {
color: var(--shard);
border-bottom-color: var(--shard);
background: rgba(0, 240, 212, 0.04);
}
.tab-content {
display: none;
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.tab-content.active {
display: block;
}
.panel-desc {
font-size: 0.7rem;
color: var(--text-dim);
margin-bottom: 1rem;
line-height: 1.5;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border);
}
.item-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.shop-item {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 0.75rem;
align-items: center;
padding: 0.75rem;
border: 1px solid var(--border);
background: var(--surface);
transition: all var(--transition);
cursor: default;
}
.shop-item.affordable {
border-color: var(--shard-dim);
cursor: pointer;
}
.shop-item.affordable:hover {
background: var(--surface-raised);
box-shadow: 0 0 20px rgba(0, 240, 212, 0.08);
transform: translateX(2px);
}
.shop-item.owned-max {
opacity: 0.5;
border-color: var(--border);
}
.shop-item.locked {
opacity: 0.35;
filter: grayscale(0.5);
}
.item-icon {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
border: 1px solid var(--border-glow);
background: var(--void-deep);
}
.item-info h4 {
font-family: var(--font-display);
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.05em;
margin-bottom: 0.15rem;
}
.item-info p {
font-size: 0.6rem;
color: var(--text-dim);
line-height: 1.4;
}
.item-meta {
text-align: right;
min-width: 80px;
}
.item-cost {
font-family: var(--font-display);
font-size: 0.75rem;
font-weight: 800;
color: var(--shard);
}
.item-cost.expensive {
color: var(--text-dim);
}
.item-cost.paradox-cost {
color: var(--paradox);
}
.item-owned {
font-size: 0.55rem;
color: var(--text-dim);
margin-top: 0.15rem;
}
/* ── Ascension ── */
.ascension-hero {
text-align: center;
}
.ascension-hero h2 {
font-family: var(--font-display);
font-size: 1.1rem;
font-weight: 800;
letter-spacing: 0.1em;
color: var(--paradox);
margin-bottom: 0.5rem;
}
.ascension-hero > p {
font-size: 0.7rem;
color: var(--text-dim);
line-height: 1.6;
margin-bottom: 1.25rem;
}
.ascension-preview {
border: 1px solid var(--border);
background: var(--surface);
margin-bottom: 1rem;
text-align: left;
}
.preview-row {
display: flex;
justify-content: space-between;
padding: 0.6rem 0.85rem;
font-size: 0.65rem;
border-bottom: 1px solid var(--border);
}
.preview-row:last-child {
border-bottom: none;
}
.preview-row.highlight {
background: rgba(255, 107, 157, 0.06);
color: var(--paradox);
font-weight: 500;
}
.preview-row.highlight span:last-child {
font-family: var(--font-display);
font-weight: 800;
}
.paradox-shop {
margin-top: 1.5rem;
text-align: left;
}
.paradox-shop h3 {
font-family: var(--font-display);
font-size: 0.75rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-dim);
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
}
/* ── Buttons ── */
.btn {
font-family: var(--font-display);
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 0.5rem 1rem;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
cursor: pointer;
transition: all var(--transition);
}
.btn:hover:not(:disabled) {
border-color: var(--shard-dim);
color: var(--shard);
}
.btn.ghost {
background: transparent;
}
.btn.danger:hover:not(:disabled) {
border-color: #ff4444;
color: #ff4444;
}
.btn.ascension {
width: 100%;
padding: 0.85rem;
border-color: var(--paradox);
color: var(--paradox);
background: rgba(255, 107, 157, 0.08);
font-size: 0.75rem;
}
.btn.ascension:hover:not(:disabled) {
background: rgba(255, 107, 157, 0.15);
box-shadow: 0 0 30px var(--paradox-glow);
}
.btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
/* ── Event Log ── */
.event-log {
position: fixed;
bottom: 1rem;
left: 1rem;
z-index: 10;
width: 260px;
max-height: 160px;
background: rgba(10, 10, 18, 0.9);
border: 1px solid var(--border);
backdrop-filter: blur(8px);
overflow: hidden;
pointer-events: none;
}
@media (max-width: 900px) {
.event-log {
display: none;
}
}
.log-header {
font-family: var(--font-display);
font-size: 0.55rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--text-dim);
padding: 0.4rem 0.6rem;
border-bottom: 1px solid var(--border);
}
#log-entries {
list-style: none;
padding: 0.4rem 0.6rem;
overflow-y: auto;
max-height: 120px;
}
#log-entries li {
font-size: 0.55rem;
color: var(--text-dim);
padding: 0.15rem 0;
line-height: 1.4;
animation: log-in 0.3s ease-out;
}
#log-entries li.highlight {
color: var(--shard);
}
@keyframes log-in {
from { opacity: 0; transform: translateX(-8px); }
to { opacity: 1; transform: translateX(0); }
}
/* ── Toasts ── */
.toast-zone {
position: fixed;
top: 5rem;
right: 1rem;
z-index: 100;
display: flex;
flex-direction: column;
gap: 0.5rem;
pointer-events: none;
}
.toast {
padding: 0.6rem 1rem;
border: 1px solid var(--shard-dim);
background: rgba(10, 10, 18, 0.95);
font-size: 0.65rem;
color: var(--shard);
animation: toast-in 0.3s ease-out, toast-out 0.3s ease-in 2.7s forwards;
backdrop-filter: blur(8px);
}
@keyframes toast-in {
from { opacity: 0; transform: translateX(20px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes toast-out {
to { opacity: 0; transform: translateX(20px); }
}
/* ── Scrollbar ── */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: var(--void);
}
::-webkit-scrollbar-thumb {
background: var(--border-glow);
}
::-webkit-scrollbar-thumb:hover {
background: var(--shard-dim);
}/**
* VOID FORGE — Incremental Reality Engine
* Pure HTML / CSS / JS
*/
(function () {
'use strict';
const SAVE_KEY = 'voidForgeSave_v1';
const TICK_MS = 100;
const LOG_MAX = 12;
// ── Game definitions ──
const GENERATORS = [
{ id: 'probe', name: 'Void Probe', icon: '◎', desc: 'A needle piercing the void membrane.', baseCost: 15, baseProd: 0.1, costMult: 1.12 },
{ id: 'rift', name: 'Micro Rift', icon: '◉', desc: 'Tiny tears that leak raw potential.', baseCost: 100, baseProd: 1, costMult: 1.13 },
{ id: 'siphon', name: 'Entropy Siphon', icon: '◐', desc: 'Drains heat death into usable shards.', baseCost: 800, baseProd: 8, costMult: 1.14 },
{ id: 'lens', name: 'Parallax Lens', icon: '◑', desc: 'Focuses distant void storms.', baseCost: 6000, baseProd: 47, costMult: 1.15 },
{ id: 'engine', name: 'Reality Engine', icon: '◈', desc: 'Industrial-scale fabric unweaving.', baseCost: 50000, baseProd: 260, costMult: 1.16 },
{ id: 'cathedral', name: 'Void Cathedral', icon: '⬡', desc: 'A monument to infinite harvest.', baseCost: 4e5, baseProd: 1400, costMult: 1.17 },
{ id: 'singularity', name: 'Null Singularity', icon: '✦', desc: 'Gravity well of pure nothing.', baseCost: 3e6, baseProd: 7800, costMult: 1.18 },
];
const UPGRADES = [
{ id: 'sharp', name: 'Sharp Edge', icon: '▸', desc: 'Double click harvest power.', cost: 50, effect: 'clickMult', value: 2, req: () => true },
{ id: 'gloves', name: 'Void Gloves', icon: '▸', desc: 'Triple click harvest power.', cost: 500, effect: 'clickMult', value: 3, req: (s) => s.upgrades.sharp },
{ id: 'pulse', name: 'Resonant Pulse', icon: '▸', desc: 'All generators produce 2×.', cost: 1000, effect: 'prodMult', value: 2, req: (s) => totalOwned(s) >= 10 },
{ id: 'harmonic', name: 'Harmonic Forge', icon: '▸', desc: 'All generators produce 2×.', cost: 12000, effect: 'prodMult', value: 2, req: (s) => s.upgrades.pulse },
{ id: 'crit', name: 'Quantum Variance', icon: '▸', desc: '10% chance for 10× critical clicks.', cost: 2500, effect: 'crit', value: 0.1, req: (s) => s.totalClicks >= 100 },
{ id: 'overdrive', name: 'Overdrive', icon: '▸', desc: '5% chance generators burst 5× for one tick.', cost: 8000, effect: 'burst', value: 0.05, req: (s) => s.upgrades.pulse },
{ id: 'echo', name: 'Temporal Echo', icon: '▸', desc: 'Auto-clicks once per second at 25% power.', cost: 15000, effect: 'autoClick', value: 0.25, req: (s) => s.lifetimeShards >= 50000 },
{ id: 'cascade', name: 'Shard Cascade', icon: '▸', desc: 'All production 1.5× per owned generator type.', cost: 75000, effect: 'cascade', value: 1.5, req: (s) => typesOwned(s) >= 4 },
];
const PARADOX_UPGRADES = [
{ id: 'memory', name: 'Echo Memory', icon: '◆', desc: 'Start each run with 100 shards.', cost: 1, effect: 'startShards', value: 100 },
{ id: 'anchor', name: 'Reality Anchor', icon: '◆', desc: 'Generators 10% cheaper.', cost: 2, effect: 'costReduction', value: 0.1 },
{ id: 'weight', name: 'Void Weight', icon: '◆', desc: 'Paradox bonus 25% stronger.', cost: 3, effect: 'paradoxBoost', value: 0.25 },
{ id: 'rift2', name: 'Persistent Rift', icon: '◆', desc: 'Keep 1 of each generator on ascension.', cost: 5, effect: 'keepGens', value: 1 },
{ id: 'infinity', name: 'Infinite Loop', icon: '◆', desc: 'All production ×2 permanently.', cost: 10, effect: 'permProdMult', value: 2 },
];
const MILESTONES = [
{ id: 'm1', shards: 100, label: 'First Breach' },
{ id: 'm2', shards: 1000, label: 'Rift Walker' },
{ id: 'm3', shards: 10000, label: 'Void Touched' },
{ id: 'm4', shards: 100000, label: 'Reality Bender' },
{ id: 'm5', shards: 1e6, label: 'Cosmic Smith' },
{ id: 'm6', shards: 1e8, label: 'Architect of Nothing' },
];
// ── State ──
let state = createInitialState();
let lastTick = performance.now();
let autoClickAcc = 0;
function createInitialState() {
return {
shards: 0,
lifetimeShards: 0,
totalClicks: 0,
paradox: 0,
ascensions: 0,
generators: Object.fromEntries(GENERATORS.map(g => [g.id, 0])),
upgrades: Object.fromEntries(UPGRADES.map(u => [u.id, false])),
paradoxUpgrades: Object.fromEntries(PARADOX_UPGRADES.map(p => [p.id, false])),
milestones: Object.fromEntries(MILESTONES.map(m => [m.id, false])),
lastSave: Date.now(),
};
}
// ── Formatting ──
const SUFFIXES = ['', 'K', 'M', 'B', 'T', 'Qa', 'Qi', 'Sx', 'Sp', 'Oc', 'No', 'Dc'];
function formatNum(n, decimals) {
if (!isFinite(n) || n < 0) return '0';
if (n < 1000) {
const d = decimals ?? (n < 10 ? 2 : n < 100 ? 1 : 0);
return n.toFixed(d).replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1');
}
const tier = Math.min(Math.floor(Math.log10(n) / 3), SUFFIXES.length - 1);
const scaled = n / Math.pow(10, tier * 3);
const d = scaled < 10 ? 2 : scaled < 100 ? 1 : 0;
return scaled.toFixed(d) + SUFFIXES[tier];
}
// ── Calculations ──
function totalOwned(s) {
return Object.values(s.generators).reduce((a, b) => a + b, 0);
}
function typesOwned(s) {
return Object.values(s.generators).filter(n => n > 0).length;
}
function getClickPower() {
let power = 1 + Math.floor(totalOwned(state) * 0.1);
if (state.upgrades.sharp) power *= 2;
if (state.upgrades.gloves) power *= 3;
power *= getParadoxMult();
if (state.paradoxUpgrades.infinity) power *= 2;
return power;
}
function getParadoxMult() {
let mult = 1 + state.paradox * 0.1;
if (state.paradoxUpgrades.weight) mult *= 1.25;
return mult;
}
function getProdMult() {
let mult = getParadoxMult();
if (state.upgrades.pulse) mult *= 2;
if (state.upgrades.harmonic) mult *= 2;
if (state.upgrades.cascade) {
mult *= Math.pow(1.5, typesOwned(state));
}
if (state.paradoxUpgrades.infinity) mult *= 2;
return mult;
}
function genCost(gen, owned) {
let cost = gen.baseCost * Math.pow(gen.costMult, owned);
if (state.paradoxUpgrades.anchor) cost *= 0.9;
return Math.ceil(cost);
}
function getProduction() {
let total = 0;
for (const gen of GENERATORS) {
const owned = state.generators[gen.id];
if (owned > 0) {
total += gen.baseProd * owned;
}
}
return total * getProdMult();
}
function calcParadoxGain() {
const s = state.lifetimeShards;
if (s < 1000) return 0;
return Math.floor(Math.sqrt(s / 1000));
}
// ── DOM refs ──
const $ = (sel) => document.querySelector(sel);
const shardCountEl = $('#shard-count');
const shardRateEl = $('#shard-rate');
const paradoxCountEl = $('#paradox-count');
const paradoxBonusEl = $('#paradox-bonus');
const clickPowerEl = $('#click-power');
const forgeBtn = $('#forge-btn');
const clickParticles = $('#click-particles');
const generatorList = $('#generator-list');
const upgradeList = $('#upgrade-list');
const paradoxList = $('#paradox-list');
const milestoneTrack = $('#milestone-track');
const logEntries = $('#log-entries');
const toastZone = $('#toast-zone');
// ── UI updates ──
function renderHUD() {
shardCountEl.textContent = formatNum(state.shards);
shardRateEl.textContent = '+' + formatNum(getProduction()) + ' / sec';
paradoxCountEl.textContent = formatNum(state.paradox, 0);
paradoxBonusEl.textContent = '×' + formatNum(getParadoxMult(), 2) + ' global';
clickPowerEl.textContent = '+' + formatNum(getClickPower());
}
function renderGenerators() {
generatorList.innerHTML = GENERATORS.map(gen => {
const owned = state.generators[gen.id];
const cost = genCost(gen, owned);
const affordable = state.shards >= cost;
const prod = gen.baseProd * getProdMult();
return `
<div class="shop-item ${affordable ? 'affordable' : ''}" data-buy-gen="${gen.id}">
<div class="item-icon">${gen.icon}</div>
<div class="item-info">
<h4>${gen.name}</h4>
<p>${gen.desc} · +${formatNum(prod)}/s each</p>
</div>
<div class="item-meta">
<div class="item-cost ${affordable ? '' : 'expensive'}">${formatNum(cost)}</div>
<div class="item-owned">Owned: ${owned}</div>
</div>
</div>`;
}).join('');
}
function renderUpgrades() {
upgradeList.innerHTML = UPGRADES.map(up => {
const owned = state.upgrades[up.id];
const unlocked = up.req(state);
const affordable = !owned && unlocked && state.shards >= up.cost;
let cls = 'shop-item';
if (owned) cls += ' owned-max';
else if (!unlocked) cls += ' locked';
else if (affordable) cls += ' affordable';
return `
<div class="${cls}" data-buy-up="${owned ? '' : up.id}">
<div class="item-icon">${up.icon}</div>
<div class="item-info">
<h4>${up.name}${owned ? ' ✓' : ''}</h4>
<p>${up.desc}</p>
</div>
<div class="item-meta">
<div class="item-cost ${affordable ? '' : 'expensive'}">${owned ? '—' : formatNum(up.cost)}</div>
</div>
</div>`;
}).join('');
}
function renderParadoxShop() {
paradoxList.innerHTML = PARADOX_UPGRADES.map(pu => {
const owned = state.paradoxUpgrades[pu.id];
const affordable = !owned && state.paradox >= pu.cost;
let cls = 'shop-item';
if (owned) cls += ' owned-max';
else if (affordable) cls += ' affordable';
return `
<div class="${cls}" data-buy-px="${owned ? '' : pu.id}">
<div class="item-icon">${pu.icon}</div>
<div class="item-info">
<h4>${pu.name}${owned ? ' ✓' : ''}</h4>
<p>${pu.desc}</p>
</div>
<div class="item-meta">
<div class="item-cost paradox-cost ${affordable ? '' : 'expensive'}">${owned ? '—' : pu.cost + ' PP'}</div>
</div>
</div>`;
}).join('');
}
function renderAscension() {
const gain = calcParadoxGain();
$('#asc-shards').textContent = formatNum(state.lifetimeShards);
$('#asc-gain').textContent = '+' + gain;
$('#asc-total').textContent = formatNum(state.paradox + gain, 0);
$('#ascend-btn').disabled = gain < 1;
}
function renderMilestones() {
milestoneTrack.innerHTML = MILESTONES.map(m => {
const unlocked = state.milestones[m.id] || state.lifetimeShards >= m.shards;
return `<span class="milestone ${unlocked ? 'unlocked' : ''}" title="${formatNum(m.shards)} shards">${m.label}</span>`;
}).join('');
}
function renderAll() {
renderHUD();
renderGenerators();
renderUpgrades();
renderParadoxShop();
renderAscension();
renderMilestones();
}
// ── Actions ──
function addShards(amount, silent) {
state.shards += amount;
state.lifetimeShards += amount;
checkMilestones();
if (!silent) renderAll();
}
function doClick(x, y) {
let power = getClickPower();
let crit = false;
if (state.upgrades.crit && Math.random() < 0.1) {
power *= 10;
crit = true;
}
state.totalClicks++;
addShards(power, 'click');
spawnFloatNum(power, x, y, crit);
forgeBtn.classList.add('clicked');
setTimeout(() => forgeBtn.classList.remove('clicked'), 150);
}
function buyGenerator(id) {
const gen = GENERATORS.find(g => g.id === id);
if (!gen) return;
const owned = state.generators[id];
const cost = genCost(gen, owned);
if (state.shards < cost) return;
state.shards -= cost;
state.generators[id]++;
log(`Acquired ${gen.name}`, true);
renderAll();
}
function buyUpgrade(id) {
const up = UPGRADES.find(u => u.id === id);
if (!up || state.upgrades[id] || !up.req(state) || state.shards < up.cost) return;
state.shards -= up.cost;
state.upgrades[id] = true;
toast(`Upgrade unlocked: ${up.name}`);
log(`Upgrade: ${up.name}`, true);
renderAll();
}
function buyParadoxUpgrade(id) {
const pu = PARADOX_UPGRADES.find(p => p.id === id);
if (!pu || state.paradoxUpgrades[id] || state.paradox < pu.cost) return;
state.paradox -= pu.cost;
state.paradoxUpgrades[id] = true;
toast(`Paradox upgrade: ${pu.name}`);
log(`Paradox: ${pu.name}`, true);
renderAll();
}
function ascend() {
const gain = calcParadoxGain();
if (gain < 1) return;
state.paradox += gain;
state.ascensions++;
const kept = state.paradoxUpgrades.rift2 ? 1 : 0;
const startBonus = state.paradoxUpgrades.memory ? 100 : 0;
state.shards = startBonus;
state.lifetimeShards = startBonus;
state.totalClicks = 0;
state.generators = Object.fromEntries(GENERATORS.map(g => [g.id, kept]));
state.upgrades = Object.fromEntries(UPGRADES.map(u => [u.id, false]));
toast(`Reality collapsed! +${gain} Paradox Points`);
log(`Ascension #${state.ascensions}: +${gain} paradox`, true);
renderAll();
}
function checkMilestones() {
for (const m of MILESTONES) {
if (!state.milestones[m.id] && state.lifetimeShards >= m.shards) {
state.milestones[m.id] = true;
toast(`Milestone: ${m.label}!`);
log(`Milestone unlocked: ${m.label}`, true);
}
}
}
// ── Game loop ──
function tick(dt) {
let production = getProduction() * (dt / 1000);
if (state.upgrades.overdrive && Math.random() < 0.05) {
production *= 5;
}
if (production > 0) {
addShards(production, true);
}
if (state.upgrades.echo) {
autoClickAcc += dt;
while (autoClickAcc >= 1000) {
autoClickAcc -= 1000;
const autoPower = getClickPower() * 0.25;
state.shards += autoPower;
state.lifetimeShards += autoPower;
checkMilestones();
}
}
}
let shopRenderAcc = 0;
function gameLoop(now) {
const dt = Math.min(now - lastTick, 200);
lastTick = now;
tick(dt);
renderHUD();
renderAscension();
shopRenderAcc += dt;
if (shopRenderAcc >= 400) {
shopRenderAcc = 0;
renderGenerators();
renderUpgrades();
renderParadoxShop();
renderMilestones();
}
requestAnimationFrame(gameLoop);
}
// ── Effects ──
function spawnFloatNum(amount, x, y, crit) {
const el = document.createElement('span');
el.className = 'float-num' + (crit ? ' crit' : '');
el.textContent = (crit ? 'CRIT! ' : '+') + formatNum(amount);
const rect = clickParticles.getBoundingClientRect();
el.style.left = (x - rect.left) + 'px';
el.style.top = (y - rect.top) + 'px';
clickParticles.appendChild(el);
setTimeout(() => el.remove(), 900);
}
function toast(msg) {
const el = document.createElement('div');
el.className = 'toast';
el.textContent = msg;
toastZone.appendChild(el);
setTimeout(() => el.remove(), 3000);
}
function log(msg, highlight) {
const li = document.createElement('li');
if (highlight) li.className = 'highlight';
li.textContent = msg;
logEntries.prepend(li);
while (logEntries.children.length > LOG_MAX) {
logEntries.lastChild.remove();
}
}
// ── Starfield canvas ──
function initStarfield() {
const canvas = $('#starfield');
const ctx = canvas.getContext('2d');
let stars = [];
let w, h;
function resize() {
w = canvas.width = window.innerWidth;
h = canvas.height = window.innerHeight;
stars = Array.from({ length: 120 }, () => ({
x: Math.random() * w,
y: Math.random() * h,
r: Math.random() * 1.5 + 0.3,
speed: Math.random() * 0.3 + 0.05,
alpha: Math.random() * 0.5 + 0.2,
}));
}
function draw() {
ctx.clearRect(0, 0, w, h);
for (const s of stars) {
s.y += s.speed;
if (s.y > h) { s.y = 0; s.x = Math.random() * w; }
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(0, 240, 212, ${s.alpha})`;
ctx.fill();
}
requestAnimationFrame(draw);
}
resize();
window.addEventListener('resize', resize);
draw();
}
// ── Save / Load ──
function save() {
state.lastSave = Date.now();
localStorage.setItem(SAVE_KEY, JSON.stringify(state));
toast('Game saved');
}
function load() {
try {
const raw = localStorage.getItem(SAVE_KEY);
if (!raw) return false;
const saved = JSON.parse(raw);
state = { ...createInitialState(), ...saved };
return true;
} catch {
return false;
}
}
function exportSave() {
const encoded = btoa(JSON.stringify(state));
navigator.clipboard?.writeText(encoded).then(() => {
toast('Save copied to clipboard');
}).catch(() => {
prompt('Copy this save code:', encoded);
});
}
function hardReset() {
if (!confirm('Permanently delete ALL progress? This cannot be undone.')) return;
localStorage.removeItem(SAVE_KEY);
state = createInitialState();
if (state.paradoxUpgrades.memory) state.shards = 100;
renderAll();
toast('Game reset');
log('Hard reset performed');
}
// ── Event wiring ──
function initEvents() {
forgeBtn.addEventListener('click', (e) => doClick(e.clientX, e.clientY));
document.addEventListener('keydown', (e) => {
if (e.code === 'Space' && e.target === document.body) {
e.preventDefault();
const rect = forgeBtn.getBoundingClientRect();
doClick(rect.left + rect.width / 2, rect.top + rect.height / 2);
}
});
generatorList.addEventListener('click', (e) => {
const item = e.target.closest('[data-buy-gen]');
if (item) buyGenerator(item.dataset.buyGen);
});
upgradeList.addEventListener('click', (e) => {
const item = e.target.closest('[data-buy-up]');
if (item?.dataset.buyUp) buyUpgrade(item.dataset.buyUp);
});
paradoxList.addEventListener('click', (e) => {
const item = e.target.closest('[data-buy-px]');
if (item?.dataset.buyPx) buyParadoxUpgrade(item.dataset.buyPx);
});
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => {
t.classList.toggle('active', t === tab);
t.setAttribute('aria-selected', t === tab);
});
document.querySelectorAll('.tab-content').forEach(panel => {
panel.classList.toggle('active', panel.id === 'tab-' + tab.dataset.tab);
});
});
});
$('#save-btn').addEventListener('click', save);
$('#export-btn').addEventListener('click', exportSave);
$('#reset-btn').addEventListener('click', hardReset);
$('#ascend-btn').addEventListener('click', ascend);
setInterval(save, 30000);
window.addEventListener('beforeunload', save);
}
// ── Boot ──
function init() {
initStarfield();
const loaded = load();
if (loaded) {
log('Save loaded — welcome back, Architect');
toast('Save loaded');
} else {
log('Void Forge online. Click to harvest.');
}
initEvents();
renderAll();
lastTick = performance.now();
requestAnimationFrame(gameLoop);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();