Claude Sonnet 5
AnthropicJune 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="#0b0807">
<meta name="description" content="CINDERFORGE — an idle game about kindling a dead star back to life.">
<title>CINDERFORGE // kindle a dead star</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%230b0807'/%3E%3Ccircle cx='50' cy='50' r='32' fill='%23ff6a1a'/%3E%3Ccircle cx='50' cy='50' r='14' fill='%23fff3d6'/%3E%3C/svg%3E">
<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=Unbounded:wght@400;600;800;900&family=Martian+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<noscript><div style="padding:48px;text-align:center;font-family:sans-serif;">CINDERFORGE requires JavaScript to run. Please enable it.</div></noscript>
<canvas id="bg-canvas" aria-hidden="true"></canvas>
<div class="vignette" aria-hidden="true"></div>
<div class="scanlines" aria-hidden="true"></div>
<canvas id="fx-canvas" aria-hidden="true"></canvas>
<div id="toast-stack" class="toast-stack" aria-live="polite"></div>
<div id="offline-modal" class="modal-overlay hidden">
<div class="modal angular-panel">
<h2>WELCOME BACK, FORGEKEEPER</h2>
<p id="offline-text"></p>
<button id="offline-close" class="btn btn-primary">RESUME FORGING</button>
</div>
</div>
<div id="reignite-modal" class="modal-overlay hidden">
<div class="modal angular-panel">
<h2>REIGNITE THE FORGE?</h2>
<p>Collapsing the forge empties your embers, generators and standard upgrades. In return, the husk condenses into <strong>Stellar Seeds</strong> — a permanent power that carries into every cycle to come.</p>
<div class="reignite-stats">
<div><span>Seeds you will gain</span><strong id="reignite-gain">0</strong></div>
<div><span>New multiplier</span><strong id="reignite-newmult">×1.00</strong></div>
</div>
<div class="modal-actions">
<button id="reignite-cancel" class="btn">CANCEL</button>
<button id="reignite-confirm" class="btn btn-danger">COLLAPSE & REIGNITE</button>
</div>
</div>
</div>
<div id="settings-panel" class="modal-overlay hidden">
<div class="modal angular-panel">
<h2>SYSTEM</h2>
<div class="settings-row"><span>Sound</span><button id="toggle-sound" class="toggle">ON</button></div>
<div class="settings-row"><span>Particle FX</span><button id="toggle-particles" class="toggle">ON</button></div>
<div class="settings-row"><span>Save now</span><button id="btn-save" class="btn">SAVE</button></div>
<div class="settings-row"><span>Export save</span><button id="btn-export" class="btn">COPY</button></div>
<div class="settings-row"><span>Import save</span><button id="btn-import" class="btn">PASTE</button></div>
<div class="settings-row"><span>Hard reset</span><button id="btn-reset" class="btn btn-danger">ERASE ALL</button></div>
<button id="settings-close" class="btn btn-primary">CLOSE</button>
</div>
</div>
<div id="app">
<header class="hud-bar">
<div class="brand">
<h1>CINDERFORGE</h1>
<span class="tagline">kindle a dead star</span>
</div>
<div class="gauges">
<div class="gauge gauge-primary">
<span class="gauge-label">EMBERS</span>
<span class="gauge-value" id="stat-embers">0</span>
<span class="gauge-sub" id="stat-eps">+0/s</span>
</div>
<div class="gauge">
<span class="gauge-label">STELLAR SEEDS</span>
<span class="gauge-value" id="stat-seeds">0</span>
<span class="gauge-sub" id="stat-mult">×1.00</span>
</div>
<div class="gauge">
<span class="gauge-label">STRIKE POWER</span>
<span class="gauge-value" id="stat-clickpower">1</span>
<span class="gauge-sub" id="stat-clicks">0 strikes</span>
</div>
</div>
<button id="settings-btn" class="icon-btn" title="System settings">[ SYS ]</button>
</header>
<main class="layout">
<section class="core-panel">
<div class="buy-amount-select" role="group" aria-label="Buy amount">
<button class="buy-amt-btn active" data-amt="1">×1</button>
<button class="buy-amt-btn" data-amt="10">×10</button>
<button class="buy-amt-btn" data-amt="25">×25</button>
<button class="buy-amt-btn" data-amt="max">MAX</button>
</div>
<div class="core-wrap">
<button id="core-btn" class="core" aria-label="Strike the core">
<svg viewBox="0 0 200 200" class="core-svg">
<defs>
<radialGradient id="coreGrad" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#fff7e6"/>
<stop offset="30%" stop-color="#ffc878"/>
<stop offset="65%" stop-color="#ff6a1a"/>
<stop offset="100%" stop-color="#7a1f02" stop-opacity="0"/>
</radialGradient>
</defs>
<g class="ring ring-outer">
<polygon points="195,100 167,167 100,195 33,167 5,100 33,33 100,5 167,33"/>
</g>
<g class="ring ring-mid">
<polygon points="100,32 168,100 100,168 32,100"/>
</g>
<circle class="core-fill" cx="100" cy="100" r="58" fill="url(#coreGrad)"/>
<g class="ring ring-inner">
<polygon points="100,55 139,77.5 139,122.5 100,145 61,122.5 61,77.5"/>
</g>
</svg>
</button>
<p class="core-hint">STRIKE THE CORE</p>
</div>
<div class="run-stats">
<div><span>This cycle</span><strong id="stat-run-earned">0</strong></div>
<div><span>All-time</span><strong id="stat-alltime-earned">0</strong></div>
</div>
</section>
<section class="console-panel angular-panel">
<nav class="tabs" role="tablist">
<button class="tab active" data-tab="generators" role="tab">GENERATORS</button>
<button class="tab" data-tab="upgrades" role="tab">UPGRADES<span class="tab-badge" id="badge-upgrades"></span></button>
<button class="tab" data-tab="achievements" role="tab">FORGE LOGS<span class="tab-badge" id="badge-achievements"></span></button>
<button class="tab" data-tab="prestige" role="tab">REIGNITE</button>
<button class="tab" data-tab="stats" role="tab">STATS</button>
</nav>
<div class="tab-panels">
<div class="tab-panel active" id="panel-generators">
<div class="list" id="list-generators"></div>
</div>
<div class="tab-panel" id="panel-upgrades">
<div class="list" id="list-upgrades"></div>
</div>
<div class="tab-panel" id="panel-achievements">
<div class="grid-achievements" id="list-achievements"></div>
</div>
<div class="tab-panel" id="panel-prestige">
<div class="prestige-panel">
<h3>REIGNITE THE FORGE</h3>
<p class="prestige-desc">When the husk has given enough, collapse it into a seed-star. Each <strong>Stellar Seed</strong> permanently empowers every cycle to come by +2%, forever.</p>
<div class="prestige-stats">
<div class="prestige-stat"><span>Current Seeds</span><strong id="prestige-current-seeds">0</strong></div>
<div class="prestige-stat"><span>Current Multiplier</span><strong id="prestige-current-mult">×1.00</strong></div>
<div class="prestige-stat"><span>Seeds On Reignite</span><strong id="prestige-gain-seeds">0</strong></div>
<div class="prestige-stat"><span>New Multiplier</span><strong id="prestige-gain-mult">×1.00</strong></div>
</div>
<button id="prestige-btn" class="btn btn-prestige" disabled>COLLAPSE & REIGNITE</button>
<p class="prestige-hint" id="prestige-hint"></p>
</div>
</div>
<div class="tab-panel" id="panel-stats">
<div id="list-stats" class="stats-list"></div>
</div>
</div>
</section>
</main>
<footer class="event-ticker">
<span class="ticker-label">LOG</span>
<div class="ticker-feed" id="ticker-feed">Forge dormant. Awaiting first strike.</div>
</footer>
</div>
<script src="game.js"></script>
</body>
</html>/* ============================================================
CINDERFORGE — obsidian/molten terminal theme
============================================================ */
:root{
--void-0:#050403;
--void-1:#0b0807;
--panel:#120d0a;
--panel-2:#1a130e;
--line:rgba(255,144,64,0.28);
--line-bright:rgba(255,166,84,0.65);
--ember-100:#fff3d6;
--ember-300:#ffc878;
--ember-500:#ff8a3d;
--ember-600:#ff6a1a;
--ember-800:#b8390a;
--ember-900:#5c1c04;
--alert-500:#ff4d4d;
--gold-400:#f3cd6b;
--gold-600:#c99a2e;
--starlight-300:#d8e8ff;
--starlight-500:#9fc1ee;
--ink-100:#f4ece1;
--ink-300:#cdbfaf;
--ink-500:#988a7a;
--ink-700:#5c5147;
--font-display:'Unbounded', system-ui, sans-serif;
--font-mono:'Martian Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace;
--cut:14px;
--cut-sm:7px;
}
*,*::before,*::after{ box-sizing:border-box; }
html{ color-scheme:dark; }
body{
margin:0;
min-height:100vh;
background:var(--void-0);
color:var(--ink-300);
font-family:var(--font-mono);
font-size:14px;
line-height:1.5;
-webkit-font-smoothing:antialiased;
overflow-x:hidden;
}
h1,h2,h3{ font-family:var(--font-display); color:var(--ink-100); margin:0; }
p{ margin:0 0 0.8em; }
button{ font-family:inherit; color:inherit; }
::selection{ background:var(--ember-600); color:var(--void-0); }
::-webkit-scrollbar{ width:10px; height:10px; }
::-webkit-scrollbar-track{ background:var(--void-1); }
::-webkit-scrollbar-thumb{ background:var(--ember-900); border:2px solid var(--void-1); }
::-webkit-scrollbar-thumb:hover{ background:var(--ember-800); }
/* ===== background layers ===== */
#bg-canvas{
position:fixed; inset:0; z-index:-2; width:100%; height:100%; display:block;
}
.vignette{
position:fixed; inset:0; z-index:-1; pointer-events:none;
background:radial-gradient(ellipse 90% 80% at 50% 38%, transparent 35%, rgba(2,1,1,0.55) 78%, rgba(2,1,1,0.92) 100%);
}
.scanlines{
position:fixed; inset:0; z-index:5; pointer-events:none;
background:repeating-linear-gradient(to bottom, rgba(255,235,210,0.035) 0px, rgba(255,235,210,0.035) 1px, transparent 1px, transparent 3px);
mix-blend-mode:overlay;
opacity:0.6;
}
#fx-canvas{
position:fixed; inset:0; z-index:60; pointer-events:none; width:100%; height:100%; display:block;
}
/* ===== angular panel shell ===== */
.angular-panel{
position:relative;
background:linear-gradient(165deg, var(--panel), var(--panel-2));
border:1px solid var(--line);
clip-path:polygon(var(--cut) 0, 100% 0, 100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%, 0 100%, 0 var(--cut));
filter:drop-shadow(0 0 0 transparent);
}
.angular-panel::before{
content:'';
position:absolute; inset:0;
background:
linear-gradient(90deg, var(--line-bright) 0 18px, transparent 18px) top left / 100% 1px no-repeat,
linear-gradient(0deg, var(--line-bright) 0 18px, transparent 18px) bottom right / 1px 100% no-repeat;
opacity:0.7;
pointer-events:none;
}
/* ===== top HUD bar ===== */
.hud-bar{
display:flex; align-items:center; gap:24px;
padding:18px clamp(16px,4vw,40px);
border-bottom:1px solid var(--line);
background:linear-gradient(to bottom, rgba(20,14,10,0.9), rgba(10,7,5,0.5));
position:sticky; top:0; z-index:40;
backdrop-filter:blur(6px);
flex-wrap:wrap;
}
.brand{ display:flex; flex-direction:column; margin-right:auto; }
.brand h1{
font-size:clamp(20px,2.4vw,28px);
font-weight:800;
letter-spacing:0.04em;
background:linear-gradient(120deg, var(--ember-100), var(--ember-500) 60%, var(--ember-800));
-webkit-background-clip:text; background-clip:text; color:transparent;
text-shadow:0 0 30px rgba(255,106,26,0.25);
}
.brand .tagline{
font-size:10px; letter-spacing:0.24em; text-transform:uppercase; color:var(--ink-500);
}
.gauges{ display:flex; gap:clamp(14px,2.4vw,34px); flex-wrap:wrap; }
.gauge{ display:flex; flex-direction:column; min-width:108px; }
.gauge-label{ font-size:9px; letter-spacing:0.18em; color:var(--ink-500); }
.gauge-value{ font-size:clamp(16px,1.8vw,21px); font-weight:600; color:var(--ink-100); font-variant-numeric:tabular-nums; }
.gauge-sub{ font-size:10px; color:var(--ember-500); font-variant-numeric:tabular-nums; }
.gauge-primary .gauge-value{ color:var(--ember-300); text-shadow:0 0 14px rgba(255,138,61,0.5); }
.icon-btn{
background:transparent; border:1px solid var(--line); color:var(--ink-300);
padding:8px 14px; cursor:pointer; letter-spacing:0.12em; font-size:11px;
transition:border-color .2s, color .2s, box-shadow .2s;
}
.icon-btn:hover{ border-color:var(--line-bright); color:var(--ember-300); box-shadow:0 0 14px rgba(255,138,61,0.18); }
/* ===== layout ===== */
.layout{
display:grid; grid-template-columns:1.05fr 1fr; gap:22px;
max-width:1360px; margin:0 auto; width:100%;
padding:26px clamp(16px,4vw,40px);
flex:1;
}
@media (max-width:920px){
.layout{ grid-template-columns:1fr; }
}
/* ===== core panel ===== */
.core-panel{
display:flex; flex-direction:column; align-items:center; justify-content:center;
gap:22px; padding:20px;
}
.buy-amount-select{ display:flex; gap:6px; }
.buy-amt-btn{
background:var(--panel); border:1px solid var(--line); color:var(--ink-500);
padding:7px 14px; cursor:pointer; font-family:var(--font-mono); font-size:12px; letter-spacing:0.06em;
clip-path:polygon(6px 0,100% 0,100% 100%,0 100%,0 6px);
transition:all .15s;
}
.buy-amt-btn:hover{ color:var(--ember-300); border-color:var(--line-bright); }
.buy-amt-btn.active{ background:var(--ember-700,#a14c12); background:linear-gradient(135deg,var(--ember-600),var(--ember-800)); color:var(--ember-100); border-color:var(--ember-500); box-shadow:0 0 16px rgba(255,106,26,0.4); }
.core-wrap{ display:flex; flex-direction:column; align-items:center; gap:16px; }
.core{
background:none; border:none; padding:0; cursor:pointer;
width:clamp(190px,30vw,320px); height:clamp(190px,30vw,320px);
position:relative;
-webkit-tap-highlight-color:transparent;
transition:transform .08s ease-out;
}
.core:active{ transform:scale(0.96); }
.core-svg{ width:100%; height:100%; overflow:visible; filter:drop-shadow(0 0 26px rgba(255,106,26,0.55)) drop-shadow(0 0 60px rgba(255,106,26,0.25)); }
.ring{ fill:none; transform-box:fill-box; transform-origin:50% 50%; }
.ring polygon{ fill:none; }
.ring-outer polygon{ stroke:var(--ember-500); stroke-width:1.4; opacity:0.55; }
.ring-mid polygon{ stroke:var(--starlight-300); stroke-width:1; opacity:0.35; }
.ring-inner polygon{ stroke:var(--ember-100); stroke-width:1.2; opacity:0.7; }
.ring-outer{ animation:spin-cw 22s linear infinite; }
.ring-mid{ animation:spin-ccw 16s linear infinite; }
.ring-inner{ animation:spin-cw 11s linear infinite; }
.core-fill{ transform-box:fill-box; transform-origin:50% 50%; animation:core-pulse 3.6s ease-in-out infinite; }
.core.struck .core-fill{ animation:core-strike 0.32s ease-out; }
.core.struck .core-svg{ filter:drop-shadow(0 0 44px rgba(255,160,80,0.85)) drop-shadow(0 0 90px rgba(255,106,26,0.4)); }
.core-hint{
font-size:11px; letter-spacing:0.3em; color:var(--ink-500); text-transform:uppercase;
animation:hint-fade 2.4s ease-in-out infinite;
}
.run-stats{ display:flex; gap:28px; font-size:11px; color:var(--ink-500); }
.run-stats div{ display:flex; flex-direction:column; align-items:center; gap:2px; }
.run-stats strong{ color:var(--ember-300); font-size:14px; font-weight:600; }
/* floating click text */
.float-text{
position:fixed; z-index:65; pointer-events:none;
font-family:var(--font-mono); font-weight:700; font-size:15px;
color:var(--ember-100);
text-shadow:0 0 8px var(--ember-500), 0 0 18px var(--ember-600);
transform:translate(-50%,-50%);
animation:float-up 0.9s ease-out forwards;
white-space:nowrap;
}
/* ===== console panel ===== */
.console-panel{
display:flex; flex-direction:column;
min-height:520px; max-height:78vh;
padding:4px;
}
.tabs{ display:flex; flex-wrap:wrap; border-bottom:1px solid var(--line); }
.tab{
background:none; border:none; padding:14px 16px; cursor:pointer;
font-family:var(--font-mono); font-size:11px; letter-spacing:0.1em; color:var(--ink-500);
position:relative; transition:color .2s;
}
.tab:hover{ color:var(--ink-100); }
.tab.active{ color:var(--ember-300); }
.tab.active::after{
content:''; position:absolute; left:14px; right:14px; bottom:-1px; height:2px;
background:linear-gradient(90deg,var(--ember-500),var(--ember-800));
box-shadow:0 0 10px var(--ember-600);
}
.tab-badge{
display:inline-block; margin-left:6px; min-width:16px; padding:0 4px; border-radius:1px;
background:var(--ember-700,#8a3a0e); background:var(--ember-600); color:var(--void-0);
font-size:9px; font-weight:700; line-height:16px; text-align:center;
}
.tab-badge:empty{ display:none; }
.tab-panels{ flex:1; overflow-y:auto; padding:14px 16px 18px; }
.tab-panel{ display:none; }
.tab-panel.active{ display:block; animation:panel-in .25s ease; }
.empty-state{ color:var(--ink-700); font-size:11px; letter-spacing:0.08em; text-align:center; padding:30px 10px; }
/* ===== row items (generators / upgrades) ===== */
.list{ display:flex; flex-direction:column; gap:8px; }
.row-item{
display:flex; align-items:center; gap:14px;
padding:12px 14px;
background:rgba(255,255,255,0.015);
border:1px solid rgba(255,144,64,0.14);
clip-path:polygon(var(--cut-sm) 0,100% 0,100% calc(100% - var(--cut-sm)),calc(100% - var(--cut-sm)) 100%,0 100%,0 var(--cut-sm));
transition:border-color .2s, background .2s;
}
.row-item:hover{ border-color:var(--line-bright); background:rgba(255,144,64,0.05); }
.row-item.locked{ opacity:0.45; }
.row-item.owned, .upg-row.owned{ opacity:0.5; }
.gen-icon{
width:34px; height:34px; flex-shrink:0; position:relative;
background:linear-gradient(135deg,var(--ember-300),var(--ember-800));
transform:rotate(45deg);
box-shadow:0 0 calc(4px + var(--i,3) * 1.4px) var(--ember-600);
}
.gen-icon::after{
content:''; position:absolute; inset:7px; background:var(--panel);
}
.gen-icon.upg-icon{ width:26px; height:26px; }
.gen-icon.upg-icon::after{ inset:5px; }
.gen-icon.locked-icon{ background:linear-gradient(135deg,var(--ink-700),var(--void-1)); box-shadow:none; }
.row-info{ flex:1; min-width:0; }
.row-title{ font-size:13px; color:var(--ink-100); font-weight:600; display:flex; align-items:center; gap:8px; }
.row-owned{ font-size:11px; color:var(--ember-500); font-weight:400; }
.row-desc{ font-size:11px; color:var(--ink-500); margin-top:2px; }
.row-meta{ font-size:10px; color:var(--ink-700); margin-top:4px; letter-spacing:0.03em; }
.buy-btn{
flex-shrink:0; display:flex; flex-direction:column; align-items:center; gap:2px;
min-width:96px;
background:var(--void-1); border:1px solid var(--line); color:var(--ink-500);
padding:8px 12px; cursor:pointer;
clip-path:polygon(6px 0,100% 0,100% 100%,0 100%,0 6px);
transition:all .15s;
}
.buy-btn .buy-qty{ font-size:10px; letter-spacing:0.08em; }
.buy-btn .buy-cost{ font-size:13px; font-weight:600; font-variant-numeric:tabular-nums; }
.buy-btn:disabled{ cursor:not-allowed; }
.buy-btn.affordable{
border-color:var(--ember-500); color:var(--ember-100);
background:linear-gradient(160deg, rgba(255,138,61,0.22), rgba(255,106,26,0.08));
box-shadow:0 0 16px rgba(255,106,26,0.28);
}
.buy-btn.affordable:hover{ transform:translateY(-1px); box-shadow:0 0 22px rgba(255,106,26,0.45); }
.buy-btn.affordable .buy-cost{ color:var(--ember-300); }
.upg-row.owned .buy-btn{ border-color:var(--ink-700); color:var(--ink-700); }
/* ===== achievements ===== */
.grid-achievements{
display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:10px;
}
.ach-card{
background:rgba(255,255,255,0.015); border:1px solid rgba(255,144,64,0.12);
padding:14px 12px; text-align:center;
clip-path:polygon(var(--cut-sm) 0,100% 0,100% calc(100% - var(--cut-sm)),calc(100% - var(--cut-sm)) 100%,0 100%,0 var(--cut-sm));
display:flex; flex-direction:column; align-items:center; gap:6px;
opacity:0.5; transition:opacity .2s, border-color .2s;
}
.ach-card.unlocked{ opacity:1; border-color:rgba(255,166,84,0.45); background:rgba(255,138,61,0.06); }
.ach-icon{
width:24px; height:24px; transform:rotate(45deg);
border:1.5px solid var(--ink-700); flex-shrink:0;
}
.ach-card.unlocked .ach-icon{
background:linear-gradient(135deg,var(--ember-300),var(--ember-700,var(--ember-600)));
border-color:var(--ember-300);
box-shadow:0 0 12px var(--ember-600);
}
.ach-name{ font-size:11px; color:var(--ink-100); font-weight:600; }
.ach-desc{ font-size:10px; color:var(--ink-500); }
.ach-tag{ font-size:8px; letter-spacing:0.16em; color:var(--ink-700); margin-top:auto; padding-top:4px; }
.ach-card.unlocked .ach-tag{ color:var(--gold-400); }
/* ===== prestige ===== */
.prestige-panel{ max-width:520px; margin:0 auto; text-align:center; padding:10px 4px 20px; }
.prestige-panel h3{ font-size:18px; letter-spacing:0.04em; color:var(--gold-400); margin-bottom:10px; }
.prestige-desc{ font-size:12px; color:var(--ink-500); }
.prestige-desc strong{ color:var(--ember-300); }
.prestige-stats{
display:grid; grid-template-columns:1fr 1fr; gap:10px; margin:18px 0;
}
.prestige-stat{
background:rgba(255,255,255,0.02); border:1px solid var(--line); padding:12px;
display:flex; flex-direction:column; gap:4px;
clip-path:polygon(var(--cut-sm) 0,100% 0,100% calc(100% - var(--cut-sm)),calc(100% - var(--cut-sm)) 100%,0 100%,0 var(--cut-sm));
}
.prestige-stat span{ font-size:9px; letter-spacing:0.12em; color:var(--ink-500); }
.prestige-stat strong{ font-size:18px; color:var(--gold-400); font-variant-numeric:tabular-nums; }
.prestige-hint{ font-size:11px; color:var(--ink-700); margin-top:14px; }
.prestige-hint strong{ color:var(--ink-300); }
/* ===== stats tab ===== */
.stats-list{ display:flex; flex-direction:column; gap:1px; }
.stat-row{
display:flex; justify-content:space-between; padding:10px 6px;
border-bottom:1px dashed rgba(255,144,64,0.15);
font-size:12px;
}
.stat-row span{ color:var(--ink-500); }
.stat-row strong{ color:var(--ink-100); font-variant-numeric:tabular-nums; }
/* ===== buttons (generic) ===== */
.btn{
background:var(--void-1); border:1px solid var(--line); color:var(--ink-300);
padding:10px 18px; cursor:pointer; font-family:var(--font-mono); font-size:12px; letter-spacing:0.08em;
clip-path:polygon(7px 0,100% 0,100% 100%,0 100%,0 7px);
transition:all .15s;
}
.btn:hover{ border-color:var(--line-bright); color:var(--ink-100); }
.btn-primary{ background:linear-gradient(135deg,var(--ember-600),var(--ember-800)); color:var(--ember-100); border-color:var(--ember-500); }
.btn-primary:hover{ box-shadow:0 0 18px rgba(255,106,26,0.4); }
.btn-danger{ border-color:var(--alert-500); color:var(--alert-500); }
.btn-danger:hover{ background:rgba(255,77,77,0.12); color:#ff8080; }
.btn-prestige{
width:100%; padding:14px; font-size:13px; letter-spacing:0.14em;
background:linear-gradient(135deg,var(--gold-400),var(--ember-800)); color:#1a0e02; border-color:var(--gold-400);
font-weight:700;
}
.btn-prestige:disabled{ background:var(--void-1); color:var(--ink-700); border-color:var(--line); cursor:not-allowed; }
.btn-prestige:not(:disabled):hover{ box-shadow:0 0 26px rgba(243,205,107,0.5); transform:translateY(-1px); }
/* ===== footer ticker ===== */
.event-ticker{
display:flex; align-items:center; gap:14px;
padding:10px clamp(16px,4vw,40px);
border-top:1px solid var(--line);
background:rgba(8,5,4,0.7);
font-size:11px;
}
.ticker-label{
flex-shrink:0; color:var(--ember-600); letter-spacing:0.2em; font-size:9px;
border:1px solid var(--line); padding:3px 8px;
}
.ticker-feed{
color:var(--ink-300); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
transition:opacity .25s ease, transform .25s ease;
}
/* ===== toasts ===== */
.toast-stack{ position:fixed; top:18px; right:18px; z-index:100; display:flex; flex-direction:column; gap:8px; max-width:300px; }
.toast{
background:linear-gradient(160deg, var(--panel), var(--panel-2));
border:1px solid var(--ember-500);
padding:12px 14px;
clip-path:polygon(var(--cut-sm) 0,100% 0,100% calc(100% - var(--cut-sm)),calc(100% - var(--cut-sm)) 100%,0 100%,0 var(--cut-sm));
box-shadow:0 0 26px rgba(255,106,26,0.3);
transform:translateX(120%); opacity:0;
transition:transform .35s cubic-bezier(.2,.9,.3,1.2), opacity .3s;
}
.toast.show{ transform:translateX(0); opacity:1; }
.toast.hide{ transform:translateX(120%); opacity:0; }
.toast-title{ font-size:11px; letter-spacing:0.1em; color:var(--gold-400); font-weight:700; }
.toast-desc{ font-size:11px; color:var(--ink-300); margin-top:3px; }
/* ===== modals ===== */
.modal-overlay{
position:fixed; inset:0; z-index:200; display:flex; align-items:center; justify-content:center;
background:rgba(3,2,1,0.78); backdrop-filter:blur(5px); padding:20px;
}
.modal-overlay.hidden{ display:none; }
.modal{
width:min(440px,100%); padding:28px 26px; animation:modal-in .25s ease;
}
.modal h2{ font-size:16px; letter-spacing:0.08em; color:var(--ember-300); margin-bottom:14px; }
.modal p{ font-size:12px; color:var(--ink-300); }
.modal p strong{ color:var(--ember-300); }
.modal-actions{ display:flex; gap:10px; margin-top:18px; }
.modal-actions .btn{ flex:1; }
.reignite-stats{ display:grid; grid-template-columns:1fr 1fr; gap:10px; margin:16px 0; }
.reignite-stats div{ background:rgba(255,255,255,0.02); border:1px solid var(--line); padding:10px; display:flex; flex-direction:column; gap:3px; }
.reignite-stats span{ font-size:9px; color:var(--ink-500); letter-spacing:0.08em; }
.reignite-stats strong{ font-size:16px; color:var(--gold-400); }
.settings-row{ display:flex; justify-content:space-between; align-items:center; padding:10px 0; border-bottom:1px dashed rgba(255,144,64,0.15); font-size:12px; }
.toggle{
background:var(--void-1); border:1px solid var(--line); color:var(--ink-500);
padding:5px 14px; cursor:pointer; font-size:11px; letter-spacing:0.08em;
clip-path:polygon(5px 0,100% 0,100% 100%,0 100%,0 5px);
}
.toggle:not(.off){ border-color:var(--ember-500); color:var(--ember-300); }
#settings-close{ width:100%; margin-top:18px; }
/* ===== app shake (prestige / big moments) ===== */
#app.pulse{ animation:shake .4s ease; }
/* ===== keyframes ===== */
@keyframes spin-cw{ from{ transform:rotate(0); } to{ transform:rotate(360deg); } }
@keyframes spin-ccw{ from{ transform:rotate(0); } to{ transform:rotate(-360deg); } }
@keyframes core-pulse{
0%,100%{ transform:scale(1); opacity:0.92; }
50%{ transform:scale(1.045); opacity:1; }
}
@keyframes core-strike{
0%{ transform:scale(1.22); opacity:1; }
100%{ transform:scale(1); opacity:0.92; }
}
@keyframes hint-fade{
0%,100%{ opacity:0.55; }
50%{ opacity:1; }
}
@keyframes float-up{
0%{ transform:translate(-50%,-50%) scale(1); opacity:1; }
100%{ transform:translate(-50%,-130%) scale(1.2); opacity:0; }
}
@keyframes panel-in{
from{ opacity:0; transform:translateY(6px); }
to{ opacity:1; transform:translateY(0); }
}
@keyframes modal-in{
from{ opacity:0; transform:scale(.96); }
to{ opacity:1; transform:scale(1); }
}
@keyframes shake{
10%,90%{ transform:translateX(-1px); }
20%,80%{ transform:translateX(2px); }
30%,50%,70%{ transform:translateX(-5px); }
40%,60%{ transform:translateX(5px); }
}
/* ===== responsive ===== */
@media (max-width:680px){
.hud-bar{ gap:14px; }
.gauges{ gap:14px; order:3; width:100%; justify-content:space-between; }
.brand{ order:1; }
#settings-btn{ order:2; }
.console-panel{ max-height:none; }
.prestige-stats{ grid-template-columns:1fr; }
}
@media (prefers-reduced-motion: reduce){
*,*::before,*::after{
animation-duration:0.01ms !important;
animation-iteration-count:1 !important;
transition-duration:0.01ms !important;
}
}'use strict';
/* ============================================================
CONFIG
============================================================ */
const SAVE_KEY = 'cinderforge_save_v1';
const TICK_MS = 100;
const AUTOSAVE_MS = 30000;
const COST_MULT = 1.15;
const SEED_DIVISOR = 1e5;
const OFFLINE_CAP_SEC = 8 * 3600;
const OFFLINE_EFFICIENCY = 0.5;
const SUFFIXES = ['','K','M','B','T','Qa','Qi','Sx','Sp','Oc','No','Dc','Ud','Dd','Td','Qad','Qid','Sxd','Spd','Ocd','Nod','Vg'];
const GENERATORS = [
{ id:'tongs', name:'Spark Tongs', desc:'Hand-forged tongs that pry loose stray embers.', baseCost:10, baseProd:0.1 },
{ id:'rake', name:'Cinder Rake', desc:'Drags smoldering ash for hidden embers.', baseCost:80, baseProd:0.6 },
{ id:'bellows', name:'Bellows Array', desc:'A bank of bellows stoking the dying flame.', baseCost:600, baseProd:4 },
{ id:'smelter', name:'Slag Smelter', desc:'Melts star-slag down to raw embers.', baseCost:4500, baseProd:25 },
{ id:'reclaimer', name:'Magnetic Reclaimer', desc:'Pulls charged dust from the husk’s remains.', baseCost:35000, baseProd:150 },
{ id:'kiln', name:'Plasma Kiln', desc:'Superheats remnants to plasma, then embers.', baseCost:280000, baseProd:900 },
{ id:'press', name:'Gravity Press', desc:'Compresses stellar debris into pure fuel.', baseCost:2200000, baseProd:5400 },
{ id:'coretap', name:'Core Tap', desc:'Drills straight into the dead star’s heart.', baseCost:18000000, baseProd:32000 },
{ id:'loom', name:'Singularity Loom', desc:'Weaves collapsing spacetime into embers.', baseCost:150000000, baseProd:190000 },
{ id:'engine', name:'Star Forge Engine', desc:'Forges entire stars from nothing at all.', baseCost:1300000000, baseProd:1150000 },
];
const GEN_UPGRADE_THRESHOLDS = [10, 25, 50, 100, 200];
const GEN_UPGRADE_PREFIXES = ['Calibrated','Reinforced','Overclocked','Resonant','Transcendent'];
const CLICK_UPGRADES = [
{ id:'cu1', name:'Reinforced Tongs', desc:'Strike power ×2.', cost:50, kind:'clickMult', value:2 },
{ id:'cu2', name:'Resonant Striker', desc:'Strike power ×2.', cost:1000, kind:'clickMult', value:2 },
{ id:'cu3', name:'Harmonic Hammer', desc:'Strike power ×2.', cost:20000, kind:'clickMult', value:2 },
{ id:'cu4', name:'Echo Capacitor', desc:'Strike power ×2. Strikes also yield 2% of your embers/sec.', cost:500000, kind:'clickMult', value:2, sps:0.02 },
{ id:'cu5', name:'Feedback Loop', desc:'Strike power ×2. Strikes also yield 3% of your embers/sec.', cost:10000000, kind:'clickMult', value:2, sps:0.03 },
{ id:'cu6', name:'Temporal Striker', desc:'Strike power ×3. Strikes also yield 5% of your embers/sec.', cost:250000000, kind:'clickMult', value:3, sps:0.05 },
{ id:'cu7', name:'Paradox Engine', desc:'Strike power ×3. Strikes also yield 5% of your embers/sec.', cost:8000000000, kind:'clickMult', value:3, sps:0.05 },
{ id:'cu8', name:'Will Of The Forge', desc:'Strike power ×5. Strikes also yield 10% of your embers/sec.', cost:200000000000, kind:'clickMult', value:5, sps:0.10 },
];
const PROD_UPGRADES = [
{ id:'pu1', name:'Efficient Conduits', desc:'All production ×1.5.', cost:200, kind:'globalMult', value:1.5 },
{ id:'pu2', name:'Refined Slag Lines', desc:'All production ×1.5.', cost:5000, kind:'globalMult', value:1.5 },
{ id:'pu3', name:'Catalytic Linings', desc:'All production ×1.5.', cost:100000, kind:'globalMult', value:1.5 },
{ id:'pu4', name:'Synchronized Furnaces', desc:'All production ×1.5.', cost:2000000, kind:'globalMult', value:1.5 },
{ id:'pu5', name:'Stable Containment', desc:'All production ×2.', cost:50000000, kind:'globalMult', value:2 },
{ id:'pu6', name:'Folded Spacetime Routing', desc:'All production ×2.', cost:1200000000, kind:'globalMult', value:2 },
{ id:'pu7', name:'Entropy Reversal Array', desc:'All production ×2.', cost:30000000000, kind:'globalMult', value:2 },
{ id:'pu8', name:'Eternal Combustion', desc:'All production ×2.5.', cost:800000000000, kind:'globalMult', value:2.5 },
];
function buildGeneratorUpgrades(){
const ups = [];
GENERATORS.forEach(gen=>{
GEN_UPGRADE_THRESHOLDS.forEach((threshold, i)=>{
const id = `gu_${gen.id}_${threshold}`;
const cost = Math.round(totalCostRange(gen.baseCost, 0, threshold) * 0.55);
ups.push({
id, genId:gen.id, name:`${GEN_UPGRADE_PREFIXES[i]} ${gen.name}`,
desc:`Doubles the output of every ${gen.name}.`,
cost, kind:'genMult', value:2,
visible:(s)=> (s.generators[gen.id]||0) >= Math.ceil(threshold*0.4) || s.upgradesBought.includes(id),
});
});
});
return ups;
}
const ALL_UPGRADES = [...buildGeneratorUpgrades(), ...CLICK_UPGRADES, ...PROD_UPGRADES];
const EARNED_MILESTONES = [
[1,'First Light','Earn your first ember.'],
[100,'Stoked','Earn 100 embers total.'],
[1e4,'Blazing','Earn 10,000 embers total.'],
[1e6,'Inferno','Earn 1,000,000 embers total.'],
[1e8,'Stellar','Earn 100,000,000 embers total.'],
[1e10,'Supernova','Earn 10 billion embers total.'],
[1e12,'Galactic','Earn 1 trillion embers total.'],
[1e15,'Cosmic','Earn 1 quadrillion embers total.'],
[1e18,'Transcendent','Earn 1 quintillion embers total.'],
[1e21,'Eternal','Earn 1 sextillion embers total.'],
];
const CLICK_MILESTONES = [
[50,'Quick Hands','Strike the core 50 times.'],
[500,'Tireless','Strike the core 500 times.'],
[5000,'Relentless','Strike the core 5,000 times.'],
[50000,'Machine-Like','Strike the core 50,000 times.'],
];
const PRESTIGE_MILESTONES = [
[1,'Reborn','Reignite the forge once.'],
[5,'Cyclical','Reignite the forge 5 times.'],
[10,'Eternal Return','Reignite the forge 10 times.'],
[25,'Star Eater','Reignite the forge 25 times.'],
[50,'Beyond Counting','Reignite the forge 50 times.'],
];
const UPGRADE_MILESTONES = [
[10,'Tinkerer','Forge 10 upgrades.'],
[25,'Engineer','Forge 25 upgrades.'],
[50,'Architect Of The Forge','Forge 50 upgrades.'],
];
function buildAchievements(){
const list = [];
EARNED_MILESTONES.forEach(([v,name,desc])=>list.push({ id:'ae_'+v, name, desc, test:s=>s.totalEarned>=v }));
CLICK_MILESTONES.forEach(([v,name,desc])=>list.push({ id:'ac_'+v, name, desc, test:s=>s.lifetimeClicks>=v }));
PRESTIGE_MILESTONES.forEach(([v,name,desc])=>list.push({ id:'ap_'+v, name, desc, test:s=>s.prestigeCount>=v }));
UPGRADE_MILESTONES.forEach(([v,name,desc])=>list.push({ id:'au_'+v, name, desc, test:s=>s.upgradesBought.length>=v }));
list.push({ id:'ag_all1', name:'Full Assembly Line', desc:'Own at least 1 of every generator.', test:s=>GENERATORS.every(g=>(s.generators[g.id]||0)>=1) });
list.push({ id:'ag_25', name:'Specialist', desc:'Own 25 of a single generator.', test:s=>GENERATORS.some(g=>(s.generators[g.id]||0)>=25) });
list.push({ id:'ag_50', name:'Mass Production', desc:'Own 50 of a single generator.', test:s=>GENERATORS.some(g=>(s.generators[g.id]||0)>=50) });
list.push({ id:'ag_100', name:'Industrial Scale', desc:'Own 100 of a single generator.', test:s=>GENERATORS.some(g=>(s.generators[g.id]||0)>=100) });
list.push({ id:'ag_200', name:'Singular Focus', desc:'Own 200 of a single generator.', test:s=>GENERATORS.some(g=>(s.generators[g.id]||0)>=200) });
list.push({ id:'ag_all50', name:'Total Industry', desc:'Own at least 50 of every generator.', test:s=>GENERATORS.every(g=>(s.generators[g.id]||0)>=50) });
return list;
}
const ACHIEVEMENTS = buildAchievements();
/* ============================================================
MATH HELPERS
============================================================ */
function totalCostRange(baseCost, fromOwned, qty){
if (qty<=0) return 0;
return baseCost * Math.pow(COST_MULT, fromOwned) * (Math.pow(COST_MULT, qty) - 1) / (COST_MULT - 1);
}
function maxAffordableQty(baseCost, owned, money){
const ratio = 1 + (money * (COST_MULT - 1)) / (baseCost * Math.pow(COST_MULT, owned));
if (ratio <= 1) return 0;
return Math.max(0, Math.floor(Math.log(ratio) / Math.log(COST_MULT)));
}
function formatNumber(num){
if (num === null || num === undefined || Number.isNaN(num)) return '0';
const neg = num < 0; num = Math.abs(num);
if (num < 1000){
if (Number.isInteger(num)) return (neg?'-':'') + num.toString();
return (neg?'-':'') + num.toFixed(num<10?2:1);
}
let tier = Math.floor(Math.log10(num)/3);
if (tier >= SUFFIXES.length) return (neg?'-':'') + num.toExponential(2).replace('e+','e');
let scaled = num / Math.pow(1000, tier);
let str = scaled.toFixed(scaled<10?2:scaled<100?1:0);
if (parseFloat(str) >= 1000){ tier++; str = (num/Math.pow(1000,tier)).toFixed(2); }
return (neg?'-':'') + str + SUFFIXES[tier];
}
function formatTime(totalSeconds){
totalSeconds = Math.max(0, Math.floor(totalSeconds));
const d = Math.floor(totalSeconds/86400);
const h = Math.floor((totalSeconds%86400)/3600);
const m = Math.floor((totalSeconds%3600)/60);
const s = totalSeconds%60;
if (d>0) return `${d}d ${h}h`;
if (h>0) return `${h}h ${m}m`;
if (m>0) return `${m}m ${s}s`;
return `${s}s`;
}
/* ============================================================
STATE
============================================================ */
function defaultState(){
return {
embers:0,
totalEarned:0,
runEarned:0,
lifetimeClicks:0,
generators:{},
upgradesBought:[],
achievementsUnlocked:[],
seeds:0,
prestigeCount:0,
playtime:0,
lastSaveTime:Date.now(),
settings:{ sound:true, particles:true },
buyAmount:1,
};
}
let state = defaultState();
function ownedCount(genId){ return state.generators[genId] || 0; }
function isUpgradeVisible(up, s){
if (s.upgradesBought.includes(up.id)) return true;
if (up.visible) return up.visible(s);
return s.totalEarned >= up.cost * 0.1;
}
function genUpgradeMultiplier(genId){
let mult = 1;
for (const u of ALL_UPGRADES){
if (u.kind==='genMult' && u.genId===genId && state.upgradesBought.includes(u.id)) mult *= u.value;
}
return mult;
}
function rawSPS(){
let total = 0;
for (const gen of GENERATORS) total += gen.baseProd * ownedCount(gen.id) * genUpgradeMultiplier(gen.id);
return total;
}
function globalUpgradeMultiplier(){
let mult = 1;
for (const u of ALL_UPGRADES){
if (u.kind==='globalMult' && state.upgradesBought.includes(u.id)) mult *= u.value;
}
return mult;
}
function achievementMultiplier(){ return 1 + state.achievementsUnlocked.length * 0.01; }
function prestigeMultiplier(){ return 1 + state.seeds * 0.02; }
function globalMultiplier(){ return globalUpgradeMultiplier() * achievementMultiplier() * prestigeMultiplier(); }
function totalSPS(){ return rawSPS() * globalMultiplier(); }
function generatorProduction(genId){
const gen = GENERATORS.find(g=>g.id===genId);
return gen.baseProd * ownedCount(genId) * genUpgradeMultiplier(genId) * globalMultiplier();
}
function clickMultiplier(){
let mult = 1;
for (const u of CLICK_UPGRADES) if (state.upgradesBought.includes(u.id)) mult *= u.value;
return mult;
}
function clickSPSPercent(){
let pct = 0;
for (const u of CLICK_UPGRADES) if (u.sps && state.upgradesBought.includes(u.id)) pct += u.sps;
return pct;
}
function clickValue(){
return (1 * clickMultiplier() + rawSPS() * clickSPSPercent()) * globalMultiplier();
}
function projectedSeeds(){ return Math.floor(Math.cbrt(state.runEarned / SEED_DIVISOR)); }
/* ============================================================
ACTIONS
============================================================ */
function strikeCore(x, y){
const value = clickValue();
state.embers += value;
state.totalEarned += value;
state.runEarned += value;
state.lifetimeClicks++;
spawnFloatingText(x, y, '+'+formatNumber(value));
spawnClickBurst(x, y);
pulseCore();
playSound('click');
}
function buyGenerator(genId){
const gen = GENERATORS.find(g=>g.id===genId);
const owned = ownedCount(genId);
const qty = state.buyAmount==='max' ? maxAffordableQty(gen.baseCost, owned, state.embers) : state.buyAmount;
if (qty<=0) return;
const cost = totalCostRange(gen.baseCost, owned, qty);
if (cost > state.embers + 1e-6) return;
state.embers -= cost;
state.generators[genId] = owned + qty;
logEvent(`Acquired ${qty}× ${gen.name}`);
renderGenerators();
renderUpgrades();
updateHUD();
playSound('buy');
}
function buyUpgrade(upId){
const up = ALL_UPGRADES.find(u=>u.id===upId);
if (!up || state.upgradesBought.includes(upId)) return;
if (state.embers < up.cost) return;
state.embers -= up.cost;
state.upgradesBought.push(upId);
logEvent(`Forged upgrade: ${up.name}`);
renderUpgrades();
renderGenerators();
updateHUD();
playSound('upgrade');
}
function doReignite(){
const gain = projectedSeeds();
if (gain<=0) return;
state.seeds += gain;
state.prestigeCount++;
state.embers = 0;
state.runEarned = 0;
state.generators = {};
state.upgradesBought = [];
logEvent(`Reignited the forge — gained ${formatNumber(gain)} Stellar Seeds`);
showToast('FORGE REIGNITED', `+${formatNumber(gain)} Stellar Seeds — multiplier now ×${prestigeMultiplier().toFixed(2)}`);
closeModal('reignite-modal');
screenPulse();
playSound('prestige');
renderAll();
save();
}
function checkAchievements(){
let unlockedNew = false;
for (const ach of ACHIEVEMENTS){
if (state.achievementsUnlocked.includes(ach.id)) continue;
if (ach.test(state)){
state.achievementsUnlocked.push(ach.id);
showToast('FORGE LOG UNLOCKED: ' + ach.name, ach.desc);
logEvent(`Forge Log unlocked: ${ach.name}`);
playSound('achievement');
unlockedNew = true;
}
}
if (unlockedNew){ renderAchievements(); }
}
/* ============================================================
SAVE / LOAD
============================================================ */
function save(){
state.lastSaveTime = Date.now();
try{ localStorage.setItem(SAVE_KEY, JSON.stringify(state)); }
catch(e){ console.warn('Save failed', e); }
}
function load(){
try{
const raw = localStorage.getItem(SAVE_KEY);
if (raw){
const parsed = JSON.parse(raw);
state = Object.assign(defaultState(), parsed);
state.generators = Object.assign({}, parsed.generators);
state.upgradesBought = Array.isArray(parsed.upgradesBought) ? parsed.upgradesBought : [];
state.achievementsUnlocked = Array.isArray(parsed.achievementsUnlocked) ? parsed.achievementsUnlocked : [];
state.settings = Object.assign({ sound:true, particles:true }, parsed.settings);
}
} catch(e){ console.warn('Load failed, starting fresh', e); state = defaultState(); }
}
function applyOfflineProgress(){
const now = Date.now();
const last = state.lastSaveTime || now;
const elapsedSec = Math.max(0, (now-last)/1000);
state.lastSaveTime = now;
if (elapsedSec < 60) return;
const effective = Math.min(elapsedSec, OFFLINE_CAP_SEC);
const earned = totalSPS() * effective * OFFLINE_EFFICIENCY;
if (earned > 0){
state.embers += earned;
state.totalEarned += earned;
state.runEarned += earned;
}
const offlineTextEl = document.getElementById('offline-text');
offlineTextEl.innerHTML = `You were away for <strong>${formatTime(elapsedSec)}</strong>${elapsedSec>OFFLINE_CAP_SEC?' (capped at 8h)':''} and the forge kept working, producing <strong>${formatNumber(earned)} embers</strong>.`;
showModal('offline-modal');
}
function exportSave(){
const data = btoa(unescape(encodeURIComponent(JSON.stringify(state))));
if (navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(data)
.then(()=>showToast('SAVE COPIED','Save string copied to clipboard.'))
.catch(()=>window.prompt('Copy your save string:', data));
} else {
window.prompt('Copy your save string:', data);
}
}
function importSave(){
const input = window.prompt('Paste your save string:');
if (!input) return;
try{
const json = decodeURIComponent(escape(atob(input.trim())));
const parsed = JSON.parse(json);
state = Object.assign(defaultState(), parsed);
state.generators = Object.assign({}, parsed.generators);
state.upgradesBought = Array.isArray(parsed.upgradesBought) ? parsed.upgradesBought : [];
state.achievementsUnlocked = Array.isArray(parsed.achievementsUnlocked) ? parsed.achievementsUnlocked : [];
state.settings = Object.assign({ sound:true, particles:true }, parsed.settings);
save();
renderAll();
showToast('SAVE IMPORTED','Your forge has been restored.');
} catch(e){
window.alert('Invalid save string.');
}
}
function hardReset(){
if (!window.confirm('This will permanently erase ALL progress, including Stellar Seeds. Are you sure?')) return;
localStorage.removeItem(SAVE_KEY);
state = defaultState();
renderAll();
closeModal('settings-panel');
showToast('FORGE ERASED','Starting anew.');
}
/* ============================================================
RENDER
============================================================ */
const generatorRowRefs = {};
const upgradeRowRefs = {};
let lastVisibleUpgradeKey = '';
function renderGenerators(){
const list = document.getElementById('list-generators');
list.innerHTML = '';
for (const key in generatorRowRefs) delete generatorRowRefs[key];
let lastVisibleIdx = -1;
GENERATORS.forEach((gen, idx)=>{
const owned = ownedCount(gen.id);
const prevOwned = idx>0 ? ownedCount(GENERATORS[idx-1].id) : 1;
const unlocked = idx===0 || prevOwned>=1 || owned>=1;
if (!unlocked) return;
lastVisibleIdx = idx;
const qty = state.buyAmount==='max' ? maxAffordableQty(gen.baseCost, owned, state.embers) : state.buyAmount;
const cost = totalCostRange(gen.baseCost, owned, Math.max(qty,1));
const prod = generatorProduction(gen.id);
const row = document.createElement('div');
row.className = 'row-item gen-row';
row.innerHTML = `
<div class="gen-icon" style="--i:${idx}"></div>
<div class="row-info">
<div class="row-title">${gen.name} <span class="row-owned">×${owned}</span></div>
<div class="row-desc">${gen.desc}</div>
<div class="row-meta">${formatNumber(gen.baseProd)} embers/s each · producing ${formatNumber(prod)}/s</div>
</div>
<button class="buy-btn" type="button">
<span class="buy-qty">BUY ${state.buyAmount==='max' ? '×'+qty : '×'+state.buyAmount}</span>
<span class="buy-cost">${formatNumber(cost)}</span>
</button>`;
list.appendChild(row);
const buyBtn = row.querySelector('.buy-btn');
buyBtn.addEventListener('click', ()=>buyGenerator(gen.id));
generatorRowRefs[gen.id] = {
row, buyBtn,
costEl: buyBtn.querySelector('.buy-cost'),
qtyEl: buyBtn.querySelector('.buy-qty'),
metaEl: row.querySelector('.row-meta'),
ownedEl: row.querySelector('.row-owned'),
};
});
if (lastVisibleIdx>=0 && lastVisibleIdx < GENERATORS.length-1){
const lockedRow = document.createElement('div');
lockedRow.className = 'row-item gen-row locked';
lockedRow.innerHTML = `
<div class="gen-icon locked-icon"></div>
<div class="row-info">
<div class="row-title">??? LOCKED</div>
<div class="row-desc">Own at least 1 ${GENERATORS[lastVisibleIdx].name} to reveal this generator.</div>
</div>`;
list.appendChild(lockedRow);
}
if (!list.children.length) list.innerHTML = '<p class="empty-state">// AWAITING FIRST STRIKE //</p>';
updateHUD();
}
function renderUpgrades(){
const list = document.getElementById('list-upgrades');
list.innerHTML = '';
for (const key in upgradeRowRefs) delete upgradeRowRefs[key];
const visible = ALL_UPGRADES.filter(u=>isUpgradeVisible(u, state));
const bought = visible.filter(u=>state.upgradesBought.includes(u.id));
const avail = visible.filter(u=>!state.upgradesBought.includes(u.id)).sort((a,b)=>a.cost-b.cost);
const ordered = [...avail, ...bought];
const badge = document.getElementById('badge-upgrades');
badge.textContent = avail.length>0 ? avail.length : '';
if (!ordered.length){
list.innerHTML = '<p class="empty-state">// NOTHING WITHIN REACH YET — KEEP FORGING //</p>';
lastVisibleUpgradeKey = '';
return;
}
ordered.forEach(u=>{
const owned = state.upgradesBought.includes(u.id);
const row = document.createElement('div');
row.className = 'row-item upg-row' + (owned?' owned':'');
row.innerHTML = `
<div class="gen-icon upg-icon"></div>
<div class="row-info">
<div class="row-title">${u.name}</div>
<div class="row-desc">${u.desc}</div>
</div>
<button class="buy-btn" type="button" ${owned?'disabled':''}>
<span class="buy-qty">${owned?'OWNED':'FORGE'}</span>
<span class="buy-cost">${owned?'':formatNumber(u.cost)}</span>
</button>`;
list.appendChild(row);
const buyBtn = row.querySelector('.buy-btn');
if (!owned) buyBtn.addEventListener('click', ()=>buyUpgrade(u.id));
upgradeRowRefs[u.id] = { row, buyBtn };
});
lastVisibleUpgradeKey = avail.map(u=>u.id).join(',');
}
function renderAchievements(){
const grid = document.getElementById('list-achievements');
grid.innerHTML = '';
ACHIEVEMENTS.forEach(a=>{
const unlocked = state.achievementsUnlocked.includes(a.id);
const card = document.createElement('div');
card.className = 'ach-card' + (unlocked?' unlocked':'');
card.innerHTML = `
<div class="ach-icon"></div>
<div class="ach-name">${a.name}</div>
<div class="ach-desc">${a.desc}</div>
<div class="ach-tag">${unlocked?'UNLOCKED':'LOCKED'}</div>`;
grid.appendChild(card);
});
const count = state.achievementsUnlocked.length;
document.getElementById('badge-achievements').textContent = count>0 ? count+'/'+ACHIEVEMENTS.length : '';
}
function renderPrestige(){
document.getElementById('prestige-current-seeds').textContent = formatNumber(state.seeds);
document.getElementById('prestige-current-mult').textContent = '×'+prestigeMultiplier().toFixed(2);
const gain = projectedSeeds();
document.getElementById('prestige-gain-seeds').textContent = formatNumber(gain);
document.getElementById('prestige-gain-mult').textContent = '×'+(1+(state.seeds+gain)*0.02).toFixed(2);
const btn = document.getElementById('prestige-btn');
btn.disabled = gain<=0;
const hint = document.getElementById('prestige-hint');
if (gain<=0){
hint.innerHTML = `Earn at least <strong>${formatNumber(SEED_DIVISOR)}</strong> embers this cycle to gain your first Stellar Seed. You have <strong>${formatNumber(state.runEarned)}</strong>.`;
} else {
hint.innerHTML = `Reigniting now nets <strong>+${formatNumber(gain)}</strong> Stellar Seeds.`;
}
}
function renderStats(){
const totalGenerators = GENERATORS.reduce((sum,g)=>sum+ownedCount(g.id),0);
const rows = [
['All-Time Embers Earned', formatNumber(state.totalEarned)],
['This Cycle Earned', formatNumber(state.runEarned)],
['Embers / Second', formatNumber(totalSPS())+'/s'],
['Total Strikes', formatNumber(state.lifetimeClicks)],
['Strike Power', formatNumber(clickValue())],
['Generators Owned', formatNumber(totalGenerators)],
['Upgrades Forged', state.upgradesBought.length+' / '+ALL_UPGRADES.length],
['Forge Logs Unlocked', state.achievementsUnlocked.length+' / '+ACHIEVEMENTS.length],
['Stellar Seeds', formatNumber(state.seeds)],
['Global Multiplier', '×'+globalMultiplier().toFixed(2)],
['Reignitions', formatNumber(state.prestigeCount)],
['Time Forging', formatTime(state.playtime)],
];
document.getElementById('list-stats').innerHTML = rows.map(([k,v])=>`<div class="stat-row"><span>${k}</span><strong>${v}</strong></div>`).join('');
}
function updateHUD(){
document.getElementById('stat-embers').textContent = formatNumber(state.embers);
document.getElementById('stat-eps').textContent = '+'+formatNumber(totalSPS())+'/s';
document.getElementById('stat-seeds').textContent = formatNumber(state.seeds);
document.getElementById('stat-mult').textContent = '×'+globalMultiplier().toFixed(2);
document.getElementById('stat-clickpower').textContent = formatNumber(clickValue());
document.getElementById('stat-clicks').textContent = formatNumber(state.lifetimeClicks)+' strikes';
document.getElementById('stat-run-earned').textContent = formatNumber(state.runEarned);
document.getElementById('stat-alltime-earned').textContent = formatNumber(state.totalEarned);
for (const gen of GENERATORS){
const ref = generatorRowRefs[gen.id];
if (!ref) continue;
const owned = ownedCount(gen.id);
const qty = state.buyAmount==='max' ? maxAffordableQty(gen.baseCost, owned, state.embers) : state.buyAmount;
const cost = totalCostRange(gen.baseCost, owned, Math.max(qty,1));
const affordable = state.buyAmount==='max' ? qty>0 : state.embers>=cost;
ref.buyBtn.classList.toggle('affordable', affordable);
ref.buyBtn.disabled = !affordable;
ref.costEl.textContent = formatNumber(cost);
ref.qtyEl.textContent = 'BUY ' + (state.buyAmount==='max' ? '×'+qty : '×'+state.buyAmount);
ref.ownedEl.textContent = '×'+owned;
ref.metaEl.textContent = `${formatNumber(gen.baseProd)} embers/s each · producing ${formatNumber(generatorProduction(gen.id))}/s`;
}
for (const u of ALL_UPGRADES){
const ref = upgradeRowRefs[u.id];
if (!ref || state.upgradesBought.includes(u.id)) continue;
ref.buyBtn.classList.toggle('affordable', state.embers>=u.cost);
}
renderPrestige();
}
function renderAll(){
renderGenerators();
renderUpgrades();
renderAchievements();
renderPrestige();
renderStats();
updateHUD();
}
function switchTab(name){
document.querySelectorAll('.tab').forEach(b=>b.classList.toggle('active', b.dataset.tab===name));
document.querySelectorAll('.tab-panel').forEach(p=>p.classList.toggle('active', p.id==='panel-'+name));
if (name==='stats') renderStats();
}
/* ============================================================
TICKER / TOASTS / MODALS
============================================================ */
let tickerQueue = [];
let tickerShowing = false;
function logEvent(text){
const stamp = new Date().toLocaleTimeString([], { hour12:false });
tickerQueue.push(`[${stamp}] ${text}`);
if (tickerQueue.length>20) tickerQueue = tickerQueue.slice(-20);
if (!tickerShowing) showNextTicker();
}
function showNextTicker(){
const feed = document.getElementById('ticker-feed');
if (!tickerQueue.length){ tickerShowing=false; return; }
tickerShowing = true;
const text = tickerQueue.shift();
feed.style.opacity = '0';
feed.style.transform = 'translateY(6px)';
setTimeout(()=>{
feed.textContent = text;
feed.style.opacity = '1';
feed.style.transform = 'translateY(0)';
}, 140);
setTimeout(showNextTicker, 2600);
}
function showToast(title, desc){
const stack = document.getElementById('toast-stack');
const el = document.createElement('div');
el.className = 'toast';
el.innerHTML = `<div class="toast-title">${title}</div><div class="toast-desc">${desc}</div>`;
stack.appendChild(el);
requestAnimationFrame(()=>requestAnimationFrame(()=>el.classList.add('show')));
setTimeout(()=>{
el.classList.remove('show');
el.classList.add('hide');
setTimeout(()=>el.remove(), 400);
}, 4200);
}
function showModal(id){ document.getElementById(id).classList.remove('hidden'); }
function closeModal(id){ document.getElementById(id).classList.add('hidden'); }
function screenPulse(){
const app = document.getElementById('app');
app.classList.remove('pulse');
void app.offsetWidth;
app.classList.add('pulse');
}
/* ============================================================
AUDIO (synthesized, no external assets)
============================================================ */
let audioCtx = null;
function ensureAudio(){
if (!audioCtx){
const Ctx = window.AudioContext || window.webkitAudioContext;
if (Ctx) audioCtx = new Ctx();
}
if (audioCtx && audioCtx.state==='suspended') audioCtx.resume();
}
function playSound(type){
if (!state.settings.sound) return;
ensureAudio();
if (!audioCtx) return;
const t0 = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain); gain.connect(audioCtx.destination);
let freq=440, dur=0.08, shape='sine';
if (type==='click'){ freq=520+Math.random()*80; dur=0.06; shape='triangle'; }
else if (type==='buy'){ freq=300; dur=0.12; shape='square'; }
else if (type==='upgrade'){ freq=660; dur=0.18; shape='sine'; }
else if (type==='achievement'){ freq=880; dur=0.32; shape='sine'; }
else if (type==='prestige'){ freq=220; dur=0.6; shape='sawtooth'; }
osc.type = shape;
osc.frequency.setValueAtTime(freq, t0);
gain.gain.setValueAtTime(0.0001, t0);
gain.gain.exponentialRampToValueAtTime(0.12, t0+0.01);
gain.gain.exponentialRampToValueAtTime(0.0001, t0+dur);
osc.start(t0);
osc.stop(t0+dur+0.02);
}
/* ============================================================
PARTICLES / BACKGROUND CANVAS
============================================================ */
const bgCanvas = document.getElementById('bg-canvas');
const bgCtx = bgCanvas.getContext('2d');
const fxCanvas = document.getElementById('fx-canvas');
const fxCtx = fxCanvas.getContext('2d');
let stars = [], ambientEmbers = [];
let clickParticles = [];
let bgTime = 0;
function resizeCanvases(){
const dpr = Math.min(window.devicePixelRatio||1, 2);
for (const c of [bgCanvas, fxCanvas]){
c.width = window.innerWidth*dpr;
c.height = window.innerHeight*dpr;
c.style.width = window.innerWidth+'px';
c.style.height = window.innerHeight+'px';
}
}
function resetAmbientEmber(){
return {
x: Math.random()*window.innerWidth,
y: window.innerHeight + Math.random()*100,
r: 1+Math.random()*2.2,
vy: 0.25+Math.random()*0.5,
vx: (Math.random()-0.5)*0.3,
age: 0,
maxAge: 220+Math.random()*220,
};
}
function initBg(){
stars = Array.from({length:140}, ()=>({
x:Math.random()*window.innerWidth, y:Math.random()*window.innerHeight,
r:Math.random()*1.4+0.3, phase:Math.random()*Math.PI*2, speed:0.4+Math.random()*0.8,
}));
ambientEmbers = Array.from({length:26}, resetAmbientEmber);
}
function bgLoop(){
const dpr = Math.min(window.devicePixelRatio||1, 2);
bgTime += 0.016;
bgCtx.setTransform(dpr,0,0,dpr,0,0);
bgCtx.clearRect(0,0,window.innerWidth,window.innerHeight);
for (const s of stars){
const a = 0.35+0.55*Math.abs(Math.sin(bgTime*s.speed+s.phase));
bgCtx.fillStyle = `rgba(216,232,255,${a.toFixed(2)})`;
bgCtx.beginPath(); bgCtx.arc(s.x,s.y,s.r,0,Math.PI*2); bgCtx.fill();
}
if (state.settings.particles){
for (const e of ambientEmbers){
e.x += e.vx; e.y -= e.vy; e.age++;
const t = e.age/e.maxAge;
const a = (t<0.5 ? t*2 : (1-t)*2) * 0.5;
bgCtx.fillStyle = `rgba(255,138,61,${Math.max(a,0).toFixed(2)})`;
bgCtx.beginPath(); bgCtx.arc(e.x,e.y,e.r,0,Math.PI*2); bgCtx.fill();
if (e.age>=e.maxAge) Object.assign(e, resetAmbientEmber());
}
}
requestAnimationFrame(bgLoop);
}
function spawnClickBurst(x,y){
if (!state.settings.particles) return;
const n = 14;
for (let i=0;i<n;i++){
const ang = Math.random()*Math.PI*2;
const spd = 1.4+Math.random()*3.4;
clickParticles.push({
x, y, vx:Math.cos(ang)*spd, vy:Math.sin(ang)*spd-1.2,
life:1, decay:0.014+Math.random()*0.016,
size:2+Math.random()*3,
color: Math.random()<0.78 ? '255,138,61' : '216,232,255',
});
}
}
function fxLoop(){
const dpr = Math.min(window.devicePixelRatio||1, 2);
fxCtx.setTransform(dpr,0,0,dpr,0,0);
fxCtx.clearRect(0,0,window.innerWidth,window.innerHeight);
for (let i=clickParticles.length-1;i>=0;i--){
const p = clickParticles[i];
p.x += p.vx; p.y += p.vy; p.vy += 0.07; p.life -= p.decay;
if (p.life<=0){ clickParticles.splice(i,1); continue; }
fxCtx.globalAlpha = Math.max(p.life,0);
fxCtx.fillStyle = `rgb(${p.color})`;
fxCtx.beginPath();
fxCtx.arc(p.x,p.y,Math.max(p.size*p.life,0.3),0,Math.PI*2);
fxCtx.fill();
}
fxCtx.globalAlpha = 1;
requestAnimationFrame(fxLoop);
}
function spawnFloatingText(x,y,text){
const el = document.createElement('div');
el.className = 'float-text';
el.textContent = text;
el.style.left = x+'px';
el.style.top = y+'px';
document.body.appendChild(el);
el.addEventListener('animationend', ()=>el.remove());
}
function pulseCore(){
const core = document.getElementById('core-btn');
core.classList.remove('struck');
void core.offsetWidth;
core.classList.add('struck');
}
/* ============================================================
MAIN LOOP
============================================================ */
function maybeRefreshUpgradeVisibility(){
const key = ALL_UPGRADES
.filter(u=>!state.upgradesBought.includes(u.id) && isUpgradeVisible(u, state))
.map(u=>u.id).join(',');
if (key !== lastVisibleUpgradeKey) renderUpgrades();
}
function tick(){
const dt = TICK_MS/1000;
const sps = totalSPS();
if (sps>0){
state.embers += sps*dt;
state.totalEarned += sps*dt;
state.runEarned += sps*dt;
}
state.playtime = (state.playtime||0) + dt;
checkAchievements();
maybeRefreshUpgradeVisibility();
updateHUD();
}
/* ============================================================
EVENTS
============================================================ */
function bindEvents(){
const coreBtn = document.getElementById('core-btn');
coreBtn.addEventListener('click', (e)=>{
let x = e.clientX, y = e.clientY;
if (!x && !y){
const r = coreBtn.getBoundingClientRect();
x = r.left + r.width/2; y = r.top + r.height/2;
}
strikeCore(x,y);
});
document.querySelectorAll('.buy-amt-btn').forEach(btn=>{
btn.addEventListener('click', ()=>{
document.querySelectorAll('.buy-amt-btn').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
const v = btn.dataset.amt;
state.buyAmount = v==='max' ? 'max' : parseInt(v,10);
renderGenerators();
});
});
document.querySelectorAll('.tab').forEach(btn=>{
btn.addEventListener('click', ()=>switchTab(btn.dataset.tab));
});
document.getElementById('prestige-btn').addEventListener('click', ()=>{
const gain = projectedSeeds();
if (gain<=0) return;
document.getElementById('reignite-gain').textContent = formatNumber(gain);
document.getElementById('reignite-newmult').textContent = '×'+(1+(state.seeds+gain)*0.02).toFixed(2);
showModal('reignite-modal');
});
document.getElementById('reignite-confirm').addEventListener('click', doReignite);
document.getElementById('reignite-cancel').addEventListener('click', ()=>closeModal('reignite-modal'));
document.getElementById('offline-close').addEventListener('click', ()=>closeModal('offline-modal'));
document.getElementById('settings-btn').addEventListener('click', ()=>showModal('settings-panel'));
document.getElementById('settings-close').addEventListener('click', ()=>closeModal('settings-panel'));
document.getElementById('btn-save').addEventListener('click', ()=>{ save(); showToast('SAVED','Your progress is secure.'); });
document.getElementById('btn-export').addEventListener('click', exportSave);
document.getElementById('btn-import').addEventListener('click', importSave);
document.getElementById('btn-reset').addEventListener('click', hardReset);
document.getElementById('toggle-sound').addEventListener('click', (e)=>{
state.settings.sound = !state.settings.sound;
e.target.textContent = state.settings.sound ? 'ON' : 'OFF';
e.target.classList.toggle('off', !state.settings.sound);
if (state.settings.sound) { ensureAudio(); playSound('click'); }
});
document.getElementById('toggle-particles').addEventListener('click', (e)=>{
state.settings.particles = !state.settings.particles;
e.target.textContent = state.settings.particles ? 'ON' : 'OFF';
e.target.classList.toggle('off', !state.settings.particles);
});
document.querySelectorAll('.modal-overlay').forEach(ov=>{
ov.addEventListener('click', (e)=>{ if (e.target===ov) ov.classList.add('hidden'); });
});
window.addEventListener('resize', resizeCanvases);
window.addEventListener('beforeunload', save);
}
/* ============================================================
INIT
============================================================ */
function init(){
load();
resizeCanvases();
initBg();
applyOfflineProgress();
renderAll();
bindEvents();
document.getElementById('toggle-sound').textContent = state.settings.sound ? 'ON' : 'OFF';
document.getElementById('toggle-sound').classList.toggle('off', !state.settings.sound);
document.getElementById('toggle-particles').textContent = state.settings.particles ? 'ON' : 'OFF';
document.getElementById('toggle-particles').classList.toggle('off', !state.settings.particles);
document.querySelectorAll('.buy-amt-btn').forEach(b=>{
b.classList.toggle('active', String(state.buyAmount)===b.dataset.amt);
});
requestAnimationFrame(bgLoop);
requestAnimationFrame(fxLoop);
setInterval(tick, TICK_MS);
setInterval(save, AUTOSAVE_MS);
}
document.addEventListener('DOMContentLoaded', init);