Spaces:
Running
Running
Download index.html from Mike0021/cookie-empire: direct link, hf CLI and curl.
- Browser
- Download file 29.8 kB
-
https://huggingface.co/spaces/Mike0021/cookie-empire/resolve/main/index.html
- Command line
-
hf download hf://spaces/Mike0021/cookie-empire/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Mike0021/cookie-empire/resolve/main/index.html
29.8 kB
| <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> | |