cookie-empire / index.html
Mike0021's picture
Cookie Empire: single-file cookie clicker
a96d6d2 verified
Raw History Blame Contribute Delete
29.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>🍪 Cookie Empire</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='46' fill='%23c1803f'/%3E%3Cg fill='%235d3311'%3E%3Ccircle cx='34' cy='34' r='9'/%3E%3Ccircle cx='68' cy='46' r='8'/%3E%3Ccircle cx='44' cy='70' r='9'/%3E%3Ccircle cx='70' cy='72' r='6'/%3E%3C/g%3E%3C/svg%3E" />
<style>
:root{
--bg:#140d0a;
--panel:rgba(255,255,255,.045);
--panel-b:rgba(255,255,255,.09);
--gold:#f5c451;
--gold-d:#c99a2e;
--text:#f6ece4;
--muted:#b39d8f;
--good:#7ee08a;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
font-family:"Segoe UI",system-ui,-apple-system,sans-serif;
color:var(--text);
background:
radial-gradient(1200px 700px at 50% -10%, #4a2a17 0%, transparent 60%),
radial-gradient(900px 600px at 10% 110%, #3a2010 0%, transparent 60%),
var(--bg);
overflow:hidden;
}
.app{
height:100vh;
display:grid;
grid-template-columns:minmax(230px,1fr) minmax(320px,1.4fr) minmax(250px,1fr);
gap:14px;
padding:14px;
}
.panel{
background:var(--panel);
border:1px solid var(--panel-b);
border-radius:16px;
display:flex;
flex-direction:column;
min-height:0;
backdrop-filter:blur(6px);
}
.panel h2{
margin:0;
padding:12px 14px;
font-size:13px;
letter-spacing:.14em;
text-transform:uppercase;
color:var(--muted);
border-bottom:1px solid var(--panel-b);
}
.scroll{overflow-y:auto;padding:10px;display:flex;flex-direction:column;gap:8px;min-height:0}
.scroll::-webkit-scrollbar{width:9px}
.scroll::-webkit-scrollbar-thumb{background:#5a3a26;border-radius:9px}
/* ---------- center ---------- */
.center{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;position:relative;padding:10px;min-height:0}
/* fixed height + tabular digits: the cookie must not move when numbers change */
.count-wrap{text-align:center;line-height:1.1;z-index:3;height:112px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px}
#cookies,#cps,#cpc{font-variant-numeric:tabular-nums}
#cookies{font-size:clamp(24px,4vw,42px);font-weight:700;text-shadow:0 2px 18px rgba(245,196,81,.35)}
#cookies span{font-size:.55em;color:var(--muted);font-weight:500;margin-left:6px}
.sub{color:var(--muted);font-size:13px;margin-top:4px}
.sub b{color:var(--gold);font-weight:600}
.cookie-stage{position:relative;display:grid;place-items:center;flex:0 1 auto;min-height:0}
.cookie{
width:min(38vh,300px);height:min(38vh,300px);
border:0;padding:0;background:none;cursor:pointer;
border-radius:50%;
filter:drop-shadow(0 18px 30px rgba(0,0,0,.55));
transition:transform .08s ease, filter .2s ease;
touch-action:manipulation;
}
/* keep the button's box still (so bots/tests can hit it) and bob the art instead */
.cookie svg{width:100%;height:100%;display:block;pointer-events:none;animation:bob 4.5s ease-in-out infinite}
.cookie:hover{filter:drop-shadow(0 18px 30px rgba(0,0,0,.55)) brightness(1.07)}
/* the button box never moves (so tests can hit it) — only the art reacts */
.cookie:active,.cookie.press{filter:drop-shadow(0 12px 22px rgba(0,0,0,.55)) brightness(.94)}
.cookie:active svg,.cookie.press svg{transform:scale(.93)}
.cookie:focus-visible{outline:3px solid var(--gold);outline-offset:6px}
@keyframes bob{0%,100%{translate:0 0}50%{translate:0 -10px}}
.cookie.shake svg{animation:shake .3s ease}
@keyframes shake{0%,100%{translate:0 0}25%{translate:-6px 2px}75%{translate:6px -2px}}
.fx{position:absolute;inset:0;pointer-events:none;z-index:4}
.pop{
position:absolute;font-weight:800;font-size:20px;color:#fff;
text-shadow:0 2px 8px rgba(0,0,0,.8),0 0 20px rgba(245,196,81,.7);
animation:pop .95s cubic-bezier(.2,.7,.3,1) forwards;white-space:nowrap;
}
@keyframes pop{0%{opacity:1;transform:translate(-50%,-50%) scale(.6)}100%{opacity:0;transform:translate(-50%,-160%) scale(1.15)}}
.crumb{
position:absolute;width:7px;height:7px;border-radius:50%;background:#a9713f;
animation:crumb .7s ease-out forwards;pointer-events:none;
}
@keyframes crumb{0%{opacity:1;transform:translate(-50%,-50%)}100%{opacity:0;transform:translate(calc(-50% + var(--dx)),calc(-50% + var(--dy))) rotate(180deg)}}
/* golden cookie */
.golden{
position:absolute;width:74px;height:74px;border:0;background:none;padding:0;cursor:pointer;z-index:5;
filter:drop-shadow(0 0 22px rgba(245,196,81,.95));
display:none;
}
.golden svg{width:100%;height:100%;display:block;pointer-events:none;animation:goldPulse 1s ease-in-out infinite}
.golden.on,.golden.grabbed{display:block}
.golden.grabbed{pointer-events:none}
.golden.grabbed svg{animation:goldGrab .32s ease-out forwards}
@keyframes goldPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.14)}}
@keyframes goldGrab{0%{transform:scale(1.1);opacity:1}100%{transform:scale(1.8);opacity:0}}
.toast{
position:absolute;top:14px;left:50%;translate:-50% 0;z-index:9;
background:linear-gradient(180deg,#ffd873,#e0a52c);color:#3a2408;font-weight:700;
padding:9px 16px;border-radius:11px;box-shadow:0 10px 26px rgba(0,0,0,.5);
opacity:0;transition:opacity .25s, translate .25s;pointer-events:none;font-size:14px;
}
.toast.on{opacity:1;translate:-50% 8px}
.buff{color:var(--gold);font-weight:700;font-size:13px;height:18px;text-align:center}
/* ---------- items ---------- */
.item{
display:flex;gap:10px;align-items:center;width:100%;text-align:left;
background:rgba(255,255,255,.035);border:1px solid var(--panel-b);color:inherit;
border-radius:12px;padding:8px 10px;cursor:pointer;transition:.14s;font:inherit;
}
.item:hover:not(:disabled){background:rgba(245,196,81,.11);border-color:rgba(245,196,81,.45);transform:translateX(2px)}
.item:disabled{opacity:.42;cursor:not-allowed}
.item.afford{border-color:rgba(126,224,138,.32)}
.item .ico{font-size:24px;width:30px;text-align:center;flex:none;display:flex;flex-direction:column;align-items:center;justify-content:center}
.item .ico .em{line-height:24px;height:24px}
.item .body{flex:1;min-width:0}
.item .name{font-weight:650;font-size:14px}
.item .meta{font-size:11.5px;color:var(--muted);margin-top:2px}
.item .cost{flex:none;text-align:right;font-size:12.5px;font-weight:700;color:var(--gold)}
.item .cost.no{color:#e0716b}
.item .owned{font-size:11px;color:var(--muted);font-weight:500;height:14px;line-height:14px}
.empty{color:var(--muted);font-size:12.5px;text-align:center;padding:16px 8px;line-height:1.5}
.tabs{display:flex;gap:6px;padding:8px 10px 0}
.tab{flex:1;background:none;border:1px solid var(--panel-b);color:var(--muted);padding:6px;border-radius:9px;cursor:pointer;font-size:12px;font-weight:600}
.tab.active{background:rgba(245,196,81,.16);color:var(--gold);border-color:rgba(245,196,81,.5)}
.footer{padding:10px 12px;border-top:1px solid var(--panel-b);display:flex;gap:8px;flex-wrap:wrap;align-items:center;justify-content:center}
.btn{background:rgba(255,255,255,.06);border:1px solid var(--panel-b);color:var(--text);border-radius:9px;padding:6px 11px;font-size:12px;cursor:pointer;font:inherit}
.btn:hover{background:rgba(245,196,81,.16);border-color:rgba(245,196,81,.5)}
.btn.danger:hover{background:rgba(224,113,107,.2);border-color:#e0716b}
#save-note{font-size:11px;color:var(--muted)}
@media (max-width:900px){
.app{grid-template-columns:1fr;grid-template-rows:auto 1fr auto;height:auto;min-height:100vh;overflow:auto}
body{overflow:auto}
.cookie{width:min(45vw,220px);height:min(45vw,220px)}
.panel{max-height:42vh}
}
</style>
</head>
<body>
<div class="app">
<!-- UPGRADES -->
<section class="panel">
<h2>Upgrades</h2>
<div class="scroll" id="upgrades"></div>
</section>
<!-- COOKIE -->
<section class="panel center">
<div class="toast" id="toast"></div>
<div class="count-wrap">
<div id="cookies">0<span>cookies</span></div>
<div class="sub">per second: <b id="cps">0</b> · per click: <b id="cpc">1</b></div>
<div class="buff" id="buff"></div>
</div>
<div class="cookie-stage">
<button class="cookie" id="cookie" aria-label="Bake a cookie">
<svg viewBox="0 0 200 200" aria-hidden="true">
<defs>
<radialGradient id="dough" cx="38%" cy="32%" r="78%">
<stop offset="0%" stop-color="#e0a45f"/>
<stop offset="65%" stop-color="#c1803f"/>
<stop offset="100%" stop-color="#93551f"/>
</radialGradient>
</defs>
<circle cx="100" cy="100" r="92" fill="url(#dough)"/>
<circle cx="100" cy="100" r="92" fill="none" stroke="#7d4517" stroke-width="4" opacity=".65"/>
<g fill="#57300f">
<circle cx="66" cy="62" r="12"/><circle cx="134" cy="80" r="10"/>
<circle cx="86" cy="122" r="13"/><circle cx="140" cy="132" r="11"/>
<circle cx="60" cy="140" r="9"/><circle cx="108" cy="168" r="8"/>
<circle cx="152" cy="48" r="7"/><circle cx="44" cy="98" r="8"/>
</g>
<g fill="#8a5a2b" opacity=".55">
<circle cx="80" cy="88" r="4"/><circle cx="120" cy="110" r="5"/>
<circle cx="100" cy="46" r="4"/><circle cx="46" cy="120" r="5"/>
</g>
</svg>
</button>
<button class="golden" id="golden" aria-label="Golden cookie">
<svg viewBox="0 0 100 100" aria-hidden="true">
<circle cx="50" cy="50" r="46" fill="#f7d777" stroke="#b8860b" stroke-width="3"/>
<g fill="#c99a2e">
<circle cx="34" cy="34" r="8"/><circle cx="66" cy="44" r="7"/>
<circle cx="46" cy="64" r="9"/><circle cx="72" cy="68" r="6"/>
</g>
</svg>
</button>
<div class="fx" id="fx"></div>
</div>
<div class="footer">
<button class="btn" id="save">💾 Save</button>
<button class="btn" id="export">⬆ Export</button>
<button class="btn" id="import">⬇ Import</button>
<button class="btn danger" id="reset">Reset</button>
<span id="save-note">autosaves</span>
</div>
</section>
<!-- STORE -->
<section class="panel">
<h2>Bakery</h2>
<div class="tabs">
<button class="tab active" data-tab="build">Buildings</button>
<button class="tab" data-tab="stats">Stats</button>
</div>
<div class="tabs" id="bulk-tabs">
<button class="tab active" data-bulk="1">×1</button>
<button class="tab" data-bulk="10">×10</button>
<button class="tab" data-bulk="100">×100</button>
<button class="tab" data-bulk="max">Max</button>
</div>
<div class="scroll" id="store"></div>
</section>
</div>
<script>
(() => {
"use strict";
/* ---------------- data ---------------- */
const BUILDINGS = [
{ id:"cursor", ico:"👆", name:"Cursor", cost:15, cps:0.1 },
{ id:"grandma", ico:"👵", name:"Grandma", cost:100, cps:1 },
{ id:"farm", ico:"🌾", name:"Farm", cost:1100, cps:8 },
{ id:"mine", ico:"⛏️", name:"Mine", cost:12000, cps:47 },
{ id:"factory", ico:"🏭", name:"Factory", cost:130000, cps:260 },
{ id:"bank", ico:"🏦", name:"Bank", cost:1.4e6, cps:1400 },
{ id:"temple", ico:"⛩️", name:"Temple", cost:2e7, cps:7800 },
{ id:"wizard", ico:"🧙", name:"Wizard Tower", cost:3.3e8, cps:44000 },
{ id:"ship", ico:"🚀", name:"Shipment", cost:5.1e9, cps:260000 },
{ id:"portal", ico:"🌀", name:"Alchemy Lab", cost:7.5e10, cps:1.6e6 },
{ id:"time", ico:"⏳", name:"Time Machine", cost:1e12, cps:1e7 },
];
const CLICK_UPGRADES = [
{ id:"index", ico:"☝️", name:"Reinforced Index Finger", cost:100, desc:"Cookie clicks give ×2", mult:2, req:s=>s.clickCount>=10 },
{ id:"carpal", ico:"🖐️", name:"Carpal Tunnel Prevention", cost:500, desc:"Cookie clicks give ×2", mult:2, req:s=>s.upgrades.index },
{ id:"thumb", ico:"👍", name:"Thousand Fingers", cost:10000, desc:"Cookie clicks give ×2", mult:2, req:s=>totalBuildings(s)>=10 },
{ id:"ambi", ico:"🤲", name:"Ambidextrous", cost:1e6, desc:"Cookie clicks give ×2", mult:2, req:s=>s.upgrades.thumb },
{ id:"robofin", ico:"🦾", name:"Robotic Fingers", cost:1e9, desc:"Cookie clicks give ×2", mult:2, req:s=>s.upgrades.ambi },
];
const GLOBAL_UPGRADES = [
{ id:"flour", ico:"🌾", name:"Better Flour", cost:5000, desc:"All production +10%", mult:1.10, req:s=>totalBuildings(s)>=3 },
{ id:"sugar", ico:"🍬", name:"Liquid Sugar", cost:250000, desc:"All production +10%", mult:1.10, req:s=>totalBuildings(s)>=8 },
{ id:"yeast", ico:"🫧", name:"Hyperactive Yeast", cost:5e7, desc:"All production +15%", mult:1.15, req:s=>totalBuildings(s)>=15 },
{ id:"ore", ico:"💎", name:"Cookie Ore", cost:1e10, desc:"All production +15%", mult:1.15, req:s=>totalBuildings(s)>=25 },
{ id:"star", ico:"⭐", name:"Starlight Icing", cost:2e12, desc:"All production +20%", mult:1.20, req:s=>totalBuildings(s)>=40 },
{ id:"void", ico:"🌌", name:"Void Essence", cost:5e14, desc:"All production +25%", mult:1.25, req:s=>totalBuildings(s)>=60 },
];
// auto-generated per-building tier upgrades
const TIERS = [];
for (const b of BUILDINGS) {
TIERS.push({ id:b.id+"_t1", b:b.id, ico:b.ico, name:b.name+" ×2", cost:b.cost*10, mult:2, req:s=>s.buildings[b.id]>=10, desc:`${b.name} production ×2` });
TIERS.push({ id:b.id+"_t2", b:b.id, ico:b.ico, name:b.name+" ×2 II", cost:b.cost*100, mult:2, req:s=>s.buildings[b.id]>=25, desc:`${b.name} production ×2` });
}
const ALL_UPGRADES = [...CLICK_UPGRADES, ...GLOBAL_UPGRADES, ...TIERS];
const UMAP = Object.fromEntries(ALL_UPGRADES.map(u=>[u.id,u]));
const BUDDY = { // cursor bonus from grandmas, classic wink
cursor:(s)=>Math.floor(s.buildings.grandma/2),
};
const SAVE_KEY = "cookie-empire-v1";
/* ---------------- state ---------------- */
const blank = () => ({
cookies:0, totalBaked:0, clickCount:0, handMade:0,
buildings:Object.fromEntries(BUILDINGS.map(b=>[b.id,0])),
upgrades:{}, frenzyUntil:0, frenzyMult:1, started:Date.now(),
});
let S = blank();
let buffUntil = 0; // click buff ("Click Frenzy")
let clickBuffMult = 1;
let goldenTimer = 25 + Math.random()*40;
/* ---------------- helpers ---------------- */
const UNITS = ["","thousand","million","billion","trillion","quadrillion","quintillion","sextillion","septillion","octillion","nonillion","decillion"];
function fmt(n){
if(!isFinite(n)) return "∞";
if(n < 1000) return (n<10 && n%1!==0) ? n.toFixed(1) : Math.floor(n).toLocaleString("en-US");
let t = 0;
while(n >= 1000 && t < UNITS.length-1){ n/=1000; t++; }
return n.toFixed(n<10?2:n<100?1:0) + " " + UNITS[t];
}
const fmtInt = n => Math.floor(n).toLocaleString("en-US");
const totalBuildings = s => BUILDINGS.reduce((a,b)=>a+s.buildings[b.id],0);
const owned = (s,id)=> s.buildings[id]+(BUDDY[id] ? BUDDY[id](s) : 0);
function buildingCps(s, b){
let base = owned(s,b.id) * b.cps;
for (const t of TIERS) if (t.b===b.id && s.upgrades[t.id]) base *= t.mult;
return base;
}
function globalMult(s){
let m = 1;
for (const u of GLOBAL_UPGRADES) if (s.upgrades[u.id]) m *= u.mult;
if (Date.now() < s.frenzyUntil) m *= s.frenzyMult;
return m;
}
function cps(s){ return BUILDINGS.reduce((a,b)=>a+buildingCps(s,b),0) * globalMult(s); }
function cpc(s){
let m = 1;
for (const u of CLICK_UPGRADES) if (s.upgrades[u.id]) m *= u.mult;
if (Date.now() < buffUntil) m *= clickBuffMult;
return (1*m) + cps(s)*0.02; // clicks also scale a little with production
}
function price(b, n){ return Math.ceil(b.cost * Math.pow(1.15, n)); }
// how many and how much the current buy-mode costs for building b
function bulkInfo(s, b, mode){
if(mode === 1) return { n:1, cost:price(b, s.buildings[b.id]) };
if(mode === "max"){
let n = 0, cost = 0;
while(n < 500){
const c = price(b, s.buildings[b.id]+n);
if(cost + c > s.cookies) break;
cost += c; n++;
}
if(n === 0){ n = 1; cost = price(b, s.buildings[b.id]); }
return { n, cost };
}
let cost = 0;
for(let i=0;i<mode;i++) cost += price(b, s.buildings[b.id]+i);
return { n:mode, cost };
}
const bulkLabel = () => bulk === 1 ? "" : (bulk === "max" ? " (max)" : " ×"+bulk);
let bulk = 1;
/* ---------------- actions ---------------- */
function addCookies(n){ S.cookies += n; S.totalBaked += n; }
function clickCookie(x, y, button){
const gain = cpc(S);
addCookies(gain); S.clickCount++; S.handMade += gain;
spawnPop(x, y, "+"+fmt(gain));
for(let i=0;i<6;i++) spawnCrumb(x,y);
button?.classList.add("press");
setTimeout(()=>button?.classList.remove("press"), 80);
paintHeader(); renderStore(); renderUpgrades();
}
function buyBuilding(id){
const b = BUILDINGS.find(x=>x.id===id);
const { n, cost } = bulkInfo(S, b, bulk);
if(n <= 0 || cost <= 0 || S.cookies < cost) return;
S.cookies -= cost;
S.buildings[id] = Math.min(1000, S.buildings[id] + n);
renderStore(); renderUpgrades(); paintHeader();
}
function buyUpgrade(id){
const u = UMAP[id];
if(!u || S.upgrades[id] || S.cookies < u.cost || !u.req(S)) return;
S.cookies -= u.cost;
S.upgrades[id] = true;
toast(`${u.ico} ${u.name}`);
renderUpgrades(); renderStore(); paintHeader();
}
/* ---------------- visuals ---------------- */
const fx = document.getElementById("fx");
function spawnPop(x, y, text){
const el = document.createElement("div");
el.className = "pop";
el.textContent = text;
el.style.left = x+"px"; el.style.top = y+"px";
fx.appendChild(el);
setTimeout(()=>el.remove(), 950);
}
function spawnCrumb(x, y){
const el = document.createElement("div");
el.className = "crumb";
el.style.left = x+"px"; el.style.top = y+"px";
el.style.setProperty("--dx", (Math.random()*90-45)+"px");
el.style.setProperty("--dy", (-Math.random()*70-15)+"px");
fx.appendChild(el);
setTimeout(()=>el.remove(), 720);
}
let toastT;
const toastEl = document.getElementById("toast");
function toast(msg){
toastEl.textContent = msg;
toastEl.classList.add("on");
clearTimeout(toastT);
toastT = setTimeout(()=>toastEl.classList.remove("on"), 2600);
}
/* ---------------- golden cookie ---------------- */
const goldenEl = document.getElementById("golden");
function spawnGolden(){
goldenEl.classList.remove("grabbed");
goldenEl.classList.add("on");
goldenEl.style.left = (8 + Math.random()*(fx.clientWidth - 84)) + "px";
goldenEl.style.top = (8 + Math.random()*(fx.clientHeight - 84)) + "px";
goldenEl.dataset.until = Date.now() + 13000;
}
function hideGolden(grabbed){
goldenEl.classList.remove("on");
goldenEl.dataset.until = 0;
goldenTimer = 45 + Math.random()*75;
if(grabbed){ // let the "grabbed" flourish play out
goldenEl.classList.add("grabbed");
setTimeout(()=>goldenEl.classList.remove("grabbed"), 340);
}
}
function clickGolden(){
if(!goldenEl.classList.contains("on")) return;
hideGolden(true);
if(Math.random() < 0.5){
const gain = Math.max(cpc(S)*13, S.cookies*0.15, 13*Math.max(1, cps(S)));
addCookies(gain);
toast("✨ Lucky! +" + fmt(gain) + " cookies");
} else {
S.frenzyMult = 7; S.frenzyUntil = Date.now()+7700;
toast("🔥 Frenzy! production ×7 for 7.7s");
}
paintHeader();
}
goldenEl.addEventListener("click", clickGolden);
/* ---------------- rendering ---------------- */
const el = {
cookies:document.getElementById("cookies"),
cps:document.getElementById("cps"),
cpc:document.getElementById("cpc"),
buff:document.getElementById("buff"),
upgrades:document.getElementById("upgrades"),
store:document.getElementById("store"),
cookie:document.getElementById("cookie"),
};
function paintHeader(){
el.cookies.innerHTML = fmt(S.cookies) + "<span>cookies</span>";
el.cps.textContent = fmt(cps(S));
el.cpc.textContent = fmt(cpc(S));
const now = Date.now();
const parts = [];
if(now < S.frenzyUntil) parts.push("🔥 Frenzy ×7 · "+ ((S.frenzyUntil-now)/1000).toFixed(1)+"s");
if(now < buffUntil) parts.push("⚡ Click Frenzy ×"+clickBuffMult+" · "+ ((buffUntil-now)/1000).toFixed(1)+"s");
el.buff.textContent = parts.join(" ");
}
function upgradeAvailable(u){
return !S.upgrades[u.id] && u.req(S);
}
let upSig = ""; // which upgrades are visible
const upRows = new Map(); // id -> {btn, cost}
function renderUpgrades(){
const list = ALL_UPGRADES
.filter(u=>!S.upgrades[u.id] && u.req(S))
.sort((a,b)=>a.cost-b.cost)
.slice(0,40);
if(!list.length){
if(upSig !== "empty"){ el.upgrades.innerHTML = `<div class="empty">No upgrades yet.<br>Bake more cookies and buy buildings to unlock them.</div>`; upSig = "empty"; upRows.clear(); }
return;
}
const sig = list.map(u=>u.id).join(",");
if(sig !== upSig){ // rebuild only when the set changes
el.upgrades.innerHTML = "";
upRows.clear();
for(const u of list){
const btn = document.createElement("button");
btn.className = "item";
btn.innerHTML = `
<div class="ico">${u.ico}</div>
<div class="body">
<div class="name">${u.name}</div>
<div class="meta">${u.desc}</div>
</div>
<div class="cost">🍪 ${fmt(u.cost)}</div>`;
btn.onclick = () => buyUpgrade(u.id);
el.upgrades.appendChild(btn);
upRows.set(u.id, { btn, cost:btn.lastElementChild });
}
upSig = sig;
}
for(const u of list){ // cheap in-place refresh
const r = upRows.get(u.id);
if(!r) continue;
const can = S.cookies >= u.cost;
r.btn.disabled = !can;
r.btn.classList.toggle("afford", can);
r.cost.classList.toggle("no", !can);
}
}
const storeRows = new Map(); // id -> {btn, ico, meta, cost}
let storeBuilt = false;
function buildStore(){
el.store.innerHTML = "";
storeRows.clear();
for(const b of BUILDINGS){
const btn = document.createElement("button");
btn.className = "item";
btn.innerHTML = `
<div class="ico"><span class="em">${b.ico}</span><div class="owned"></div></div>
<div class="body">
<div class="name">${b.name}</div>
<div class="meta"></div>
</div>
<div class="cost"></div>`;
btn.onclick = () => buyBuilding(b.id);
el.store.appendChild(btn);
storeRows.set(b.id, {
btn,
owned:btn.querySelector(".owned"),
meta:btn.querySelector(".meta"),
cost:btn.querySelector(".cost"),
});
}
storeBuilt = true;
}
function renderStore(){
if(storeTab === "stats"){ renderStats(); return; }
if(!storeBuilt || el.store.dataset.view !== "build"){ buildStore(); el.store.dataset.view = "build"; }
for(const b of BUILDINGS){
const r = storeRows.get(b.id);
const n = S.buildings[b.id];
const { cost } = bulkInfo(S, b, bulk);
const affordable = S.cookies >= cost;
const each = owned(S,b.id) ? buildingCps(S,b)/owned(S,b.id) : b.cps;
r.owned.textContent = n ? n : "";
r.meta.textContent = `${fmt(buildingCps(S,b))} cookies/s · ${fmt(each)} each`;
r.cost.textContent = `🍪 ${fmt(cost)}${bulkLabel()}`;
r.btn.disabled = !affordable;
r.btn.classList.toggle("afford", affordable);
r.cost.classList.toggle("no", !affordable);
}
}
function renderStats(){
const t = (Date.now()-S.started)/1000;
const rows = [
["Cookies in bank", fmt(S.cookies)],
["Cookies baked (all time)", fmt(S.totalBaked)],
["Cookies per second", fmt(cps(S))],
["Cookies per click", fmt(cpc(S))],
["Hand-made cookies", fmt(S.handMade)],
["Total clicks", fmtInt(S.clickCount)],
["Buildings owned", fmtInt(totalBuildings(S))],
["Upgrades bought", fmtInt(Object.keys(S.upgrades).length) + " / " + ALL_UPGRADES.length],
["Golden cookies missed", fmtInt(S.missedGoldens||0)],
["Playing for", Math.floor(t/3600)+"h "+Math.floor(t/60)%60+"m "+Math.floor(t%60)+"s"],
];
el.store.dataset.view = "stats";
el.store.innerHTML = `<div style="padding:4px 6px;font-size:13px;line-height:1.9">
${rows.map(([k,v])=>`<div style="display:flex;justify-content:space-between;gap:10px;border-bottom:1px dashed rgba(255,255,255,.08)"><span style="color:var(--muted)">${k}</span><b>${v}</b></div>`).join("")}
</div>`;
}
let storeTab = "build";
document.querySelectorAll(".tab").forEach(t=>{
t.onclick = () => {
storeTab = t.dataset.tab;
document.querySelectorAll(".tabs > .tab[data-tab]").forEach(x=>x.classList.toggle("active", x===t));
document.getElementById("bulk-tabs").style.display = storeTab === "build" ? "" : "none";
renderStore();
};
});
/* ---------------- input ---------------- */
el.cookie.addEventListener("pointerdown", (e)=>{
const r = fx.getBoundingClientRect();
clickCookie(e.clientX-r.left, e.clientY-r.top, el.cookie);
el.cookie.classList.add("shake");
setTimeout(()=>el.cookie.classList.remove("shake"), 300);
});
window.addEventListener("keydown", (e)=>{
if(e.code === "Space" && e.target === document.body){
e.preventDefault();
const r = fx.getBoundingClientRect();
clickCookie(r.width/2, r.height/2, el.cookie);
}
});
document.querySelectorAll("[data-bulk]").forEach(b=>{
b.onclick = ()=>{
bulk = b.dataset.bulk === "max" ? "max" : Number(b.dataset.bulk);
document.querySelectorAll("#bulk-tabs .tab").forEach(x=>x.classList.toggle("active", x===b));
renderStore();
};
});
/* ---------------- save / load ---------------- */
function save(quiet){
try{
localStorage.setItem(SAVE_KEY, JSON.stringify(S));
if(!quiet){ document.getElementById("save-note").textContent = "saved ✓"; }
}catch(e){ /* ignore */ }
}
function load(){
try{
const raw = localStorage.getItem(SAVE_KEY);
if(!raw) return false;
const d = JSON.parse(raw);
S = Object.assign(blank(), d);
S.buildings = Object.assign(blank().buildings, d.buildings||{});
// offline progress: earn at 50% rate for up to 2 hours
if(S.lastSave){
const secs = Math.min((Date.now()-S.lastSave)/1000, 7200);
if(secs > 60){
const gain = cps(S) * secs * 0.5;
if(gain > 0){ addCookies(gain); setTimeout(()=>toast("😴 While you were away: +"+fmt(gain)), 600); }
}
}
return true;
}catch(e){ return false; }
}
function reset(all){
if(!confirm(all ? "Erase ALL progress, including upgrades?" : "Start a new bakery?")) return;
localStorage.removeItem(SAVE_KEY);
S = blank();
if(all){ /* same */ }
renderUpgrades(); renderStore(); paintHeader();
toast("Fresh dough 🍪");
}
document.getElementById("save").onclick = ()=>save(false);
document.getElementById("reset").onclick = ()=>reset(true);
document.getElementById("export").onclick = async ()=>{
const code = btoa(JSON.stringify(S));
try{ await navigator.clipboard.writeText(code); toast("Save code copied 📋"); }
catch(e){ prompt("Save code:", code); }
};
document.getElementById("import").onclick = ()=>{
const code = prompt("Paste a save code:");
if(!code) return;
try{
const d = JSON.parse(atob(code.trim()));
S = Object.assign(blank(), d);
S.buildings = Object.assign(blank().buildings, d.buildings||{});
S.lastSave = undefined;
renderUpgrades(); renderStore(); paintHeader(); save(true);
toast("Save loaded ✓");
}catch(e){ toast("⚠️ Invalid code"); }
};
/* ---------------- loop ---------------- */
load();
renderUpgrades(); renderStore(); paintHeader();
let last = performance.now();
setInterval(()=>{
const now = performance.now();
const dt = Math.min((now-last)/1000, 1);
last = now;
// passive production
if(cps(S) > 0) addCookies(cps(S)*dt);
// golden cookies
if(Date.now() < (Number(goldenEl.dataset.until)||0)){
// still visible; missed?
} else if(goldenEl.classList.contains("on")){
hideGolden(false); S.missedGoldens = (S.missedGoldens||0)+1;
} else {
goldenTimer -= dt;
if(goldenTimer <= 0) spawnGolden();
}
paintHeader();
}, 100);
setInterval(()=>{ renderStore(); renderUpgrades(); }, 250);
setInterval(()=>{ S.lastSave = Date.now(); save(true); }, 10000);
window.addEventListener("beforeunload", ()=>{ S.lastSave = Date.now(); save(true); });
// hidden: click the cookie 100 times fast? nah. keep a friendly greeting
console.log("%c🍪 Cookie Empire ready. Type cookieEmpire.state() for cheats.", "color:#f5c451;font-weight:bold");
window.cookieEmpire = {
state:()=>S, save:()=>save(false), reset:()=>reset(true),
cheat:(n)=>{ addCookies(n); paintHeader(); renderStore(); renderUpgrades(); },
golden:()=>spawnGolden(),
};
})();
</script>
</body>
</html>