game AFBN COSMOS <!DOCTYPE html> <html lang=”en”> <head> <meta charset=”UTF-8″> <meta name=”viewport” content=”width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover”> <meta http-equiv=”X-UA-Compatible” content=”IE=edge”> <title>AFBN COSMOS</title> <script src=”https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js”></script> <link rel=”stylesheet” href=”https://unpkg.com/leaflet@1.9.4/dist/leaflet.css”/> <script src=”https://unpkg.com/leaflet@1.9.4/dist/leaflet.js”></script> <script src=”https://unpkg.com/@solana/web3.js@1.95.3/dist/index.iife.min.js”></script> <script>if (!window.solanaWeb3) { document.write(‘<script src=”https://cdn.jsdelivr.net/npm/@solana/web3.js@1.95.3/dist/index.iife.min.js”><\/script>’); }</script> <script> (function(){ if(typeof window.Buffer===”undefined”){ window.Buffer={}; window.Buffer.from=function(data,enc){ if(data instanceof Uint8Array)return data; if(typeof data===”string”){ if(enc===”base64″||enc===”base64url”){ var s=String(data).replace(/-/g,”+”).replace(/_/g,”/”); while(s.length%4!==0)s+=”=”; var bin=atob(s); var out=new Uint8Array(bin.length); for(var i=0;i<bin.length;i++)out[i]=bin.charCodeAt(i); return out; } var enc2=new TextEncoder(); return enc2.encode(data); } if(Array.isArray(data))return new Uint8Array(data); return new Uint8Array(0); }; window.Buffer.alloc=function(n){return new Uint8Array(n||0);}; window.Buffer.isBuffer=function(b){return b instanceof Uint8Array;}; } if(typeof window.global===”undefined”)window.global=window; if(typeof window.process===”undefined”)window.process={env:{}}; })(); </script> <script>window.MWA_transact = null; window.MWA_getWallets = null; window.MWA_READY = false; window.WALLET_STANDARD_WALLETS = []; window.addEventListener(‘mwa-ready’, function() { window.MWA_READY = true; });</script> <script>try { (function() { function captureWallets() { try { if (window.navigator && window.navigator.wallets && typeof window.navigator.wallets.getWallets === ‘function’) { var ws = window.navigator.wallets.getWallets(); if (ws && ws.length) { window.WALLET_STANDARD_WALLETS = ws.slice(); } } } catch(e) {} } captureWallets(); setInterval(captureWallets, 250); try { if (window.navigator && window.navigator.wallets && typeof window.navigator.wallets.on === ‘function’) { window.navigator.wallets.on(‘register’, function() { captureWallets(); }); } } catch(e) {} })(); } catch(e) {}</script> <script type=”module”>(async function() { try { var mod = await import(‘https://esm.sh/@solana-mobile/mobile-wallet-adapter-protocol-web3js@2.2.9?target=es2020’); if (mod && mod.transact) window.MWA_transact = mod.transact; if (mod && mod.getWallets) window.MWA_getWallets = mod.getWallets; } catch (e) { try { var mod2 = await import(‘https://cdn.jsdelivr.net/npm/@solana-mobile/mobile-wallet-adapter-protocol-web3js@2.2.9/+esm’); if (mod2 && mod2.transact) window.MWA_transact = mod2.transact; if (mod2 && mod2.getWallets) window.MWA_getWallets = mod2.getWallets; } catch (e2) {} } window.dispatchEvent(new Event(‘mwa-ready’)); })();</script> <style> html,body{margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:#000;touch-action:none;-webkit-text-size-adjust:100%;font-family:system-ui,-apple-system,BlinkMacSystemFont,”Segoe UI”,Roboto,sans-serif} input,button,select,textarea{-webkit-user-select:text;user-select:text} #c{position:fixed;top:0;left:0;width:100%;height:100%;display:block;z-index:1} #s{position:fixed;top:0;left:0;width:100%;height:100%;overflow-y:scroll;overflow-x:hidden;z-index:2;-webkit-overflow-scrolling:touch;overscroll-behavior:none;scroll-snap-type:y proximity} #sp{width:1px;pointer-events:none} .snap{width:1px;height:90vh;scroll-snap-align:center;scroll-snap-stop:always;pointer-events:none} #hud{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:10;color:#e8f0ff} #logo{position:absolute;top:18px;left:22px;font-size:22px;font-weight:700;letter-spacing:6px;color:#cce6ff;text-shadow:0 0 18px rgba(120,180,255,0.85),0 0 40px rgba(80,140,255,0.4);opacity:0.92} #logo span{display:inline-block;width:10px;height:10px;border-radius:50%;background:radial-gradient(circle at 35% 35%,#fff,#6af,#28f);margin-left:10px;box-shadow:0 0 12px #4af,0 0 24px #28f;animation:pulse 2.4s ease-in-out infinite} @keyframes pulse{0%,100%{transform:scale(1);opacity:0.9}50%{transform:scale(1.35);opacity:1}} #toast{position:absolute;left:50%;right:auto;top:100px;bottom:auto;transform:translate(-50%,-20px);width:calc(100% – 44px);max-width:340px;box-sizing:border-box;padding:14px 44px 14px 20px;background:rgba(4,10,22,0.78);border:1px solid rgba(120,180,255,0.3);border-radius:14px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);opacity:0;pointer-events:none;transition:opacity 0.4s ease,transform 0.4s ease;text-align:center;box-shadow:0 8px 32px rgba(0,12,40,0.5);z-index:16} #toast.visible{opacity:0.55;transform:translate(-50%,0);pointer-events:auto} #toast.visible:hover,#toast.visible:active,#toast.visible:focus-within{opacity:1} .toast-action{display:none;margin-top:10px;width:100%;background:linear-gradient(135deg,#1a3a7a,#2a5ab0);border:none;color:#fff;padding:10px 16px;border-radius:999px;font-size:12px;letter-spacing:1.2px;cursor:pointer;font-weight:700;pointer-events:auto} .toast-action:hover,.toast-action:active{filter:brightness(1.15)} #toast h3{margin:0 0 6px;font-size:16px;font-weight:600;color:#e0f0ff;letter-spacing:1.2px} #toast p{margin:0;font-size:12.5px;line-height:1.45;color:#a0c0e0} .toast-close{position:absolute;top:8px;right:8px;width:26px;height:26px;border-radius:50%;background:rgba(255,40,40,0.18);border:1.5px solid rgba(255,80,80,0.75);color:#ff8080;font-size:15px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;line-height:1;pointer-events:auto;transition:background 0.2s ease,color 0.2s ease;z-index:2} .toast-close:hover,.toast-close:active{background:#ff4444;color:#000} #game-notifications{position:absolute;left:50%;right:auto;top:60px;bottom:auto;transform:translateX(-50%);width:min(360px,calc(100% – 20px));display:flex;flex-direction:column-reverse;gap:6px;z-index:45;pointer-events:none} .gnote{position:relative;pointer-events:auto;box-sizing:border-box;width:100%;padding:9px 32px 9px 12px;background:rgba(4,10,22,0.8);border:1px solid rgba(120,180,255,0.42);border-radius:12px;box-shadow:0 6px 24px rgba(0,12,40,0.55);text-align:left;animation:gnoteIn 0.28s ease;opacity:0.62;transition:opacity 0.2s ease} .gnote:hover,.gnote:active{opacity:1} .gnote h4{margin:0 0 3px;font-size:11.5px;letter-spacing:1px;color:#e0f0ff;font-weight:700} .gnote p{margin:0;font-size:10.5px;line-height:1.35;color:#a0c0e0} .gnote .gnote-x{position:absolute;top:4px;right:4px;width:22px;height:22px;border-radius:50%;background:rgba(255,40,40,0.18);border:1px solid rgba(255,80,80,0.7);color:#ff8080;font-size:13px;font-weight:700;line-height:1;display:flex;align-items:center;justify-content:center;padding:0;cursor:pointer} .gnote .gnote-x:hover{background:#ff4444;color:#000} .gnote .gnote-action{display:block;width:100%;margin-top:6px;background:linear-gradient(135deg,#1a3a7a,#2a5ab0);border:none;color:#fff;padding:8px 12px;border-radius:999px;font-size:10.5px;letter-spacing:0.8px;cursor:pointer;font-weight:700;pointer-events:auto} .gnote .gnote-action:hover,.gnote .gnote-action:active{filter:brightness(1.15)} .gnote.leaving{animation:gnoteOut 0.24s ease forwards} @keyframes gnoteIn{from{opacity:0;transform:translateY(-12px)}to{opacity:0.62;transform:translateY(0)}} @keyframes gnoteOut{to{opacity:0;transform:translateY(-10px)}} #mute-btn{position:absolute;bottom:22px;left:22px;pointer-events:auto;height:42px;width:42px;border-radius:999px;background:rgba(8,16,32,0.72);border:1px solid rgba(120,180,255,0.35);color:#d0e8ff;font-size:18px;cursor:pointer;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);display:flex;align-items:center;justify-content:center;transition:all 0.25s ease;padding:0;font-weight:700;letter-spacing:1px} #mute-btn:hover{background:rgba(20,40,80,0.85);border-color:rgba(160,210,255,0.6)} #mute-btn.off{opacity:0.6;border-color:rgba(255,100,100,0.55)} #profile-btn{position:absolute;bottom:22px;right:22px;pointer-events:auto;height:42px;width:42px;border-radius:999px;background:rgba(8,16,32,0.72);border:1px solid rgba(120,180,255,0.35);color:#d0e8ff;font-size:18px;cursor:pointer;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);display:flex;align-items:center;justify-content:center;transition:all 0.25s ease;padding:0;font-weight:700;letter-spacing:1px} #profile-btn:hover{background:rgba(20,40,80,0.85);border-color:rgba(160,210,255,0.6)} #fps{position:absolute;top:18px;right:22px;font-size:11px;color:rgba(160,200,255,0.45);letter-spacing:1px} .hbtn{position:absolute;left:50%;transform:translateX(-50%);background:rgba(8,16,32,0.85);border:1px solid rgba(120,180,255,0.45);color:#d8ecff;padding:10px 22px;border-radius:999px;font-size:12px;letter-spacing:1.4px;cursor:pointer;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);box-shadow:0 0 22px rgba(60,120,255,0.25);opacity:0;pointer-events:none;transition:opacity 0.3s ease,background 0.25s ease;white-space:normal;word-wrap:break-word;text-align:center;max-width:calc(100% – 140px);line-height:1.35;text-transform:uppercase;z-index:18} .hbtn.show{opacity:1;pointer-events:auto} .hbtn:hover{background:rgba(20,40,80,0.95);border-color:rgba(160,210,255,0.75)} #bunny-btn{bottom:22px;border-color:rgba(255,180,220,0.6);color:#ffe8f6;font-size:13px;padding:10px 22px} #cosmos-controls{position:absolute;bottom:80px;left:50%;transform:translateX(-50%);display:flex;gap:8px;pointer-events:auto;z-index:100;opacity:0.8;transition:opacity 0.3s ease;flex-wrap:wrap;justify-content:center;width:min(92vw,680px)} #cosmos-controls:hover{opacity:1} .c-ctrl{background:rgba(8,16,32,0.5);border:1px solid rgba(120,180,255,0.3);color:#d0e8ff;padding:6px 10px;border-radius:999px;font-size:10px;letter-spacing:1px;cursor:pointer;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);white-space:nowrap;font-weight:600} .c-ctrl:hover{background:rgba(20,40,80,0.8);border-color:rgba(160,210,255,0.6)} #worlds-btn{position:absolute;top:62px;left:22px;pointer-events:auto;background:rgba(8,16,32,0.72);border:1px solid rgba(120,180,255,0.35);color:#d0e8ff;padding:8px 16px;border-radius:999px;font-size:12px;letter-spacing:1.4px;cursor:pointer;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)} #worlds-btn:hover{background:rgba(20,40,80,0.85);border-color:rgba(160,210,255,0.6)} #wallet-bar{position:absolute;top:62px;right:22px;pointer-events:auto;display:flex;gap:8px;z-index:18} #wallet-btn{background:rgba(8,16,32,0.78);border:1px solid rgba(120,180,255,0.42);color:#d0e8ff;padding:8px 16px;border-radius:999px;font-size:12px;letter-spacing:1.4px;cursor:pointer;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);font-weight:700} #wallet-btn.connected{border-color:rgba(120,255,180,0.75);color:#c8ffe0;box-shadow:0 0 18px rgba(80,255,160,0.3)} #wallet-btn:hover{background:rgba(20,40,80,0.85);border-color:rgba(160,210,255,0.65)} #shop-hud-btn{background:linear-gradient(135deg,#0a4a3a,#00a87a);border:1px solid rgba(120,255,200,0.7);color:#fff;padding:8px 16px;border-radius:999px;font-size:12px;letter-spacing:1.4px;cursor:pointer;font-weight:700;box-shadow:0 0 18px rgba(80,255,180,0.35)} #shop-hud-btn:hover{filter:brightness(1.15)} #welcome{position:fixed;inset:0;background:rgba(0,0,4,0.93);z-index:100;display:flex;align-items:center;justify-content:center;pointer-events:auto;transition:opacity 0.8s ease} #welcome.hidden{opacity:0;pointer-events:none} #welcome-inner{text-align:center;max-width:440px;padding:44px 36px;background:rgba(6,12,28,0.92);border:1px solid rgba(120,180,255,0.32);border-radius:26px;box-shadow:0 0 90px rgba(40,100,255,0.28)} #welcome-inner h1{margin:0 0 14px;font-size:30px;letter-spacing:9px;color:#e0f0ff;text-shadow:0 0 32px rgba(100,160,255,0.75)} #welcome-inner p{margin:0 0 30px;font-size:14.5px;line-height:1.65;color:#a0c0e0} #welcome-inner button{background:linear-gradient(135deg,#1a3a7a,#2a5ab0);border:none;color:#fff;padding:13px 34px;border-radius:999px;font-size:14.5px;letter-spacing:2.2px;cursor:pointer;box-shadow:0 0 26px rgba(60,120,255,0.42);transition:transform 0.2s,box-shadow 0.2s} #welcome-inner button:hover{transform:scale(1.045);box-shadow:0 0 40px rgba(80,150,255,0.58)} #modal{position:fixed;inset:0;background:rgba(0,0,6,0.92);z-index:200;display:none;align-items:center;justify-content:center;pointer-events:auto} #modal.active{display:flex} #modal-inner{position:relative;width:92vw;max-width:900px;height:80vh;max-height:720px;background:#060c1c;border:1px solid rgba(120,180,255,0.35);border-radius:18px;overflow:hidden;box-shadow:0 0 80px rgba(40,100,255,0.3)} #modal-head{position:absolute;top:0;left:0;right:0;height:52px;border-bottom:1px solid rgba(120,180,255,0.2);background:#050a18;z-index:5} #modal-title{position:absolute;top:0;left:20px;right:70px;height:52px;line-height:52px;font-size:12px;font-weight:700;letter-spacing:2px;color:#cfe4ff;z-index:10;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;text-transform:uppercase} #modal-inner iframe{position:absolute;top:52px;left:0;width:100%;height:calc(100% – 52px);border:none;display:block;background:#000} #modal-loading{position:absolute;top:52px;left:0;width:100%;height:calc(100% – 52px);background:rgba(4,10,22,0.95);z-index:4;display:none;flex-direction:column;align-items:center;justify-content:center;color:#cce6ff;font-family:system-ui,sans-serif;letter-spacing:2px;transition:opacity 0.3s ease;opacity:0;pointer-events:none} #modal-loading.active{display:flex;opacity:1;pointer-events:auto} .spinner{width:40px;height:40px;border:3px solid rgba(120,180,255,0.2);border-top-color:#7fffc4;border-radius:50%;animation:spin 1s linear infinite;margin-bottom:20px} @keyframes spin{to{transform:rotate(360deg)}} .loading-text{font-size:12px;letter-spacing:3px;animation:pulse 1.5s ease-in-out infinite} #modal-info{position:absolute;top:52px;left:0;right:0;bottom:64px;overflow-y:auto;padding:30px;box-sizing:border-box;display:none;flex-direction:column;align-items:center;justify-content:flex-start;text-align:left;color:#fff;line-height:1.6;font-size:14px;touch-action:pan-y} #modal-info .modal-highlight{color:#00ff9d;font-weight:bold;font-size:20px;margin:10px 0} #modal-close-x{position:absolute;top:6px;right:12px;width:40px;height:40px;border-radius:50%;background:rgba(255,40,40,0.15);border:2px solid #ff4444;color:#ff4444;font-size:22px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;pointer-events:auto} #modal-close-x:hover{background:#ff4444;color:#000} #modal-close-btn{position:absolute;bottom:14px;left:50%;transform:translateX(-50%);background:rgba(8,16,32,0.9);border:1px solid rgba(120,180,255,0.4);color:#d0e8ff;padding:10px 28px;border-radius:999px;font-size:13px;letter-spacing:1px;cursor:pointer;z-index:10;pointer-events:auto} #modal-close-btn:hover{background:rgba(20,40,80,0.95);border-color:rgba(160,210,255,0.6)} .modal-x{position:absolute;top:10px;right:10px;width:38px;height:38px;border-radius:50%;background:rgba(255,40,40,0.15);border:2px solid #ff4444;color:#ff4444;font-size:22px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:30;padding:0;line-height:1;pointer-events:auto} .modal-x:hover{background:#ff4444;color:#000} #outpost{position:fixed;inset:0;z-index:60;display:none;align-items:center;justify-content:center;background:rgba(0,0,6,0.78);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);pointer-events:auto} #outpost.active{display:flex} #outpost-card{position:relative;width:94vw;max-width:980px;height:86vh;max-height:760px;background:rgba(6,12,28,0.96);border:1px solid rgba(120,180,255,0.35);border-radius:22px;box-shadow:0 0 90px rgba(40,100,255,0.3);display:flex;flex-direction:column;overflow:hidden;color:#e0f0ff} #outpost-head{display:flex;align-items:center;gap:16px;padding:20px 64px 10px 22px;flex-shrink:0} #outpost-icon{font-size:38px;width:62px;height:62px;border-radius:16px;background:#000;border:1px solid rgba(120,180,255,0.4);display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 0 24px rgba(60,120,255,0.3)} #outpost-title{margin:0;font-size:18px;letter-spacing:2px;color:#e8f4ff;text-shadow:0 0 18px rgba(100,160,255,0.6)} #outpost-cat{margin-top:5px;font-size:11px;letter-spacing:2px;color:#7fffc4;text-transform:uppercase} #outpost-fact{margin:0;padding:4px 22px 12px;font-size:13px;line-height:1.55;color:#a0c0e0;flex-shrink:0} #outpost-search{margin:0 22px 12px;padding:11px 16px;border-radius:999px;border:1px solid rgba(120,180,255,0.35);background:rgba(4,10,22,0.9);color:#e0f0ff;font-size:13px;letter-spacing:1px;outline:none;flex-shrink:0} #outpost-search:focus{border-color:rgba(160,210,255,0.75)} #outpost-body{flex-grow:1;overflow-y:auto;overflow-x:hidden;padding:6px 18px 26px;touch-action:pan-y;-webkit-overflow-scrolling:touch} #outpost-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(82px,1fr));gap:20px 10px} .otile{display:flex;flex-direction:column;align-items:center;cursor:pointer;transition:transform 0.15s} .otile:active{transform:scale(0.94)} .oicon{width:52px;height:52px;border-radius:13px;background:#000;border:1px solid #444;display:flex;align-items:center;justify-content:center;font-size:22px;margin-bottom:6px;box-shadow:0 4px 10px rgba(0,0,0,0.5);transition:border-color 0.2s,box-shadow 0.2s} .otile:hover .oicon{border-color:rgba(160,210,255,0.8);box-shadow:0 0 18px rgba(80,160,255,0.4)} .olabel{font-size:9.5px;font-weight:600;opacity:0.8;text-align:center;text-transform:uppercase;letter-spacing:0.5px;line-height:1.3;width:100%;word-break:normal;overflow-wrap:normal} #outpost-empty{display:none;text-align:center;padding:30px;color:#7090b0;font-size:13px;letter-spacing:1px} #outpost-close{position:absolute;top:14px;right:14px;width:40px;height:40px;border-radius:50%;background:rgba(255,40,40,0.15);border:2px solid #ff4444;color:#ff4444;font-size:22px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10} #outpost-close:hover{background:#ff4444;color:#000} #worlds{position:fixed;top:0;left:0;bottom:0;width:min(88vw,340px);z-index:70;background:rgba(4,10,22,0.97);border-right:1px solid rgba(120,180,255,0.35);transform:translateX(-105%);transition:transform 0.35s cubic-bezier(0.16,1,0.3,1);display:flex;flex-direction:column;pointer-events:auto;box-shadow:0 0 60px rgba(40,100,255,0.25)} #worlds.active{transform:translateX(0)} #worlds-head{display:flex;align-items:center;justify-content:space-between;padding:18px 18px 12px;color:#cce6ff;font-size:14px;letter-spacing:4px;font-weight:700;flex-shrink:0} #worlds-close{width:34px;height:34px;border-radius:50%;background:rgba(255,255,255,0.08);border:1px solid rgba(120,180,255,0.35);color:#d0e8ff;font-size:18px;cursor:pointer} #worlds-quick{margin:0 12px 10px;padding:12px 14px;border-radius:12px;background:linear-gradient(135deg,#7a1a4a,#b02a6a);border:1px solid rgba(255,150,200,0.55);color:#fff;font-size:12.5px;letter-spacing:1.6px;cursor:pointer;font-weight:700;flex-shrink:0;text-transform:uppercase} #worlds-quick:hover{background:linear-gradient(135deg,#9a245a,#d03a80)} #worlds-list{flex-grow:1;overflow-y:auto;padding:0 12px 24px;touch-action:pan-y} .wrow{display:flex;align-items:center;gap:12px;padding:11px 10px;border-radius:12px;cursor:pointer;color:#d8ecff;border:1px solid transparent} .wrow:hover{background:rgba(40,80,160,0.25);border-color:rgba(120,180,255,0.3)} .wrow.current{background:rgba(40,120,90,0.25);border-color:rgba(120,255,200,0.4)} .wicon{font-size:22px;width:38px;height:38px;border-radius:10px;background:#000;border:1px solid #444;display:flex;align-items:center;justify-content:center;flex-shrink:0} .wname{font-size:13px;font-weight:600;letter-spacing:0.6px} .wcat{font-size:10px;letter-spacing:1.4px;color:#7fffc4;margin-top:3px;text-transform:uppercase} #godseye-overlay{position:fixed;inset:0;z-index:86;display:none;background:#000;opacity:0;transition:opacity 0.55s ease} #godseye-overlay.active{display:block;opacity:1} #godseye-map{position:absolute;inset:0;width:100%;height:100%;background:#000;z-index:1} #godseye-top{position:absolute;top:calc(10px + env(safe-area-inset-top,0px));left:10px;right:10px;z-index:1000;display:flex;flex-wrap:wrap;gap:8px;align-items:center;pointer-events:none} #godseye-top>*{pointer-events:auto} #godseye-search{flex:1 1 150px;min-width:0;padding:11px 16px;border-radius:999px;border:1px solid rgba(120,180,255,0.45);background:rgba(4,10,22,0.92);color:#e0f0ff;font-size:13px;outline:none} #godseye-search:focus{border-color:rgba(160,210,255,0.8)} #godseye-go{background:rgba(8,16,32,0.9);border:1px solid rgba(120,180,255,0.45);color:#d0e8ff;padding:11px 14px;border-radius:999px;font-size:11px;letter-spacing:1.2px;cursor:pointer;flex-shrink:0} #godseye-go:hover{background:rgba(20,40,80,0.95)} #godseye-me,#godseye-zoomin,#godseye-zoomout{height:42px;flex-shrink:0;border-radius:999px;border:1.5px solid rgba(120,180,255,0.6);color:#d8ecff;font-size:11px;font-weight:700;letter-spacing:0.8px;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0 12px;background:rgba(8,16,32,0.9);white-space:nowrap} #godseye-me:hover,#godseye-zoomin:hover,#godseye-zoomout:hover{background:rgba(20,40,80,0.95)} #godseye-zoomout{border-color:rgba(160,200,255,0.75)} #godseye-hint{position:absolute;left:50%;transform:translateX(-50%);bottom:calc(16px + env(safe-area-inset-bottom,0px));z-index:1000;background:rgba(4,10,22,0.85);border:1px solid rgba(120,180,255,0.3);border-radius:999px;padding:8px 16px;font-size:10.5px;letter-spacing:1.4px;color:#a0c0e0;pointer-events:none;white-space:nowrap} #game-overlay{position:fixed;inset:0;z-index:90;display:none;background:#0a0f1a;pointer-events:auto;opacity:0;transition:opacity 0.65s ease;flex-direction:column;color:#e0f0ff} #game-overlay.active{display:flex;opacity:1} #game-top{flex-shrink:0;display:flex;align-items:center;gap:10px;padding:10px 12px;background:rgba(4,10,22,0.98);border-bottom:1px solid rgba(120,180,255,0.25);flex-wrap:wrap} #game-title{font-size:13px;letter-spacing:2px;color:#cfe4ff;font-weight:700;flex:1;min-width:120px} #game-coins{font-size:12px;color:#7fffc4;letter-spacing:1px} .gtop{background:rgba(8,16,32,0.85);border:1px solid rgba(120,180,255,0.45);color:#d0e8ff;width:36px;height:36px;border-radius:50%;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;flex-shrink:0} #game-exit{background:rgba(8,16,32,0.85);border:1px solid rgba(120,180,255,0.45);color:#d0e8ff;padding:8px 14px;border-radius:999px;font-size:11px;letter-spacing:1.2px;cursor:pointer} #game-exit:hover{background:rgba(20,40,80,0.95)} #game-map{flex:1;min-height:0;position:relative} #leaflet-map{position:absolute;inset:0;width:100%;height:100%;z-index:1} #lucky-badge{position:absolute;top:12px;left:50%;transform:translateX(-50%);z-index:900;background:rgba(4,10,22,0.85);border:1px solid rgba(255,215,80,0.35);border-radius:999px;padding:6px 14px;font-size:10.5px;letter-spacing:1px;color:#ffe9a8;font-weight:700;pointer-events:none;white-space:nowrap;box-shadow:0 0 12px rgba(255,215,80,0.18)} #lucky-badge.active{background:rgba(70,55,10,0.92);border-color:rgba(255,215,80,0.85);color:#fff2a8;box-shadow:0 0 22px rgba(255,215,80,0.6);animation:pulse 1.6s ease-in-out infinite} #game-status{position:absolute;left:8px;bottom:8px;z-index:5;pointer-events:auto;cursor:pointer;background:rgba(4,10,22,0.82);border:1px solid rgba(120,180,255,0.25);border-radius:10px;padding:6px 10px;font-size:10.5px;letter-spacing:0.8px;color:#a0e8ff;max-width:calc(100% – 80px);line-height:1.4;transition:border-color 0.2s} #game-status:hover{border-color:rgba(160,210,255,0.6)} #game-bottom{flex-shrink:0;display:flex;align-items:center;justify-content:space-between;gap:10px;background:rgba(4,10,22,0.98);border-top:1px solid rgba(120,180,255,0.25);padding:10px 14px} #gb-toggle{background:rgba(8,16,32,0.9);border:1px solid rgba(120,180,255,0.45);color:#d0e8ff;padding:10px 20px;border-radius:999px;font-size:12px;font-weight:700;letter-spacing:1.2px;cursor:pointer;display:flex;align-items:center;gap:6px;flex:1;max-width:260px;justify-content:center} #gb-toggle:hover{background:rgba(20,40,80,0.95);border-color:rgba(160,210,255,0.7)} #gb-tally{font-size:12px;letter-spacing:1px;color:#ffd93d;font-weight:700;background:rgba(70,55,10,0.6);border:1px solid rgba(255,215,80,0.55);padding:8px 14px;border-radius:999px;cursor:pointer;white-space:nowrap;flex-shrink:0} #gb-tally.zero{color:#708090;background:rgba(20,30,45,0.6);border-color:rgba(120,180,255,0.25)} #game-panel{position:absolute;inset:0;z-index:20;display:none;background:rgba(0,0,6,0.92);align-items:center;justify-content:center;padding:10px} #game-panel.active{display:flex} #game-panel-card{position:relative;width:100%;max-width:420px;max-height:92%;overflow-y:auto;overflow-x:hidden;background:rgba(6,12,28,0.98);border:1px solid rgba(120,180,255,0.35);border-radius:18px;padding:18px;color:#e0f0ff;touch-action:pan-y;-webkit-overflow-scrolling:touch;box-sizing:border-box} #game-panel-card.compact{max-height:96%;padding:10px 12px;display:flex;flex-direction:column;overflow:hidden} #game-panel-card.compact #panel-body{overflow-y:auto;overflow-x:hidden;flex:1 1 auto;min-height:0;touch-action:pan-y;-webkit-overflow-scrolling:touch} #game-panel-card.compact h3{margin:0 0 4px;font-size:13px;letter-spacing:1.5px;flex-shrink:0} #game-panel-card.compact p{margin:0 0 4px;font-size:11px;line-height:1.3;color:#a0c0e0} #game-panel-card.compact .big{font-size:36px;margin:2px 0} #game-panel-card.compact #catch-stage{width:140px;height:140px;margin:2px auto;flex-shrink:0} #game-panel-card.compact #catch-target,#game-panel-card.compact #catch-ring{width:66px;height:66px} #game-panel-card.compact #catch-icon{font-size:28px} #game-panel-card.compact .pbtn.wide{margin-top:4px;padding:9px;font-size:12px;flex-shrink:0} #game-panel-card.compact .shardbar{margin:3px 0;height:7px} #game-panel-card.compact #game-panel-close{margin-top:6px;padding:8px 22px;font-size:11px;flex-shrink:0} #game-panel-card h3{margin:0 0 12px;font-size:16px;letter-spacing:2px;padding-right:44px} #game-panel-card p{margin:0 0 10px;font-size:13px;line-height:1.5;color:#a0c0e0} #panel-body b{color:#e8f4ff} #panel-body em{font-style:normal;font-size:10px;letter-spacing:1px;color:#7fffc4} #game-panel-close{display:block;margin:16px auto 0;background:rgba(8,16,32,0.9);border:1px solid rgba(120,180,255,0.4);color:#d0e8ff;padding:10px 28px;border-radius:999px;font-size:12px;letter-spacing:1px;cursor:pointer} #game-panel-close.flee-mode{background:linear-gradient(135deg,#7a1a1a,#b02a2a);border-color:rgba(255,140,120,0.7);color:#fff;box-shadow:0 0 22px rgba(255,60,40,0.4)} .shop-item,.quest-item{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px;border:1px solid rgba(120,180,255,0.2);border-radius:12px;margin-bottom:8px;font-size:13px} .pbtn,.shop-item button,.quest-item button{background:linear-gradient(135deg,#1a3a7a,#2a5ab0);border:none;color:#fff;padding:8px 14px;border-radius:999px;font-size:11px;letter-spacing:1px;cursor:pointer;flex-shrink:0} .pbtn.wide{display:block;width:100%;margin-top:10px;padding:12px} .pbtn.alt{background:rgba(8,16,32,0.9);border:1px solid rgba(120,180,255,0.45)} .pbtn.danger{background:linear-gradient(135deg,#7a1a1a,#b02a2a)} .pbtn.gold{background:linear-gradient(135deg,#7a5a1a,#b08a2a)} .pbtn.resonator{background:linear-gradient(135deg,#0a5a7a,#00a0d0);border:1px solid rgba(120,220,255,0.7);box-shadow:0 0 22px rgba(80,200,255,0.3)} .hl{color:#00ff9d;font-weight:700;font-size:18px;text-align:center} .big{font-size:56px;text-align:center;margin:6px 0} .tabs{display:flex;gap:6px;margin-bottom:12px;flex-wrap:wrap} .tab{flex:1;min-width:64px;background:rgba(8,16,32,0.9);border:1px solid rgba(120,180,255,0.35);color:#a0c0e0;padding:9px 4px;border-radius:999px;font-size:10.5px;letter-spacing:1px;cursor:pointer} .tab.on{background:linear-gradient(135deg,#1a3a7a,#2a5ab0);color:#fff;border-color:rgba(160,210,255,0.7)} .mission{border:1px solid rgba(120,180,255,0.2);border-radius:12px;padding:10px 12px;margin-bottom:8px} .mtop{display:flex;justify-content:space-between;gap:8px;font-size:13px} .mtop span{color:#7fffc4;font-size:12px;flex-shrink:0} .mdesc{font-size:11.5px;color:#8fb0d0;margin:3px 0} .mbot{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:#a0c0e0;min-height:30px} .pbar{height:6px;background:rgba(255,255,255,0.1);border-radius:4px;overflow:hidden;margin:6px 0} .pbar div{height:100%;background:linear-gradient(90deg,#2a5ab0,#7fffc4)} .cgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px} .ccell{border:1px solid rgba(120,180,255,0.25);border-radius:10px;padding:8px 4px;text-align:center;font-size:10.5px;color:#cfe4ff;overflow:hidden} .ccell .ci{font-size:28px;display:block} .ccell.off{opacity:0.35;filter:grayscale(1)} .logline{font-size:11.5px;color:#a0c0e0;padding:4px 0;border-bottom:1px solid rgba(120,180,255,0.12)} #catch-stage{position:relative;width:250px;height:250px;margin:6px auto;overflow:hidden} #catch-target{position:absolute;left:50%;top:50%;width:110px;height:110px;box-sizing:border-box;border-radius:50%;border:2px dashed rgba(127,255,196,0.9);transform:translate(-50%,-50%)} #catch-ring{position:absolute;left:50%;top:50%;width:110px;height:110px;box-sizing:border-box;border-radius:50%;border:4px solid #ff5a5a;transform:translate(-50%,-50%) scale(2.2)} #catch-ring.hit{border-color:#7fffc4;box-shadow:0 0 18px #7fffc4} #catch-icon{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:46px} #weave-stage{position:relative;width:270px;height:170px;margin:8px auto;display:flex;align-items:center;justify-content:center;gap:16px;background:radial-gradient(circle at 50% 50%,rgba(0,180,255,0.16),rgba(0,20,50,0.1) 62%,transparent 75%);border:1px solid rgba(120,220,255,0.22);border-radius:20px;overflow:hidden;box-shadow:inset 0 0 34px rgba(0,140,255,0.16)} .weave-ring{position:absolute;inset:12px;border:1px dashed rgba(120,220,255,0.22);border-radius:50%;pointer-events:none} .weave-node{position:relative;width:64px;height:64px;border-radius:50%;border:2px solid rgba(120,220,255,0.45);background:rgba(4,14,30,0.92);color:#bfefff;font-size:26px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:transform 0.12s,box-shadow 0.2s,background 0.2s,color 0.2s;user-select:none;padding:0;z-index:2} .weave-node.lit{background:radial-gradient(circle at 40% 35%,#e8ffff,#00b8ff);color:#001018;box-shadow:0 0 28px rgba(0,220,255,0.9),0 0 60px rgba(0,160,255,0.5);transform:scale(1.12)} .weave-node.hit{background:radial-gradient(circle at 40% 35%,#eaffdf,#7fffc4);color:#001810;box-shadow:0 0 26px rgba(127,255,196,0.9)} #weave-status{text-align:center;font-size:12px;letter-spacing:1px;color:#a0e8ff;margin:6px 0;min-height:18px} .shardbar{height:10px;background:rgba(255,255,255,0.1);border-radius:6px;overflow:hidden;margin:8px 0} .shardbar div{height:100%;background:linear-gradient(90deg,#ff8a3d,#ffd93d,#7fffc4)} .mk{display:flex;align-items:center;justify-content:center;font-size:22px;width:40px;height:40px;box-sizing:border-box;border-radius:50%;background:rgba(6,12,28,0.92);border:2px solid #7fffc4;box-shadow:0 0 14px rgba(80,255,200,0.45);cursor:pointer} .mk.cool{border-color:#667788;box-shadow:none;opacity:0.7} .mk-relic{display:flex;align-items:center;justify-content:center;font-size:22px;width:44px;height:44px;box-sizing:border-box;border-radius:12px;background:radial-gradient(circle at 30% 30%,rgba(180,230,255,0.95),rgba(60,140,220,0.85),rgba(20,60,140,0.9));border:2px solid rgba(140,220,255,0.9);box-shadow:0 0 18px rgba(120,210,255,0.75),inset 0 0 12px rgba(255,255,255,0.4);cursor:pointer;animation:relicShimmer 2.4s ease-in-out infinite;transform:rotate(45deg)} .mk-relic span{transform:rotate(-45deg);font-size:18px} @keyframes relicShimmer{0%,100%{box-shadow:0 0 18px rgba(120,210,255,0.75),inset 0 0 12px rgba(255,255,255,0.4);transform:rotate(45deg) scale(1)}50%{box-shadow:0 0 28px rgba(160,240,255,1),inset 0 0 18px rgba(255,255,255,0.65);transform:rotate(45deg) scale(1.08)}} .mk-you{width:22px;height:22px;border-radius:50%;background:#ff3b3b;border:3px solid #fff;box-sizing:border-box;box-shadow:0 0 0 6px rgba(255,59,59,0.28),0 0 18px rgba(255,59,59,0.8)} .mk-you.neon{background:#00e5ff;box-shadow:0 0 0 6px rgba(0,229,255,0.28),0 0 22px rgba(0,229,255,0.95)} .mk-arena{display:flex;align-items:center;justify-content:center;font-size:22px;width:44px;height:44px;box-sizing:border-box;border-radius:50%;background:rgba(28,10,4,0.95);border:2.5px solid #ffd93d;box-shadow:0 0 20px rgba(255,215,80,0.7);cursor:pointer;animation:arenaPulse 1.6s ease-in-out infinite} @keyframes arenaPulse{0%,100%{box-shadow:0 0 20px rgba(255,215,80,0.7);transform:scale(1)}50%{box-shadow:0 0 32px rgba(255,215,80,1);transform:scale(1.08)}} #safety-banner{flex-shrink:0;background:rgba(180,40,20,0.95);color:#fff;padding:10px 16px;font-size:12.5px;text-align:center;display:none;letter-spacing:0.5px;line-height:1.4} #safety-banner.show{display:block} .leaflet-control-attribution{font-size:9px!important} .petgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(74px,1fr));gap:8px} .petcard{border:1px solid rgba(120,180,255,0.25);border-radius:12px;padding:8px 4px;text-align:center;font-size:9.5px;color:#cfe4ff;cursor:pointer;background:rgba(8,16,32,0.6);transition:transform 0.15s,border-color 0.2s} .petcard:active{transform:scale(0.95)} .petcard.on{border-color:#7fffc4;box-shadow:0 0 14px rgba(127,255,196,0.45)} .petcard .pi{font-size:26px;display:block;line-height:1.15} .petcard .pn{font-size:9px;line-height:1.2;margin-top:2px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis} .petcard .pr{font-size:8px;letter-spacing:0.6px;margin-top:2px} .petcard .pl{font-size:8px;color:#7fffc4} .petcard .anomaly{color:#ffd700;text-shadow:0 0 8px #ffd700;font-size:10px} .teamrow{display:flex;align-items:center;gap:10px;padding:10px;border:1px solid rgba(120,180,255,0.22);border-radius:12px;margin-bottom:8px;font-size:12.5px} .teamrow .ti{font-size:26px} .npcrow{display:flex;align-items:center;gap:10px;padding:10px;border:1px solid rgba(120,180,255,0.22);border-radius:12px;margin-bottom:8px;font-size:12.5px} .battlebox{border:1px solid rgba(255,140,120,0.4);border-radius:14px;padding:14px;margin-bottom:10px;background:rgba(40,10,10,0.35)} .vs{display:flex;align-items:center;justify-content:space-around;gap:8px;margin:10px 0;text-align:center} .vs .side{flex:1} .vs .mid{font-size:20px;font-weight:700;color:#ff8a6a;letter-spacing:2px} .bigpower{font-size:26px;font-weight:700;color:#7fffc4} .duelrow{border:1px solid rgba(120,180,255,0.25);border-radius:12px;padding:8px 10px;margin-bottom:8px;background:rgba(8,16,32,0.6)} .duelrow.foe{border-color:rgba(255,120,120,0.4)} .duelrow.you{border-color:rgba(120,255,200,0.4)} .duelname{display:flex;justify-content:space-between;gap:8px;font-size:11.5px;margin-bottom:2px} .hpwrap{height:10px;background:rgba(255,255,255,0.12);border-radius:6px;overflow:hidden;margin:4px 0} .hpwrap div{height:100%;background:linear-gradient(90deg,#2a5ab0,#7fffc4);transition:width 0.3s ease} .hpwrap div.warn{background:linear-gradient(90deg,#b08a1a,#ffd93d)} .hpwrap div.crit{background:linear-gradient(90deg,#7a1a1a,#ff5a5a)} .duel-log{height:120px;overflow-y:auto;font-size:11px;line-height:1.45;color:#a0c0e0;border:1px solid rgba(120,180,255,0.18);border-radius:10px;padding:8px;margin:8px 0;background:rgba(0,0,0,0.28)} .duel-log div{padding:2px 0;border-bottom:1px solid rgba(120,180,255,0.08)} .duel-moves{display:grid;grid-template-columns:1fr 1fr;gap:6px} .duel-moves button{background:linear-gradient(135deg,#1a3a7a,#2a5ab0);border:none;color:#fff;padding:10px 6px;border-radius:10px;font-size:11px;cursor:pointer;text-align:center;position:relative} .duel-moves button:hover{filter:brightness(1.18)} .duel-moves button:disabled{opacity:0.35;cursor:not-allowed} .duel-moves button small{display:block;font-size:9px;opacity:0.78;margin-top:3px;letter-spacing:0.4px} .duel-moves button .pptag{position:absolute;top:4px;right:6px;font-size:8px;opacity:0.8} .status-tag{display:inline-block;font-size:9px;padding:1px 6px;border-radius:6px;margin-left:6px;font-weight:700;letter-spacing:0.5px;vertical-align:middle} .st-brn{background:#d2691e;color:#fff} .st-psn{background:#8b008b;color:#fff} .st-tox{background:#5c1a5c;color:#fff} .st-par{background:#b8860b;color:#fff} .st-slp{background:#4b4b6b;color:#fff} .st-frz{background:#4682b4;color:#fff} .st-cnf{background:#a0522d;color:#fff} .battle-side{text-align:center;padding:8px;border-radius:10px} .battle-side.foe{border:1px solid rgba(255,100,100,0.4);background:rgba(50,10,10,0.4)} .battle-side.you{border:1px solid rgba(120,255,200,0.4);background:rgba(10,50,40,0.4)} .battle-sprite{font-size:42px;line-height:1.1;margin-bottom:2px;transition:transform 0.1s, opacity 0.1s;} .anim-attack-right { animation: attackRight 0.4s ease; } .anim-attack-left { animation: attackLeft 0.4s ease; } .anim-damage { animation: takeDamage 0.4s ease; } @keyframes attackRight { 0% { transform: translateX(0); } 30% { transform: translateX(40px) scale(1.1); } 100% { transform: translateX(0); } } @keyframes attackLeft { 0% { transform: translateX(0); } 30% { transform: translateX(-40px) scale(1.1); } 100% { transform: translateX(0); } } @keyframes takeDamage { 0% { opacity: 1; transform: translateX(0); } 20% { opacity: 0.4; transform: translateX(-10px); } 40% { opacity: 0.4; transform: translateX(10px); } 60% { opacity: 0.4; transform: translateX(-10px); } 80% { opacity: 0.4; transform: translateX(10px); } 100% { opacity: 1; transform: translateX(0); } } .arena-card{border:1px solid rgba(255,215,80,0.45);border-radius:14px;padding:12px;margin-bottom:10px;background:rgba(40,30,5,0.45);cursor:pointer} .arena-card:hover{border-color:rgba(255,215,80,0.9);background:rgba(60,45,10,0.55)} .arena-card.locked{opacity:0.5;cursor:not-allowed;border-color:rgba(120,180,255,0.25);background:rgba(8,16,32,0.6)} .arena-card .arow{display:flex;justify-content:space-between;align-items:center;gap:8px} .arena-card .aname{font-size:13px;font-weight:700;color:#ffe9c2} .arena-card .ateam{font-size:10px;color:#a0c0e0;margin-top:3px;line-height:1.4} .arena-card .areward{font-size:10px;color:#7fffc4;margin-top:3px} .story-card{border:1px solid rgba(120,180,255,0.35);border-radius:14px;padding:12px;margin-bottom:10px;background:rgba(8,16,32,0.6)} .story-card.done{border-color:rgba(127,255,196,0.5);background:rgba(10,40,25,0.5)} .story-card.locked{opacity:0.5;border-color:rgba(120,180,255,0.18)} .story-card .stitle{font-size:13px;font-weight:700;color:#e0f0ff;margin-bottom:4px} .story-card .sdesc{font-size:11px;color:#a0c0e0;line-height:1.5} .story-card .sreward{font-size:11px;color:#7fffc4;margin-top:6px} .breed-slot{border:1px dashed rgba(120,180,255,0.45);border-radius:12px;padding:10px;text-align:center;font-size:11px;color:#a0c0e0;min-height:62px;display:flex;flex-direction:column;justify-content:center} .breed-slot.filled{border-style:solid;border-color:rgba(127,255,196,0.55);background:rgba(10,40,25,0.4)} .breed-slot .bsi{font-size:28px} .eggcard{border:1px solid rgba(255,215,80,0.4);border-radius:12px;padding:10px;margin-bottom:8px;background:rgba(40,30,5,0.35)} .eggcard .ename{font-size:12px;font-weight:700;color:#ffe9a8} .eggcard .etime{font-size:10.5px;color:#a0c0e0;margin-top:3px} .shopsearch{width:100%;box-sizing:border-box;margin-bottom:10px;padding:11px 16px;border-radius:999px;border:1px solid rgba(120,180,255,0.35);background:rgba(4,10,22,0.9);color:#e0f0ff;font-size:13px;outline:none} .shopsearch:focus{border-color:rgba(160,210,255,0.75)} .shopcat{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap} .shopcat button{flex:1;min-width:60px;background:rgba(8,16,32,0.9);border:1px solid rgba(120,180,255,0.35);color:#a0c0e0;padding:8px 4px;border-radius:999px;font-size:10px;letter-spacing:1px;cursor:pointer} .shopcat button.on{background:linear-gradient(135deg,#1a3a7a,#2a5ab0);color:#fff;border-color:rgba(160,210,255,0.7)} .cmd-section{margin-bottom:14px} .cmd-head{font-size:10px;letter-spacing:2px;color:#5f7fa0;font-weight:700;margin-bottom:6px;padding-left:4px;text-transform:uppercase} .cmd-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px} .cmd-btn{background:rgba(8,16,32,0.9);border:1px solid rgba(120,180,255,0.4);color:#d0e8ff;padding:12px 8px;border-radius:12px;font-size:12px;font-weight:600;letter-spacing:0.5px;cursor:pointer;text-align:center;white-space:normal;word-wrap:break-word;line-height:1.3} .cmd-btn:hover{background:rgba(20,40,80,0.95);border-color:rgba(160,210,255,0.7)} .cmd-btn.primary{border-color:rgba(120,255,200,0.6);color:#c8ffe6} .cmd-btn.pet{border-color:rgba(255,180,220,0.55);color:#ffe0f4} .cmd-btn.battle{border-color:rgba(255,140,120,0.55);color:#ffd8cc} .cmd-btn.duel{border-color:rgba(255,190,90,0.6);color:#ffe9c2} .cmd-btn.pass{border-color:rgba(255,215,80,0.65);color:#ffeeb0} .cmd-btn.daily{border-color:rgba(120,220,255,0.65);color:#c8f4ff} .cmd-btn.gold{border-color:rgba(255,215,80,0.85);color:#fff2a8} .cmd-btn.arena{border-color:rgba(255,120,120,0.7);color:#ffd0d0} .cmd-btn.story{border-color:rgba(180,120,255,0.7);color:#e0d0ff} .cmd-btn.resonator{border-color:rgba(120,220,255,0.8);color:#c8f4ff;background:rgba(10,40,70,0.85)} .cmd-btn.onchain{border-color:rgba(80,255,190,0.85);color:#c8ffe8;background:linear-gradient(135deg,#064a36,#00996e);box-shadow:0 0 18px rgba(60,255,180,0.28)} #you-label{position:absolute;color:#ff5a5a;font-size:11px;font-weight:700;letter-spacing:1px;text-shadow:0 0 10px #ff0000,0 0 4px #000;opacity:0;transition:opacity 0.4s;pointer-events:none;z-index:11} .flash{animation:flashpulse 0.9s ease} @keyframes flashpulse{0%{filter:brightness(1)}35%{filter:brightness(2.1)}100%{filter:brightness(1)}} @keyframes shardpop{0%{transform:scale(0.4);opacity:0}45%{transform:scale(1.25);opacity:1}100%{transform:scale(1);opacity:1}} .shardpop{animation:shardpop 0.55s cubic-bezier(0.2,1.4,0.4,1)} @keyframes floatup{0%{transform:translateY(0);opacity:1}100%{transform:translateY(-46px);opacity:0}} .floatup{animation:floatup 1.1s ease-out forwards} @keyframes depotspin{0%{transform:rotate(0deg) scale(1)}60%{transform:rotate(540deg) scale(1.15)}100%{transform:rotate(720deg) scale(0.85)}} .spin-anim{animation:depotspin 1s cubic-bezier(0.16,1,0.3,1);display:inline-block} .statrow{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid rgba(120,180,255,0.15);font-size:12px} .statrow b{color:#7fffc4} .helpsection{margin-bottom:16px} .helpsection h4{margin:0 0 6px;font-size:14px;letter-spacing:1px;color:#cfe4ff} .helpsection p{margin:0 0 8px;font-size:12.5px;line-height:1.55;color:#a0c0e0} #intro-modal{position:fixed;inset:0;background:rgba(0,0,6,0.95);z-index:500;display:none;align-items:center;justify-content:center;pointer-events:auto} #intro-modal.active{display:flex} #intro-inner{position:relative;width:90vw;max-width:520px;background:rgba(6,12,28,0.98);border:1px solid rgba(120,180,255,0.35);border-radius:24px;padding:36px 28px;text-align:center;color:#e0f0ff;box-shadow:0 0 80px rgba(40,100,255,0.4)} #intro-inner h2{margin:0 0 16px;font-size:22px;letter-spacing:4px;color:#cce6ff} #intro-inner p{margin:0 0 16px;font-size:14px;line-height:1.7;color:#a0c0e0;text-align:left} #intro-inner .highlight{color:#00ff9d;font-weight:bold} #intro-ok{background:linear-gradient(135deg,#1a3a7a,#2a5ab0);border:none;color:#fff;padding:15px 48px;border-radius:999px;font-size:16px;letter-spacing:2px;cursor:pointer;box-shadow:0 0 30px rgba(60,120,255,0.5);margin-top:10px} #intro-ok:hover{transform:scale(1.05)} #unlock-modal{position:fixed;inset:0;background:rgba(0,0,6,0.92);z-index:300;display:none;align-items:center;justify-content:center;pointer-events:auto} #unlock-modal.active{display:flex} #unlock-inner{position:relative;width:90vw;max-width:420px;background:rgba(6,12,28,0.98);border:1px solid rgba(120,180,255,0.35);border-radius:20px;padding:30px 24px;text-align:center;color:#e0f0ff;box-shadow:0 0 60px rgba(40,100,255,0.4)} #unlock-inner h3{margin:0 0 14px;font-size:18px;letter-spacing:2px;color:#cce6ff} #unlock-inner p{margin:0 0 24px;font-size:14px;line-height:1.6;color:#a0c0e0} #unlock-buttons{display:flex;gap:12px;justify-content:center} #unlock-buttons button{flex:1;padding:12px 20px;border-radius:999px;font-size:14px;letter-spacing:1.5px;cursor:pointer;border:none;font-weight:600} #unlock-yes{background:linear-gradient(135deg,#1a3a7a,#2a5ab0);color:#fff;box-shadow:0 0 20px rgba(60,120,255,0.4)} #unlock-no{background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.2);color:#a0c0e0} #outorbs-modal{position:fixed;inset:0;background:rgba(0,0,6,0.92);z-index:350;display:none;align-items:center;justify-content:center;pointer-events:auto} #outorbs-modal.active{display:flex} #outorbs-inner{position:relative;width:90vw;max-width:460px;background:rgba(6,12,28,0.98);border:1px solid rgba(255,140,90,0.45);border-radius:20px;padding:30px 24px;text-align:center;color:#e0f0ff;box-shadow:0 0 60px rgba(255,100,60,0.35)} #outorbs-inner h3{margin:0 0 14px;font-size:20px;letter-spacing:3px;color:#ff8a6a} #outorbs-inner p{margin:0 0 24px;font-size:14px;line-height:1.6;color:#a0c0e0} #outorbs-buttons{display:flex;gap:12px;justify-content:center} #outorbs-buttons button{flex:1;padding:12px 20px;border-radius:999px;font-size:14px;letter-spacing:1.5px;cursor:pointer;border:none;font-weight:600} #outorbs-shop{background:linear-gradient(135deg,#1a3a7a,#2a5ab0);color:#fff;box-shadow:0 0 20px rgba(60,120,255,0.4)} #outorbs-cancel{background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.2);color:#a0c0e0} .orbpick{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin:6px 0} .orbpick button{background:rgba(8,16,32,0.9);border:1px solid rgba(120,180,255,0.35);color:#a0c0e0;padding:7px 10px;border-radius:999px;font-size:11px;letter-spacing:1px;cursor:pointer} .orbpick button.on{background:linear-gradient(135deg,#1a3a7a,#2a5ab0);color:#fff;border-color:rgba(160,210,255,0.75)} .orbpick button.empty{opacity:0.45} #char-modal{position:fixed;inset:0;background:rgba(0,0,6,0.95);z-index:520;display:none;align-items:center;justify-content:center;pointer-events:auto;overflow-y:auto} #char-modal.active{display:flex} #char-inner{position:relative;width:92vw;max-width:560px;background:rgba(6,12,28,0.98);border:1px solid rgba(120,180,255,0.35);border-radius:24px;padding:32px 24px;text-align:center;color:#e0f0ff;box-shadow:0 0 80px rgba(40,100,255,0.4);max-height:92vh;overflow-y:auto} #char-inner h2{margin:0 0 16px;font-size:20px;letter-spacing:4px;color:#cce6ff} #char-inner p{margin:8px 0;font-size:13px;color:#a0c0e0} #char-name{width:100%;box-sizing:border-box;padding:12px 16px;border-radius:999px;border:1px solid rgba(120,180,255,0.4);background:rgba(4,10,22,0.9);color:#e0f0ff;font-size:14px;outline:none;margin-bottom:12px} #char-name:focus{border-color:rgba(160,210,255,0.85)} #char-pets{display:grid;grid-template-columns:repeat(auto-fill,minmax(74px,1fr));gap:8px;margin:12px 0} #char-pets .petcard{cursor:pointer} #char-pets .petcard.on{border-color:#7fffc4;box-shadow:0 0 14px rgba(127,255,196,0.45)} #char-create{background:linear-gradient(135deg,#1a3a7a,#2a5ab0);border:none;color:#fff;padding:14px 36px;border-radius:999px;font-size:14px;letter-spacing:2px;cursor:pointer;margin-top:12px} #char-create:disabled{opacity:0.5;cursor:default} #passenger-modal{position:fixed;inset:0;background:rgba(0,0,6,0.94);z-index:510;display:none;align-items:center;justify-content:center;pointer-events:auto} #passenger-modal.active{display:flex} #passenger-inner{position:relative;width:90vw;max-width:460px;background:rgba(6,12,28,0.98);border:1px solid rgba(255,140,90,0.5);border-radius:20px;padding:28px 24px;text-align:center;color:#e0f0ff;box-shadow:0 0 70px rgba(255,90,40,0.4)} #passenger-inner h3{margin:0 0 14px;font-size:18px;letter-spacing:2px;color:#ff8a6a} #passenger-inner p{margin:0 0 22px;font-size:13px;line-height:1.6;color:#c0d0e0;text-align:left} #passenger-buttons{display:flex;gap:12px;justify-content:center} #passenger-buttons button{flex:1;padding:12px 14px;border-radius:999px;font-size:12.5px;letter-spacing:1px;cursor:pointer;border:none;font-weight:600} #passenger-yes{background:linear-gradient(135deg,#1a3a7a,#2a5ab0);color:#fff} #passenger-no{background:linear-gradient(135deg,#7a1a1a,#b02a2a);color:#fff} #weather-modal{position:fixed;inset:0;background:rgba(0,0,6,0.92);z-index:505;display:none;align-items:center;justify-content:center;pointer-events:auto} #weather-modal.active{display:flex} #weather-inner{position:relative;width:90vw;max-width:460px;background:rgba(6,12,28,0.98);border:1px solid rgba(120,180,255,0.4);border-radius:20px;padding:32px 24px;text-align:center;color:#e0f0ff;box-shadow:0 0 60px rgba(40,100,255,0.4)} #weather-inner h3{margin:0 0 14px;font-size:18px;letter-spacing:2px;color:#cce6ff} #weather-msg{margin:0 0 20px;font-size:13.5px;line-height:1.6;color:#a0c0e0;text-align:left} #weather-close{background:linear-gradient(135deg,#1a3a7a,#2a5ab0);border:none;color:#fff;padding:12px 32px;border-radius:999px;font-size:13px;letter-spacing:1.5px;cursor:pointer} #card-modal{position:fixed;inset:0;background:rgba(0,0,6,0.94);z-index:515;display:none;align-items:center;justify-content:center;pointer-events:auto;overflow-y:auto} #card-modal.active{display:flex} #card-inner{position:relative;width:92vw;max-width:480px;background:rgba(6,12,28,0.98);border:1px solid rgba(120,180,255,0.4);border-radius:20px;padding:24px;text-align:center;color:#e0f0ff;box-shadow:0 0 70px rgba(40,100,255,0.4);max-height:94vh;overflow-y:auto} #card-inner h3{margin:0 0 14px;font-size:16px;letter-spacing:3px;color:#cce6ff} #card-canvas{max-width:100%;height:auto;border-radius:12px;border:1px solid rgba(120,180,255,0.3);margin-bottom:14px;background:#02040a} #card-download{background:linear-gradient(135deg,#1a3a7a,#2a5ab0);border:none;color:#fff;padding:12px 28px;border-radius:999px;font-size:13px;letter-spacing:1.5px;cursor:pointer;margin:4px} #card-share{background:linear-gradient(135deg,#111,#000);border:1px solid rgba(255,255,255,0.55);color:#fff;padding:12px 28px;border-radius:999px;font-size:13px;letter-spacing:1.5px;cursor:pointer;margin:4px} #card-close{background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.2);color:#c0d0e0;padding:12px 28px;border-radius:999px;font-size:13px;letter-spacing:1.5px;cursor:pointer;margin:4px} #settings-modal{position:fixed;inset:0;background:rgba(0,0,6,0.94);z-index:518;display:none;align-items:center;justify-content:center;pointer-events:auto;overflow-y:auto} #settings-modal.active{display:flex} #settings-inner{position:relative;width:92vw;max-width:500px;background:rgba(6,12,28,0.98);border:1px solid rgba(120,180,255,0.4);border-radius:20px;padding:26px 22px;text-align:center;color:#e0f0ff;box-shadow:0 0 70px rgba(40,100,255,0.4);max-height:94vh;overflow-y:auto} #settings-inner h3{margin:0 0 16px;font-size:18px;letter-spacing:3px;color:#cce6ff} #settings-inner .setrow{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px;border:1px solid rgba(120,180,255,0.2);border-radius:12px;margin-bottom:10px;font-size:13px;text-align:left} #settings-inner .setrow b{color:#7fffc4;font-size:14px;display:block} #settings-inner .setrow small{display:block;font-size:10.5px;color:#8fb0d0;margin-top:2px} #step-goal-input{width:80px;padding:8px 10px;border-radius:8px;border:1px solid rgba(120,180,255,0.5);background:rgba(4,10,22,0.9);color:#e0f0ff;font-size:14px;text-align:center;outline:none} #step-goal-input:focus{border-color:rgba(160,210,255,0.85)} #step-goal-save{background:linear-gradient(135deg,#1a3a7a,#2a5ab0);border:none;color:#fff;padding:10px 22px;border-radius:999px;font-size:12px;letter-spacing:1.5px;cursor:pointer} #settings-close{background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.2);color:#c0d0e0;padding:10px 28px;border-radius:999px;font-size:13px;letter-spacing:1.5px;cursor:pointer;margin-top:12px} .money-float{position:fixed;transform:translate(-50%,-50%);color:#ff5a5a;font-weight:700;font-size:15px;letter-spacing:1px;pointer-events:none;z-index:99999;text-shadow:0 0 8px rgba(255,0,0,0.6);animation:moneyfloat 1.2s ease-out forwards} @keyframes moneyfloat{0%{opacity:1;transform:translate(-50%,-50%) scale(1)}100%{opacity:0;transform:translate(-50%,-140%) scale(1.3)}} .itemlogrow{display:flex;align-items:center;gap:8px;padding:9px 10px;border:1px solid rgba(120,180,255,0.22);border-radius:10px;margin-bottom:6px;font-size:12px;color:#cfe4ff;cursor:pointer;background:rgba(8,16,32,0.6)} .itemlogrow:hover{border-color:rgba(160,210,255,0.7);background:rgba(20,40,80,0.7)} .itemlogtime{font-size:10px;color:#7090b0;flex-shrink:0;min-width:60px} .bag-highlight{animation:baghighlight 2.2s ease} @keyframes baghighlight{0%{box-shadow:0 0 0 0 rgba(127,255,196,0.9)}30%{box-shadow:0 0 0 4px rgba(127,255,196,0.9)}100%{box-shadow:0 0 0 0 rgba(127,255,196,0)}} #tune-meter{position:relative;height:26px;background:rgba(255,255,255,0.08);border-radius:13px;overflow:hidden;border:1px solid rgba(120,220,255,0.4);margin:10px 0} #tune-meter-fill{position:absolute;top:0;left:0;height:100%;width:0;background:linear-gradient(90deg,#0a5a7a,#00a0d0,#7fffd0);transition:width 0.1s linear} #tune-meter-label{position:absolute;top:0;left:0;right:0;height:100%;display:flex;align-items:center;justify-content:center;font-size:11px;letter-spacing:2px;color:#e0f0ff;font-weight:700;text-shadow:0 0 6px rgba(0,0,0,0.8)} #relic-crystal{font-size:70px;text-align:center;transition:transform 0.15s ease;filter:drop-shadow(0 0 20px rgba(120,220,255,0.9))} #relic-crystal.shake{animation:relicShake 0.2s ease infinite} @keyframes relicShake{0%,100%{transform:translateX(0) rotate(0)}25%{transform:translateX(-4px) rotate(-2deg)}75%{transform:translateX(4px) rotate(2deg)}} .shard-row{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin:10px 0;min-height:32px} .shard-icon{font-size:22px;opacity:0.35;transition:opacity 0.3s,transform 0.3s} .shard-icon.collected{opacity:1;transform:scale(1.1);filter:drop-shadow(0 0 8px rgba(127,255,220,0.9))} #onchain-shop{position:fixed;inset:0;z-index:530;display:none;align-items:center;justify-content:center;background:rgba(0,0,6,0.94);pointer-events:auto} #onchain-shop.active{display:flex} #onchain-inner{position:relative;width:94vw;max-width:620px;height:88vh;max-height:820px;background:rgba(6,12,28,0.98);border:1px solid rgba(80,255,190,0.5);border-radius:22px;box-shadow:0 0 90px rgba(40,255,180,0.28);display:flex;flex-direction:column;overflow:hidden;color:#e0f0ff} #onchain-head{padding:18px 60px 12px 20px;border-bottom:1px solid rgba(80,255,190,0.25);flex-shrink:0} #onchain-head h2{margin:0;font-size:16px;letter-spacing:3px;color:#c8ffe8;text-shadow:0 0 18px rgba(80,255,190,0.55)} #onchain-head .sub{margin-top:5px;font-size:11px;letter-spacing:1.5px;color:#7fffc4} #onchain-body{flex:1;overflow-y:auto;padding:14px 18px 22px;touch-action:pan-y;-webkit-overflow-scrolling:touch} #onchain-price{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:space-between;padding:10px 12px;border:1px solid rgba(80,255,190,0.35);border-radius:12px;background:rgba(4,28,20,0.65);margin-bottom:12px;font-size:12px;letter-spacing:0.5px} #onchain-price b{color:#7fffc4;font-size:14px} #onchain-price button{background:rgba(8,32,24,0.9);border:1px solid rgba(80,255,190,0.55);color:#c8ffe8;padding:6px 12px;border-radius:999px;font-size:10.5px;letter-spacing:1px;cursor:pointer;font-weight:700} #onchain-price button:hover{background:rgba(20,60,45,0.95)} .onchain-item{border:1px solid rgba(80,255,190,0.35);border-radius:14px;padding:12px;margin-bottom:10px;background:rgba(4,20,16,0.6);display:flex;align-items:center;gap:12px} .onchain-item .oi-icon{font-size:32px;flex-shrink:0;width:52px;height:52px;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.35);border:1px solid rgba(80,255,190,0.4);border-radius:12px} .onchain-item .oi-main{flex:1;min-width:0} .onchain-item .oi-name{font-size:13px;font-weight:700;color:#e8fff4;letter-spacing:0.5px} .onchain-item .oi-desc{font-size:11px;color:#a0c0e0;margin-top:3px;line-height:1.4} .onchain-item .oi-price{margin-top:5px;font-size:11.5px} .onchain-item .oi-price b{color:#7fffc4;font-size:13px} .onchain-item .oi-price span{color:#a0e8d0;font-size:11px;margin-left:6px;letter-spacing:0.5px} .onchain-item .oi-buy{background:linear-gradient(135deg,#064a36,#00a87a);border:1px solid rgba(120,255,200,0.7);color:#fff;padding:9px 16px;border-radius:999px;font-size:11px;letter-spacing:1px;cursor:pointer;font-weight:700;flex-shrink:0} .onchain-item .oi-buy:hover{filter:brightness(1.15)} .onchain-item .oi-buy:disabled{opacity:0.5;cursor:not-allowed;filter:none} #onchain-close{position:absolute;top:14px;right:14px;width:40px;height:40px;border-radius:50%;background:rgba(255,40,40,0.15);border:2px solid #ff4444;color:#ff4444;font-size:22px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10} #onchain-close:hover{background:#ff4444;color:#000} .onchain-status{margin-top:10px;padding:10px 12px;border-radius:10px;border:1px solid rgba(80,255,190,0.35);background:rgba(4,28,20,0.7);font-size:11.5px;letter-spacing:0.5px;color:#c8ffe8;line-height:1.4;word-break:break-all} .onchain-status.err{border-color:rgba(255,120,120,0.6);background:rgba(40,8,8,0.75);color:#ffb0b0} #wallet-modal{position:fixed;inset:0;z-index:540;display:none;align-items:center;justify-content:center;background:rgba(0,0,6,0.94);pointer-events:auto} #wallet-modal.active{display:flex} #wallet-inner{position:relative;width:92vw;max-width:440px;background:rgba(6,12,28,0.98);border:1px solid rgba(120,180,255,0.42);border-radius:20px;padding:24px 22px;color:#e0f0ff;box-shadow:0 0 70px rgba(40,100,255,0.4)} #wallet-inner h3{margin:0 0 12px;font-size:16px;letter-spacing:3px;color:#cce6ff} #wallet-inner p{margin:0 0 14px;font-size:12.5px;line-height:1.55;color:#a0c0e0} #wallet-list{display:flex;flex-direction:column;gap:8px;margin-bottom:14px} .wallet-btn{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid rgba(120,180,255,0.35);border-radius:12px;background:rgba(8,16,32,0.85);color:#e0f0ff;font-size:13px;font-weight:600;letter-spacing:0.5px;cursor:pointer;text-align:left} .wallet-btn:hover{background:rgba(20,40,80,0.95);border-color:rgba(160,210,255,0.7)} .wallet-btn .wicon{font-size:22px;width:36px;height:36px;border-radius:9px;background:#000;border:1px solid rgba(120,180,255,0.35);display:flex;align-items:center;justify-content:center;flex-shrink:0} .wallet-btn .wmeta{flex:1;min-width:0} .wallet-btn .wmeta small{display:block;font-size:10.5px;color:#8fb0d0;margin-top:2px;font-weight:400;letter-spacing:0.4px} #wallet-disconnect{background:rgba(255,60,60,0.2);border:1px solid rgba(255,120,120,0.6);color:#ffb0b0;padding:10px 20px;border-radius:999px;font-size:12px;letter-spacing:1px;cursor:pointer;font-weight:700;width:100%} #wallet-close{position:absolute;top:10px;right:10px;width:36px;height:36px;border-radius:50%;background:rgba(255,40,40,0.15);border:2px solid #ff4444;color:#ff4444;font-size:20px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;line-height:1} #wallet-close:hover{background:#ff4444;color:#000} @media (max-width:520px){ #cosmos-controls{left:50%;bottom:80px;gap:6px;width:calc(100% – 24px)} #toast{top:100px} #bunny-btn{bottom:20px} .c-ctrl{padding:7px 9px;font-size:9px} #mute-btn{bottom:14px;left:12px;width:38px;height:38px;font-size:16px} #profile-btn{bottom:14px;right:12px;width:38px;height:38px;font-size:16px} #wallet-bar{top:auto;bottom:118px;right:12px;left:12px;justify-content:center} #wallet-bar button{flex:1;font-size:11px;padding:9px 10px} } @media (max-width:400px){ #game-panel-card.compact{padding:8px 10px} #game-panel-card.compact h3{font-size:12px;margin:0 0 3px} #game-panel-card.compact p{font-size:10.5px;margin:0 0 3px;line-height:1.25} #game-panel-card.compact .big{font-size:30px;margin:1px 0} #game-panel-card.compact #catch-stage{width:110px;height:110px;margin:1px auto} #game-panel-card.compact #catch-target,#game-panel-card.compact #catch-ring{width:52px;height:52px} #game-panel-card.compact #catch-icon{font-size:22px} #game-panel-card.compact #weave-stage{width:200px;height:140px;gap:10px} #game-panel-card.compact .weave-node{width:52px;height:52px;font-size:22px} #game-panel-card.compact .pbtn.wide{padding:7px;font-size:11px;margin-top:3px} #game-panel-card.compact #game-panel-close{padding:6px 16px;font-size:10px;margin-top:3px} #game-panel-card.compact .shardbar{height:5px;margin:2px 0} #game-panel-card.compact .duel-log { height: 60px; margin: 2px 0; padding: 4px; } #game-panel-card.compact .duel-moves { gap: 4px; } #game-panel-card.compact .duel-moves button { padding: 6px 2px; font-size: 9px; } #game-panel-card.compact .battle-sprite { font-size: 28px; } #game-panel-card.compact .battle-side { padding: 4px; } #game-panel-card.compact .battle-side p { font-size: 9px; margin: 1px 0; } } @media (min-width:600px){ html,body,#c,#s,#hud,#game-overlay,#welcome,#modal,#outpost,#worlds,#godseye-overlay,#unlock-modal,#intro-modal,#outorbs-modal,#char-modal,#passenger-modal,#weather-modal,#card-modal,#settings-modal{width:100%!important;height:100%!important;max-width:100%!important;left:0!important;right:0!important} #logo{font-size:26px;letter-spacing:7px} #worlds-btn{font-size:14px;padding:10px 20px;top:70px} #wallet-bar{top:70px} #wallet-btn,#shop-hud-btn{font-size:13px;padding:10px 18px} .hbtn{font-size:14px;padding:13px 30px} #bunny-btn{font-size:16px;bottom:30px} #mute-btn{height:52px;width:52px;font-size:22px;bottom:26px;left:26px} #profile-btn{height:52px;width:52px;font-size:22px;bottom:26px;right:26px} #fps{font-size:13px} #toast{max-width:440px;padding:18px 52px 18px 26px;top:110px} #toast h3{font-size:19px} #toast p{font-size:15px} .toast-close{width:30px;height:30px;font-size:17px;top:10px;right:10px} #game-notifications{top:72px;bottom:auto;width:min(420px,calc(100% – 24px))} .gnote h4{font-size:13px} .gnote p{font-size:12px} #welcome-inner{max-width:560px;padding:56px 48px} #welcome-inner h1{font-size:40px;letter-spacing:12px} #welcome-inner p{font-size:17.5px} #welcome-inner button{font-size:17px;padding:16px 44px} #worlds{width:400px} #worlds-head{font-size:18px;padding:22px 22px 14px} #worlds-quick{font-size:15px;padding:15px 18px} .wrow{padding:14px 12px} .wicon{width:46px;height:46px;font-size:26px} .wname{font-size:16px} .wcat{font-size:12px} #outpost-card{max-width:1100px;height:88vh;max-height:840px} #outpost-title{font-size:23px} #outpost-cat{font-size:13px} #outpost-fact{font-size:16px;padding:6px 26px 16px} #outpost-search{font-size:16px;padding:14px 20px;margin:0 26px 16px} #outpost-grid{grid-template-columns:repeat(auto-fill,minmax(112px,1fr));gap:26px 14px} .oicon{width:70px;height:70px;font-size:30px;border-radius:16px} .olabel{font-size:12.5px} #outpost-body{padding:8px 22px 32px} #modal-inner{max-width:1200px;height:84vh;max-height:880px} #modal-title{font-size:15px;height:60px;line-height:60px} #modal-head{height:60px} #modal-inner iframe{top:60px;height:calc(100% – 60px)} #modal-loading{top:60px;height:calc(100% – 60px)} #modal-info{top:60px;font-size:16px} #modal-close-btn{font-size:15px;padding:13px 36px} #godseye-me,#godseye-zoomin,#godseye-zoomout{font-size:13px;height:46px;padding:0 16px} #godseye-search{font-size:15px;padding:13px 20px} #godseye-go{font-size:13px;padding:12px 20px} #godseye-hint{font-size:12px;padding:10px 20px} #game-top{padding:12px 18px;gap:14px} #game-title{font-size:17px;letter-spacing:3px} #game-coins{font-size:15px} .gtop{width:46px;height:46px;font-size:20px} #game-exit{font-size:14px;padding:11px 20px} #game-status{font-size:13px;padding:9px 14px;left:14px;bottom:14px} #lucky-badge{font-size:12px;padding:8px 18px;top:14px} #gb-toggle{font-size:14px;padding:12px 24px} #gb-tally{font-size:14px;padding:10px 18px} #game-panel{padding:16px} #game-panel-card{max-width:720px;padding:30px;border-radius:22px;max-height:92%} #game-panel-card.compact{max-height:96%;padding:16px} #game-panel-card.compact h3{font-size:16px;margin:0 0 6px} #game-panel-card.compact p{font-size:13px;line-height:1.4} #game-panel-card.compact .big{font-size:48px} #game-panel-card.compact #catch-stage{width:190px;height:190px} #game-panel-card.compact #catch-target,#game-panel-card.compact #catch-ring{width:88px;height:88px} #game-panel-card.compact #catch-icon{font-size:38px} #game-panel-card.compact #weave-stage{width:330px;height:190px;gap:20px} #game-panel-card.compact .weave-node{width:82px;height:82px;font-size:32px} #game-panel-card.compact .pbtn.wide{padding:12px;font-size:14px} #game-panel-card h3{font-size:23px;letter-spacing:3px} #game-panel-card p{font-size:16px} .shop-item,.quest-item{font-size:16px;padding:15px} .pbtn,.shop-item button,.quest-item button{font-size:14px;padding:11px 20px} .pbtn.wide{padding:15px;font-size:15px} .tab{font-size:13px;padding:12px 6px} .mission{padding:14px 16px} .mtop{font-size:16px} .mtop span{font-size:14px} .mdesc{font-size:14px} .mbot{font-size:13px} .hl{font-size:23px} .big{font-size:70px} .cgrid{grid-template-columns:repeat(5,1fr)} .ccell{font-size:13px;padding:11px 6px} .ccell .ci{font-size:36px} .logline{font-size:14px;padding:6px 0} .petgrid{grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:12px} .petcard{padding:12px 6px} .petcard .pi{font-size:34px} .petcard .pn{font-size:12px} .petcard .pr,.petcard .pl{font-size:10.5px} .teamrow,.npcrow{font-size:15px;padding:13px} .teamrow .ti{font-size:32px} .shopsearch{font-size:16px;padding:14px 20px} .shopcat button{font-size:12.5px;padding:11px 6px} #catch-stage{width:330px;height:330px} #catch-target,#catch-ring{width:150px;height:150px} #catch-icon{font-size:62px} #safety-banner{font-size:15px;padding:14px 20px} #intro-inner{max-width:600px;padding:50px 40px} #intro-inner h2{font-size:30px} #intro-inner p{font-size:17px} #intro-ok{font-size:18px;padding:18px 60px} #unlock-inner{max-width:500px;padding:40px 36px} #unlock-inner h3{font-size:24px} #unlock-inner p{font-size:17px} #unlock-buttons button{font-size:16px;padding:15px 24px} #outorbs-inner{max-width:560px;padding:40px 36px} #outorbs-inner h3{font-size:26px} #outorbs-inner p{font-size:17px} #outorbs-buttons button{font-size:16px;padding:15px 24px} .orbpick button{font-size:13px;padding:9px 14px} .cmd-grid{grid-template-columns:repeat(4,minmax(0,1fr))} .cmd-btn{font-size:14px;padding:14px 10px} #cosmos-controls{left:50%;gap:10px;bottom:90px;width:min(80vw,760px)} .c-ctrl{padding:8px 14px;font-size:12px} #char-inner{max-width:640px;padding:48px 40px} #char-inner h2{font-size:26px} #char-inner p{font-size:16px} #char-name{font-size:17px;padding:15px 20px} #char-pets{grid-template-columns:repeat(auto-fill,minmax(104px,1fr))} #char-create{font-size:16px;padding:16px 44px} #passenger-inner,#weather-inner{max-width:560px;padding:40px 34px} #passenger-inner h3,#weather-inner h3{font-size:23px} #passenger-inner p,#weather-msg{font-size:16px} #passenger-buttons button,#weather-close{font-size:15px;padding:15px 20px} #card-inner{max-width:600px;padding:32px} #card-inner h3{font-size:20px} #card-download,#card-share,#card-close{font-size:15px;padding:14px 32px} #game-panel-card.compact .duel-log { height: 150px; } #game-panel-card.compact .duel-moves button { padding: 12px 8px; font-size: 13px; } #settings-inner{max-width:620px;padding:36px} #settings-inner h3{font-size:24px} #settings-inner .setrow{font-size:15px;padding:15px} #settings-inner .setrow b{font-size:16px} #settings-inner .setrow small{font-size:12px} #step-goal-input{width:100px;font-size:16px;padding:10px 12px} #step-goal-save{font-size:14px;padding:12px 26px} #settings-close{font-size:15px;padding:13px 32px} #onchain-inner{max-width:760px;height:90vh;max-height:900px} #onchain-head h2{font-size:22px} #onchain-head .sub{font-size:13px} #onchain-price{font-size:14px;padding:14px 16px} #onchain-price b{font-size:16px} .onchain-item{padding:16px;gap:16px} .onchain-item .oi-icon{width:64px;height:64px;font-size:38px} .onchain-item .oi-name{font-size:16px} .onchain-item .oi-desc{font-size:13px} .onchain-item .oi-price{font-size:13px} .onchain-item .oi-price b{font-size:15px} .onchain-item .oi-price span{font-size:12px} .onchain-item .oi-buy{font-size:13px;padding:12px 20px} #wallet-inner{max-width:520px;padding:32px 28px} #wallet-inner h3{font-size:20px} #wallet-inner p{font-size:14px} .wallet-btn{font-size:15px;padding:14px 16px} .wallet-btn .wicon{width:42px;height:42px;font-size:26px} #wallet-disconnect{font-size:14px;padding:13px 24px} } @media (min-width:1000px){ html,body,#c,#s,#hud,#game-overlay,#welcome,#modal,#outpost,#worlds,#godseye-overlay,#unlock-modal,#intro-modal,#outorbs-modal,#char-modal,#passenger-modal,#weather-modal,#card-modal,#settings-modal{width:100%!important;height:100%!important;max-width:100%!important} #game-panel-card{max-width:880px} .cgrid{grid-template-columns:repeat(6,1fr)} .petgrid{grid-template-columns:repeat(auto-fill,minmax(118px,1fr))} .cmd-grid{grid-template-columns:repeat(5,minmax(0,1fr))} } @media (min-width:1400px){ html,body,#c,#s,#hud,#game-overlay,#welcome,#modal,#outpost,#worlds,#godseye-overlay,#unlock-modal,#intro-modal,#outorbs-modal,#char-modal,#passenger-modal,#weather-modal,#card-modal,#settings-modal{width:100%!important;height:100%!important;max-width:100%!important} #game-panel-card{max-width:1020px} #outpost-card{max-width:1280px} .cgrid{grid-template-columns:repeat(8,1fr)} } </style> </head> <body> <canvas id=”c”></canvas> <div id=”s”><div id=”sp”> <div class=”snap”></div><div class=”snap”></div><div class=”snap”></div><div class=”snap”></div><div class=”snap”></div> <div class=”snap”></div><div class=”snap”></div><div class=”snap”></div><div class=”snap”></div><div class=”snap”></div> <div class=”snap”></div><div class=”snap”></div><div class=”snap”></div><div class=”snap”></div><div class=”snap”></div> <div class=”snap”></div><div class=”snap”></div><div class=”snap”></div><div class=”snap”></div><div class=”snap”></div> <div class=”snap”></div><div class=”snap”></div> </div></div> <div id=”hud”> <div id=”logo”>AFBN COSMOS<span></span></div> <div id=”fps”></div> <button id=”worlds-btn”>โฐ WORLDS</button> <div id=”wallet-bar”> <button id=”wallet-btn”>๐ CONNECT WALLET</button> <button id=”shop-hud-btn”>๐ ON-CHAIN SHOP</button> </div> <button id=”mute-btn”>๐</button> <button id=”profile-btn”>๐ค</button> <div id=”cosmos-controls”> <button class=”c-ctrl” id=”cosmos-prev-btn”>โ PREV</button> <button class=”c-ctrl” id=”cosmos-zoomin-btn”>๐ ZOOM IN</button> <button class=”c-ctrl” id=”cosmos-next-btn”>NEXT โถ</button> <button class=”c-ctrl” id=”cosmos-music-btn”>๐ต MUSIC: SPACE</button> </div> <button id=”bunny-btn” class=”hbtn”></button> <div id=”toast”><button class=”toast-close” type=”button” aria-label=”Close”>ร</button><h3></h3><p></p><button class=”toast-action” id=”toast-gps-btn” type=”button”>GRANT GPS PERMISSIONS</button></div> </div> <div id=”welcome”> <div id=”welcome-inner”> <h1>AFBN COSMOS</h1> <p>A scroll-driven journey through the A Few Bad Newbies ecosystem. Each world is a chapter. Tap the ecosystem button on any world that hosts the ecosystem, or open the Worlds menu and enter Street Claim. Explore. Become part of the cosmos.</p> <button id=”enter-btn”>Enter the Cosmos</button> </div> </div> <div id=”intro-modal”> <div id=”intro-inner”> <button id=”intro-close-x” class=”modal-x” type=”button” aria-label=”Close”>ร</button> <h2>AFBN COSMOS</h2> <p>Welcome to the AFBN Cosmos, a scroll-driven 5D journey through the A Few Bad Newbies ecosystem. Use the Worlds menu or scroll up and down to navigate between planets and galaxies. Drag with one finger to rotate the camera in 5D, and pinch with two fingers to zoom in and out. Extreme zoom in and extreme zoom out are supported. Zoom in on Earth to open God’s Eye View, then zoom in further on God’s Eye View to enter Street Claim. Zoom out from Street Claim to return to God’s Eye View, and zoom out from God’s Eye View to return to the Cosmos. Future planets will unlock progressive levels and custom maps. Press OK to begin.</p> <button id=”intro-ok”>OK</button> </div> </div> <div id=”worlds”> <div id=”worlds-head”><span>WORLDS</span><button id=”worlds-close”>ร</button></div> <button id=”worlds-quick”>โก ENTER STREET CLAIM</button> <div id=”worlds-list”></div> </div> <div id=”outpost”> <div id=”outpost-card”> <button id=”outpost-close”>ร</button> <div id=”outpost-head”> <div id=”outpost-icon”></div> <div><h2 id=”outpost-title”></h2><div id=”outpost-cat”></div></div> </div> <p id=”outpost-fact”></p> <input id=”outpost-search” type=”text” placeholder=”Search this outpost” autocomplete=”off”> <div id=”outpost-body”> <div id=”outpost-grid”></div> <div id=”outpost-empty”>NOTHING FOUND IN THIS OUTPOST</div> </div> </div> </div> <div id=”modal”> <div id=”modal-inner”> <div id=”modal-head”></div> <div id=”modal-title”></div> <button id=”modal-close-x”>ร</button> <iframe id=”modal-frame” src=”about:blank” allow=”autoplay; fullscreen; clipboard-write; encrypted-media; picture-in-picture”></iframe> <div id=”modal-loading”> <div class=”spinner”></div> <div class=”loading-text”>LOADING COSMOS CONTENT…</div> </div> <div id=”modal-info”></div> <button id=”modal-close-btn”>CLOSE</button> </div> </div> <div id=”unlock-modal”> <div id=”unlock-inner”> <button id=”unlock-close-x” class=”modal-x” type=”button” aria-label=”Close”>ร</button> <h3>UNLOCKABLE CONTENT</h3> <p>Unlockable content coming soon. Would you like to visit Earth’s Street Claim instead?</p> <div id=”unlock-buttons”> <button id=”unlock-yes”>YES</button> <button id=”unlock-no”>NO</button> </div> </div> </div> <div id=”outorbs-modal”> <div id=”outorbs-inner”> <button id=”outorbs-close-x” class=”modal-x” type=”button” aria-label=”Close”>ร</button> <h3>OUT OF WEAVES</h3> <p id=”outorbs-msg”>You have no weaves left. Visit the shop to acquire more, or cancel to continue exploring.</p> <div id=”outorbs-buttons”> <button id=”outorbs-shop”>VISIT SHOP</button> <button id=”outorbs-cancel”>CANCEL</button> </div> </div> </div> <div id=”char-modal”> <div id=”char-inner”> <h2>CHARACTER CREATION</h2> <p>Choose a name, confirm your nationality, and pick one COMMON starter pet.</p> <input id=”char-name” type=”text” placeholder=”Your character name” maxlength=”24″ autocomplete=”off”> <p id=”char-nation”>Nationality: detecting…</p> <p style=”text-align:left”><b>CHOOSE YOUR STARTER COMMON PET</b></p> <div id=”char-pets”></div> <button id=”char-create” disabled>CREATE CHARACTER</button> </div> </div> <div id=”passenger-modal”> <div id=”passenger-inner”> <h3>SAFETY WARNING</h3> <p>Playing Street Claim while driving is extremely dangerous and against our safety guidance. Do not look at your phone while operating a vehicle. If you are driving, pull over and stop before you continue playing. If you are a passenger, confirm below to continue.</p> <div id=”passenger-buttons”> <button id=”passenger-yes”>I AM A PASSENGER</button> <button id=”passenger-no”>I WILL STOP</button> </div> </div> </div> <div id=”weather-modal”> <div id=”weather-inner”> <button id=”weather-close-x” class=”modal-x” type=”button” aria-label=”Close”>ร</button> <h3>WEATHER ALERT</h3> <p id=”weather-msg”></p> <button id=”weather-close”>CLOSE</button> </div> </div> <div id=”card-modal”> <div id=”card-inner”> <h3>PLAYER CARD</h3> <canvas id=”card-canvas” width=”720″ height=”1024″></canvas> <button id=”card-download”>DOWNLOAD PNG</button> <button id=”card-share”>SHARE TO X</button> <button id=”card-close”>CLOSE</button> </div> </div> <div id=”settings-modal”> <div id=”settings-inner”> <button id=”settings-close-x” class=”modal-x” type=”button” aria-label=”Close”>ร</button> <h3>SETTINGS</h3> <div class=”setrow”> <div> <b>Daily Step Goal</b> <small>Adjust your daily step target. Rewards scale with streaks.</small> </div> <input id=”step-goal-input” type=”number” min=”500″ max=”50000″ step=”500″ value=”5000″> </div> <div class=”setrow”> <div> <b>Current Streak</b> <small>Consecutive days meeting your step goal.</small> </div> <span id=”step-streak-display” style=”color:#ffd93d;font-weight:700;font-size:18px”>0</span> </div> <div class=”setrow”> <div> <b>Today’s Progress</b> <small>Steps walked today toward your goal.</small> </div> <span id=”step-today-display” style=”color:#7fffc4;font-weight:700;font-size:16px”>0</span> </div> <button id=”step-goal-save”>SAVE STEP GOAL</button> <button id=”settings-close”>CLOSE</button> </div> </div> <div id=”godseye-overlay”> <div id=”godseye-map”></div> <div id=”godseye-top”> <input id=”godseye-search” type=”text” placeholder=”Search any place or type lat, lon” autocomplete=”off”> <button id=”godseye-go”>GO</button> <button id=”godseye-me” title=”Center on my location”>๐ ME</button> <button id=”godseye-zoomin” title=”Zoom in to Street Claim”>ZOOM IN</button> <button id=”godseye-zoomout” title=”Zoom out to Cosmos”>ZOOM OUT</button> </div> <div id=”godseye-hint”>ZOOM IN FOR STREET VIEW ยท ZOOM OUT FOR COSMOS</div> </div> <div id=”game-overlay”> <div id=”game-top”> <div id=”game-title”>STREET CLAIM</div> <div id=”game-coins”>0 CREDITS</div> <button id=”game-mute” class=”gtop”>๐</button> <button id=”game-gps-btn” class=”gtop” style=”display:none; color:#ff8a8a; border-color:rgba(255,100,100,0.6); animation:pulse 1.5s infinite;”>๐</button> <button id=”game-exit”>ZOOM OUT</button> </div> <div id=”safety-banner”>SAFETY: Stay aware of your surroundings. Playing while driving is dangerous.</div> <div id=”game-map”><div id=”leaflet-map”></div> <div id=”lucky-badge”></div> <div id=”game-status”>Acquiring GPS signal</div> <div id=”game-panel”> <div id=”game-panel-card”> <button id=”game-panel-close-x” class=”modal-x” type=”button” aria-label=”Close”>ร</button> <h3 id=”panel-title”></h3> <div id=”panel-body”></div> <button id=”game-panel-close”>CLOSE</button> </div> </div> </div> <div id=”game-notifications”></div> <div id=”game-bottom”> <button id=”gb-toggle” type=”button”>โฐ COMMAND MENU</button> <span id=”gb-tally”>๐ 0</span> </div> </div> <div id=”onchain-shop”> <div id=”onchain-inner”> <button id=”onchain-close”>ร</button> <div id=”onchain-head”> <h2>ON-CHAIN SHOP</h2> <div class=”sub”>Pay with SOL ยท Treasury: CA4n7Lc8g6S9E7CMvY234xsovtsEYAjtuJZuf7yasGND</div> </div> <div id=”onchain-body”> <div id=”onchain-price”> <div>SOL / USD: <b id=”sol-price-val”>loadingโฆ</b></div> <div style=”display:flex;gap:8px;align-items:center;”> <span id=”sol-refresh-ts” style=”font-size:10px;color:#a0e8d0″></span> <button id=”sol-refresh-btn” type=”button”>REFRESH</button> </div> </div> <div id=”onchain-wallet-row” style=”margin-bottom:12px;padding:10px 12px;border:1px solid rgba(80,255,190,0.35);border-radius:12px;background:rgba(4,28,20,0.65);font-size:12px;letter-spacing:0.5px;”></div> <div id=”onchain-items”></div> <div id=”onchain-status” class=”onchain-status” style=”display:none”></div> </div> </div> </div> <div id=”wallet-modal”> <div id=”wallet-inner”> <button id=”wallet-close”>ร</button> <h3>CONNECT WALLET</h3> <p id=”wallet-hint”>Choose a wallet to connect. Mobile Wallet Adapter is used automatically on mobile devices that support it. On desktop, Phantom, Solflare, Backpack, and other injected wallets are detected from the browser window.</p> <div id=”wallet-list”></div> <button id=”wallet-disconnect” style=”display:none”>DISCONNECT WALLET</button> </div> </div> <script> (function(){ “use strict”; const BUILD=”2026.10.21.3″; const TREASURY_WALLET=”CA4n7Lc8g6S9E7CMvY234xsovtsEYAjtuJZuf7yasGND”; const SOL_RPC_ENDPOINTS=[ “https://solana-mainnet.g.alchemy.com/v2/fomoGpp-lOCMiZcM5h_r5”, “https://solana-mainnet.g.alchemy.com/v2/o0Ecdd4GZU3SZGQWMN2QV-HS74_TWBrB”, “https://mainnet.helius-rpc.com/?api-key=f1035b8b-2f83-4e7a-8cbe-651df34f0163”, “https://mainnet.helius-rpc.com/?api-key=b7746563-a19e-4b1a-bc72-663fd7f41232”, “https://api.mainnet-beta.solana.com” ]; const COINGECKO_SOL=”https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd”; const ONCHAIN_ITEMS=[ {id:”starter_pack”,name:”Starter Pack”,desc:”+5 Echo Weaves, +500 credits, +1 Prism Weave”,usd:1.99,icon:”๐”,apply:function(){gState.items.orb=(gState.items.orb||0)+5;gState.items.orb_enhanced=(gState.items.orb_enhanced||0)+1;gState.credits+=500;}}, {id:”weave_bundle”,name:”Weave Bundle”,desc:”+25 Echo Weaves”,usd:4.99,icon:”ใฐ๏ธ”,apply:function(){gState.items.orb=(gState.items.orb||0)+25;}}, {id:”prism_bundle”,name:”Prism Bundle”,desc:”+10 Prism Weaves”,usd:9.99,icon:”๐น”,apply:function(){gState.items.orb_enhanced=(gState.items.orb_enhanced||0)+10;}}, {id:”arc_bundle”,name:”Arc Bundle”,desc:”+10 Arc Weaves”,usd:14.99,icon:”โก”,apply:function(){gState.items.orb_precision=(gState.items.orb_precision||0)+10;}}, {id:”crown_bundle”,name:”Crown Bundle”,desc:”+10 Crown Weaves”,usd:24.99,icon:”๐ถ”,apply:function(){gState.items.orb_master=(gState.items.orb_master||0)+10;}}, {id:”nova_bundle”,name:”Nova Bundle”,desc:”+5 Nova Weaves”,usd:49.99,icon:”๐”,apply:function(){gState.items.orb_legendary=(gState.items.orb_legendary||0)+5;}}, {id:”void_bundle”,name:”Void Bundle”,desc:”+5 Void Weaves”,usd:74.99,icon:”๐”,apply:function(){gState.items.orb_anomaly=(gState.items.orb_anomaly||0)+5;}}, {id:”credit_pack_s”,name:”Credit Pack S”,desc:”+5,000 credits”,usd:2.99,icon:”๐ฐ”,apply:function(){gState.credits+=5000;}}, {id:”credit_pack_m”,name:”Credit Pack M”,desc:”+15,000 credits”,usd:7.99,icon:”๐ฐ”,apply:function(){gState.credits+=15000;}}, {id:”credit_pack_l”,name:”Credit Pack L”,desc:”+50,000 credits”,usd:19.99,icon:”๐ฐ”,apply:function(){gState.credits+=50000;}}, {id:”shield_bundle”,name:”Shield Bundle”,desc:”+5 Turf Shields”,usd:4.99,icon:”๐ก๏ธ”,apply:function(){gState.items.shield=(gState.items.shield||0)+5;}}, {id:”radar_bundle”,name:”Radar Bundle”,desc:”+5 Radar Packs”,usd:4.99,icon:”๐ก”,apply:function(){gState.items.radar=(gState.items.radar||0)+5;}}, {id:”boost_bundle”,name:”Boost Bundle”,desc:”+5 Claim Boosts”,usd:6.99,icon:”โก”,apply:function(){gState.items.boost=(gState.items.boost||0)+5;}} ]; const WALLET={kind:null,publicKey:null,connected:false,connecting:false,provider:null,adapter:null,label:””,buyInProgress:false}; let SOL_USD=null; let SOL_USD_TS=0; let priceTimer=null; let SOL_WEB3=null; let SOL_CONN=null; let phantomBridgePending=null; let phantomConnecting=false; let mwaConnecting=false; const MWA_CHAIN=”solana:mainnet”; const MWA_IDENTITY={name:”AFBN COSMOS”,uri:(typeof window!==”undefined”&&window.location&&window.location.origin)?window.location.origin:”https://afbn.me”,icon:”/favicon.ico”}; function solWeb3(){ if(SOL_WEB3)return SOL_WEB3; if(window.solanaWeb3)SOL_WEB3=window.solanaWeb3; return SOL_WEB3; } function getSolConnection(){ const w=solWeb3(); if(!w)return null; if(SOL_CONN&&SOL_CONN.rpcEndpoint===SOL_RPC_ENDPOINTS[0])return SOL_CONN; SOL_CONN=new w.Connection(SOL_RPC_ENDPOINTS[0],”confirmed”); return SOL_CONN; } function shortAddr(a){return a?String(a).slice(0,4)+”โฆ”+String(a).slice(-4):””;} function fmtSol(n){ if(n===null||n===undefined||isNaN(n))return “โ”; if(n>=1)return n.toFixed(4); if(n>=0.001)return n.toFixed(5); return n.toFixed(7); } function isInIframe(){ try{return window.self!==window.top;}catch(e){return true;} } function getPhantomProvider(){ try{ var wins=[window]; try{if(window.parent&&window.parent!==window)wins.push(window.parent);}catch(e){} try{if(window.top&&window.top!==window&&window.top!==window.parent)wins.push(window.top);}catch(e){} for(var i=0;i<wins.length;i++){ var w=wins[i]; try{ if(w.phantom&&w.phantom.solana){var p1=w.phantom.solana;if(p1&&(p1.isPhantom||typeof p1.connect===”function”))return p1;} }catch(e){} try{if(w.solana&&w.solana.isPhantom===true)return w.solana;}catch(e){} try{if(w.solana&&typeof w.solana.connect===”function”&&w.solana.name&&String(w.solana.name).toLowerCase().indexOf(“phantom”)!==-1)return w.solana;}catch(e){} } }catch(e){} return null; } function requestPhantomConnectViaBridge(onlyIfTrusted){ return new Promise(function(resolve,reject){ if(phantomBridgePending){reject(new Error(“Connection already in progress”));return;} phantomBridgePending={resolve:resolve,reject:reject}; var msg={source:”afbn-cosmos-game”,type:”phantom-connect”,onlyIfTrusted:!!onlyIfTrusted}; try{ if(window.parent&&window.parent!==window){window.parent.postMessage(msg,”*”);} else{window.postMessage(msg,”*”);} }catch(e){ phantomBridgePending=null;reject(e);return; } setTimeout(function(){ if(phantomBridgePending){var p=phantomBridgePending;phantomBridgePending=null;p.reject(new Error(“Phantom bridge timeout”));} },45000); }); } window.addEventListener(“message”,function(event){ try{ if(!event.data||event.data.source!==”afbn-cosmos-parent”)return; if(event.data.type===”phantom-connect-result”){ if(!phantomBridgePending)return; var pending=phantomBridgePending; phantomBridgePending=null; if(event.data.error){pending.reject(new Error(event.data.error));} else if(event.data.publicKey){pending.resolve({publicKey:event.data.publicKey});} else{pending.reject(new Error(“No public key returned”));} } }catch(e){} }); function extractPubkey(account){ if(!account)return null; if(typeof account===”string”)return account; if(typeof account.toBase58===”function”){try{return account.toBase58();}catch(e){}} if(account.publicKey){ if(typeof account.publicKey===”string”)return account.publicKey; if(typeof account.publicKey.toBase58===”function”)return account.publicKey.toBase58(); } if(account.address){ if(typeof account.address!==”string”)return null; try{ var w=solWeb3(); if(w&&w.PublicKey){ var s=String(account.address).replace(/-/g,”+”).replace(/_/g,”/”); while(s.length%4!==0)s+=”=”; var bin=atob(s); var bytes=new Uint8Array(bin.length); for(var i=0;i<bin.length;i++)bytes[i]=bin.charCodeAt(i); if(bytes.length===32)return new w.PublicKey(bytes).toBase58(); } }catch(e){} return account.address; } if(account.accounts&&account.accounts.length>0)return extractPubkey(account.accounts[0]); return null; } function detectInjectedWallets(){ const out=[]; const seen=new Set(); function add(name,icon,kind,getProvider){ if(seen.has(name))return; seen.add(name); const provider=getProvider(); if(!provider)return; out.push({name:name,icon:icon,kind:kind,getProvider:getProvider}); } add(“Phantom”,”๐ป”,”injected”,function(){return getPhantomProvider();}); add(“Solflare”,”๐”,”injected”,function(){try{if(window.solflare&&(typeof window.solflare.connect===”function”||window.solflare.isSolflare===true))return window.solflare;if(window.solflare&&window.solflare.solana&&typeof window.solflare.solana.connect===”function”)return window.solflare.solana;}catch(e){}return null;}); add(“Backpack”,”๐”,”injected”,function(){try{if(window.backpack&&window.backpack.isBackpack)return window.backpack;if(window.backpack&&typeof window.backpack.connect===”function”)return window.backpack;}catch(e){}return null;}); add(“Glow”,”โจ”,”injected”,function(){try{return window.glow&&window.glow.isGlow?window.glow:null;}catch(e){return null;}}); add(“OKX”,”๐ฆ”,”injected”,function(){try{return (window.okxwallet&&window.okxwallet.solana)?window.okxwallet.solana:null;}catch(e){return null;}}); add(“BitKeep”,”๐จ”,”injected”,function(){try{return (window.bitkeep&&window.bitkeep.solana)?window.bitkeep.solana:null;}catch(e){return null;}}); add(“Coin98″,”๐ฉ”,”injected”,function(){try{return (window.coin98&&window.coin98.sol)?window.coin98.sol:null;}catch(e){return null;}}); add(“Exodus”,”๐ต”,”injected”,function(){try{return (window.exodus&&window.exodus.solana)?window.exodus.solana:null;}catch(e){return null;}}); add(“MetaMask”,”๐ฆ”,”injected”,function(){try{if(window.metamask&&window.metamask.solana&&typeof window.metamask.solana.connect===”function”)return window.metamask.solana;if(window.ethereum&&window.ethereum.solana&&typeof window.ethereum.solana.connect===”function”)return window.ethereum.solana;}catch(e){}return null;}); if(window.solana&&typeof window.solana.connect===”function”&&!window.solana.isPhantom){ add(“Solana Wallet”,”๐”,”injected”,function(){return window.solana;}); } if(window.WALLET_STANDARD_WALLETS&&window.WALLET_STANDARD_WALLETS.length){ window.WALLET_STANDARD_WALLETS.forEach(function(sw){ if(sw&&sw.name&&!seen.has(sw.name)){ add(sw.name,”๐”,”standard”,function(){return sw;}); } }); } return out; } function detectMobileWalletAdapter(){ try{ if(window.MWA_transact||window.MWA_READY){return {name:”Mobile Wallet Adapter”,icon:”๐ฑ”,kind:”mwa”};} }catch(e){} if(window.WALLET_STANDARD_WALLETS&&window.WALLET_STANDARD_WALLETS.length){ try{ for(var i=0;i<window.WALLET_STANDARD_WALLETS.length;i++){ var w=window.WALLET_STANDARD_WALLETS[i]; if(w&&w.features&&w.features[“solana:signAndSendTransaction”]){return {name:”Mobile Wallet Adapter”,icon:”๐ฑ”,kind:”mwa”};} } }catch(e){} } return null; } async function connectPhantom(provider){ if(phantomConnecting){showToast(“Wallet”,”Connection already in progress.”);return false;} phantomConnecting=true; try{ if(isInIframe()||!provider){ const bridgeResp=await requestPhantomConnectViaBridge(false); if(!bridgeResp||!bridgeResp.publicKey)throw new Error(“Phantom bridge returned no public key”); const pk=String(bridgeResp.publicKey); WALLET.kind=”injected”; WALLET.provider=null; WALLET.publicKey=pk; WALLET.label=”Phantom”; WALLET.connected=true; WALLET.connecting=false; phantomConnecting=false; updateWalletHud(); renderOnchainItems(); showToast(“Wallet Connected”,”Phantom ยท “+shortAddr(pk)); sfx(“claim”); return true; } let resp=null; try{resp=await provider.connect({onlyIfTrusted:false});} catch(e1){resp=await provider.connect();} let pub=null; if(resp&&resp.publicKey){ pub=typeof resp.publicKey===”string”?resp.publicKey:(typeof resp.publicKey.toBase58===”function”?resp.publicKey.toBase58():String(resp.publicKey)); }else if(provider.publicKey){ pub=typeof provider.publicKey===”string”?provider.publicKey:(typeof provider.publicKey.toBase58===”function”?provider.publicKey.toBase58():String(provider.publicKey)); } if(!pub)throw new Error(“Phantom did not return a public key”); WALLET.kind=”injected”; WALLET.provider=provider; WALLET.publicKey=pub; WALLET.label=”Phantom”; WALLET.connected=true; WALLET.connecting=false; phantomConnecting=false; try{ if(typeof provider.on===”function”){ provider.on(“disconnect”,function(){disconnectWallet();}); } }catch(e){} updateWalletHud(); renderOnchainItems(); showToast(“Wallet Connected”,”Phantom ยท “+shortAddr(pub)); sfx(“claim”); return true; }catch(e){ phantomConnecting=false; const msg=(e&&e.message)||”Unable to connect Phantom.”; if(msg.indexOf(“User rejected”)!==-1||msg.indexOf(“4001”)!==-1){ showToast(“Wallet”,”You cancelled the Phantom connection.”); }else if(msg.indexOf(“already pending”)!==-1||msg.indexOf(“-32002”)!==-1){ showToast(“Wallet”,”A Phantom request is already pending. Open Phantom to approve it.”); }else{ showToast(“Wallet Error”,msg); } return false; } } async function connectInjected(wallet){ try{ WALLET.connecting=true; const provider=wallet.getProvider(); if(!provider)throw new Error(“Wallet not available”); if(wallet.name===”Phantom”||(provider&&provider.isPhantom)||getPhantomProvider()===provider){ const ok=await connectPhantom(provider); if(!ok)WALLET.connecting=false; return ok; } let resp=null; if(typeof provider.connect===”function”){ resp=await provider.connect(); }else if(typeof provider.request===”function”){ resp=await provider.request({method:”connect”}); }else{ throw new Error(“Wallet does not expose connect()”); } let pub=null; if(resp&&resp.publicKey){ pub=extractPubkey(resp.publicKey); }else if(provider.publicKey){ pub=extractPubkey(provider.publicKey); }else if(resp&&resp.accounts&&resp.accounts.length){ pub=extractPubkey(resp.accounts[0]); }else if(resp&&resp.address){ pub=resp.address; }else if(resp){ pub=extractPubkey(resp); } if(!pub)throw new Error(“No public key returned from wallet”); WALLET.kind=wallet.kind===”standard”?”standard”:”injected”; WALLET.provider=provider; WALLET.publicKey=pub; WALLET.label=wallet.name; WALLET.connected=true; WALLET.connecting=false; updateWalletHud(); renderOnchainItems(); showToast(“Wallet Connected”,wallet.name+” ยท “+shortAddr(pub)); sfx(“claim”); return true; }catch(e){ WALLET.connecting=false; const msg=(e&&e.message)||”Unable to connect wallet.”; if(msg.indexOf(“User rejected”)!==-1||msg.indexOf(“4001”)!==-1){ showToast(“Wallet”,”You cancelled the wallet connection.”); }else{ showToast(“Wallet Error”,msg); } return false; } } async function connectMwa(){ if(mwaConnecting){showToast(“Wallet”,”MWA connection already in progress.”);return false;} mwaConnecting=true; try{ if(typeof window.MWA_transact!==”function”){ if(!window.MWA_READY){ await new Promise(function(r){setTimeout(r,1500);}); } if(typeof window.MWA_transact!==”function”){ throw new Error(“Mobile Wallet Adapter library not loaded yet. Please wait a moment and try again, or use an injected wallet.”); } } const authParams={identity:MWA_IDENTITY,chain:MWA_CHAIN}; const auth=await window.MWA_transact(async function(wallet){ const a=await wallet.authorize(authParams); return a; }); if(!auth||!auth.accounts||!auth.accounts.length)throw new Error(“Mobile Wallet Adapter returned no account”); const pub=extractPubkey(auth.accounts[0]); if(!pub)throw new Error(“Mobile Wallet Adapter did not return a public key”); WALLET.kind=”mwa”; WALLET.provider=null; WALLET.adapter=null; WALLET.publicKey=pub; WALLET.label=”Mobile Wallet Adapter”; WALLET.connected=true; WALLET.connecting=false; mwaConnecting=false; updateWalletHud(); renderOnchainItems(); showToast(“Wallet Connected”,”MWA ยท “+shortAddr(pub)); sfx(“claim”); return true; }catch(e){ mwaConnecting=false; const msg=(e&&e.message)||”Unable to connect mobile wallet.”; if(msg.indexOf(“User rejected”)!==-1||msg.indexOf(“4001”)!==-1||msg.indexOf(“cancel”)!==-1){ showToast(“Wallet”,”Mobile wallet connection was cancelled.”); }else{ showToast(“MWA Error”,msg); } return false; } } function disconnectWallet(){ const p=WALLET.provider; WALLET.kind=null; WALLET.provider=null; WALLET.adapter=null; WALLET.publicKey=null; WALLET.connected=false; WALLET.label=””; try{if(p&&typeof p.disconnect===”function”){p.disconnect().catch(function(){});}}catch(e){} updateWalletHud(); renderOnchainItems(); showToast(“Wallet”,”Disconnected.”); } function openWalletModal(){ const injected=detectInjectedWallets(); const mwa=detectMobileWalletAdapter(); const list=document.getElementById(“wallet-list”); const disc=document.getElementById(“wallet-disconnect”); if(!list)return; list.innerHTML=””; if(mwa){ const btn=document.createElement(“button”); btn.className=”wallet-btn”; btn.innerHTML='<div class=”wicon”>’+mwa.icon+'</div><div class=”wmeta”><div>’+mwa.name+'</div><small>Automatic on mobile (Android Chrome). Supports Solana Mobile Stack wallets.</small></div>’; btn.addEventListener(“click”,async function(){ const ok=await connectMwa(); if(ok)closeWalletModal(); }); list.appendChild(btn); } injected.forEach(function(w){ const btn=document.createElement(“button”); btn.className=”wallet-btn”; btn.innerHTML='<div class=”wicon”>’+w.icon+'</div><div class=”wmeta”><div>’+w.name+'</div><small>Browser wallet detected.</small></div>’; btn.addEventListener(“click”,async function(){ const ok=await connectInjected(w); if(ok)closeWalletModal(); }); list.appendChild(btn); }); if(!list.children.length){ if(isInIframe()){ const btn=document.createElement(“button”); btn.className=”wallet-btn”; btn.innerHTML='<div class=”wicon”>๐ป</div><div class=”wmeta”><div>Phantom (via parent)</div><small>Opens Phantom using the parent window bridge.</small></div>’; btn.addEventListener(“click”,async function(){ const ok=await connectPhantom(getPhantomProvider()); if(ok)closeWalletModal(); }); list.appendChild(btn); } const p=document.createElement(“p”); p.style.color=”#ffb0b0″; p.style.fontSize=”12.5px”; p.textContent=”No Solana wallets detected in this browser. Install Phantom, Solflare, Backpack, or use a mobile wallet via MWA.”; list.appendChild(p); } disc.style.display=WALLET.connected?”block”:”none”; document.getElementById(“wallet-modal”).classList.add(“active”); } function closeWalletModal(){ document.getElementById(“wallet-modal”).classList.remove(“active”); } async function signAndSendTransaction(transaction){ if(!WALLET.connected)throw new Error(“Wallet not connected”); if(WALLET.kind===”injected”&&WALLET.provider){ const p=WALLET.provider; if(typeof p.signAndSendTransaction===”function”){ const res=await p.signAndSendTransaction(transaction); if(typeof res===”string”)return res; if(res&&res.signature)return res.signature; if(res&&res.signatures&&res.signatures.length)return res.signatures[0]; throw new Error(“Unexpected wallet response”); } if(typeof p.signTransaction===”function”&&solWeb3()){ const signed=await p.signTransaction(transaction); const conn=getSolConnection(); const sig=await conn.sendRawTransaction(signed.serialize(),{skipPreflight:false,maxRetries:3}); return sig; } throw new Error(“Wallet does not support signing”); } if(WALLET.kind===”mwa”){ if(typeof window.MWA_transact!==”function”)throw new Error(“MWA not available”); const authParams={identity:MWA_IDENTITY,chain:MWA_CHAIN}; const results=await window.MWA_transact(async function(wallet){ await wallet.authorize(authParams); const signed=await wallet.signAndSendTransactions({transactions:[transaction]}); return signed; }); if(!results||!results.length)throw new Error(“No signature returned from mobile wallet”); const sig=results[0]; if(typeof sig===”string”)return sig; if(sig&&sig.signature)return sig.signature; if(sig&&sig.signatures&&sig.signatures.length)return sig.signatures[0]; throw new Error(“Unexpected mobile wallet response”); } throw new Error(“No signer available”); } async function fetchSolPrice(){ try{ const ctrl=new AbortController(); const to=setTimeout(function(){try{ctrl.abort();}catch(e){}},9000); const r=await fetch(COINGECKO_SOL,{cache:”no-store”,signal:ctrl.signal}); clearTimeout(to); if(!r.ok)return null; const j=await r.json(); const v=j&&j.solana&&typeof j.solana.usd===”number”?j.solana.usd:null; if(v&&v>0){SOL_USD=v;SOL_USD_TS=Date.now();} return SOL_USD; }catch(e){return null;} } async function getSolUsdCached(){ if(SOL_USD&&Date.now()-SOL_USD_TS<45000)return SOL_USD; const v=await fetchSolPrice(); return v||SOL_USD; } function usdToSol(usd,solUsd){if(!solUsd||solUsd<=0)return null;return usd/solUsd;} function usdToLamports(usd,solUsd){ const sol=usdToSol(usd,solUsd); if(sol===null)return null; return Math.ceil(sol*1e9); } function updateWalletHud(){ const btn=document.getElementById(“wallet-btn”); if(!btn)return; if(WALLET.connected&&WALLET.publicKey){ btn.textContent=”๐ “+shortAddr(WALLET.publicKey); btn.classList.add(“connected”); }else{ btn.textContent=”๐ CONNECT WALLET”; btn.classList.remove(“connected”); } } async function purchaseOnchain(item){ if(WALLET.buyInProgress){ showOnchainStatus(“Another purchase is in progress. Please wait.”,”err”); return; } if(!WALLET.connected||!WALLET.publicKey){ openWalletModal(); showOnchainStatus(“Connect a wallet first.”,”err”); return; } const w=solWeb3(); if(!w){ showOnchainStatus(“Solana web3 library failed to load. Reload the page and try again.”,”err”); return; } WALLET.buyInProgress=true; renderOnchainItems(); const solUsd=await getSolUsdCached(); if(!solUsd){ showOnchainStatus(“Could not fetch SOL/USD price from CoinGecko. Try REFRESH then retry.”,”err”); WALLET.buyInProgress=false; renderOnchainItems(); return; } const lamports=usdToLamports(item.usd,solUsd); if(!lamports||lamports<=0){ showOnchainStatus(“Invalid SOL amount. Try again.”,”err”); WALLET.buyInProgress=false; renderOnchainItems(); return; } try{ showOnchainStatus(“Building transaction for “+item.name+” ยท “+fmtSol(lamports/1e9)+” SOL (โ$”+item.usd.toFixed(2)+”)”,””); const conn=getSolConnection(); if(!conn)throw new Error(“No Solana connection”); const fromPubkey=new w.PublicKey(WALLET.publicKey); const toPubkey=new w.PublicKey(TREASURY_WALLET); const {blockhash,lastValidBlockHeight}=await conn.getLatestBlockhash(“confirmed”); const tx=new w.Transaction({recentBlockhash:blockhash,feePayer:fromPubkey}); tx.add(w.SystemProgram.transfer({fromPubkey:fromPubkey,toPubkey:toPubkey,lamports:lamports})); const sig=await signAndSendTransaction(tx); showOnchainStatus(“Transaction submitted: “+sig+” โ confirmingโฆ”,””); try{ await conn.confirmTransaction({signature:sig,blockhash:blockhash,lastValidBlockHeight:lastValidBlockHeight},”confirmed”); }catch(confirmErr){ try{await conn.confirmTransaction(sig,”confirmed”);}catch(e2){throw confirmErr;} } try{ item.apply(); gState.stats.purchases=(gState.stats.purchases||0)+1; addXp(30); logEvent(“On-chain purchase confirmed: “+item.name+” ยท “+sig.slice(0,8)+”โฆ”); addItemLog(“๐ On-chain purchase: “+item.name+” (“+sig.slice(0,8)+”โฆ)”,”bag-orb”); saveGameState(gState); updateGameHud(); }catch(e){} showOnchainStatus(“โ Confirmed on Solana. “+item.name+” applied. Signature: “+sig,””); showToast(“Purchase Confirmed”,item.name+” unlocked. Tx “+sig.slice(0,10)+”โฆ”); sfx(“coin”); }catch(e){ showOnchainStatus(“Payment failed: “+((e&&e.message)||”unknown error”),”err”); sfx(“error”); }finally{ WALLET.buyInProgress=false; renderOnchainItems(); } } function showOnchainStatus(msg,cls){ const el=document.getElementById(“onchain-status”); if(!el)return; if(!msg){el.style.display=”none”;el.textContent=””;el.className=”onchain-status”;return;} el.style.display=”block”; el.textContent=msg; el.className=”onchain-status”+(cls===”err”?” err”:””); } function renderOnchainItems(){ const wrap=document.getElementById(“onchain-items”); if(!wrap)return; const solUsd=SOL_USD; let h=””; ONCHAIN_ITEMS.forEach(function(it){ const sol=usdToSol(it.usd,solUsd); const priceLine= sol!==null ? “<b>$”+it.usd.toFixed(2)+”</b><span>โ “+fmtSol(sol)+” SOL</span>” : “<b>$”+it.usd.toFixed(2)+”</b><span>SOL price loadingโฆ</span>”; const disabled=WALLET.buyInProgress?” disabled”:””; h+=”<div class=\”onchain-item\”>”; h+=”<div class=\”oi-icon\”>”+it.icon+”</div>”; h+=”<div class=\”oi-main\”><div class=\”oi-name\”>”+it.name+”</div><div class=\”oi-desc\”>”+it.desc+”</div><div class=\”oi-price\”>”+priceLine+”</div></div>”; h+=”<button class=\”oi-buy\” data-onchain-buy=\””+it.id+”\””+disabled+”>”+(WALLET.buyInProgress?”BUSY”:”BUY”)+”</button>”; h+=”</div>”; }); wrap.innerHTML=h; wrap.onclick=function(e){ const b=e.target.closest(“[data-onchain-buy]”); if(!b)return; if(WALLET.buyInProgress)return; const id=b.getAttribute(“data-onchain-buy”); const it=ONCHAIN_ITEMS.find(function(x){return x.id===id;}); if(!it)return; purchaseOnchain(it); }; const wrow=document.getElementById(“onchain-wallet-row”); if(wrow){ if(WALLET.connected&&WALLET.publicKey){ wrow.innerHTML=”<span style=\”color:#7fffc4\”>Wallet:</span> “+WALLET.label+” ยท “+shortAddr(WALLET.publicKey)+” <button id=\”onchain-wallet-disconnect\” style=\”float:right;background:rgba(255,60,60,0.2);border:1px solid rgba(255,120,120,0.6);color:#ffb0b0;padding:4px 12px;border-radius:999px;font-size:10.5px;letter-spacing:0.8px;cursor:pointer;font-weight:700\”>DISCONNECT</button>”; const db=document.getElementById(“onchain-wallet-disconnect”); if(db)db.addEventListener(“click”,function(){disconnectWallet();}); }else{ wrow.innerHTML=”<span style=\”color:#ffb0b0\”>Wallet not connected.</span> <button id=\”onchain-wallet-connect\” style=\”float:right;background:linear-gradient(135deg,#064a36,#00a87a);border:1px solid rgba(120,255,200,0.7);color:#fff;padding:4px 12px;border-radius:999px;font-size:10.5px;letter-spacing:0.8px;cursor:pointer;font-weight:700\”>CONNECT</button>”; const cb=document.getElementById(“onchain-wallet-connect”); if(cb)cb.addEventListener(“click”,function(){openWalletModal();}); } } } function renderSolPriceLine(){ const el=document.getElementById(“sol-price-val”); const ts=document.getElementById(“sol-refresh-ts”); if(el){ if(SOL_USD)el.textContent=”$”+SOL_USD.toFixed(2)+” / SOL”; else el.textContent=”unavailable”; } if(ts){ if(SOL_USD_TS)ts.textContent=”updated “+new Date(SOL_USD_TS).toLocaleTimeString(); else ts.textContent=””; } } async function refreshSolPrice(){ renderSolPriceLine(); const v=await fetchSolPrice(); renderSolPriceLine(); renderOnchainItems(); return v; } function openOnchainShop(){ const el=document.getElementById(“onchain-shop”); if(!el)return; el.classList.add(“active”); renderSolPriceLine(); renderOnchainItems(); showOnchainStatus(“”, “”); refreshSolPrice(); if(priceTimer)clearInterval(priceTimer); priceTimer=setInterval(function(){ if(!el.classList.contains(“active”)){clearInterval(priceTimer);priceTimer=null;return;} refreshSolPrice(); },60000); } function closeOnchainShop(){ const el=document.getElementById(“onchain-shop”); if(el)el.classList.remove(“active”); if(priceTimer){clearInterval(priceTimer);priceTimer=null;} } function wireWalletUi(){ const wb=document.getElementById(“wallet-btn”); const sb=document.getElementById(“shop-hud-btn”); const obc=document.getElementById(“onchain-close”); const wc=document.getElementById(“wallet-close”); const wd=document.getElementById(“wallet-disconnect”); const sr=document.getElementById(“sol-refresh-btn”); if(wb)wb.addEventListener(“click”,function(){ if(WALLET.connected){openWalletModal();}else{openWalletModal();} }); if(sb)sb.addEventListener(“click”,function(){openOnchainShop();}); if(obc)obc.addEventListener(“click”,closeOnchainShop); if(wc)wc.addEventListener(“click”,closeWalletModal); if(wd)wd.addEventListener(“click”,function(){disconnectWallet();closeWalletModal();}); if(sr)sr.addEventListener(“click”,function(){refreshSolPrice();}); const wm=document.getElementById(“wallet-modal”); if(wm)wm.addEventListener(“click”,function(e){if(e.target===wm)closeWalletModal();}); const os=document.getElementById(“onchain-shop”); if(os)os.addEventListener(“click”,function(e){if(e.target===os)closeOnchainShop();}); } const STADIA_API_PRIMARY=”90767656-2ae2-44ff-867f-e09b2eb1fff4″; const STADIA_API_SECONDARY=”ca190f7a-235e-4081-a353-49ba19674914″; const WALK_REWARD_PER_METER=0.025; const SHOP_COST_MULTIPLIER=10; const NPC_ATTACK_INTERVAL_MS=21600000; const NPC_ATTACK_CHANCE=0.18; const SEASON_LENGTH_MS=1814400000; const LUCKY_CYCLE=4*3600000; const LUCKY_DURATION=15*60000; const HOME_BUBBLE_METERS=60000; const OUTSIDE_BUBBLE_SPAWN_MULT=1.6; const GPS_REFRESH_MS=45000; const GPS_WATCHDOG_MS=20000; const GPS_STALE_MS=60000; const GPS_GRACE_MS=180000; const ARENA_DISTANCE_METERS=550; const ARENA_ENTER_RADIUS=80; const ARENA_MAX_WINS=10; const ARENA_WINDOW_MS=86400000; const ARENA_REFRESH_MS=30000; const BREED_COOLDOWN_MS=1800000; const BREED_HATCH_MIN_MS=1800000; const BREED_HATCH_MAX_MS=3600000; const STEPS_PER_METER=1/0.75; const STEP_GOAL_DEFAULT=5000; const STEP_GOAL_MIN=500; const STEP_GOAL_MAX=50000; const STEP_STREAK_REWARDS=[100,150,225,340,510,760,1140]; const RESONATOR_TUNE_MS=3000; const RESONATOR_RESIST_PULSE_MS=280; const RESONATOR_SHARD_COUNT=6; const RESONATOR_HOURLY_LIMIT=10; const RELIC_SPAWN_RATE=0.055; const RELIC_EXPIRY_MS=180000; const RESONATOR_DURABILITY_MAX=8; const canvas=document.getElementById(“c”); const scrollEl=document.getElementById(“s”); const toastEl=document.getElementById(“toast”); const toastTitle=toastEl.querySelector(“h3”); const toastBody=toastEl.querySelector(“p”); const toastCloseBtn=toastEl.querySelector(“.toast-close”); const gameNotesEl=document.getElementById(“game-notifications”); const fpsEl=document.getElementById(“fps”); const muteBtn=document.getElementById(“mute-btn”); const profileBtn=document.getElementById(“profile-btn”); const gameMute=document.getElementById(“game-mute”); const welcome=document.getElementById(“welcome”); const enterBtn=document.getElementById(“enter-btn”); const introModal=document.getElementById(“intro-modal”); const introOk=document.getElementById(“intro-ok”); const introCloseX=document.getElementById(“intro-close-x”); const modal=document.getElementById(“modal”); const modalFrame=document.getElementById(“modal-frame”); const modalLoading=document.getElementById(“modal-loading”); const modalInfo=document.getElementById(“modal-info”); const modalTitle=document.getElementById(“modal-title”); const modalCloseX=document.getElementById(“modal-close-x”); const modalCloseBtn=document.getElementById(“modal-close-btn”); const unlockModal=document.getElementById(“unlock-modal”); const unlockYes=document.getElementById(“unlock-yes”); const unlockNo=document.getElementById(“unlock-no”); const unlockCloseX=document.getElementById(“unlock-close-x”); const outOrbsModal=document.getElementById(“outorbs-modal”); const outOrbsMsg=document.getElementById(“outorbs-msg”); const outOrbsShop=document.getElementById(“outorbs-shop”); const outOrbsCancel=document.getElementById(“outorbs-cancel”); const outOrbsCloseX=document.getElementById(“outorbs-close-x”); const charModal=document.getElementById(“char-modal”); const charName=document.getElementById(“char-name”); const charNation=document.getElementById(“char-nation”); const charPets=document.getElementById(“char-pets”); const charCreate=document.getElementById(“char-create”); const passengerModal=document.getElementById(“passenger-modal”); const passengerYes=document.getElementById(“passenger-yes”); const passengerNo=document.getElementById(“passenger-no”); const weatherModal=document.getElementById(“weather-modal”); const weatherMsg=document.getElementById(“weather-msg”); const weatherClose=document.getElementById(“weather-close”); const weatherCloseX=document.getElementById(“weather-close-x”); const cardModal=document.getElementById(“card-modal”); const cardCanvas=document.getElementById(“card-canvas”); const cardDownload=document.getElementById(“card-download”); const cardClose=document.getElementById(“card-close”); const settingsModal=document.getElementById(“settings-modal”); const stepGoalInput=document.getElementById(“step-goal-input”); const stepGoalSave=document.getElementById(“step-goal-save”); const stepStreakDisplay=document.getElementById(“step-streak-display”); const stepTodayDisplay=document.getElementById(“step-today-display”); const settingsClose=document.getElementById(“settings-close”); const settingsCloseX=document.getElementById(“settings-close-x”); const bunnyBtn=document.getElementById(“bunny-btn”); const worldsBtn=document.getElementById(“worlds-btn”); const worldsQuick=document.getElementById(“worlds-quick”); const worldsPanel=document.getElementById(“worlds”); const worldsList=document.getElementById(“worlds-list”); const worldsClose=document.getElementById(“worlds-close”); const outpostEl=document.getElementById(“outpost”); const outpostIcon=document.getElementById(“outpost-icon”); const outpostTitle=document.getElementById(“outpost-title”); const outpostCat=document.getElementById(“outpost-cat”); const outpostFact=document.getElementById(“outpost-fact”); const outpostSearch=document.getElementById(“outpost-search”); const outpostGrid=document.getElementById(“outpost-grid”); const outpostEmpty=document.getElementById(“outpost-empty”); const outpostClose=document.getElementById(“outpost-close”); const gameOverlay=document.getElementById(“game-overlay”); const gameExit=document.getElementById(“game-exit”); const gameCoinsEl=document.getElementById(“game-coins”); const gameTitleEl=document.getElementById(“game-title”); const gameStatusEl=document.getElementById(“game-status”); const gameGpsBtn=document.getElementById(“game-gps-btn”); const safetyBanner=document.getElementById(“safety-banner”); const gamePanel=document.getElementById(“game-panel”); const gamePanelCard=document.getElementById(“game-panel-card”); const panelTitle=document.getElementById(“panel-title”); const panelBody=document.getElementById(“panel-body”); const gamePanelClose=document.getElementById(“game-panel-close”); const gamePanelCloseX=document.getElementById(“game-panel-close-x”); const gbToggle=document.getElementById(“gb-toggle”); const gbTally=document.getElementById(“gb-tally”); const luckyBadge=document.getElementById(“lucky-badge”); const godseyeOverlay=document.getElementById(“godseye-overlay”); const godseyeMapEl=document.getElementById(“godseye-map”); const godseyeSearch=document.getElementById(“godseye-search”); const godseyeGo=document.getElementById(“godseye-go”); const godseyeMe=document.getElementById(“godseye-me”); const godseyeZoomIn=document.getElementById(“godseye-zoomin”); const godseyeZoomOut=document.getElementById(“godseye-zoomout”); const cosmosControls=document.getElementById(“cosmos-controls”); const cosmosPrevBtn=document.getElementById(“cosmos-prev-btn”); const cosmosNextBtn=document.getElementById(“cosmos-next-btn”); const cosmosZoomInBtn=document.getElementById(“cosmos-zoomin-btn”); const cosmosMusicBtn=document.getElementById(“cosmos-music-btn”); function lsGet(k){try{return window.localStorage.getItem(k);}catch(e){return null;}} function lsSet(k,v){try{window.localStorage.setItem(k,v);}catch(e){}} function lsDel(k){try{window.localStorage.removeItem(k);}catch(e){}} let W=0,H=0,DPR=1; let isLowEnd=false; let starCount=62000; let milkyCount=28000; let astCount=3200; let fpsSamples=[]; let lastFpsCheck=0; let avgFps=60; let audioMuted=lsGet(“cosmosMuted”)===”1″; let lastToast=null; let lastToastBody=null; let lastToastAction=null; let toastTimer=0; let pauseTime=0; let lastProgress=0; let currentSnapIdx=-1; let snapTargetIdx=0; let userLat=40.7128; let userLon=-74.0060; let activeIdx=-1; let outpostItems=[]; let lastHudKey=””; let tap=null; let inGame=false; let pendingZoom=-1; let pendingZoomAt=0; let uiLockUntil=0; let leafletMap=null; let youMarker=null; let rangeCircle=null; let blocksLayer=null; let spawnLayer=null; let relicLayer=null; let arenaLayer=null; let followPlayer=true; let centeredOnFix=false; let watchId=null; let lastGeo={lat:null,lon:null,t:0,acc:9999}; let anchor=null; let hasFix=false; let lastCell=null; let currentSpeed=0; let speedLocked=false; let gpsMessage=”Acquiring GPS signal”; let gpsWatchdogTimer=null; let gpsFailCount=0; let gpsLastSuccessAt=0; let gpsPermissionGranted=false; let gpsPermissionState=”unknown”; let gpsRetryTimer=null; let gpsRetryAttempts=0; let gameTimer=null; let tickCount=0; let lastSave=0; let missionTab=”hourly”; let shopTab=”base”; let shopQuery=””; let petTab=”collection”; let petQuery=””; let petSort=”power”; let petCat=”all”; let petSel=[]; let catchAnim=0; let catchScale=2.2; let catchBand=0.3; let selectedOrb=”orb”; let unlockDismissedIdx=-1; let unlockTriggerIdx=-1; let unlockClosedAt=0; let panStartCenter=null; let lastPanCenter=null; let twoFingerStartDist=0; let godseyeOpen=false; let godseyeMap=null; let godseyeLat=40.7128; let godseyeLon=-74.0060; let godseyeOpenAt=0; let godseyeCloseAt=0; let godseyeTarget=null; let streetOpenAt=0; let duel=null; let duelUIState=null; let musicModes=[“space”,”game”,”godseye”]; let currentMusicIdx=0; let bottomCollapsed=true; let awaitingEntryZoom=false; let zoomAnimationActive=false; let gpsRefreshTimer=null; let weatherCheckTimer=null; let trespassTimer=null; let passengerWarnTimer=null; let arenaTickTimer=null; let charSelectedStarter=null; let charPendingCountry=””; let passengerConfirmedUntil=0; let lastWeatherAlert=””; let lastTrespassAt=0; let lastPassengerPromptAt=0; let pendingTerritoryTarget=null; let shardTargetId=””; let battleState=null; let arenaState=null; let breedState=null; let storyState=null; let needsMapRefresh=false; let relicState=null; let relicExtractTimer=null; let relicPulseTimer=null; const CATCH={active:false,critter:null,sequence:[],input:[],accepting:false,timers:[],orbType:null}; const COSMOS={alive:true,contextLost:false,texturesReady:0,texturesTotal:0}; const SAVE_KEY=”streetClaimV4″; const PET_MILESTONES=[ {count:5,reward:75}, {count:10,reward:200}, {count:25,reward:600}, {count:50,reward:1500}, {count:100,reward:4000}, {count:200,reward:12000}, {count:300,reward:30000}, {count:400,reward:80000}, {count:500,reward:200000} ]; const ORB_TYPES=[ {id:”orb”,name:”Echo Weave”,icon:”ใฐ๏ธ”,cost:25,band:1.00,minShard:2,maxShard:2,targetShard:2,rarityBoost:0.00,anomalyBonus:0,desc:”A basic harmonic lattice. A clean three-pulse weave extracts 2 signal shards.”}, {id:”orb_enhanced”,name:”Prism Weave”,icon:”๐น”,cost:120,band:1.35,minShard:3,maxShard:3,targetShard:3,rarityBoost:0.10,anomalyBonus:0,desc:”A refracted lattice with a forgiving pulse window. Extracts 3 shards.”}, {id:”orb_precision”,name:”Arc Weave”,icon:”โก”,cost:320,band:1.75,minShard:3,maxShard:3,targetShard:3,rarityBoost:0.15,anomalyBonus:0,desc:”A high-focus arc lattice. Wide pulse window and 3 shards.”}, {id:”orb_master”,name:”Crown Weave”,icon:”๐ถ”,cost:900,band:1.50,minShard:4,maxShard:4,targetShard:4,rarityBoost:0.20,anomalyBonus:0,desc:”A royal harmonic crown. Extracts 4 shards from a stable weave.”}, {id:”orb_legendary”,name:”Nova Weave”,icon:”๐”,cost:2400,band:2.40,minShard:5,maxShard:5,targetShard:5,rarityBoost:0.50,anomalyBonus:0,desc:”A collapsing-star lattice with an enormous pulse window. Extracts 5 shards.”}, {id:”orb_anomaly”,name:”Void Weave”,icon:”๐”,cost:5000,band:2.00,minShard:5,maxShard:5,targetShard:5,rarityBoost:0.35,anomalyBonus:0.05,desc:”A void-tuned weave. Extracts 5 shards and can force an Anomaly on success.”} ]; const ORB_TYPE_BY_ID={}; ORB_TYPES.forEach(function(o){ORB_TYPE_BY_ID[o.id]=o;}); const SPIN_PRIZES=[ {label:”100 Credits”,type:”credits”,amount:100}, {label:”250 Credits”,type:”credits”,amount:250}, {label:”500 Credits”,type:”credits”,amount:500}, {label:”2 Echo Weaves”,type:”orb”,amount:2}, {label:”5 Echo Weaves”,type:”orb”,amount:5}, {label:”1 Prism Weave”,type:”orbx”,amount:1,orbType:”orb_enhanced”}, {label:”1 Turf Shield”,type:”shield”,amount:1}, {label:”JACKPOT: 2000 Credits”,type:”credits”,amount:2000} ]; const POKE_TYPE_CHART={ normal:{rock:0.5,ghost:0,steel:0.5}, fire:{fire:0.5,water:0.5,grass:2,ice:2,bug:2,rock:0.5,dragon:0.5,steel:2}, water:{fire:2,water:0.5,grass:0.5,ground:2,rock:2,dragon:0.5}, electric:{water:2,electric:0.5,grass:0.5,ground:0,flying:2,dragon:0.5}, grass:{fire:0.5,water:2,grass:0.5,poison:0.5,ground:2,flying:0.5,bug:0.5,rock:2,dragon:0.5,steel:0.5}, ice:{fire:0.5,water:0.5,grass:2,ice:0.5,ground:2,flying:2,dragon:2,steel:0.5}, fighting:{normal:2,ice:2,poison:0.5,flying:0.5,psychic:0.5,bug:0.5,rock:2,ghost:0,dark:2,steel:2,fairy:0.5}, poison:{grass:2,poison:0.5,ground:0.5,rock:0.5,ghost:0.5,steel:0,fairy:2}, ground:{fire:2,electric:2,grass:0.5,poison:2,flying:0,bug:0.5,rock:2,steel:2}, flying:{electric:0.5,grass:2,fighting:2,bug:2,rock:0.5,steel:0.5}, psychic:{fighting:2,poison:2,psychic:0.5,dark:0,steel:0.5}, bug:{fire:0.5,grass:2,fighting:0.5,poison:0.5,flying:0.5,psychic:2,ghost:0.5,dark:2,steel:0.5,fairy:0.5}, rock:{fire:2,ice:2,fighting:0.5,ground:0.5,flying:2,bug:2,steel:0.5}, ghost:{normal:0,psychic:2,ghost:2,dark:0.5}, dragon:{dragon:2,steel:0.5,fairy:0}, dark:{fighting:0.5,psychic:2,ghost:2,dark:0.5,fairy:0.5}, steel:{fire:0.5,water:0.5,electric:0.5,ice:2,rock:2,steel:0.5,fairy:2}, fairy:{fire:0.5,fighting:2,poison:0.5,dragon:2,dark:2,steel:0.5} }; const ELEMENT_TO_TYPE={fire:”fire”,water:”water”,earth:”grass”,air:”flying”,electric:”electric”,shadow:”ghost”,light:”fairy”,ice:”ice”,metal:”steel”,cosmic:”psychic”}; const POKE_MOVES={ normal:[ {name:”Tackle”,power:40,acc:1.0,pp:35,type:”normal”,cat:”phys”}, {name:”Quick Attack”,power:40,acc:1.0,pp:30,type:”normal”,cat:”phys”,priority:1}, {name:”Body Slam”,power:85,acc:1.0,pp:15,type:”normal”,cat:”phys”,status:”par”,statusChance:0.3}, {name:”Hyper Beam”,power:150,acc:0.9,pp:5,type:”normal”,cat:”spec”,recharge:true} ], fire:[ {name:”Ember”,power:40,acc:1.0,pp:25,type:”fire”,cat:”spec”,status:”brn”,statusChance:0.1}, {name:”Flamethrower”,power:90,acc:1.0,pp:15,type:”fire”,cat:”spec”,status:”brn”,statusChance:0.1}, {name:”Fire Fang”,power:65,acc:0.95,pp:15,type:”fire”,cat:”phys”,status:”brn”,statusChance:0.1}, {name:”Will-O-Wisp”,power:0,acc:0.85,pp:15,type:”fire”,cat:”status”,status:”brn”,statusChance:1.0} ], water:[ {name:”Water Gun”,power:40,acc:1.0,pp:25,type:”water”,cat:”spec”}, {name:”Surf”,power:90,acc:1.0,pp:15,type:”water”,cat:”spec”}, {name:”Bubble Beam”,power:65,acc:1.0,pp:20,type:”water”,cat:”spec”,statDrop:”spd”,statChance:0.1}, {name:”Aqua Jet”,power:40,acc:1.0,pp:20,type:”water”,cat:”phys”,priority:1} ], electric:[ {name:”Thunder Shock”,power:40,acc:1.0,pp:30,type:”electric”,cat:”spec”,status:”par”,statusChance:0.1}, {name:”Thunderbolt”,power:90,acc:1.0,pp:15,type:”electric”,cat:”spec”,status:”par”,statusChance:0.1}, {name:”Spark”,power:65,acc:1.0,pp:20,type:”electric”,cat:”phys”,status:”par”,statusChance:0.3}, {name:”Thunder Wave”,power:0,acc:0.9,pp:20,type:”electric”,cat:”status”,status:”par”,statusChance:1.0} ], grass:[ {name:”Vine Whip”,power:45,acc:1.0,pp:25,type:”grass”,cat:”phys”}, {name:”Energy Ball”,power:90,acc:1.0,pp:10,type:”grass”,cat:”spec”,statDrop:”spdef”,statChance:0.1}, {name:”Leech Seed”,power:0,acc:0.9,pp:10,type:”grass”,cat:”status”,leech:true}, {name:”Sleep Powder”,power:0,acc:0.75,pp:15,type:”grass”,cat:”status”,status:”slp”,statusChance:1.0} ], ice:[ {name:”Ice Shard”,power:40,acc:1.0,pp:30,type:”ice”,cat:”phys”,priority:1}, {name:”Ice Beam”,power:90,acc:1.0,pp:10,type:”ice”,cat:”spec”,status:”frz”,statusChance:0.1}, {name:”Aurora Beam”,power:65,acc:1.0,pp:20,type:”ice”,cat:”spec”,statDrop:”atk”,statChance:0.1}, {name:”Blizzard”,power:110,acc:0.7,pp:5,type:”ice”,cat:”spec”,status:”frz”,statusChance:0.1} ], fighting:[ {name:”Karate Chop”,power:50,acc:1.0,pp:25,type:”fighting”,cat:”phys”,critBoost:true}, {name:”Brick Break”,power:75,acc:1.0,pp:15,type:”fighting”,cat:”phys”}, {name:”Close Combat”,power:120,acc:1.0,pp:5,type:”fighting”,cat:”phys”,selfDrop:[“def”,”spd”]}, {name:”Bulk Up”,power:0,acc:1.0,pp:20,type:”fighting”,cat:”status”,selfBoost:[“atk”,”def”]} ], poison:[ {name:”Poison Sting”,power:15,acc:1.0,pp:35,type:”poison”,cat:”phys”,status:”psn”,statusChance:0.3}, {name:”Sludge Bomb”,power:90,acc:1.0,pp:10,type:”poison”,cat:”spec”,status:”psn”,statusChance:0.3}, {name:”Acid”,power:40,acc:1.0,pp:30,type:”poison”,cat:”spec”,statDrop:”spdef”,statChance:0.1}, {name:”Toxic”,power:0,acc:0.9,pp:10,type:”poison”,cat:”status”,status:”tox”,statusChance:1.0} ], flying:[ {name:”Gust”,power:40,acc:1.0,pp:35,type:”flying”,cat:”spec”}, {name:”Wing Attack”,power:60,acc:1.0,pp:35,type:”flying”,cat:”phys”}, {name:”Air Slash”,power:75,acc:0.95,pp:15,type:”flying”,cat:”spec”,flinch:true}, {name:”Hurricane”,power:110,acc:0.7,pp:10,type:”flying”,cat:”spec”,status:”cnf”,statusChance:0.3} ], psychic:[ {name:”Confusion”,power:50,acc:1.0,pp:25,type:”psychic”,cat:”spec”,status:”cnf”,statusChance:0.1}, {name:”Psychic”,power:90,acc:1.0,pp:10,type:”psychic”,cat:”spec”,statDrop:”spdef”,statChance:0.1}, {name:”Psybeam”,power:65,acc:1.0,pp:20,type:”psychic”,cat:”spec”,status:”cnf”,statusChance:0.1}, {name:”Hypnosis”,power:0,acc:0.6,pp:20,type:”psychic”,cat:”status”,status:”slp”,statusChance:1.0} ], bug:[ {name:”Bug Bite”,power:60,acc:1.0,pp:20,type:”bug”,cat:”phys”}, {name:”X-Scissor”,power:80,acc:1.0,pp:15,type:”bug”,cat:”phys”}, {name:”String Shot”,power:0,acc:0.95,pp:40,type:”bug”,cat:”status”,statDrop:”spd”,statChance:1.0}, {name:”U-Turn”,power:70,acc:1.0,pp:20,type:”bug”,cat:”phys”} ], rock:[ {name:”Rock Throw”,power:50,acc:0.9,pp:15,type:”rock”,cat:”phys”}, {name:”Rock Slide”,power:75,acc:0.9,pp:10,type:”rock”,cat:”phys”,flinch:true}, {name:”Stone Edge”,power:100,acc:0.8,pp:5,type:”rock”,cat:”phys”,critBoost:true}, {name:”Rock Polish”,power:0,acc:1.0,pp:20,type:”rock”,cat:”status”,selfBoost:[“spd”]} ], ghost:[ {name:”Lick”,power:30,acc:1.0,pp:30,type:”ghost”,cat:”phys”,status:”par”,statusChance:0.3}, {name:”Shadow Ball”,power:80,acc:1.0,pp:15,type:”ghost”,cat:”spec”,statDrop:”spdef”,statChance:0.2}, {name:”Night Shade”,power:60,acc:1.0,pp:15,type:”ghost”,cat:”spec”}, {name:”Confuse Ray”,power:0,acc:1.0,pp:10,type:”ghost”,cat:”status”,status:”cnf”,statusChance:1.0} ], dragon:[ {name:”Dragon Rage”,power:40,acc:1.0,pp:10,type:”dragon”,cat:”spec”}, {name:”Dragon Claw”,power:80,acc:1.0,pp:15,type:”dragon”,cat:”phys”}, {name:”Dragon Pulse”,power:85,acc:1.0,pp:10,type:”dragon”,cat:”spec”}, {name:”Dragon Dance”,power:0,acc:1.0,pp:20,type:”dragon”,cat:”status”,selfBoost:[“atk”,”spd”]} ], dark:[ {name:”Bite”,power:60,acc:1.0,pp:25,type:”dark”,cat:”phys”,flinch:true}, {name:”Crunch”,power:80,acc:1.0,pp:15,type:”dark”,cat:”phys”,statDrop:”def”,statChance:0.2}, {name:”Dark Pulse”,power:80,acc:1.0,pp:15,type:”dark”,cat:”spec”,flinch:true}, {name:”Nasty Plot”,power:0,acc:1.0,pp:20,type:”dark”,cat:”status”,selfBoost:[“atk”]} ], steel:[ {name:”Metal Claw”,power:50,acc:0.95,pp:35,type:”steel”,cat:”phys”,selfBoost:[“atk”],statChance:0.1}, {name:”Iron Head”,power:80,acc:1.0,pp:15,type:”steel”,cat:”phys”,flinch:true}, {name:”Flash Cannon”,power:80,acc:1.0,pp:10,type:”steel”,cat:”spec”,statDrop:”spdef”,statChance:0.1}, {name:”Iron Defense”,power:0,acc:1.0,pp:15,type:”steel”,cat:”status”,selfBoost:[“def”]} ], fairy:[ {name:”Fairy Wind”,power:40,acc:1.0,pp:30,type:”fairy”,cat:”spec”}, {name:”Dazzling Gleam”,power:80,acc:1.0,pp:10,type:”fairy”,cat:”spec”}, {name:”Play Rough”,power:90,acc:0.9,pp:10,type:”fairy”,cat:”phys”,statDrop:”atk”,statChance:0.1}, {name:”Moonblast”,power:95,acc:1.0,pp:15,type:”fairy”,cat:”spec”,statDrop:”spatk”,statChance:0.3} ] }; const STATUS_LABEL={brn:”BRN”,psn:”PSN”,tox:”TOX”,par:”PAR”,slp:”SLP”,frz:”FRZ”,cnf:”CNF”}; const STATUS_CLASS={brn:”st-brn”,psn:”st-psn”,tox:”st-tox”,par:”st-par”,slp:”st-slp”,frz:”st-frz”,cnf:”st-cnf”}; const ARENA_TRAINERS=[ {id:”a1″,name:”Bruiser Kai”,tier:1,team:[“g0″,”g5″,”g9″],desc:”Arena hopeful testing the ropes.”,mult:1.0}, {id:”a2″,name:”Veteran Rell”,tier:2,team:[“g12″,”g30″,”g44″],desc:”Three-time arena runner-up.”,mult:1.4}, {id:”a3″,name:”Sworn Duelist”,tier:3,team:[“g60″,”g88″,”g110″,”g140″],desc:”Arena veteran with a deadly balanced team.”,mult:2.0}, {id:”a4″,name:”Warden Orix”,tier:4,team:[“g180″,”g220″,”g260″,”g300″],desc:”Arena gatekeeper. Only champions pass.”,mult:2.8}, {id:”a5″,name:”Champion Vex”,tier:5,team:[“g350″,”g400″,”g450″,”g490″,”moth”],desc:”The Arena Champion. Defeat yields a legendary reward.”,mult:4.0} ]; const STORY_MISSIONS=[ {id:”s1″,title:”First Contact”,desc:”Catch your first pet in Street Claim.”,stat:”pets”,target:1,reward:120,xp:60,item:null,prereq:null}, {id:”s2″,title:”Path of the Walker”,desc:”Walk 1000 meters total.”,stat:”distance”,target:1000,reward:180,xp:80,item:{type:”orb”,amount:3},prereq:”s1″}, {id:”s3″,title:”Triple Threat”,desc:”Own at least 3 pets.”,stat:”pets”,target:3,reward:300,xp:120,item:{type:”orb”,amount:5},prereq:”s2″}, {id:”s4″,title:”First Duel”,desc:”Win 1 pet duel.”,stat:”wins”,target:1,reward:400,xp:180,item:{type:”orb_enhanced”,amount:2},prereq:”s3″}, {id:”s5″,title:”Gym Challenger”,desc:”Win 1 Arena battle.”,stat:”arenaWins”,target:1,reward:600,xp:250,item:{type:”orb_precision”,amount:2},prereq:”s4″}, {id:”s6″,title:”Breeder’s Touch”,desc:”Hatch 1 egg.”,stat:”hatched”,target:1,reward:800,xp:350,item:{type:”orb_master”,amount:1},prereq:”s5″}, {id:”s7″,title:”Arena Veteran”,desc:”Win 5 Arena battles total.”,stat:”arenaWins”,target:5,reward:1500,xp:600,item:{type:”orb_legendary”,amount:1},prereq:”s6″}, {id:”s8″,title:”Champion’s Path”,desc:”Win 10 Arena battles total.”,stat:”arenaWins”,target:10,reward:3000,xp:1200,item:{type:”orb_anomaly”,amount:1},prereq:”s7″}, {id:”s9″,title:”Duel Master”,desc:”Win 10 pet duels.”,stat:”wins”,target:10,reward:2200,xp:900,item:{type:”orb_legendary”,amount:2},prereq:”s8″}, {id:”s10″,title:”Signal Breaker”,desc:”Catch 25 pets total.”,stat:”pets”,target:25,reward:5000,xp:2000,item:{type:”pet”,amount:1},prereq:”s9″} ]; function freshState(){ return{credits:100,xp:0,items:{orb:10,orb_enhanced:0,orb_precision:0,orb_master:0,orb_legendary:0,orb_anomaly:0,shield:0,boost:0,radar:0,shard:0,train:0,resonator:1},blocks:{},stats:{claimed:0,captured:0,distance:0,spins:0,caught:0,purchases:0,collected:0,reinforced:0,lost:0,battles:0,wins:0,merged:0,released:0,hired:0,trained:0,walkedToday:0,anomalies:0,walkCredits:0,walkCreditAcc:0,petMilestones:0,arenaWins:0,hatched:0,duelsWon:0,breedsDone:0,relicsBroken:0},once:{},daily:{key:””,base:{},claimed:{}},hourly:{key:””,base:{},claimed:{}},walkDaily:{key:””,progress:0,claimed:{}},codex:{},depots:{},seen:{},relicsSeen:{},effects:{shield:0,boost:0,radar:0},lastIncome:Date.now(),lastTick:Date.now(),lastOnline:Date.now(),npcClock:0,streak:0,lastDay:””,neon:false,markerNeon:false,log:[],levelName:”Earth”,pets:{},team:[],shards:{},hired:[],battles:{wins:0,losses:0},trainer:{lv:1,xp:0},petMilestonesClaimed:{},pass:{season:0,claimed:{}},dailySpinLast:””,charName:””,charCountry:””,charCreatedAt:0,hasHome:false,homeLat:0,homeLon:0,playSeconds:0,shardTarget:””,arenaWins:[],arenaLog:[],eggs:[],breedCooldowns:{},storyProgress:{},storyClaimed:{},itemLog:[],stepGoal:STEP_GOAL_DEFAULT,stepStreak:0,lastStepDay:””,stepAchievementsClaimed:{},resonatorUses:[]}; } function mergeState(base,src){ const out={}; for(const k in base){ const b=base[k]; const v=src?src[k]:undefined; if(b!==null&&typeof b===”object”&&!Array.isArray(b)){ if(Object.keys(b).length===0){out[k]=(v&&typeof v===”object”&&!Array.isArray(v))?v:{};} else{out[k]=mergeState(b,(v&&typeof v===”object”&&!Array.isArray(v))?v:{});} }else if(Array.isArray(b)){ out[k]=Array.isArray(v)?v:b; }else if(typeof v===typeof b){ out[k]=v; }else{ out[k]=b; } } return out; } function loadGameState(){ const f=freshState(); let s=null; try{s=JSON.parse(lsGet(SAVE_KEY)||”null”);}catch(e){s=null;} if(!s||typeof s!==”object”){ try{ const old=JSON.parse(lsGet(“streetClaimV3″)||”null”); if(old&&typeof old===”object”)return mergeState(f,old); }catch(e){} try{ const old2=JSON.parse(lsGet(“streetClaimV2″)||”null”); if(old2&&typeof old2===”object”)return mergeState(f,old2); }catch(e){} try{ const old3=JSON.parse(lsGet(“streetClaimState”)||”null”); if(old3&&typeof old3.credits===”number”)f.credits=old3.credits; if(old3&&typeof old3.distanceWalked===”number”)f.stats.distance=old3.distanceWalked; }catch(e){} return f; } return mergeState(f,s); } function saveGameState(s){ s.lastOnline=Date.now(); lsSet(SAVE_KEY,JSON.stringify(s)); lastSave=Date.now(); } let gState=loadGameState(); if(!gState.items.orb_enhanced&&gState.items.orb_enhanced!==0)gState.items.orb_enhanced=0; if(!gState.items.orb_precision&&gState.items.orb_precision!==0)gState.items.orb_precision=0; if(!gState.items.orb_master&&gState.items.orb_master!==0)gState.items.orb_master=0; if(!gState.items.orb_legendary&&gState.items.orb_legendary!==0)gState.items.orb_legendary=0; if(!gState.items.orb_anomaly&&gState.items.orb_anomaly!==0)gState.items.orb_anomaly=0; if(!gState.items.resonator&&gState.items.resonator!==0)gState.items.resonator=1; if(!gState.pass||typeof gState.pass!==”object”)gState.pass={season:0,claimed:{}}; if(typeof gState.dailySpinLast!==”string”)gState.dailySpinLast=””; if(typeof gState.charName!==”string”)gState.charName=””; if(typeof gState.charCountry!==”string”)gState.charCountry=””; if(typeof gState.charCreatedAt!==”number”)gState.charCreatedAt=0; if(typeof gState.playSeconds!==”number”)gState.playSeconds=0; if(typeof gState.shardTarget!==”string”)gState.shardTarget=””; if(!Array.isArray(gState.arenaWins))gState.arenaWins=[]; if(!Array.isArray(gState.arenaLog))gState.arenaLog=[]; if(!Array.isArray(gState.eggs))gState.eggs=[]; if(!Array.isArray(gState.itemLog))gState.itemLog=[]; if(!gState.breedCooldowns||typeof gState.breedCooldowns!==”object”)gState.breedCooldowns={}; if(!gState.storyProgress||typeof gState.storyProgress!==”object”)gState.storyProgress={}; if(!gState.storyClaimed||typeof gState.storyClaimed!==”object”)gState.storyClaimed={}; if(typeof gState.stats.arenaWins!==”number”)gState.stats.arenaWins=0; if(typeof gState.stats.hatched!==”number”)gState.stats.hatched=0; if(typeof gState.stats.duelsWon!==”number”)gState.stats.duelsWon=0; if(typeof gState.stats.breedsDone!==”number”)gState.stats.breedsDone=0; if(typeof gState.stats.relicsBroken!==”number”)gState.stats.relicsBroken=0; if(typeof gState.stepGoal!==”number”||gState.stepGoal<STEP_GOAL_MIN||gState.stepGoal>STEP_GOAL_MAX)gState.stepGoal=STEP_GOAL_DEFAULT; if(typeof gState.stepStreak!==”number”)gState.stepStreak=0; if(typeof gState.lastStepDay!==”string”)gState.lastStepDay=””; if(!gState.stepAchievementsClaimed||typeof gState.stepAchievementsClaimed!==”object”)gState.stepAchievementsClaimed={}; if(!gState.relicsSeen||typeof gState.relicsSeen!==”object”)gState.relicsSeen={}; if(!Array.isArray(gState.resonatorUses))gState.resonatorUses=[]; saveGameState(gState); const foundationItems=[ {name:”Anime”,icon:”โฉ๏ธ”,type:”web”,url:”https://afbn.me/watch-anime”}, {name:”Music<br>Player”,icon:”๐ต”,type:”web”,url:”https://newbius.afbn.me/core/”}, {name:”Sound<br>Chamber”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/sound-chamber/”}, {name:”Fortune<br>Cookie”,icon:”๐ฅ ”,type:”web”,url:”https://fortune.afbn.me/”} ]; const academyItems=[ {name:”Web3<br>Academy”,icon:”๐”,type:”web”,url:”https://academy.afbn.me”}, {name:”AI<br>Protocol”,icon:”๐ง ”,type:”web”,url:”https://newbius.afbn.me/ai-protocol/”}, {name:”Blockchain<br>Mastery”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/blockchain-mastery/”}, {name:”DeFi<br>Protocol”,icon:”๐น”,type:”web”,url:”https://newbius.afbn.me/defi-protocol/”}, {name:”Phantom<br>& Solana”,icon:”๐ป”,type:”web”,url:”https://newbius.afbn.me/phantom-and-solana/”}, {name:”Machine<br>Learning”,icon:”๐ค”,type:”web”,url:”https://newbius.afbn.me/machine-learning-protocol/”}, {name:”Web3<br>Architecture”,icon:”๐๏ธ”,type:”web”,url:”https://newbius.afbn.me/web3-architecture-protocol/”} ]; const toolsItems=[ {name:”Solana<br>TradeBot”,icon:”๐ค”,type:”web”,url:”https://newbius.afbn.me/tradetesting/”}, {name:”Auto<br>Auditor”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/holy-shit”}, {name:”NFT<br>Generator”,icon:”๐ผ๏ธ”,type:”web”,url:”https://newbius.afbn.me/nft-generator/”}, {name:”Risk<br>Sentinel”,icon:”๐ก๏ธ”,type:”web”,url:”https://newbius.afbn.me/risk-sentinel/”}, {name:”Transaction<br>Compiler”,icon:”๐”,type:”web”,url:”https://tax.afbn.me”} ]; const gamesItems=[ {name:”Games Hub”,icon:”๐ฎ”,type:”web”,url:”https://afbn.me/free-games”}, {name:”RPG”,icon:”๐ก๏ธ”,type:”web”,url:”https://newbius.afbn.me/rpg/”}, {name:”Pocket<br>Bits”,icon:”๐พ”,type:”web”,url:”https://newbius.afbn.me/pocket-bitz/”}, {name:”Hunter”,icon:”๐ฏ”,type:”web”,url:”https://newbius.afbn.me/hunter/”}, {name:”Cyber<br>Dice”,icon:”๐ฒ”,type:”web”,url:”https://newbius.afbn.me/cyber-dice/”}, {name:”Shatter<br>Protocol”,icon:”๐ฅ”,type:”web”,url:”https://newbius.afbn.me/shatter-protocol/”}, {name:”Quantum<br>Drift”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/quantum-drift/”}, {name:”AI<br>D&D”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/ai-dnd”}, {name:”Terminal<br>Escape”,icon:”๐ฅ๏ธ”,type:”web”,url:”https://newbius.afbn.me/terminal-escape/”}, {name:”Surge<br>Hunter”,icon:”โก”,type:”web”,url:”https://newbius.afbn.me/surge-hunter/”}, {name:”Surge<br>Commander”,icon:”๐๏ธ”,type:”web”,url:”https://newbius.afbn.me/surge-commander/”} ]; const businessItems=[ {name:”Consultation”,icon:”๐น”,type:”web”,url:”https://consultation.afbn.me”}, {name:”Custom dApp<br>Development”,icon:”๐ป”,type:”web”,url:”https://dev.afbn.me/elevate-your-web3-journey-with-an-affordable-dapp-developer/”}, {name:”Books by<br>William Simmons”,icon:”๐”,type:”link”,url:”https://amazon.com/author/afbn”}, {name:”Donations”,icon:”โค๏ธ”,type:”web”,url:”https://newbius.afbn.me/donations”} ]; const hubItems=[ {name:”X”,icon:”โ”,type:”link”,url:”https://x.com/AFewBadNewbies”}, {name:”Merch<br>Shop”,icon:”๐๏ธ”,type:”web”,url:”https://afbn.me/shop”}, {name:”Business<br>Plan”,icon:”๐”,type:”web”,url:”https://afbn.me”}, {name:”Spatial”,icon:”๐”,type:”web”,url:”https://afbn.me/metaverse-spatial”}, {name:”Radio”,icon:”๐ป”,type:”web”,url:”https://afbn.me/hear-music”} ]; const controlItems=[ {name:”Sound<br>On / Off”,icon:”๐”,type:”action”,action:”sound”}, {name:”Replay<br>Intro”,icon:”๐ฌ”,type:”action”,action:”intro”}, {name:”Player<br>Card”,icon:”๐”,type:”action”,action:”playercard”} ]; const roadmapItems=[ {name:”01: RevShare<br>NFTs”,icon:”๐”,type:”web”,url:”https://afbn.me/RevShare”}, {name:”02: DeFi<br>Casino”,icon:”๐ฐ”,type:”web”,url:”https://casino.afbn.me”}, {name:”03: WoD<br>MMORPG”,icon:”โ๏ธ”,type:”web”,url:”https://afbn.me/newbieverse/”}, {name:”04: ETH<br>Deployment”,icon:”๐ท”,type:”web”,url:”https://afbn.me/newbie-token/”}, {name:”05: Internet TV<br>Network + Prize”,icon:”๐”,type:”info”,text:”GRAND PRIZE:<br><span class=\”modal-highlight\”>$10,000 USDC<br>Tesla Model Y<br>1yr Insurance</span><br>Paid in the Winner’s Name.”}, {name:”06: Eco-friendly<br>Gaming Hotel”,icon:”๐จ”,type:”web”,url:”https://afbn.me/gaming-hotel/”} ]; const holoItems=[ {name:”Holo<br>Blackjack”,icon:”โ ๏ธ”,type:”web”,url:”https://newbius.afbn.me/holo-blackjack/”}, {name:”Holo<br>Basketball”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/holo-basketball/”}, {name:”Holo<br>Blade”,icon:”โ๏ธ”,type:”web”,url:”https://newbius.afbn.me/holo-blade/”}, {name:”Holo<br>Bound”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/holo-bound/”}, {name:”Holo<br>Builder”,icon:”๐งฑ”,type:”web”,url:”https://newbius.afbn.me/holo-builder/”}, {name:”Holo<br>Derby”,icon:”๐๏ธ”,type:”web”,url:”https://newbius.afbn.me/holo-derby/”}, {name:”Holo<br>Memory”,icon:”๐ง ”,type:”web”,url:”https://newbius.afbn.me/holo-memory-link/”}, {name:”Holo<br>Void”,icon:”๐ณ๏ธ”,type:”web”,url:”https://newbius.afbn.me/holo-void/”}, {name:”Holo<br>Cups”,icon:”๐ฅค”,type:”web”,url:”https://newbius.afbn.me/holo-cups/”}, {name:”Holo<br>Darts”,icon:”๐ฏ”,type:”web”,url:”https://newbius.afbn.me/holo-darts/”}, {name:”Holo<br>Hangman”,icon:”๐ค”,type:”web”,url:”https://newbius.afbn.me/holo-hangman/”}, {name:”Holo<br>Solitaire”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/holo-solitaire/”} ]; const arenaItems=[ {name:”Compete”,icon:”๐”,type:”web”,url:”https://compete.afbn.me”}, {name:”Stream”,icon:”๐บ”,type:”web”,url:”https://stream.afbn.me”}, {name:”Chatroom”,icon:”๐ฌ”,type:”web”,url:”https://newbius.afbn.me/chatroom/”}, {name:”Ping<br>Pong”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/ping-pong/”}, {name:”Holo<br>Chess”,icon:”โ๏ธ”,type:”web”,url:”https://newbius.afbn.me/holo-chess/”}, {name:”Holo<br>Checkers”,icon:”๐ด”,type:”web”,url:”https://newbius.afbn.me/holo-checkers/”}, {name:”Marble<br>Racer”,icon:”๐ฎ”,type:”web”,url:”https://newbius.afbn.me/marble-racer/”}, {name:”Knowledge<br>Race”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/knowledge-race/”}, {name:”Neon<br>Overdrive”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/neon-overdrive/”}, {name:”Neon<br>Street Duel”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/neon-street-duel/”}, {name:”Space<br>Invaders”,icon:”๐พ”,type:”web”,url:”https://newbius.afbn.me/space-invaders/”}, {name:”Cyber<br>Runner”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/cyber-runner-protocol/”} ]; const securityItems=[ {name:”Cyber<br>Security”,icon:”๐ก๏ธ”,type:”web”,url:”https://newbius.afbn.me/cyber-security-protocol/”}, {name:”Cryptography<br>Protocol”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/cryptography-protocol/”}, {name:”Whitehat<br>Protocol”,icon:”๐ฉ”,type:”web”,url:”https://newbius.afbn.me/whitehat-protocol/”}, {name:”MEV<br>Protocol”,icon:”โก”,type:”web”,url:”https://newbius.afbn.me/mev-protocol/”} ]; const devItems=[ {name:”HTML<br>& CSS”,icon:”๐จ”,type:”web”,url:”https://newbius.afbn.me/html-and-css/”}, {name:”HTML5<br>Course”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/html5-course/”}, {name:”JavaScript”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/javascript/”}, {name:”React<br>Course”,icon:”โ๏ธ”,type:”web”,url:”https://newbius.afbn.me/react-course/”}, {name:”Python”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/python/”}, {name:”PHP”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/php/”}, {name:”Rust”,icon:”๐ฆ”,type:”web”,url:”https://newbius.afbn.me/rust/”}, {name:”Solidity<br>Course”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/solidity-course/”}, {name:”Vanilla<br>JS”,icon:”โ”,type:”web”,url:”https://newbius.afbn.me/vanilla-js/”}, {name:”Typesetting<br>Course”,icon:”๐”,type:”web”,url:”https://newbius.afbn.me/typesetting-course/”} ]; const tradeItems=[ {name:”$BOAR”,icon:”๐๏ธ”,type:”web”,url:”https://boar.afbn.me”}, {name:”Gecko”,icon:”๐ฆ”,type:”web”,url:”https://www.geckoterminal.com/solana/tokens/BSt36411RV6PVmP5bgR6a5mZv7G1TnR1HevxdE85wCng”} ]; const CATS={ foundation:{icon:”๐๏ธ”,title:”Free Foundation”,items:foundationItems}, academy:{icon:”๐”,title:”Free Education”,items:academyItems}, tools:{icon:”๐ ๏ธ”,title:”Free Web3 Tools”,items:toolsItems}, games:{icon:”๐น๏ธ”,title:”Free Games”,items:gamesItems}, business:{icon:”๐ผ”,title:”Optional Business Plan”,items:businessItems}, hub:{icon:”๐”,title:”AFBN Hub & Social”,items:hubItems}, control:{icon:”๐๏ธ”,title:”Control Deck”,items:controlItems}, roadmap:{icon:”๐บ๏ธ”,title:”AFBN Roadmap”,items:roadmapItems}, holo:{icon:”๐ฎ”,title:”Holo Games”,items:holoItems}, arena:{icon:”๐”,title:”Arena & Community”,items:arenaItems}, security:{icon:”๐ก๏ธ”,title:”Security & Audit”,items:securityItems}, dev:{icon:”๐ป”,title:”Developer Courses”,items:devItems}, trade:{icon:”๐ฐ”,title:”Trade $BOAR”,items:tradeItems} }; const ecoData=[ {name:”Earth ยท Home Base”,fact:”A Few Bad Newbies originates here. The origin node of the entire ecosystem. Every journey begins on solid ground. Street Claim awaits.”,idx:0,icon:”๐”,cat:”foundation”,zd:7.0,level:”Earth”}, {name:”Earth ยท Home Base”,fact:”A Few Bad Newbies originates here. The origin node of the entire ecosystem. Every journey begins on solid ground. Street Claim awaits.”,idx:1,icon:”๐”,cat:null,zd:7.0,level:”Earth”}, {name:”The Moon ยท Reflection”,fact:”The silent observer. Where early ideas first took shape under reflected light. A place of quiet iteration.”,idx:2,icon:”๐”,cat:”academy”,zd:2.3,level:”Moon”}, {name:”Mars ยท Expansion”,fact:”The frontier of growth. New territories, new experiments, new chapters of the AFBN story.”,idx:3,icon:”๐ด”,cat:”tools”,zd:5.7,level:”Mars”}, {name:”Asteroid Belt ยท Chaos Layer”,fact:”Raw material and collision. The creative debris field where unfinished ideas become fuel.”,idx:4,icon:”โ๏ธ”,cat:”games”,zd:25,level:”Asteroid”}, {name:”Jupiter ยท Gravity Well”,fact:”The massive attractor. Community, culture, and gravitational pull that holds the system together.”,idx:5,icon:”๐ช”,cat:”business”,zd:26,level:”Jupiter”}, {name:”Saturn ยท Rings of Order”,fact:”Structure and elegance. Systems, rules, and the beautiful architecture that keeps everything in orbit.”,idx:6,icon:”๐ช”,cat:”hub”,zd:19,level:”Saturn”}, {name:”Uranus ยท Tilted Vision”,fact:”Perspective shift. Seeing the ecosystem from an angle no one else considered.”,idx:7,icon:”๐ต”,cat:”control”,zd:11.4,level:”Uranus”}, {name:”Neptune ยท Deep Current”,fact:”The farthest classical world. Long-term vision, deep liquidity, and the slow powerful currents of the project.”,idx:8,icon:”๐”,cat:”roadmap”,zd:11.1,level:”Neptune”}, {name:”Black Hole ยท Singularity”,fact:”The point of no return. Where value concentrates and time itself bends around the mission.”,idx:9,icon:”โซ”,cat:null,zd:18,level:”Singularity”}, {name:”Supernova ยท Ignition”,fact:”The explosive birth of new value. Moments of rapid expansion that seed the next generation of ideas.”,idx:10,icon:”๐ฅ”,cat:null,zd:8.3,level:”Supernova”}, {name:”Andromeda ยท Parallel Universe”,fact:”A neighboring reality. Alternate expressions of the same creative force.”,idx:11,icon:”๐”,cat:”holo”,zd:60,level:”Andromeda”}, {name:”Andromeda System”,fact:”Worlds within worlds. Nested experiences inside the larger AFBN multiverse.”,idx:12,icon:”๐”,cat:null,zd:9.2,level:”Andromeda2″}, {name:”Whirlpool ยท Interaction”,fact:”Where galaxies collide and new stars form. Collaboration and creative friction.”,idx:13,icon:”๐”,cat:”arena”,zd:60,level:”Whirlpool”}, {name:”Whirlpool System”,fact:”Dense clusters of activity. High-energy zones of the ecosystem.”,idx:14,icon:”๐”,cat:null,zd:12.8,level:”Whirlpool2″}, {name:”Sombrero ยท Edge of Dust”,fact:”Beauty at the boundary. The elegant line between order and chaos.”,idx:15,icon:”๐ก๏ธ”,cat:”security”,zd:60,level:”Sombrero”}, {name:”Sombrero System”,fact:”Twilight worlds. Places where light and shadow coexist in perfect balance.”,idx:16,icon:”๐”,cat:null,zd:8.3,level:”Sombrero2″}, {name:”Pinwheel ยท Open Spiral”,fact:”Open invitation. The face-on view that welcomes everyone into the story.”,idx:17,icon:”๐ป”,cat:”dev”,zd:60,level:”Pinwheel”}, {name:”Pinwheel System”,fact:”Young stars and new light. Fresh chapters still being written.”,idx:18,icon:”โญ”,cat:null,zd:13.5,level:”Pinwheel2″}, {name:”Triangulum ยท Local Group”,fact:”Part of something larger. The AFBN constellation within the wider creative universe.”,idx:19,icon:”๐บ”,cat:null,zd:60,level:”Triangulum”}, {name:”Triangulum System”,fact:”Distant but connected. Every node remains part of the same network.”,idx:20,icon:”๐บ”,cat:null,zd:10.8,level:”Triangulum2″}, {name:”Deep Space ยท A Few Bad Newbies”,fact:”The final frontier. The stars themselves now spell the next chapter of A Few Bad Newbies.”,idx:21,icon:”โจ”,cat:”trade”,zd:120,level:”DeepSpace”} ]; function cleanName(name){ return String(name).replace(/<br\s*\/?>/gi,” “).replace(/\s+/g,” “).trim(); } function detectDevice(){ const mem=navigator.deviceMemory||8; const cores=navigator.hardwareConcurrency||4; if(mem<=4||cores<=4||/Android|iPhone|iPad|Mobile/i.test(navigator.userAgent)){ isLowEnd=true; starCount=22000; milkyCount=10000; astCount=1400; } } detectDevice(); const renderer=new THREE.WebGLRenderer({canvas:canvas,antialias:!isLowEnd,alpha:false,powerPreference:”high-performance”,logarithmicDepthBuffer:true}); renderer.setClearColor(0x000000,1); renderer.physicallyCorrectLights=true; renderer.toneMapping=THREE.ACESFilmicToneMapping; renderer.toneMappingExposure=2.35; const scene=new THREE.Scene(); const camera=new THREE.PerspectiveCamera(36,1,0.001,500000); const amb=new THREE.AmbientLight(0x0c1525,0.38); scene.add(amb); const sunLight=new THREE.PointLight(0xfff8e8,56,280000,0.65); scene.add(sunLight); const fillLight=new THREE.DirectionalLight(0x7799dd,0.48); fillLight.position.set(-220,120,320); scene.add(fillLight); const hemi=new THREE.HemisphereLight(0x1a2a48,0x080610,0.26); scene.add(hemi); function resize(){ DPR=Math.min(window.devicePixelRatio||1,isLowEnd?1.5:2.5); W=window.innerWidth;H=window.innerHeight; renderer.setPixelRatio(DPR); renderer.setSize(W,H,false); camera.aspect=W/H; camera.updateProjectionMatrix(); if(leafletMap)setTimeout(function(){if(leafletMap)leafletMap.invalidateSize();},200); if(godseyeMap)setTimeout(function(){if(godseyeMap)godseyeMap.invalidateSize();},200); } resize(); window.addEventListener(“resize”,resize,{passive:true}); window.addEventListener(“orientationchange”,function(){setTimeout(resize,160);},{passive:true}); canvas.addEventListener(“webglcontextlost”,function(e){e.preventDefault();COSMOS.contextLost=true;COSMOS.alive=false;},{passive:false}); canvas.addEventListener(“webglcontextrestored”,function(){COSMOS.contextLost=false;COSMOS.alive=true;resize();requestAnimationFrame(frame);},{passive:true}); function makeStarTexture(){ const c=document.createElement(“canvas”); c.width=512;c.height=512; const g=c.getContext(“2d”); const grd=g.createRadialGradient(256,256,0,256,256,256); grd.addColorStop(0,”rgba(255,255,255,1)”); grd.addColorStop(0.02,”rgba(255,255,255,0.99)”); grd.addColorStop(0.08,”rgba(240,245,255,0.82)”); grd.addColorStop(0.22,”rgba(210,225,255,0.35)”); grd.addColorStop(0.5,”rgba(150,180,255,0.1)”); grd.addColorStop(1,”rgba(0,0,0,0)”); g.fillStyle=grd;g.fillRect(0,0,512,512); const tex=new THREE.CanvasTexture(c);tex.needsUpdate=true;return tex; } const starTex=makeStarTexture(); function makeGasTexture(baseR,baseG,baseB,bandStrength){ const c=document.createElement(“canvas”); c.width=1024;c.height=512; const g=c.getContext(“2d”); for(let y=0;y<512;y++){ const ny=y/512; const band=Math.sin(ny*28)*0.5+Math.sin(ny*11)*0.3+Math.sin(ny*47)*0.15+Math.sin(ny*73)*0.08+Math.sin(ny*101)*0.04; const turb=Math.sin(ny*3.1+Math.cos(ny*7.3))*0.14+Math.sin(ny*19.7)*0.07+Math.sin(ny*41)*0.03; const v=0.6+band*bandStrength+turb; const r=Math.min(255,Math.floor(baseR*v)); const gr=Math.min(255,Math.floor(baseG*v*0.96)); const b=Math.min(255,Math.floor(baseB*v*0.92)); g.fillStyle=”rgb(“+r+”,”+gr+”,”+b+”)”; g.fillRect(0,y,1024,1); } for(let i=0;i<280;i++){ const y=Math.random()*512; const h=1+Math.random()*5.5; g.fillStyle=”rgba(0,0,0,”+(0.03+Math.random()*0.08)+”)”; g.fillRect(0,y,1024,h); } for(let i=0;i<80;i++){ const x=Math.random()*1024; const y=Math.random()*512; const rx=28+Math.random()*95; const ry=7+Math.random()*22; g.fillStyle=”rgba(255,255,255,”+(0.03+Math.random()*0.08)+”)”; g.beginPath();g.ellipse(x,y,rx,ry,0,0,Math.PI*2);g.fill(); } const tex=new THREE.CanvasTexture(c);tex.anisotropy=isLowEnd?8:32;tex.needsUpdate=true;return tex; } function makeRockyTexture(baseR,baseG,baseB){ const c=document.createElement(“canvas”); c.width=2048;c.height=1024; const g=c.getContext(“2d”); g.fillStyle=”rgb(“+baseR+”,”+baseG+”,”+baseB+”)”; g.fillRect(0,0,2048,1024); for(let i=0;i<52000;i++){ const x=Math.random()*2048;const y=Math.random()*1024; const v=Math.floor((Math.random()-0.5)*80); g.fillStyle=”rgb(“+Math.max(0,Math.min(255,baseR+v))+”,”+Math.max(0,Math.min(255,baseG+v*0.72))+”,”+Math.max(0,Math.min(255,baseB+v*0.52))+”)”; g.fillRect(x,y,1+Math.random()*3,1+Math.random()*3); } for(let i=0;i<520;i++){ const x=Math.random()*2048;const y=Math.random()*1024; const r=6+Math.random()*58; g.fillStyle=”rgba(0,0,0,”+(0.06+Math.random()*0.3)+”)”; g.beginPath();g.arc(x,y,r,0,Math.PI*2);g.fill(); } for(let i=0;i<220;i++){ const x=Math.random()*2048;const y=Math.random()*1024; const r=4+Math.random()*22; g.fillStyle=”rgba(255,255,255,”+(0.05+Math.random()*0.15)+”)”; g.beginPath();g.arc(x,y,r,0,Math.PI*2);g.fill(); } const tex=new THREE.CanvasTexture(c);tex.anisotropy=isLowEnd?8:32;tex.needsUpdate=true;return tex; } function makeRingTexture(baseR,baseG,baseB){ const c=document.createElement(“canvas”); c.width=2048;c.height=128; const g=c.getContext(“2d”); for(let x=0;x<2048;x++){ const nx=x/2048; const band=Math.sin(nx*40)*0.4+Math.sin(nx*90)*0.25+Math.sin(nx*180)*0.12+Math.sin(nx*320)*0.06; const v=0.78+band*0.35; const r=Math.min(255,Math.floor(baseR*v)); const gr=Math.min(255,Math.floor(baseG*v*0.97)); const b=Math.min(255,Math.floor(baseB*v*0.94)); g.fillStyle=”rgb(“+r+”,”+gr+”,”+b+”)”; g.fillRect(x,0,1,128); } for(let i=0;i<180;i++){ const x=Math.random()*2048; const w=1+Math.random()*5; g.fillStyle=”rgba(0,0,0,”+(0.15+Math.random()*0.35)+”)”; g.fillRect(x,0,w,128); } const tex=new THREE.CanvasTexture(c);tex.anisotropy=isLowEnd?8:24;tex.needsUpdate=true;return tex; } function makeFallbackPlanetTexture(r,g,b){ const c=document.createElement(“canvas”); c.width=1024;c.height=512; const ctx=c.getContext(“2d”); const grd=ctx.createLinearGradient(0,0,0,512); grd.addColorStop(0,”rgb(“+Math.min(255,r+55)+”,”+Math.min(255,g+45)+”,”+Math.min(255,b+35)+”)”); grd.addColorStop(0.5,”rgb(“+r+”,”+g+”,”+b+”)”); grd.addColorStop(1,”rgb(“+Math.max(0,r-55)+”,”+Math.max(0,g-45)+”,”+Math.max(0,b-35)+”)”); ctx.fillStyle=grd;ctx.fillRect(0,0,1024,512); const tex=new THREE.CanvasTexture(c);tex.needsUpdate=true;return tex; } const jupTex=makeGasTexture(225,185,105,0.44); const satTex=makeGasTexture(235,205,155,0.36); const uraTex=makeGasTexture(155,215,225,0.28); const nepTex=makeGasTexture(75,115,215,0.34); const marsTex=makeRockyTexture(205,95,55); const mercTex=makeRockyTexture(175,160,145); const ringTex=makeRingTexture(250,225,165); const loader=new THREE.TextureLoader(); loader.setCrossOrigin(“anonymous”); function safeLoad(url,fallbackR,fallbackG,fallbackB,aniso){ COSMOS.texturesTotal++; const tex=loader.load(url,function(){COSMOS.texturesReady++;},undefined,function(){ const fb=makeFallbackPlanetTexture(fallbackR,fallbackG,fallbackB); tex.image=fb.image; tex.needsUpdate=true; COSMOS.texturesReady++; }); tex.anisotropy=aniso||16; return tex; } const earthMap=safeLoad(“https://threejs.org/examples/textures/planets/earth_atmos_2048.jpg”,55,120,190,16); const earthSpec=safeLoad(“https://threejs.org/examples/textures/planets/earth_specular_2048.jpg”,45,45,55,12); const earthCloudMap=safeLoad(“https://threejs.org/examples/textures/planets/earth_clouds_1024.png”,230,230,240,12); const earthNormal=safeLoad(“https://threejs.org/examples/textures/planets/earth_normal_2048.jpg”,128,128,128,16); const moonMap=safeLoad(“https://threejs.org/examples/textures/planets/moon_1024.jpg”,155,155,155,12); const starGeo=new THREE.BufferGeometry(); const starPos=new Float32Array(starCount*3); const starCol=new Float32Array(starCount*3); for(let i=0;i<starCount;i++){ const i3=i*3; const r=28+Math.pow(Math.random(),0.38)*185000; const theta=Math.random()*Math.PI*2; const phi=Math.acos(2*Math.random()-1); starPos[i3]=r*Math.sin(phi)*Math.cos(theta); starPos[i3+1]=r*Math.sin(phi)*Math.sin(theta); starPos[i3+2]=r*Math.cos(phi)-1800; const b=0.12+Math.random()*0.88; const h=Math.random(); if(h<0.38){starCol[i3]=b;starCol[i3+1]=b;starCol[i3+2]=b;} else if(h<0.56){starCol[i3]=b*0.52;starCol[i3+1]=b*0.68;starCol[i3+2]=b;} else if(h<0.72){starCol[i3]=b;starCol[i3+1]=b*0.72;starCol[i3+2]=b*0.45;} else if(h<0.87){starCol[i3]=b;starCol[i3+1]=b*0.5;starCol[i3+2]=b*0.7;} else{starCol[i3]=b*0.58;starCol[i3+1]=b;starCol[i3+2]=b*0.78;} } starGeo.setAttribute(“position”,new THREE.BufferAttribute(starPos,3)); starGeo.setAttribute(“color”,new THREE.BufferAttribute(starCol,3)); const stars=new THREE.Points(starGeo,new THREE.PointsMaterial({size:4.2,map:starTex,vertexColors:true,transparent:true,opacity:0.998,depthWrite:false,sizeAttenuation:true,blending:THREE.AdditiveBlending,alphaTest:0.002})); scene.add(stars); const textStr=”A Few Bad Newbies”; const textCount=isLowEnd?2200:5200; const textCanvas=document.createElement(“canvas”); textCanvas.width=1400;textCanvas.height=160; const tctx=textCanvas.getContext(“2d”); tctx.fillStyle=”#000″; tctx.fillRect(0,0,1400,160); tctx.fillStyle=”#fff”; tctx.font=”bold 92px Arial, Helvetica, sans-serif”; tctx.textAlign=”center”; tctx.textBaseline=”middle”; tctx.fillText(textStr,700,82); const tImg=tctx.getImageData(0,0,1400,160).data; const onPix=[]; for(let py=0;py<160;py++){ for(let px=0;px<1400;px++){ if(tImg[(py*1400+px)*4]>128)onPix.push(px,py); } } const textGeo=new THREE.BufferGeometry(); const textPos=new Float32Array(textCount*3); const textCol=new Float32Array(textCount*3); const onCount=onPix.length/2; for(let i=0;i<textCount;i++){ const i3=i*3; const k=Math.floor(Math.random()*onCount)*2; const px=onPix[k]+(Math.random()-0.5); const py=onPix[k+1]+(Math.random()-0.5); textPos[i3]=(px/1400-0.5)*380; textPos[i3+1]=-(py/160-0.5)*44; textPos[i3+2]=(Math.random()-0.5)*3; const b=0.7+Math.random()*0.3; textCol[i3]=0.4*b;textCol[i3+1]=0.85*b;textCol[i3+2]=b; } textGeo.setAttribute(“position”,new THREE.BufferAttribute(textPos,3)); textGeo.setAttribute(“color”,new THREE.BufferAttribute(textCol,3)); const textPoints=new THREE.Points(textGeo,new THREE.PointsMaterial({size:1.7,map:starTex,vertexColors:true,transparent:true,opacity:0.96,depthWrite:false,sizeAttenuation:true,blending:THREE.AdditiveBlending})); textPoints.position.set(10200,0,-21000); scene.add(textPoints); const milkyGeo=new THREE.BufferGeometry(); const milkyPos=new Float32Array(milkyCount*3); const milkyCol=new Float32Array(milkyCount*3); for(let i=0;i<milkyCount;i++){ const i3=i*3; const ang=Math.random()*Math.PI*2; const rad=150+Math.random()*125000; const band=(Math.random()-0.5)*5600; milkyPos[i3]=Math.cos(ang)*rad; milkyPos[i3+1]=band*0.048+(Math.random()-0.5)*95; milkyPos[i3+2]=Math.sin(ang)*rad*0.17-2000; const b=0.03+Math.random()*0.24; milkyCol[i3]=b*0.94;milkyCol[i3+1]=b*0.84;milkyCol[i3+2]=b; } milkyGeo.setAttribute(“position”,new THREE.BufferAttribute(milkyPos,3)); milkyGeo.setAttribute(“color”,new THREE.BufferAttribute(milkyCol,3)); const milky=new THREE.Points(milkyGeo,new THREE.PointsMaterial({size:2.6,map:starTex,vertexColors:true,transparent:true,opacity:0.46,depthWrite:false,sizeAttenuation:true,blending:THREE.AdditiveBlending})); scene.add(milky); function createAtmosphere(radius,color,opacity){ const geo=new THREE.SphereGeometry(radius,isLowEnd?64:128,isLowEnd?64:128); const mat=new THREE.MeshPhongMaterial({color:color,transparent:true,opacity:opacity,side:THREE.BackSide,shininess:2,depthWrite:false,blending:THREE.AdditiveBlending}); return new THREE.Mesh(geo,mat); } const sun=new THREE.Mesh(new THREE.SphereGeometry(30,isLowEnd?96:192,isLowEnd?96:192),new THREE.MeshBasicMaterial({color:0xfff6b8})); sun.position.set(0,0,0); scene.add(sun); sunLight.position.copy(sun.position); sun.add(createAtmosphere(38,0xffaa44,0.36)); sun.add(createAtmosphere(54,0xff7722,0.2)); sun.add(createAtmosphere(72,0xff4400,0.1)); const coronaGeo=new THREE.BufferGeometry(); const coronaCount=isLowEnd?1100:3200; const coronaPos=new Float32Array(coronaCount*3); const coronaCol=new Float32Array(coronaCount*3); for(let i=0;i<coronaCount;i++){ const i3=i*3; const r=33+Math.random()*68; const theta=Math.random()*Math.PI*2; const phi=Math.acos(2*Math.random()-1); coronaPos[i3]=r*Math.sin(phi)*Math.cos(theta); coronaPos[i3+1]=r*Math.sin(phi)*Math.sin(theta); coronaPos[i3+2]=r*Math.cos(phi); const b=0.45+Math.random()*0.55; coronaCol[i3]=b;coronaCol[i3+1]=b*0.72;coronaCol[i3+2]=b*0.28; } coronaGeo.setAttribute(“position”,new THREE.BufferAttribute(coronaPos,3)); coronaGeo.setAttribute(“color”,new THREE.BufferAttribute(coronaCol,3)); const corona=new THREE.Points(coronaGeo,new THREE.PointsMaterial({size:3.1,map:starTex,vertexColors:true,transparent:true,opacity:0.64,depthWrite:false,sizeAttenuation:true,blending:THREE.AdditiveBlending})); sun.add(corona); const mercury=new THREE.Mesh(new THREE.SphereGeometry(2.1,isLowEnd?96:160,isLowEnd?96:160),new THREE.MeshPhongMaterial({map:mercTex,bumpMap:mercTex,bumpScale:0.08,emissive:0x2a2218,emissiveIntensity:0.42,shininess:52,specular:0x999999})); mercury.position.set(68,0,0); scene.add(mercury); const venus=new THREE.Mesh(new THREE.SphereGeometry(4.3,isLowEnd?96:176,isLowEnd?96:176),new THREE.MeshPhongMaterial({color:0xe8c498,emissive:0x3a2410,emissiveIntensity:0.48,shininess:62,specular:0x998877})); venus.position.set(108,0,0); scene.add(venus); venus.add(createAtmosphere(4.95,0xffcc88,0.4)); const earthGroup=new THREE.Group(); const earthMat=new THREE.MeshPhongMaterial({map:earthMap,specularMap:earthSpec,normalMap:earthNormal,normalScale:new THREE.Vector2(0.85,0.85),specular:new THREE.Color(0xaabbcc),shininess:32,emissive:0x0c2040,emissiveIntensity:1.12}); const earth=new THREE.Mesh(new THREE.SphereGeometry(5.7,isLowEnd?200:400,isLowEnd?200:400),earthMat); earthGroup.add(earth); earthGroup.add(createAtmosphere(6.35,0x66b8ff,0.55)); earthGroup.add(createAtmosphere(6.72,0x99d8ff,0.24)); earthGroup.add(createAtmosphere(7.15,0x3366cc,0.1)); const cloudsMat=new THREE.MeshPhongMaterial({map:earthCloudMap,transparent:true,opacity:0.62,depthWrite:false,side:THREE.DoubleSide}); const earthClouds=new THREE.Mesh(new THREE.SphereGeometry(5.84,isLowEnd?128:256,isLowEnd?128:256),cloudsMat); earthGroup.add(earthClouds); earthGroup.position.set(150,0,0); scene.add(earthGroup); const userMarker=new THREE.Mesh(new THREE.SphereGeometry(0.09,20,20),new THREE.MeshBasicMaterial({color:0xff0a00})); earth.add(userMarker); const userMarkerRing=new THREE.Mesh(new THREE.RingGeometry(0.13,0.18,24),new THREE.MeshBasicMaterial({color:0xff3b3b,transparent:true,opacity:0.85,side:THREE.DoubleSide})); earth.add(userMarkerRing); const youLabel=document.createElement(“div”); youLabel.id=”you-label”; youLabel.textContent=”YOU ARE HERE”; document.getElementById(“hud”).appendChild(youLabel); const moon=new THREE.Mesh(new THREE.SphereGeometry(1.52,isLowEnd?96:160,isLowEnd?96:160),new THREE.MeshPhongMaterial({map:moonMap,bumpMap:moonMap,bumpScale:0.06,emissive:0x0c0c0c,emissiveIntensity:0.32,shininess:6,specular:0x444444})); moon.position.set(168,2.4,4.2); scene.add(moon); const mars=new THREE.Mesh(new THREE.SphereGeometry(3.8,isLowEnd?96:176,isLowEnd?96:176),new THREE.MeshPhongMaterial({map:marsTex,bumpMap:marsTex,bumpScale:0.07,emissive:0x3a1205,emissiveIntensity:0.42,shininess:18,specular:0x664433})); mars.position.set(220,0,0); scene.add(mars); mars.add(createAtmosphere(4.32,0xff6644,0.24)); const jupiter=new THREE.Mesh(new THREE.SphereGeometry(17.5,isLowEnd?128:224,isLowEnd?128:224),new THREE.MeshPhongMaterial({map:jupTex,bumpMap:jupTex,bumpScale:0.04,emissive:0x3a2810,emissiveIntensity:0.38,shininess:12,specular:0x555544})); jupiter.position.set(380,0,0); scene.add(jupiter); const saturnGroup=new THREE.Group(); const saturn=new THREE.Mesh(new THREE.SphereGeometry(12.3,isLowEnd?128:224,isLowEnd?128:224),new THREE.MeshPhongMaterial({map:satTex,bumpMap:satTex,bumpScale:0.03,emissive:0x2a1c10,emissiveIntensity:0.4,shininess:32,specular:0x776644})); saturnGroup.add(saturn); const ring=new THREE.Mesh(new THREE.RingGeometry(16.5,29.5,isLowEnd?256:512),new THREE.MeshPhongMaterial({map:ringTex,color:0xffe8b0,side:THREE.DoubleSide,transparent:true,opacity:0.99,shininess:120,specular:0x998855})); ring.rotation.x=Math.PI/2+0.05; saturnGroup.add(ring); const ringInner=new THREE.Mesh(new THREE.RingGeometry(14.8,16.4,isLowEnd?160:320),new THREE.MeshPhongMaterial({color:0xddbb77,side:THREE.DoubleSide,transparent:true,opacity:0.94,shininess:90,specular:0x887755})); ringInner.rotation.x=Math.PI/2+0.05; saturnGroup.add(ringInner); const ringOuter=new THREE.Mesh(new THREE.RingGeometry(29.8,33.5,isLowEnd?160:320),new THREE.MeshPhongMaterial({color:0xf0e0c0,side:THREE.DoubleSide,transparent:true,opacity:0.5,shininess:35,specular:0x776655})); ringOuter.rotation.x=Math.PI/2+0.03; saturnGroup.add(ringOuter); const ringDust=new THREE.Mesh(new THREE.RingGeometry(33.8,37.5,isLowEnd?128:256),new THREE.MeshPhongMaterial({color:0xe8d8b8,side:THREE.DoubleSide,transparent:true,opacity:0.2,shininess:18})); ringDust.rotation.x=Math.PI/2+0.02; saturnGroup.add(ringDust); saturnGroup.position.set(550,0,0); scene.add(saturnGroup); const uranus=new THREE.Mesh(new THREE.SphereGeometry(7.6,isLowEnd?96:176,isLowEnd?96:176),new THREE.MeshPhongMaterial({map:uraTex,bumpMap:uraTex,bumpScale:0.03,emissive:0x0e2230,emissiveIntensity:0.38,shininess:22,specular:0x556677})); uranus.position.set(720,0,0); scene.add(uranus); const neptune=new THREE.Mesh(new THREE.SphereGeometry(7.4,isLowEnd?96:176,isLowEnd?96:176),new THREE.MeshPhongMaterial({map:nepTex,bumpMap:nepTex,bumpScale:0.03,emissive:0x081530,emissiveIntensity:0.4,shininess:24,specular:0x445577})); neptune.position.set(870,0,0); scene.add(neptune); neptune.add(createAtmosphere(8.28,0x4488ff,0.3)); const pluto=new THREE.Mesh(new THREE.SphereGeometry(1.4,96,96),new THREE.MeshPhongMaterial({color:0xd0c0a8,emissive:0x221c14,emissiveIntensity:0.32,shininess:14,specular:0x665544})); pluto.position.set(980,8,-40); scene.add(pluto); const eris=new THREE.Mesh(new THREE.SphereGeometry(1.15,80,80),new THREE.MeshPhongMaterial({color:0xd8e0e8,emissive:0x141c22,emissiveIntensity:0.3,shininess:18})); eris.position.set(1020,-12,60); scene.add(eris); const bhGroup=new THREE.Group(); const bhCore=new THREE.Mesh(new THREE.SphereGeometry(12,isLowEnd?64:128,isLowEnd?64:128),new THREE.MeshBasicMaterial({color:0x000000})); bhGroup.add(bhCore); const photonRing=new THREE.Mesh(new THREE.RingGeometry(12.1,14.2,isLowEnd?128:256),new THREE.MeshBasicMaterial({color:0xffffff,side:THREE.DoubleSide,transparent:true,opacity:0.96})); photonRing.rotation.x=1.12; bhGroup.add(photonRing); const disk=new THREE.Mesh(new THREE.RingGeometry(14.3,80,isLowEnd?256:512),new THREE.MeshBasicMaterial({color:0xff7700,side:THREE.DoubleSide,transparent:true,opacity:0.98})); disk.rotation.x=1.12; bhGroup.add(disk); const diskHot=new THREE.Mesh(new THREE.RingGeometry(14.5,38,isLowEnd?160:320),new THREE.MeshBasicMaterial({color:0xffee77,side:THREE.DoubleSide,transparent:true,opacity:0.92})); diskHot.rotation.x=1.15; bhGroup.add(diskHot); const diskMid=new THREE.Mesh(new THREE.RingGeometry(40,64,isLowEnd?160:320),new THREE.MeshBasicMaterial({color:0xff3300,side:THREE.DoubleSide,transparent:true,opacity:0.58})); diskMid.rotation.x=1.09; bhGroup.add(diskMid); const diskOuter=new THREE.Mesh(new THREE.RingGeometry(66,95,isLowEnd?160:320),new THREE.MeshBasicMaterial({color:0x7711dd,side:THREE.DoubleSide,transparent:true,opacity:0.38})); diskOuter.rotation.x=1.06; bhGroup.add(diskOuter); const diskFar=new THREE.Mesh(new THREE.RingGeometry(97,125,isLowEnd?128:256),new THREE.MeshBasicMaterial({color:0x330077,side:THREE.DoubleSide,transparent:true,opacity:0.16})); diskFar.rotation.x=1.03; bhGroup.add(diskFar); const bhParticles=isLowEnd?900:2800; const bhPGeo=new THREE.BufferGeometry(); const bhPPos=new Float32Array(bhParticles*3); const bhPCol=new Float32Array(bhParticles*3); for(let i=0;i<bhParticles;i++){ const i3=i*3; const ang=Math.random()*Math.PI*2; const rad=15+Math.random()*105; const y=(Math.random()-0.5)*10; bhPPos[i3]=Math.cos(ang)*rad; bhPPos[i3+1]=y; bhPPos[i3+2]=Math.sin(ang)*rad; const t=rad/120; bhPCol[i3]=1;bhPCol[i3+1]=0.42+t*0.5;bhPCol[i3+2]=0.12+t*0.65; } bhPGeo.setAttribute(“position”,new THREE.BufferAttribute(bhPPos,3)); bhPGeo.setAttribute(“color”,new THREE.BufferAttribute(bhPCol,3)); const bhDust=new THREE.Points(bhPGeo,new THREE.PointsMaterial({size:1.8,map:starTex,vertexColors:true,transparent:true,opacity:0.8,depthWrite:false,sizeAttenuation:true,blending:THREE.AdditiveBlending})); bhGroup.add(bhDust); bhGroup.position.set(1120,80,-520); scene.add(bhGroup); const supernovaGroup=new THREE.Group(); const snCore=new THREE.Mesh(new THREE.SphereGeometry(5.5,isLowEnd?64:128,isLowEnd?64:128),new THREE.MeshBasicMaterial({color:0xffffff})); supernovaGroup.add(snCore); const snShell=new THREE.Mesh(new THREE.SphereGeometry(9.5,isLowEnd?64:128,isLowEnd?64:128),new THREE.MeshBasicMaterial({color:0xffaa44,transparent:true,opacity:0.88,side:THREE.BackSide})); supernovaGroup.add(snShell); const snShell2=new THREE.Mesh(new THREE.SphereGeometry(17,isLowEnd?64:128,isLowEnd?64:128),new THREE.MeshBasicMaterial({color:0xff4400,transparent:true,opacity:0.58,side:THREE.BackSide})); supernovaGroup.add(snShell2); const snShell3=new THREE.Mesh(new THREE.SphereGeometry(28,isLowEnd?48:96,isLowEnd?48:96),new THREE.MeshBasicMaterial({color:0xff2200,transparent:true,opacity:0.28,side:THREE.BackSide})); supernovaGroup.add(snShell3); const snShell4=new THREE.Mesh(new THREE.SphereGeometry(42,isLowEnd?32:64,isLowEnd?32:64),new THREE.MeshBasicMaterial({color:0xaa1100,transparent:true,opacity:0.12,side:THREE.BackSide})); supernovaGroup.add(snShell4); const snCount=isLowEnd?650:1900; const snPGeo=new THREE.BufferGeometry(); const snPPos=new Float32Array(snCount*3); const snPCol=new Float32Array(snCount*3); for(let i=0;i<snCount;i++){ const i3=i*3; const r=6+Math.random()*48; const theta=Math.random()*Math.PI*2; const phi=Math.acos(2*Math.random()-1); snPPos[i3]=r*Math.sin(phi)*Math.cos(theta); snPPos[i3+1]=r*Math.sin(phi)*Math.sin(theta); snPPos[i3+2]=r*Math.cos(phi); const b=0.55+Math.random()*0.45; snPCol[i3]=b;snPCol[i3+1]=b*0.52;snPCol[i3+2]=b*0.18; } snPGeo.setAttribute(“position”,new THREE.BufferAttribute(snPPos,3)); snPGeo.setAttribute(“color”,new THREE.BufferAttribute(snPCol,3)); const snParticles=new THREE.Points(snPGeo,new THREE.PointsMaterial({size:2.5,map:starTex,vertexColors:true,transparent:true,opacity:0.84,depthWrite:false,sizeAttenuation:true,blending:THREE.AdditiveBlending})); supernovaGroup.add(snParticles); supernovaGroup.position.set(1450,-90,-950); scene.add(supernovaGroup); let snPhase=0; function createNebula(pos,scale,color1,color2,color3){ const group=new THREE.Group(); const geo=new THREE.SphereGeometry(1,isLowEnd?32:56,isLowEnd?32:56); const m1=new THREE.Mesh(geo,new THREE.MeshBasicMaterial({color:color1,transparent:true,opacity:0.18,side:THREE.BackSide,depthWrite:false,blending:THREE.AdditiveBlending})); m1.scale.set(scale,scale*0.52,scale*0.68); group.add(m1); const m2=new THREE.Mesh(geo,new THREE.MeshBasicMaterial({color:color2,transparent:true,opacity:0.15,side:THREE.BackSide,depthWrite:false,blending:THREE.AdditiveBlending})); m2.scale.set(scale*0.52,scale*0.78,scale*0.42); m2.position.set(scale*0.24,0,-scale*0.2); group.add(m2); if(color3){ const m3=new THREE.Mesh(geo,new THREE.MeshBasicMaterial({color:color3,transparent:true,opacity:0.12,side:THREE.BackSide,depthWrite:false,blending:THREE.AdditiveBlending})); m3.scale.set(scale*0.7,scale*0.4,scale*0.55); m3.position.set(-scale*0.18,scale*0.1,scale*0.12); group.add(m3); } group.position.copy(pos); scene.add(group); return group; } const neb1=createNebula(new THREE.Vector3(950,-140,-2400),270,0x00ffcc,0xff00aa,0x4488ff); const neb2=createNebula(new THREE.Vector3(2050,160,-4400),310,0xaa00ff,0x00ccff,0xff66aa); const neb3=createNebula(new THREE.Vector3(3400,-180,-6800),350,0xff2288,0x22ffcc,0x8866ff); const neb4=createNebula(new THREE.Vector3(5100,110,-9600),290,0x8844ff,0xff66cc,0x44aaff); const neb5=createNebula(new THREE.Vector3(7000,-100,-13400),370,0x00aaff,0xff44aa,0xaa88ff); const neb6=createNebula(new THREE.Vector3(8600,80,-16200),230,0xff8844,0x44ffcc,0x6688ff); const astGeo=new THREE.BufferGeometry(); const astPos=new Float32Array(astCount*3); const astData=[]; for(let i=0;i<astCount;i++){ const angle=Math.random()*Math.PI*2; const radius=260+(Math.random()-0.5)*130; const y=(Math.random()-0.5)*30; astData.push({angle:angle,radius:radius,y:y,speed:0.000055+Math.random()*0.00042}); astPos[i*3]=Math.cos(angle)*radius; astPos[i*3+1]=y; astPos[i*3+2]=Math.sin(angle)*radius; } astGeo.setAttribute(“position”,new THREE.BufferAttribute(astPos,3)); const asteroids=new THREE.Points(astGeo,new THREE.PointsMaterial({size:2.1,map:starTex,color:0xa0907a,transparent:true,opacity:0.98,depthWrite:false,sizeAttenuation:true})); scene.add(asteroids); const cometCount=isLowEnd?5:10; const comets=[]; for(let i=0;i<cometCount;i++){ const cGroup=new THREE.Group(); const head=new THREE.Mesh(new THREE.SphereGeometry(0.65+Math.random()*0.55,16,16),new THREE.MeshBasicMaterial({color:0xf0ffff})); cGroup.add(head); const tailGeo=new THREE.BufferGeometry(); const tailN=isLowEnd?60:120; const tPos=new Float32Array(tailN*3); const tCol=new Float32Array(tailN*3); for(let j=0;j<tailN;j++){ const t=j/tailN; tPos[j*3]=-t*22-Math.random()*2.5; tPos[j*3+1]=(Math.random()-0.5)*1.5; tPos[j*3+2]=(Math.random()-0.5)*1.5; tCol[j*3]=0.75*(1-t);tCol[j*3+1]=0.88*(1-t);tCol[j*3+2]=1*(1-t); } tailGeo.setAttribute(“position”,new THREE.BufferAttribute(tPos,3)); tailGeo.setAttribute(“color”,new THREE.BufferAttribute(tCol,3)); const tail=new THREE.Points(tailGeo,new THREE.PointsMaterial({size:1.6,map:starTex,vertexColors:true,transparent:true,opacity:0.88,depthWrite:false,sizeAttenuation:true,blending:THREE.AdditiveBlending})); cGroup.add(tail); const baseR=380+Math.random()*950; const baseA=Math.random()*Math.PI*2; const baseY=(Math.random()-0.5)*85; cGroup.position.set(Math.cos(baseA)*baseR,baseY,Math.sin(baseA)*baseR-200); cGroup.userData={angle:baseA,radius:baseR,y:baseY,speed:0.00014+Math.random()*0.00032,tilt:Math.random()*0.4}; scene.add(cGroup); comets.push(cGroup); } function makeGalaxy(count,armCount,scale,pos,coreColor,armColor){ const geo=new THREE.BufferGeometry(); const posA=new Float32Array(count*3); const colA=new Float32Array(count*3); for(let i=0;i<count;i++){ const i3=i*3; const arm=Math.floor(Math.random()*armCount); const t=Math.random()*12.5; const r=1.05+t*42+Math.random()*18; const a=t*0.72+arm*(Math.PI*2/armCount)+Math.random()*0.48; posA[i3]=Math.cos(a)*r; posA[i3+1]=(Math.random()-0.5)*15; posA[i3+2]=Math.sin(a)*r*0.38; const b=0.22+Math.random()*0.78; if(t<1.0){colA[i3]=coreColor[0]*b;colA[i3+1]=coreColor[1]*b;colA[i3+2]=coreColor[2]*b;} else{colA[i3]=armColor[0]*b;colA[i3+1]=armColor[1]*b;colA[i3+2]=armColor[2]*b;} } geo.setAttribute(“position”,new THREE.BufferAttribute(posA,3)); geo.setAttribute(“color”,new THREE.BufferAttribute(colA,3)); const mat=new THREE.PointsMaterial({size:3.0,map:starTex,vertexColors:true,transparent:true,opacity:0.99,depthWrite:false,sizeAttenuation:true,blending:THREE.AdditiveBlending}); const g=new THREE.Points(geo,mat); g.position.copy(pos); g.scale.set(scale,scale,scale); scene.add(g); return g; } const gCount=isLowEnd?8000:15000; const galaxy1=makeGalaxy(gCount,5,9.0,new THREE.Vector3(1800,70,-3800),[1,0.96,0.92],[0.38,0.55,1]); const galaxy2=makeGalaxy(gCount*0.92,4,8.0,new THREE.Vector3(3300,-100,-6800),[1,0.74,0.58],[0.84,0.34,0.44]); const galaxy3=makeGalaxy(gCount,6,9.8,new THREE.Vector3(5000,120,-10000),[0.94,0.94,1],[0.3,0.5,1]); const galaxy4=makeGalaxy(gCount*0.88,3,7.0,new THREE.Vector3(6900,-80,-13600),[1,0.58,0.38],[0.62,0.24,0.72]); const galaxy5=makeGalaxy(gCount*1.12,5,10.8,new THREE.Vector3(9400,90,-18000),[0.96,0.96,1],[0.4,0.48,1]); const g1p1=new THREE.Mesh(new THREE.SphereGeometry(6.1,isLowEnd?80:160,isLowEnd?80:160),new THREE.MeshPhongMaterial({color:0x90b0d0,emissive:0x0c1828,emissiveIntensity:0.38,shininess:32})); g1p1.position.set(1845,40,-3780); scene.add(g1p1); const g1p2=new THREE.Mesh(new THREE.SphereGeometry(4.1,isLowEnd?64:144,isLowEnd?64:144),new THREE.MeshPhongMaterial({color:0xd09070,emissive:0x1c140a,emissiveIntensity:0.34,shininess:26})); g1p2.position.set(1755,55,-3840); scene.add(g1p2); const g2p1=new THREE.Mesh(new THREE.SphereGeometry(8.5,isLowEnd?80:160,isLowEnd?80:160),new THREE.MeshPhongMaterial({color:0xb07050,emissive:0x1c0c06,emissiveIntensity:0.34,shininess:24})); g2p1.position.set(3345,-85,-6780); scene.add(g2p1); const g3p1=new THREE.Mesh(new THREE.SphereGeometry(5.5,isLowEnd?64:144,isLowEnd?64:144),new THREE.MeshPhongMaterial({color:0x70d0b0,emissive:0x0c1c18,emissiveIntensity:0.38,shininess:28})); g3p1.position.set(5045,105,-9980); scene.add(g3p1); const g4p1=new THREE.Mesh(new THREE.SphereGeometry(9.0,isLowEnd?80:160,isLowEnd?80:160),new THREE.MeshPhongMaterial({color:0xc060b0,emissive:0x180c18,emissiveIntensity:0.32,shininess:22})); g4p1.position.set(6945,-65,-13580); scene.add(g4p1); const g5p1=new THREE.Mesh(new THREE.SphereGeometry(7.2,isLowEnd?80:160,isLowEnd?80:160),new THREE.MeshPhongMaterial({color:0x80a0e0,emissive:0x0c1428,emissiveIntensity:0.34,shininess:26})); g5p1.position.set(9445,60,-17980); scene.add(g5p1); function makeProxy(geometry,pos){ const m=new THREE.Mesh(geometry,new THREE.MeshBasicMaterial({visible:false})); m.position.copy(pos); scene.add(m); return m; } const asteroidProxy=makeProxy(new THREE.TorusGeometry(260,75,8,48),new THREE.Vector3(0,0,0)); asteroidProxy.rotation.x=Math.PI/2; const galaxyProxy1=makeProxy(new THREE.SphereGeometry(220,12,12),galaxy1.position); const galaxyProxy2=makeProxy(new THREE.SphereGeometry(220,12,12),galaxy2.position); const galaxyProxy3=makeProxy(new THREE.SphereGeometry(220,12,12),galaxy3.position); const galaxyProxy4=makeProxy(new THREE.SphereGeometry(220,12,12),galaxy4.position); const galaxyProxy5=makeProxy(new THREE.SphereGeometry(220,12,12),galaxy5.position); const textProxy=makeProxy(new THREE.BoxGeometry(380,80,60),textPoints.position); const primaryPicks=[ [earthGroup,0],[moon,2],[mars,3],[jupiter,5],[saturnGroup,6],[uranus,7],[neptune,8],[bhGroup,9],[supernovaGroup,10], [g1p1,12],[g1p2,12],[g2p1,14],[g3p1,16],[g4p1,18],[g5p1,20] ]; const secondaryPicks=[ [asteroidProxy,4],[galaxyProxy1,11],[galaxyProxy2,13],[galaxyProxy3,15],[galaxyProxy4,17],[galaxyProxy5,19],[textProxy,21] ]; const primaryMap=new Map(); const secondaryMap=new Map(); const primaryObjs=[]; const secondaryObjs=[]; for(let i=0;i<primaryPicks.length;i++){primaryMap.set(primaryPicks[i][0],primaryPicks[i][1]);primaryObjs.push(primaryPicks[i][0]);} for(let i=0;i<secondaryPicks.length;i++){secondaryMap.set(secondaryPicks[i][0],secondaryPicks[i][1]);secondaryObjs.push(secondaryPicks[i][0]);} const waypoints=[ {pos:new THREE.Vector3(150,2.2,12),look:new THREE.Vector3(150,0,0),fov:34}, {pos:new THREE.Vector3(150,6,25),look:new THREE.Vector3(150,0,0),fov:38}, {pos:new THREE.Vector3(168,3.2,15),look:new THREE.Vector3(168,2.4,4.2),fov:36}, {pos:new THREE.Vector3(220,4,24),look:new THREE.Vector3(220,0,0),fov:36}, {pos:new THREE.Vector3(270,14,90),look:new THREE.Vector3(270,0,0),fov:44}, {pos:new THREE.Vector3(380,16,65),look:new THREE.Vector3(380,0,0),fov:38}, {pos:new THREE.Vector3(550,14,60),look:new THREE.Vector3(550,0,0),fov:38}, {pos:new THREE.Vector3(720,12,50),look:new THREE.Vector3(720,0,0),fov:36}, {pos:new THREE.Vector3(870,12,50),look:new THREE.Vector3(870,0,0),fov:36}, {pos:new THREE.Vector3(1120,60,-380),look:new THREE.Vector3(1120,80,-520),fov:40}, {pos:new THREE.Vector3(1450,-50,-820),look:new THREE.Vector3(1450,-90,-950),fov:40}, {pos:new THREE.Vector3(1800,100,-3500),look:new THREE.Vector3(1800,70,-3800),fov:44}, {pos:new THREE.Vector3(1845,50,-3700),look:new THREE.Vector3(1845,40,-3780),fov:36}, {pos:new THREE.Vector3(3300,40,-6500),look:new THREE.Vector3(3300,-100,-6800),fov:44}, {pos:new THREE.Vector3(3345,-50,-6650),look:new THREE.Vector3(3345,-85,-6780),fov:36}, {pos:new THREE.Vector3(5000,140,-9700),look:new THREE.Vector3(5000,120,-10000),fov:44}, {pos:new THREE.Vector3(5045,115,-9850),look:new THREE.Vector3(5045,105,-9980),fov:36}, {pos:new THREE.Vector3(6900,40,-13300),look:new THREE.Vector3(6900,-80,-13600),fov:44}, {pos:new THREE.Vector3(6945,-40,-13450),look:new THREE.Vector3(6945,-65,-13580),fov:36}, {pos:new THREE.Vector3(9400,120,-17700),look:new THREE.Vector3(9400,90,-18000),fov:44}, {pos:new THREE.Vector3(9445,90,-17850),look:new THREE.Vector3(9445,60,-17980),fov:36}, {pos:new THREE.Vector3(10500,70,-20000),look:new THREE.Vector3(10200,0,-21000),fov:46} ]; const N=waypoints.length-1; const state={progress:0,targetProgress:0,velocity:0,yaw:0,pitch:0,distMul:1,targetDistMul:1,drag:false,pinch:false,lx:0,ly:0,lp:0,t:0,warp:false,free:false}; function setUserLocation(lat,lon){ userLat=lat;userLon=lon; const phi=(90-lat)*(Math.PI/180); const theta=(lon+180)*(Math.PI/180); const r=5.85; const x=-r*Math.sin(phi)*Math.cos(theta); const y=r*Math.cos(phi); const z=r*Math.sin(phi)*Math.sin(theta); userMarker.position.set(x,y,z); userMarker.lookAt(x*2,y*2,z*2); userMarkerRing.position.set(x,y,z); userMarkerRing.lookAt(x*2,y*2,z*2); } setUserLocation(40.7128,-74.0060); if(navigator.geolocation){ navigator.geolocation.getCurrentPosition(function(pos){ setUserLocation(pos.coords.latitude,pos.coords.longitude); },function(){setUserLocation(40.7128,-74.0060);},{enableHighAccuracy:true,timeout:8000,maximumAge:0}); navigator.geolocation.watchPosition(function(pos){ if(!inGame)setUserLocation(pos.coords.latitude,pos.coords.longitude); },function(){},{enableHighAccuracy:true,timeout:15000,maximumAge:5000}); } const Snd={ctx:null,master:null,musicGain:null,musicIn:null,sfxGain:null,noiseBuf:null,whooshGain:null,whooshFilter:null,timer:null,nextTime:0,step:0,mode:”space”,ready:false}; const CHORDS_SPACE=[ {bass:45,notes:[57,60,64,67]}, {bass:41,notes:[57,60,65,69]}, {bass:48,notes:[55,60,64,67]}, {bass:43,notes:[55,59,62,67]} ]; const CHORDS_GAME=[ {bass:45,notes:[57,60,64,67]}, {bass:41,notes:[57,60,65,69]}, {bass:48,notes:[55,60,64,67]}, {bass:43,notes:[55,59,62,67]} ]; const CHORDS_GODSEYE=[ {bass:33,notes:[45,48,52,55]}, {bass:36,notes:[48,52,55,59]}, {bass:40,notes:[52,55,59,62]}, {bass:31,notes:[43,46,50,53]} ]; const ARP_SPACE=[0,-1,2,-1,1,3,-1,2]; const ARP_GAME=[0,2,1,3,0,2,3,1]; const ARP_GODSEYE=[0,2,1,3,-1,1,2,-1]; function midiToHz(m){return 440*Math.pow(2,(m-69)/12);} function makeImpulse(ctx,sec,decay){ const len=Math.floor(ctx.sampleRate*sec); const buf=ctx.createBuffer(2,len,ctx.sampleRate); for(let ch=0;ch<2;ch++){ const d=buf.getChannelData(ch); for(let i=0;i<len;i++){d[i]=(Math.random()*2-1)*Math.pow(1-i/len,decay);} } return buf; } function tone(freq,t,dur,type,vol,slideTo){ const ctx=Snd.ctx; const o=ctx.createOscillator(); o.type=type; o.frequency.setValueAtTime(freq,t); if(slideTo)o.frequency.exponentialRampToValueAtTime(slideTo,t+dur); const g=ctx.createGain(); g.gain.setValueAtTime(0.0001,t); g.gain.linearRampToValueAtTime(vol,t+0.012); g.gain.exponentialRampToValueAtTime(0.0001,t+dur); o.connect(g); g.connect(Snd.sfxGain); o.start(t); o.stop(t+dur+0.05); } function padChord(notes,t,dur,lpFreq){ const ctx=Snd.ctx; for(let i=0;i<notes.length;i++){ const f=midiToHz(notes[i]); const g=ctx.createGain(); g.gain.setValueAtTime(0.0001,t); g.gain.linearRampToValueAtTime(0.07,t+Math.min(1.1,dur*0.3)); g.gain.setValueAtTime(0.07,t+dur*0.7); g.gain.linearRampToValueAtTime(0.0001,t+dur+0.9); const lp=ctx.createBiquadFilter(); lp.type=”lowpass”; lp.frequency.value=lpFreq||1100; lp.Q.value=0.4; const o1=ctx.createOscillator(); o1.type=”triangle”; o1.frequency.value=f; o1.detune.value=-7; const o2=ctx.createOscillator(); o2.type=”sawtooth”; o2.frequency.value=f; o2.detune.value=7; const g2=ctx.createGain(); g2.gain.value=0.35; o1.connect(lp); o2.connect(g2); g2.connect(lp); lp.connect(g); g.connect(Snd.musicIn); o1.start(t);o2.start(t); o1.stop(t+dur+1);o2.stop(t+dur+1); } } function bassNote(note,t,dur){ const ctx=Snd.ctx; const f=midiToHz(note); const g=ctx.createGain(); g.gain.setValueAtTime(0.0001,t); g.gain.linearRampToValueAtTime(0.24,t+0.03); g.gain.exponentialRampToValueAtTime(0.0001,t+dur); const o1=ctx.createOscillator(); o1.type=”sine”; o1.frequency.value=f; const o2=ctx.createOscillator(); o2.type=”triangle”; o2.frequency.value=f*2; const g2=ctx.createGain(); g2.gain.value=0.4; o1.connect(g); o2.connect(g2); g2.connect(g); g.connect(Snd.musicGain); o1.start(t);o2.start(t); o1.stop(t+dur+0.05);o2.stop(t+dur+0.05); } function pluck(f,t,dur,vol){ const ctx=Snd.ctx; const o=ctx.createOscillator(); o.type=”triangle”; o.frequency.value=f; const o2=ctx.createOscillator(); o2.type=”sine”; o2.frequency.value=f*2; const g2=ctx.createGain(); g2.gain.value=0.3; const g=ctx.createGain(); g.gain.setValueAtTime(0.0001,t); g.gain.linearRampToValueAtTime(vol,t+0.01); g.gain.exponentialRampToValueAtTime(0.0001,t+dur); o.connect(g); o2.connect(g2); g2.connect(g); g.connect(Snd.musicIn); o.start(t);o2.start(t); o.stop(t+dur+0.05);o2.stop(t+dur+0.05); } function kick(t){ const ctx=Snd.ctx; const o=ctx.createOscillator(); o.type=”sine”; o.frequency.setValueAtTime(150,t); o.frequency.exponentialRampToValueAtTime(45,t+0.14); const g=ctx.createGain(); g.gain.setValueAtTime(0.5,t); g.gain.exponentialRampToValueAtTime(0.0001,t+0.24); o.connect(g); g.connect(Snd.musicGain); o.start(t); o.stop(t+0.28); } function hat(t){ const ctx=Snd.ctx; const s=ctx.createBufferSource(); s.buffer=Snd.noiseBuf; const hp=ctx.createBiquadFilter(); hp.type=”highpass”; hp.frequency.value=7000; const g=ctx.createGain(); g.gain.setValueAtTime(0.09,t); g.gain.exponentialRampToValueAtTime(0.0001,t+0.06); s.connect(hp); hp.connect(g); g.connect(Snd.musicGain); s.start(t,Math.random()*1.5); s.stop(t+0.08); } function playStep(step,t,e){ let bar,pos,ch,chords,arp; if(Snd.mode===”game”){ chords=CHORDS_GAME; arp=ARP_GAME; }else if(Snd.mode===”godseye”){ chords=CHORDS_GODSEYE; arp=ARP_GODSEYE; }else{ chords=CHORDS_SPACE; arp=ARP_SPACE; } bar=Math.floor(step/8)%chords.length; pos=step%8; ch=chords[bar]; if(pos===0){ padChord(ch.notes,t,e*8,Snd.mode===”godseye”?450:1100); bassNote(ch.bass,t,e*4); } if(Snd.mode===”game”){ if(pos===0||pos===4)kick(t); if(pos%2===1)hat(t); if(pos===4)bassNote(ch.bass,t,e*3); pluck(midiToHz(ch.notes[arp[pos]]+12),t,e*1.6,0.15); }else if(Snd.mode===”godseye”){ if(pos===4)kick(t); const a=arp[pos]; if(a>=0)pluck(midiToHz(ch.notes[a]+12),t,e*4,0.08); }else{ const a=arp[pos]; if(a>=0)pluck(midiToHz(ch.notes[a]+12),t,e*3,0.12); } } function schedule(){ const ctx=Snd.ctx; if(!ctx||audioMuted||ctx.state!==”running”)return; let e=0.43; if(Snd.mode===”game”)e=0.29; else if(Snd.mode===”godseye”)e=0.65; if(Snd.nextTime<ctx.currentTime-0.5)Snd.nextTime=ctx.currentTime+0.05; while(Snd.nextTime<ctx.currentTime+0.25){ playStep(Snd.step,Snd.nextTime,e); Snd.nextTime+=e; Snd.step++; } } function updateMuteButtons(){ const label=audioMuted?”๐”:”๐”; muteBtn.textContent=label; gameMute.textContent=label; muteBtn.classList.toggle(“off”,audioMuted); gameMute.classList.toggle(“off”,audioMuted); muteBtn.title=audioMuted?”Sound off”:”Sound on”; gameMute.title=audioMuted?”Sound off”:”Sound on”; } function initAudio(){ if(Snd.ready){ if(Snd.ctx&&!audioMuted&&Snd.ctx.state===”suspended”)Snd.ctx.resume(); return; } try{ const AC=window.AudioContext||window.webkitAudioContext; if(!AC)return; const ctx=new AC(); Snd.ctx=ctx; const comp=ctx.createDynamicsCompressor(); comp.threshold.value=-18; comp.knee.value=24; comp.ratio.value=4; comp.attack.value=0.01; comp.release.value=0.25; const master=ctx.createGain(); master.gain.value=audioMuted?0:0.9; master.connect(comp); comp.connect(ctx.destination); Snd.master=master; const musicGain=ctx.createGain(); musicGain.gain.value=0.5; musicGain.connect(master); Snd.musicGain=musicGain; const sfxGain=ctx.createGain(); sfxGain.gain.value=0.6; sfxGain.connect(master); Snd.sfxGain=sfxGain; const verb=ctx.createConvolver(); verb.buffer=makeImpulse(ctx,3.2,2.6); const verbGain=ctx.createGain(); verbGain.gain.value=0.5; verb.connect(verbGain); verbGain.connect(musicGain); const delay=ctx.createDelay(1.0); delay.delayTime.value=0.38; const fb=ctx.createGain(); fb.gain.value=0.35; const dlp=ctx.createBiquadFilter(); dlp.type=”lowpass”; dlp.frequency.value=2400; delay.connect(dlp); dlp.connect(fb); fb.connect(delay); const dGain=ctx.createGain(); dGain.gain.value=0.4; dlp.connect(dGain); dGain.connect(musicGain); const musicIn=ctx.createGain(); musicIn.gain.value=1; musicIn.connect(musicGain); musicIn.connect(verb); musicIn.connect(delay); Snd.musicIn=musicIn; const nb=ctx.createBuffer(1,ctx.sampleRate*2,ctx.sampleRate); const nd=nb.getChannelData(0); for(let i=0;i<nd.length;i++){nd[i]=Math.random()*2-1;} Snd.noiseBuf=nb; const ns=ctx.createBufferSource(); ns.buffer=nb; ns.loop=true; const nf=ctx.createBiquadFilter(); nf.type=”bandpass”; nf.frequency.value=500; nf.Q.value=0.7; const wg=ctx.createGain(); wg.gain.value=0; ns.connect(nf); nf.connect(wg); wg.connect(sfxGain); ns.start(); Snd.whooshGain=wg; Snd.whooshFilter=nf; Snd.nextTime=ctx.currentTime+0.1; Snd.step=0; Snd.timer=setInterval(schedule,40); Snd.ready=true; if(audioMuted){ctx.suspend();} else if(ctx.state===”suspended”){ctx.resume();} }catch(e){ Snd.ready=false; } } function unlockAudio(){ initAudio(); if(Snd.ctx&&!audioMuted&&Snd.ctx.state===”suspended”)Snd.ctx.resume(); } [“pointerup”,”touchend”,”click”,”keydown”].forEach(function(ev){ window.addEventListener(ev,unlockAudio,{passive:true}); }); function setMuted(m){ audioMuted=m; lsSet(“cosmosMuted”,m?”1″:”0″); updateMuteButtons(); if(!Snd.ready){ if(!m)initAudio(); return; } const t=Snd.ctx.currentTime; Snd.master.gain.cancelScheduledValues(t); if(m){ Snd.master.gain.setTargetAtTime(0,t,0.08); setTimeout(function(){if(audioMuted&&Snd.ctx)Snd.ctx.suspend();},450); }else{ Snd.ctx.resume(); Snd.master.gain.setTargetAtTime(0.9,t,0.1); Snd.nextTime=Snd.ctx.currentTime+0.1; } } function toggleSound(){ setMuted(!audioMuted); } function setMusicMode(m){ Snd.mode=m; if(cosmosMusicBtn)cosmosMusicBtn.textContent=”๐ต MUSIC: “+m.toUpperCase(); } function sfx(name){ if(!Snd.ready||audioMuted||!Snd.ctx||Snd.ctx.state!==”running”)return; const t=Snd.ctx.currentTime; if(name===”claim”){ [523.25,659.25,783.99,1046.5].forEach(function(f,i){tone(f,t+i*0.07,0.26,”triangle”,0.26);}); }else if(name===”capture”){ [392,523.25,659.25,783.99,1046.5].forEach(function(f,i){tone(f,t+i*0.06,0.3,”square”,0.14);}); }else if(name===”coin”){ tone(988,t,0.09,”square”,0.12); tone(1319,t+0.08,0.28,”square”,0.12); }else if(name===”error”){ tone(190,t,0.28,”sawtooth”,0.2,110); }else if(name===”attack”){ [440,370,311,247].forEach(function(f,i){tone(f,t+i*0.11,0.22,”sawtooth”,0.18);}); }else if(name===”catch”){ [659.25,783.99,987.77,1318.5].forEach(function(f,i){tone(f,t+i*0.08,0.3,”triangle”,0.28);}); }else if(name===”shard”){ [880,1174.66,1567.98].forEach(function(f,i){tone(f,t+i*0.05,0.2,”sine”,0.24);}); }else if(name===”spin”){ [440,554.37,659.25,880].forEach(function(f,i){tone(f,t+i*0.05,0.2,”sine”,0.25);}); }else if(name===”level”){ [523.25,659.25,783.99,1046.5,1318.5].forEach(function(f,i){tone(f,t+i*0.09,0.4,”triangle”,0.3);}); }else if(name===”battle”){ [196,246.94,293.66,392].forEach(function(f,i){tone(f,t+i*0.1,0.36,”sawtooth”,0.22);}); }else if(name===”win”){ [523.25,659.25,783.99,1046.5,1318.5,1567.98].forEach(function(f,i){tone(f,t+i*0.08,0.42,”triangle”,0.3);}); }else if(name===”pass”){ [523.25,659.25,783.99,1046.5,1318.5,1567.98,2093].forEach(function(f,i){tone(f,t+i*0.07,0.34,”triangle”,0.28);}); }else if(name===”super”){ [659.25,880,1174.66,1567.98,2093].forEach(function(f,i){tone(f,t+i*0.06,0.3,”square”,0.22);}); }else if(name===”crit”){ [1500,1800,2200].forEach(function(f,i){tone(f,t+i*0.04,0.15,”square”,0.28);}); }else if(name===”status”){ [440,660,440].forEach(function(f,i){tone(f,t+i*0.1,0.2,”sine”,0.2);}); }else if(name===”arena”){ [196,293.66,392,587.33,783.99].forEach(function(f,i){tone(f,t+i*0.1,0.42,”triangle”,0.28);}); }else if(name===”relic”){ [587.33,783.99,1046.5,1318.5,1567.98].forEach(function(f,i){tone(f,t+i*0.05,0.3,”sine”,0.22);}); }else{ tone(660,t,0.06,”sine”,0.15); } } function updateAudio(){ if(!Snd.ready||audioMuted||!Snd.whooshGain||Snd.ctx.state!==”running”)return; const t=Snd.ctx.currentTime; const w=state.velocity>3?Math.min(0.11,state.velocity*0.006):0; Snd.whooshGain.gain.setTargetAtTime(w,t,0.1); Snd.whooshFilter.frequency.setTargetAtTime(300+Math.min(2200,state.velocity*140),t,0.1); } muteBtn.addEventListener(“click”,toggleSound); gameMute.addEventListener(“click”,toggleSound); updateMuteButtons(); wireWalletUi(); updateWalletHud(); enterBtn.addEventListener(“click”,function(){ welcome.classList.add(“hidden”); lsSet(“cosmosEntered”,”1″); unlockAudio(); if(!gState.charCreatedAt){ openCharCreation(); return; } awaitingEntryZoom=true; if(lsGet(“introShown”)!==”1″){ setTimeout(function(){ if(awaitingEntryZoom)introModal.classList.add(“active”); },700); }else{ playCosmosToStreetZoom(); } }); function completeEntry(){ if(!awaitingEntryZoom)return; introModal.classList.remove(“active”); lsSet(“introShown”,”1″); unlockAudio(); setTimeout(function(){ playCosmosToStreetZoom(); },300); } function playCosmosToStreetZoom(){ awaitingEntryZoom=false; if(zoomAnimationActive)return; zoomAnimationActive=true; scrollEl.scrollTop=0; state.progress=0; state.targetProgress=0; state.yaw=0; state.pitch=0; state.targetDistMul=1; state.distMul=1; state.warp=false; scrollEl.style.pointerEvents=”none”; const start=performance.now(); const dur=2400; function animateZoom(now){ scrollEl.scrollTop=0; const t=Math.min(1,(now-start)/dur); const ease=t<0.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2; state.targetDistMul=1-ease*0.99; if(t<1){ requestAnimationFrame(animateZoom); }else{ scrollEl.style.pointerEvents=””; zoomAnimationActive=false; enterGame(0); } } requestAnimationFrame(animateZoom); } function clearGameNotes(){ if(!gameNotesEl)return; while(gameNotesEl.firstChild)gameNotesEl.removeChild(gameNotesEl.firstChild); } function removeGameNote(note){ if(!note||!note.parentNode)return; if(note._timer){clearTimeout(note._timer);note._timer=0;} note.classList.add(“leaving”); setTimeout(function(){if(note.parentNode)note.parentNode.removeChild(note);},240); } function resetGameNoteTimer(note){ if(note._timer)clearTimeout(note._timer); const delay=(note.dataset&ยฌe.dataset.action===”gps”)?15000:3800; note._timer=setTimeout(function(){removeGameNote(note);},delay); } function showGameNote(title,body,opts){ opts=opts||{}; if(!gameNotesEl)return; const action=opts.action||””; const kids=gameNotesEl.children; for(let i=0;i<kids.length;i++){ const el=kids[i]; if(el.dataset.title===title&&el.dataset.body===body&&el.dataset.action===action){resetGameNoteTimer(el);return;} } const note=document.createElement(“div”); note.className=”gnote”; note.dataset.title=title; note.dataset.body=body; note.dataset.action=action; const h=document.createElement(“h4”); h.textContent=title; const p=document.createElement(“p”); p.textContent=body; const x=document.createElement(“button”); x.type=”button”; x.className=”gnote-x”; x.setAttribute(“aria-label”,”Close”); x.textContent=”ร”; note.appendChild(h);note.appendChild(p); if(action===”gps”){ const gb=document.createElement(“button”); gb.type=”button”; gb.className=”gnote-action”; gb.textContent=”GRANT GPS PERMISSIONS”; gb.addEventListener(“click”,function(ev){ev.preventDefault();ev.stopPropagation();requestGpsPermission();}); note.appendChild(gb); } note.appendChild(x); gameNotesEl.appendChild(note); while(gameNotesEl.children.length>3){ const old=gameNotesEl.children[0]; if(old._timer){clearTimeout(old._timer);old._timer=0;} gameNotesEl.removeChild(old); } resetGameNoteTimer(note); } function hideToasts(){ toastEl.classList.remove(“visible”); if(toastTimer){clearTimeout(toastTimer);toastTimer=0;} lastToast=null; lastToastBody=null; lastToastAction=null; const tb=document.getElementById(“toast-gps-btn”); if(tb)tb.style.display=”none”; clearGameNotes(); } toastCloseBtn.addEventListener(“click”,function(e){ e.preventDefault(); e.stopPropagation(); hideToasts(); }); if(gameNotesEl){ gameNotesEl.addEventListener(“click”,function(e){ const x=e.target.closest(“.gnote-x”); if(!x)return; const note=x.parentElement; if(note)removeGameNote(note); }); } document.addEventListener(“click”,function(e){ if(toastEl&&toastEl.classList.contains(“visible”)){ if(!toastEl.contains(e.target)){ toastEl.classList.remove(“visible”); if(toastTimer){clearTimeout(toastTimer);toastTimer=0;} lastToast=null; lastToastBody=null; lastToastAction=null; const tb=document.getElementById(“toast-gps-btn”); if(tb)tb.style.display=”none”; } } if(gameNotesEl&&gameNotesEl.children.length){ if(!gameNotesEl.contains(e.target)){ const kids=Array.prototype.slice.call(gameNotesEl.children); kids.forEach(function(note){removeGameNote(note);}); } } },{passive:true,capture:true}); function modalReset(){ modalFrame.src=”about:blank”; modalFrame.style.display=”none”; modalLoading.classList.remove(“active”); modalInfo.style.display=”none”; modalInfo.innerHTML=””; } function showModal(title){ modalTitle.textContent=title||””; modal.classList.add(“active”); syncHud(); } modalFrame.onload = function() { if (modalFrame.src !== “about:blank”) { modalLoading.classList.remove(“active”); } }; function openModal(url,title){ modalReset(); modalFrame.style.display=”block”; modalLoading.classList.add(“active”); modalFrame.src=url; showModal(title); } function openModalInfo(html,title){ modalReset(); modalInfo.innerHTML=html; modalInfo.style.display=”flex”; showModal(title); } function closeModal(){ modal.classList.remove(“active”); modalReset(); syncHud(); } modalCloseX.addEventListener(“click”,closeModal); modalCloseBtn.addEventListener(“click”,closeModal); modal.addEventListener(“click”,function(e){if(e.target===modal)closeModal();}); function openUnlockModal(){ unlockModal.style.display=””; unlockModal.style.visibility=””; unlockModal.classList.add(“active”); } function closeUnlockModal(){ unlockModal.classList.remove(“active”); pendingZoom=-1; unlockClosedAt=performance.now(); if(state.targetDistMul<0.15)state.targetDistMul=0.45; syncHud(); } unlockNo.addEventListener(“click”,function(e){ e.preventDefault(); e.stopPropagation(); unlockDismissedIdx=activeIdx>=0?activeIdx:unlockTriggerIdx; closeUnlockModal(); }); unlockCloseX.addEventListener(“click”,function(e){ e.preventDefault(); e.stopPropagation(); unlockDismissedIdx=activeIdx>=0?activeIdx:unlockTriggerIdx; closeUnlockModal(); }); unlockYes.addEventListener(“click”,function(e){ e.preventDefault(); e.stopPropagation(); unlockDismissedIdx=-1; closeUnlockModal(); scrollEl.scrollTop=0; setTimeout(function(){ enterGame(0); },600); }); unlockModal.addEventListener(“click”,function(e){ if(e.target===unlockModal){ e.preventDefault(); e.stopPropagation(); unlockDismissedIdx=activeIdx>=0?activeIdx:unlockTriggerIdx; closeUnlockModal(); } }); function showOutOfOrbsModal(orbType){ const o=ORB_TYPE_BY_ID[orbType]||ORB_TYPE_BY_ID.orb; outOrbsMsg.textContent=”You have no “+o.name+” left. Visit the shop to acquire more, or cancel to keep exploring.”; outOrbsModal.classList.add(“active”); } function hideOutOfOrbsModal(){ outOrbsModal.classList.remove(“active”); } outOrbsShop.addEventListener(“click”,function(e){ e.preventDefault(); e.stopPropagation(); hideOutOfOrbsModal(); closePanel(); shopTab=”orb”; setTimeout(function(){showShop();},150); }); outOrbsCancel.addEventListener(“click”,function(e){ e.preventDefault(); e.stopPropagation(); hideOutOfOrbsModal(); }); outOrbsCloseX.addEventListener(“click”,function(e){ e.preventDefault(); e.stopPropagation(); hideOutOfOrbsModal(); }); outOrbsModal.addEventListener(“click”,function(e){ if(e.target===outOrbsModal)hideOutOfOrbsModal(); }); introOk.addEventListener(“click”,function(){ if(awaitingEntryZoom){ completeEntry(); }else{ introModal.classList.remove(“active”); lsSet(“introShown”,”1″); } unlockAudio(); }); introCloseX.addEventListener(“click”,function(){ if(awaitingEntryZoom){ completeEntry(); }else{ introModal.classList.remove(“active”); lsSet(“introShown”,”1″); } unlockAudio(); }); introModal.addEventListener(“click”,function(e){ if(e.target===introModal){ if(awaitingEntryZoom){ completeEntry(); }else{ introModal.classList.remove(“active”); lsSet(“introShown”,”1″); } } }); function openCharCreation(){ charSelectedStarter=null; charPendingCountry=””; charName.value=gState.charName||””; charNation.textContent=”Nationality: detecting…”; const commons=PET_POOLS.common.slice(0,10); let h=””; commons.forEach(function(p){ const r=RARITY[p.r]; h+=”<div class=\”petcard\” data-charpet=\””+p.id+”\” style=\”border-color:”+r.color+”\”><span class=\”pi\”>”+p.icon+”</span><div class=\”pn\”>”+p.name+”</div><div class=\”pr\” style=\”color:”+r.color+”\”>”+r.label+”</div><div class=\”pl\”>Power “+p.power+”</div></div>”; }); charPets.innerHTML=h; charCreate.disabled=true; charModal.classList.add(“active”); detectCountry(); } function detectCountry(){ try{ const tz=Intl.DateTimeFormat().resolvedOptions().timeZone||””; const tzMap={“America/New_York”:”United States”,”America/Chicago”:”United States”,”America/Denver”:”United States”,”America/Los_Angeles”:”United States”,”America/Phoenix”:”United States”,”America/Toronto”:”Canada”,”America/Vancouver”:”Canada”,”America/Mexico_City”:”Mexico”,”America/Sao_Paulo”:”Brazil”,”America/Buenos_Aires”:”Argentina”,”America/Bogota”:”Colombia”,”Europe/London”:”United Kingdom”,”Europe/Dublin”:”Ireland”,”Europe/Paris”:”France”,”Europe/Berlin”:”Germany”,”Europe/Madrid”:”Spain”,”Europe/Rome”:”Italy”,”Europe/Amsterdam”:”Netherlands”,”Europe/Stockholm”:”Sweden”,”Europe/Oslo”:”Norway”,”Europe/Copenhagen”:”Denmark”,”Europe/Helsinki”:”Finland”,”Europe/Warsaw”:”Poland”,”Europe/Lisbon”:”Portugal”,”Europe/Athens”:”Greece”,”Europe/Moscow”:”Russia”,”Europe/Istanbul”:”Turkey”,”Asia/Tokyo”:”Japan”,”Asia/Seoul”:”South Korea”,”Asia/Shanghai”:”China”,”Asia/Hong_Kong”:”Hong Kong”,”Asia/Singapore”:”Singapore”,”Asia/Kolkata”:”India”,”Asia/Dubai”:”United Arab Emirates”,”Asia/Bangkok”:”Thailand”,”Asia/Jakarta”:”Indonesia”,”Asia/Manila”:”Philippines”,”Australia/Sydney”:”Australia”,”Australia/Melbourne”:”Australia”,”Pacific/Auckland”:”New Zealand”,”Africa/Cairo”:”Egypt”,”Africa/Johannesburg”:”South Africa”,”Africa/Lagos”:”Nigeria”,”Africa/Nairobi”:”Kenya”}; const la=(navigator.language||””).toLowerCase(); const regionByLang={“en-us”:”United States”,”en-gb”:”United Kingdom”,”en-ca”:”Canada”,”en-au”:”Australia”,”en-nz”:”New Zealand”,”en-ie”:”Ireland”,”en-in”:”India”,”en-za”:”South Africa”,”en-ng”:”Nigeria”,”es-es”:”Spain”,”es-mx”:”Mexico”,”fr-fr”:”France”,”fr-ca”:”Canada”,”de-de”:”Germany”,”it-it”:”Italy”,”pt-br”:”Brazil”,”pt-pt”:”Portugal”,”nl-nl”:”Netherlands”,”sv-se”:”Sweden”,”no-no”:”Norway”,”da-dk”:”Denmark”,”fi-fi”:”Finland”,”pl-pl”:”Poland”,”ru-ru”:”Russia”,”tr-tr”:”Turkey”,”ja-jp”:”Japan”,”ko-kr”:”South Korea”,”zh-cn”:”China”,”zh-hk”:”Hong Kong”,”hi-in”:”India”,”ar-ae”:”United Arab Emirates”,”th-th”:”Thailand”,”id-id”:”Indonesia”,”tl-ph”:”Philippines”,”el-gr”:”Greece”,”ar-eg”:”Egypt”}; let country=””; if(tzMap[tz])country=tzMap[tz]; if(!country){ const langs=Array.isArray(navigator.languages)?navigator.languages:[la]; for(let i=0;i<langs.length;i++){ const key=String(langs[i]).toLowerCase(); if(regionByLang[key]){country=regionByLang[key];break;} } } if(!country&&la.indexOf(“-“)>0){ const part=la.split(“-“)[1].toUpperCase(); if(part.length===2)country=part; } charPendingCountry=country||”Global”; charNation.textContent=”Nationality: “+charPendingCountry; }catch(e){ charPendingCountry=”Global”; charNation.textContent=”Nationality: Global”; } } charPets.addEventListener(“click”,function(e){ const card=e.target.closest(“[data-charpet]”); if(!card)return; const id=card.getAttribute(“data-charpet”); charSelectedStarter=id; const cards=charPets.querySelectorAll(“.petcard”); for(let i=0;i<cards.length;i++)cards[i].classList.remove(“on”); card.classList.add(“on”); validateChar(); }); charName.addEventListener(“input”,validateChar); function validateChar(){ charCreate.disabled=!(charSelectedStarter&&charName.value.trim().length>=2); } charCreate.addEventListener(“click”,function(){ const nm=charName.value.trim(); if(!nm||nm.length<2||!charSelectedStarter)return; gState.charName=nm; gState.charCountry=charPendingCountry; gState.charCreatedAt=Date.now(); const starter=PET_BY_ID[charSelectedStarter]; if(starter){ gState.pets[starter.id]={lv:1,xp:0,caught:1,anomaly:false,anomalyPerk:””,stats:{str:10,agi:10,int:10,vit:10,luk:10}}; if(gState.team.length<3)gState.team.push(starter.id); } saveGameState(gState); charModal.classList.remove(“active”); showToast(“Character Created”,”Welcome, “+nm+” of “+gState.charCountry+”! “+starter.icon+” “+starter.name+” is your starter.”); sfx(“level”); awaitingEntryZoom=true; if(lsGet(“introShown”)!==”1″){ setTimeout(function(){ if(awaitingEntryZoom)introModal.classList.add(“active”); },700); }else{ playCosmosToStreetZoom(); } }); passengerYes.addEventListener(“click”,function(){ passengerConfirmedUntil=Date.now()+120000; passengerModal.classList.remove(“active”); lastPassengerPromptAt=Date.now(); }); passengerNo.addEventListener(“click”,function(){ passengerModal.classList.remove(“active”); if(inGame)closePanel(); showToast(“Safety”,”Please stop the vehicle before playing.”); }); passengerModal.addEventListener(“click”,function(e){ if(e.target===passengerModal)passengerModal.classList.remove(“active”); }); weatherClose.addEventListener(“click”,function(){ weatherModal.classList.remove(“active”); }); weatherCloseX.addEventListener(“click”,function(){ weatherModal.classList.remove(“active”); }); weatherModal.addEventListener(“click”,function(e){ if(e.target===weatherModal)weatherModal.classList.remove(“active”); }); cardClose.addEventListener(“click”,function(){ cardModal.classList.remove(“active”); }); cardModal.addEventListener(“click”,function(e){ if(e.target===cardModal)cardModal.classList.remove(“active”); }); cardDownload.addEventListener(“click”,function(){ try{ const url=cardCanvas.toDataURL(“image/png”); const a=document.createElement(“a”); a.href=url; a.download=(gState.charName||”player”)+”-afbn-card.png”; document.body.appendChild(a); a.click(); setTimeout(function(){document.body.removeChild(a);},200); }catch(e){ showToast(“Card”,”Download unavailable on this browser.”); } }); var cardShare=document.getElementById(“card-share”); cardShare.addEventListener(“click”,function(){ try{ var dataUrl=cardCanvas.toDataURL(“image/png”); var parts=dataUrl.split(“,”); var byteString=atob(parts[1]); var ab=new ArrayBuffer(byteString.length); var ia=new Uint8Array(ab); for(var i=0;i<byteString.length;i++)ia[i]=byteString.charCodeAt(i); var blob=new Blob([ab],{type:”image/png”}); var safeName=String(gState.charName||”player”).replace(/[^a-z0-9\-_]+/gi,”_”); var fileName=safeName+”-afbn-card.png”; var fileObj=new File([blob],fileName,{type:”image/png”}); if(navigator.canShare&&navigator.canShare({files:[fileObj]})&&navigator.share){ navigator.share({files:[fileObj],title:”My AFBN COSMOS Player Card”,text:”Check out my AFBN COSMOS player card!”}).catch(function(){}); }else{ var a=document.createElement(“a”); a.href=dataUrl; a.download=fileName; document.body.appendChild(a); a.click(); setTimeout(function(){document.body.removeChild(a);},200); var tweet=”My AFBN COSMOS player card! #AFBN #StreetClaim #AFBNCosmos”; var intent=”https://twitter.com/intent/tweet?text=”+encodeURIComponent(tweet)+”&url=”+encodeURIComponent(“https://afbn.me”); window.open(intent,”_blank”,”noopener”); showToast(“Share to X”,”Image downloaded. Attach it to your X post!”); } }catch(e){ showToast(“Share”,”Sharing is unavailable on this browser.”); } }); settingsClose.addEventListener(“click”,function(){ settingsModal.classList.remove(“active”); }); settingsCloseX.addEventListener(“click”,function(){ settingsModal.classList.remove(“active”); }); settingsModal.addEventListener(“click”,function(e){ if(e.target===settingsModal)settingsModal.classList.remove(“active”); }); stepGoalSave.addEventListener(“click”,function(){ let v=parseInt(stepGoalInput.value,10); if(isNaN(v))v=STEP_GOAL_DEFAULT; if(v<STEP_GOAL_MIN)v=STEP_GOAL_MIN; if(v>STEP_GOAL_MAX)v=STEP_GOAL_MAX; gState.stepGoal=v; stepGoalInput.value=v; saveGameState(gState); showToast(“Settings Saved”,”Daily step goal set to “+v+” steps.”); sfx(“coin”); refreshStepSettingsDisplay(); }); function refreshStepSettingsDisplay(){ if(stepGoalInput)stepGoalInput.value=gState.stepGoal; if(stepStreakDisplay)stepStreakDisplay.textContent=String(gState.stepStreak||0); const stepsToday=Math.round((gState.walkDaily.progress||0)*STEPS_PER_METER); if(stepTodayDisplay)stepTodayDisplay.textContent=String(stepsToday); } function showStepSettings(){ refreshStepSettingsDisplay(); settingsModal.classList.add(“active”); } function checkStepGoal(){ ensurePeriods(); const today=dayKey(new Date()); const stepsToday=Math.round((gState.walkDaily.progress||0)*STEPS_PER_METER); const goal=gState.stepGoal||STEP_GOAL_DEFAULT; if(stepsToday>=goal){ const key=today; if(!gState.stepAchievementsClaimed[key]){ gState.stepAchievementsClaimed[key]=true; let streak=1; if(gState.lastStepDay){ const yesterday=new Date(); yesterday.setDate(yesterday.getDate()-1); if(gState.lastStepDay===dayKey(yesterday))streak=(gState.stepStreak||0)+1; } gState.stepStreak=streak; gState.lastStepDay=today; const idx=Math.min(streak-1,STEP_STREAK_REWARDS.length-1); const baseReward=STEP_STREAK_REWARDS[idx]; const totalReward=Math.round(baseReward); gState.credits+=totalReward; addXp(Math.round(totalReward*0.35)); logEvent(“Daily step goal reached! Streak “+streak+”. +”+totalReward+” credits.”); showToast(“Step Goal Reached”,”Streak “+streak+” day”+(streak===1?””:”s”)+”. +”+totalReward+” credits.”); sfx(“level”); saveGameState(gState); updateGameHud(); } } } function drawPlayerCard(){ if(!cardCanvas)return; const ctx=cardCanvas.getContext(“2d”); const w=cardCanvas.width; const h=cardCanvas.height; ctx.clearRect(0,0,w,h); function rr(x,y,ww,hh,r){ ctx.beginPath(); ctx.moveTo(x+r,y); ctx.lineTo(x+ww-r,y); ctx.quadraticCurveTo(x+ww,y,x+ww,y+r); ctx.lineTo(x+ww,y+hh-r); ctx.quadraticCurveTo(x+ww,y+hh,x+ww-r,y+hh); ctx.lineTo(x+r,y+hh); ctx.quadraticCurveTo(x,y+hh,x,y+hh-r); ctx.lineTo(x,y+r); ctx.quadraticCurveTo(x,y,x+r,y); ctx.closePath(); } const bg=ctx.createRadialGradient(w*0.5,h*0.3,50,w*0.5,h*0.3,h*0.8); bg.addColorStop(0,”#0b1e42″); bg.addColorStop(0.45,”#060f26″); bg.addColorStop(1,”#01030a”); ctx.fillStyle=bg; ctx.fillRect(0,0,w,h); const dg=ctx.createLinearGradient(0,0,w,h); dg.addColorStop(0,”rgba(40,110,230,0.22)”); dg.addColorStop(0.5,”rgba(0,0,0,0)”); dg.addColorStop(1,”rgba(130,40,210,0.18)”); ctx.fillStyle=dg; ctx.fillRect(0,0,w,h); for(let i=0;i<320;i++){ const x=Math.random()*w; const y=Math.random()*h; const r=Math.random()*1.8+0.2; const a=Math.random()*0.75+0.15; ctx.fillStyle=”rgba(190,225,255,”+a+”)”; ctx.beginPath(); ctx.arc(x,y,r,0,Math.PI*2); ctx.fill(); } ctx.save(); ctx.shadowColor=”rgba(90,170,255,0.95)”; ctx.shadowBlur=32; ctx.strokeStyle=”rgba(140,210,255,0.95)”; ctx.lineWidth=3.5; rr(18,18,w-36,h-36,28); ctx.stroke(); ctx.restore(); ctx.strokeStyle=”rgba(127,255,196,0.55)”; ctx.lineWidth=1.6; rr(34,34,w-68,h-68,22); ctx.stroke(); ctx.strokeStyle=”rgba(127,255,196,0.9)”; ctx.lineWidth=2.5; [[46,46,1,1],[w-46,46,-1,1],[46,h-46,1,-1],[w-46,h-46,-1,-1]].forEach(function(c){ ctx.beginPath(); ctx.moveTo(c[0],c[1]+22*c[3]); ctx.lineTo(c[0],c[1]); ctx.lineTo(c[0]+22*c[2],c[1]); ctx.stroke(); }); ctx.textAlign=”left”; ctx.textBaseline=”alphabetic”; ctx.fillStyle=”#e8f4ff”; ctx.font=”bold 34px system-ui, sans-serif”; ctx.fillText(“AFBN COSMOS”,48,92); ctx.fillStyle=”#7fffc4″; ctx.font=”bold 15px system-ui, sans-serif”; ctx.fillText(“P L A Y E R C A R D ยท A L L – T I M E S T A T S”,48,118); const name=gState.charName||”Unnamed Player”; const country=gState.charCountry||”Global”; ctx.textAlign=”center”; ctx.shadowColor=”rgba(100,170,255,0.8)”; ctx.shadowBlur=18; ctx.fillStyle=”#ffffff”; ctx.font=”bold 46px system-ui, sans-serif”; ctx.fillText(name,w/2,196); ctx.shadowBlur=0; ctx.fillStyle=”#7fffc4″; ctx.font=”18px system-ui, sans-serif”; ctx.fillText(country,w/2,228); ctx.strokeStyle=”rgba(120,200,255,0.35)”; ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(60,252); ctx.lineTo(w-60,252); ctx.stroke(); const tsec=gState.playSeconds||0; const hh=Math.floor(tsec/3600); const mm=Math.floor((tsec%3600)/60); const ss=Math.floor(tsec%60); const st=gState.stats||{}; const totalSteps=Math.round((st.distance||0)/0.75); const todaySteps=Math.round((gState.walkDaily.progress||0)/0.75); const petCount=Object.keys(gState.pets).length; const blocks=Object.keys(gState.blocks).filter(function(k){return gState.blocks[k].o===”you”;}).length; let shardTotal=0; Object.keys(gState.shards).forEach(function(k){shardTotal+=gState.shards[k];}); const left=[ [“Play Time”,hh+”h “+mm+”m “+ss+”s”], [“Total Steps”,String(totalSteps)], [“Steps Today”,String(todaySteps)], [“Step Goal”,String(gState.stepGoal||STEP_GOAL_DEFAULT)], [“Step Streak”,String(gState.stepStreak||0)], [“Total Distance”,Math.round(st.distance||0)+” m”], [“Distance Today”,Math.round(gState.walkDaily.progress||0)+” m”], [“Login Streak”,gState.streak+”d”], [“Level”,String(levelOf(gState.xp))], [“Credits”,String(gState.credits)], [“Walk Credits”,String(st.walkCredits||0)], [“Pets Owned”,String(petCount)], [“Shards Held”,String(shardTotal)], [“Blocks Owned”,String(blocks)], [“Blocks Claimed”,String(st.claimed||0)], [“Blocks Captured”,String(st.captured||0)], [“Blocks Lost”,String(st.lost||0)], [“Blocks Reinforced”,String(st.reinforced||0)] ]; const right=[ [“Battles Won”,String(gState.battles.wins||0)], [“Battles Lost”,String(gState.battles.losses||0)], [“Arena Wins”,String(st.arenaWins||0)], [“Duel Wins”,String(st.duelsWon||0)], [“Eggs Hatched”,String(st.hatched||0)], [“Breedings”,String(st.breedsDone||0)], [“Pets Fused”,String(st.merged||0)], [“Anomalies”,String(st.anomalies||0)], [“Relics Broken”,String(st.relicsBroken||0)], [“Purchases”,String(st.purchases||0)], [“Depot Spins”,String(st.spins||0)], [“Income Collected”,String(st.collected||0)], [“Signal Catches”,String(st.caught||0)], [“Crew Hired”,gState.hired.length+” / “+NPCS.length], [“Team Power”,String(teamPowerTotal())], [“Milestones”,String(st.petMilestones||0)], [“Walk Credits”,String(st.walkCredits||0)], [“Trained”,String(st.trained||0)] ]; const startY=278; const rowH=28; const leftX=60; const rightX=w/2+30; const leftValX=w/2-40; const rightValX=w-60; ctx.textAlign=”left”; for(let i=0;i<left.length;i++){ const y=startY+i*rowH; ctx.fillStyle=”#7fffc4″; ctx.font=”bold 13px system-ui, sans-serif”; ctx.fillText(left[i][0],leftX,y); ctx.fillStyle=”#e8f4ff”; ctx.textAlign=”right”; ctx.font=”bold 13px system-ui, sans-serif”; ctx.fillText(left[i][1],leftValX,y); ctx.textAlign=”left”; if(i<right.length){ ctx.fillStyle=”#7fffc4″; ctx.textAlign=”left”; ctx.fillText(right[i][0],rightX,y); ctx.fillStyle=”#e8f4ff”; ctx.textAlign=”right”; ctx.fillText(right[i][1],rightValX,y); ctx.textAlign=”left”; } } ctx.strokeStyle=”rgba(120,200,255,0.18)”; ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(w/2,262); ctx.lineTo(w/2,startY+left.length*rowH-6); ctx.stroke(); const teamY=startY+left.length*rowH+26; ctx.textAlign=”center”; ctx.fillStyle=”#7fffc4″; ctx.font=”bold 14px system-ui, sans-serif”; ctx.fillText(“BATTLE TEAM”,w/2,teamY); const pets=gState.team.map(function(id){return PET_BY_ID[id];}).filter(Boolean); const petStartX=w/2-((pets.length-1)*90)/2; if(pets.length===0){ ctx.fillStyle=”#a0c0e0″; ctx.font=”13px system-ui, sans-serif”; ctx.fillText(“No team assigned”,w/2,teamY+30); }else{ pets.forEach(function(p,i){ const px=petStartX+i*90; ctx.font=”34px system-ui, sans-serif”; ctx.fillText(p.icon,px,teamY+42); ctx.fillStyle=”#cfe4ff”; ctx.font=”10px system-ui, sans-serif”; const nm=p.name.length>12?p.name.slice(0,11)+”โฆ”:p.name; ctx.fillText(nm,px,teamY+60); }); } ctx.textAlign=”center”; ctx.fillStyle=”#7fffc4″; ctx.font=”bold 22px system-ui, sans-serif”; ctx.fillText(“A FEW BAD NEWBIES”,w/2,h-70); ctx.fillStyle=”#a0c0e0″; ctx.font=”15px system-ui, sans-serif”; ctx.fillText(“afbn.me ยท AFBN COSMOS”,w/2,h-44); ctx.strokeStyle=”rgba(120,200,255,0.25)”; ctx.beginPath(); ctx.moveTo(60,h-96); ctx.lineTo(w-60,h-96); ctx.stroke(); } function showPlayerCard(){ drawPlayerCard(); cardModal.classList.add(“active”); } function showProfile(){ const petCount=Object.keys(gState.pets).length; const lvl=levelOf(gState.xp); const nextXp=petXpForLevel(lvl+1); const prevXp=petXpForLevel(lvl); const curXp=Math.max(0,gState.xp-prevXp); const needXp=Math.max(1,nextXp-prevXp); const xpPct=Math.min(100,Math.round(curXp/needXp*100)); const tsec=gState.playSeconds||0; const hh=Math.floor(tsec/3600); const mm=Math.floor((tsec%3600)/60); const ss=Math.floor(tsec%60); const playTimeStr=hh+”h “+mm+”m “+ss+”s”; let storyDone=0; STORY_MISSIONS.forEach(function(s){if(gState.storyClaimed[s.id])storyDone++;}); let passDone=0; PASS_QUESTS.forEach(function(q){if(gState.pass.claimed[q.id])passDone++;}); let milestoneDone=0; PET_MILESTONES.forEach(function(m){if(gState.petMilestonesClaimed[“m”+m.count])milestoneDone++;}); const seasonEnd=(gState.pass.season+1)*SEASON_LENGTH_MS; const daysLeft=Math.max(0,Math.ceil((seasonEnd-Date.now())/86400000)); const st=gState.stats||{}; let shardTotal=0; Object.keys(gState.shards).forEach(function(k){shardTotal+=gState.shards[k];}); let h=”<div style=’width:100%;max-width:560px;text-align:left’>”; h+=”<div style=’text-align:center;margin-bottom:20px’>”; h+=”<div style=’font-size:22px;letter-spacing:3px;color:#e8f4ff;text-shadow:0 0 20px rgba(100,160,255,0.6)’>”+(gState.charName||”Anonymous Player”)+”</div>”; h+=”<div style=’font-size:12px;letter-spacing:2px;color:#7fffc4;margin-top:6px’>”+(gState.charCountry||”Global”)+”</div>”; h+=”</div>”; h+=”<h4 style=’font-size:13px;letter-spacing:2px;color:#cfe4ff;margin:16px 0 8px’>LEVEL PROGRESS</h4>”; h+=”<div class=’statrow’><span>Level</span><b>”+lvl+”</b></div>”; h+=”<div class=’pbar’><div style=’width:”+xpPct+”%’></div></div>”; h+=”<div style=’font-size:11px;text-align:center;margin:4px 0′>”+curXp+” / “+needXp+” XP to Level “+(lvl+1)+”</div>”; h+=”<h4 style=’font-size:13px;letter-spacing:2px;color:#cfe4ff;margin:16px 0 8px’>ALL-TIME ACCOUNT STATS</h4>”; h+=”<div class=’statrow’><span>Credits</span><b>”+gState.credits+”</b></div>”; h+=”<div class=’statrow’><span>Play Time</span><b>”+playTimeStr+”</b></div>”; h+=”<div class=’statrow’><span>Total Distance Walked</span><b>”+Math.round(st.distance||0)+” m</b></div>”; h+=”<div class=’statrow’><span>Total Steps (approx)</span><b>”+Math.round((st.distance||0)/0.75)+”</b></div>”; h+=”<div class=’statrow’><span>Daily Step Goal</span><b>”+(gState.stepGoal||STEP_GOAL_DEFAULT)+” steps</b></div>”; h+=”<div class=’statrow’><span>Step Streak</span><b>”+(gState.stepStreak||0)+” day”+(gState.stepStreak===1?””:”s”)+”</b></div>”; h+=”<div class=’statrow’><span>Distance Today</span><b>”+Math.round(gState.walkDaily.progress||0)+” m</b></div>”; h+=”<div class=’statrow’><span>Steps Today (approx)</span><b>”+Math.round((gState.walkDaily.progress||0)/0.75)+”</b></div>”; h+=”<div class=’statrow’><span>Login Streak</span><b>”+gState.streak+” day”+(gState.streak===1?””:”s”)+”</b></div>”; h+=”<div class=’statrow’><span>Walk Reward Credits</span><b>”+(st.walkCredits||0)+”</b></div>”; h+=”<div class=’statrow’><span>Shards Held</span><b>”+shardTotal+”</b></div>”; h+=”<div class=’statrow’><span>Blocks Owned</span><b>”+ownedKeys().length+”</b></div>”; h+=”<div class=’statrow’><span>Blocks Claimed Total</span><b>”+(st.claimed||0)+”</b></div>”; h+=”<div class=’statrow’><span>Blocks Captured Total</span><b>”+(st.captured||0)+”</b></div>”; h+=”<div class=’statrow’><span>Blocks Lost to Rivals</span><b>”+(st.lost||0)+”</b></div>”; h+=”<div class=’statrow’><span>Blocks Reinforced</span><b>”+(st.reinforced||0)+”</b></div>”; h+=”<div class=’statrow’><span>NPCs Hired</span><b>”+gState.hired.length+” / “+NPCS.length+”</b></div>”; h+=”<h4 style=’font-size:13px;letter-spacing:2px;color:#cfe4ff;margin:16px 0 8px’>MARKET STATS</h4>”; h+=”<div class=’statrow’><span>Items Purchased</span><b>”+(st.purchases||0)+”</b></div>”; h+=”<div class=’statrow’><span>Depot Spins</span><b>”+(st.spins||0)+”</b></div>”; h+=”<div class=’statrow’><span>Income Collections</span><b>”+(st.collected||0)+”</b></div>”; h+=”<div class=’statrow’><span>Signal Catches</span><b>”+(st.caught||0)+”</b></div>”; h+=”<div class=’statrow’><span>Pet Trainings</span><b>”+(st.trained||0)+”</b></div>”; h+=”<div class=’statrow’><span>Relics Broken</span><b>”+(st.relicsBroken||0)+”</b></div>”; h+=”<h4 style=’font-size:13px;letter-spacing:2px;color:#cfe4ff;margin:16px 0 8px’>PET COLLECTION</h4>”; h+=”<div class=’statrow’><span>Pets Collected</span><b>”+petCount+” / “+PETS.length+”</b></div>”; h+=”<div class=’statrow’><span>Anomalies Found</span><b>”+(st.anomalies||0)+”</b></div>”; h+=”<div class=’statrow’><span>Eggs Hatched</span><b>”+(st.hatched||0)+”</b></div>”; h+=”<div class=’statrow’><span>Breedings Done</span><b>”+(st.breedsDone||0)+”</b></div>”; h+=”<div class=’statrow’><span>Pet Fusions</span><b>”+(st.merged||0)+”</b></div>”; h+=”<div class=’statrow’><span>Pet Milestones</span><b>”+milestoneDone+” / “+PET_MILESTONES.length+”</b></div>”; h+=”<h4 style=’font-size:13px;letter-spacing:2px;color:#cfe4ff;margin:16px 0 8px’>BATTLE RECORD</h4>”; h+=”<div class=’statrow’><span>Street Battles Won</span><b>”+gState.battles.wins+”</b></div>”; h+=”<div class=’statrow’><span>Street Battles Lost</span><b>”+gState.battles.losses+”</b></div>”; h+=”<div class=’statrow’><span>Arena Wins</span><b>”+(st.arenaWins||0)+”</b></div>”; h+=”<div class=’statrow’><span>Duel Wins</span><b>”+(st.duelsWon||0)+”</b></div>”; h+=”<h4 style=’font-size:13px;letter-spacing:2px;color:#cfe4ff;margin:16px 0 8px’>STORY MISSIONS (“+storyDone+”/”+STORY_MISSIONS.length+”)</h4>”; STORY_MISSIONS.forEach(function(s){ const claimed=!!gState.storyClaimed[s.id]; const cur=storyProgressOf(s); h+=”<div class=’statrow’ style=’opacity:”+(claimed?’1′:’0.6′)+”‘><span>”+(claimed?’โ ’:’โฌ’)+” “+s.title+”</span><b>”+(claimed?’COMPLETE’:(Math.min(cur,s.target)+”/”+s.target))+”</b></div>”; }); h+=”<h4 style=’font-size:13px;letter-spacing:2px;color:#cfe4ff;margin:16px 0 8px’>SEASON PASS (“+passDone+”/”+PASS_QUESTS.length+”)</h4>”; h+=”<div class=’statrow’><span>Current Season</span><b>”+gState.pass.season+”</b></div>”; h+=”<div class=’statrow’><span>Days Remaining</span><b>”+daysLeft+”</b></div>”; h+=”<h4 style=’font-size:13px;letter-spacing:2px;color:#cfe4ff;margin:16px 0 8px’>PET MILESTONES</h4>”; PET_MILESTONES.forEach(function(m){ const done=!!gState.petMilestonesClaimed[“m”+m.count]; h+=”<div class=’statrow’ style=’opacity:”+(done?’1′:’0.5′)+”‘><span>”+(done?’โ ’:’โฌ’)+” “+m.count+” pets</span><b>”+(done?’CLAIMED’:’LOCKED’)+”</b></div>”; }); h+=”</div>”; openModalInfo(h,”PROFILE”); } profileBtn.addEventListener(“click”,function(e){ e.preventDefault(); e.stopPropagation(); showProfile(); }); function checkPassengerWarning(){ if(!inGame)return; if(currentSpeed<6)return; const now=Date.now(); if(now<passengerConfirmedUntil)return; if(now-lastPassengerPromptAt<180000)return; if(passengerModal.classList.contains(“active”))return; if(gamePanel.classList.contains(“active”))return; if(busyPanelOnly())return; lastPassengerPromptAt=now; passengerModal.classList.add(“active”); } function busyPanelOnly(){ return outOrbsModal.classList.contains(“active”)||unlockModal.classList.contains(“active”)||introModal.classList.contains(“active”)||charModal.classList.contains(“active”)||weatherModal.classList.contains(“active”)||settingsModal.classList.contains(“active”); } function checkWeather(){ if(!inGame||!hasFix)return; const now=Date.now(); if(now-(checkWeather.last||0)<900000)return; checkWeather.last=now; const url=”https://api.open-meteo.com/v1/forecast?latitude=”+lastGeo.lat.toFixed(4)+”&longitude=”+lastGeo.lon.toFixed(4)+”ยคt=temperature_2m,precipitation,weather_code,wind_speed_10m”; fetch(url).then(function(r){return r.json();}).then(function(data){ if(!data||!data.current)return; const c=data.current; const temp=c.temperature_2m; const precip=c.precipitation; const wind=c.wind_speed_10m; const code=c.weather_code; const alerts=[]; if(typeof precip===”number”&&precip>0.5)alerts.push(“Rain or precipitation is occurring. Slippery surfaces and reduced visibility are possible.”); if(typeof wind===”number”&&wind>35)alerts.push(“Strong winds detected (“+Math.round(wind)+” km/h). Be careful of debris and unstable footing.”); if(typeof temp===”number”&&temp>38)alerts.push(“Extreme heat (“+Math.round(temp)+”ยฐC). Stay hydrated and avoid prolonged sun exposure.”); if(typeof temp===”number”&&temp<-5)alerts.push(“Extreme cold (“+Math.round(temp)+”ยฐC). Wear warm layers and watch for ice.”); if(typeof code===”number”&&(code===95||code===96||code===99))alerts.push(“Thunderstorm activity detected. Seek shelter and avoid open areas.”); if(!alerts.length)return; const message=alerts.join(” “); const sig=message.substring(0,40); if(sig===lastWeatherAlert)return; lastWeatherAlert=sig; weatherMsg.innerHTML=message+”<br><br>Play safely and only in conditions that are safe for walking.”; weatherModal.classList.add(“active”); }).catch(function(){}); } function checkTrespass(){ if(!inGame)return; const now=Date.now(); if(now-lastTrespassAt<900000)return; lastTrespassAt=now; showToast(“Friendly Reminder”,”Please do not trespass. Only walk in public areas you are allowed to be in. Respect private property and local laws.”); } godseyeMe.addEventListener(“click”,function(){ if(!godseyeMap)return; if(navigator.geolocation){ navigator.geolocation.getCurrentPosition(function(pos){ godseyeMap.setView([pos.coords.latitude,pos.coords.longitude],13,{animate:true}); showToast(“God’s Eye”,”Centered on your approved location.”); },function(){ godseyeMap.setView([userLat,userLon],13,{animate:true}); }); }else{ godseyeMap.setView([userLat,userLon],13,{animate:true}); } }); godseyeGo.addEventListener(“click”,godsEyeSearchGo); godseyeSearch.addEventListener(“keydown”,function(e){if(e.key===”Enter”){e.preventDefault();godsEyeSearchGo();}}); godseyeZoomIn.addEventListener(“click”,enterStreetFromGodsEye); godseyeZoomOut.addEventListener(“click”,closeGodsEye); cosmosPrevBtn.addEventListener(“click”,function(){ goTo(snapTargetIdx – 1); }); cosmosNextBtn.addEventListener(“click”,function(){ goTo(snapTargetIdx + 1); }); cosmosZoomInBtn.addEventListener(“click”,function(){ const cur = snapTargetIdx; const d=ecoData.find(function(f){return f.idx===cur;}); if(d&&d.level===”Earth”&&!godseyeOpen&&!inGame){ let tgt = getEarthCenterCoords(); if(!tgt) tgt = godseyeTarget || {lat:userLat,lon:userLon}; openGodsEye(tgt.lat,tgt.lon); } else { zoomInto(cur); } }); cosmosMusicBtn.addEventListener(“click”,function(){ currentMusicIdx=(currentMusicIdx+1)%musicModes.length; setMusicMode(musicModes[currentMusicIdx]); }); function parseLatLon(text){ const m=String(text).match(/^\s*(-?\d+(?:\.\d+)?)\s*[, ]\s*(-?\d+(?:\.\d+)?)\s*$/); if(!m)return null; const a=parseFloat(m[1]); const b=parseFloat(m[2]); if(Math.abs(a)>90||Math.abs(b)>180)return null; return{lat:a,lon:b}; } function haversine(lat1,lon1,lat2,lon2){ const R=6371000; const toRad=Math.PI/180; const dLat=(lat2-lat1)*toRad; const dLon=(lon2-lon1)*toRad; const a=Math.sin(dLat/2)*Math.sin(dLat/2)+Math.cos(lat1*toRad)*Math.cos(lat2*toRad)*Math.sin(dLon/2)*Math.sin(dLon/2); return R*2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)); } const CELL=0.001; const RANGE=60; const FAR_RANGE=25000; const FACTIONS=[ {id:”wolves”,name:”Rust Wolves”,color:”#ff7a3d”}, {id:”foxes”,name:”Neon Foxes”,color:”#ff4fd8″}, {id:”owls”,name:”Void Owls”,color:”#9d6bff”}, {id:”hawks”,name:”Iron Hawks”,color:”#4fc3ff”} ]; const DEPOT_NAMES=[“Alpha Depot”,”Beacon Depot”,”Cobalt Depot”,”Delta Depot”,”Ember Depot”,”Flux Depot”,”Garnet Depot”,”Helix Depot”]; const RARITY={ common:{label:”COMMON”,color:”#9fb4c8″,band:0.4,flee:0.12,cr:8,xp:10,shards:20,power:6}, uncommon:{label:”UNCOMMON”,color:”#6dff9d”,band:0.3,flee:0.2,cr:15,xp:20,shards:35,power:14}, rare:{label:”RARE”,color:”#5aa8ff”,band:0.22,flee:0.3,cr:30,xp:40,shards:60,power:30}, epic:{label:”EPIC”,color:”#c07bff”,band:0.15,flee:0.38,cr:60,xp:80,shards:100,power:62}, legendary:{label:”LEGENDARY”,color:”#ffc83d”,band:0.1,flee:0.45,cr:150,xp:200,shards:160,power:130} }; const RARITY_ORDER=[“common”,”uncommon”,”rare”,”epic”,”legendary”]; const RARITY_INDEX={common:0,uncommon:1,rare:2,epic:3,legendary:4}; const ELEMENTS=[ {k:”fire”,icon:”๐ฅ”,color:”#ff6b3d”}, {k:”water”,icon:”๐ง”,color:”#3da5ff”}, {k:”earth”,icon:”๐ฟ”,color:”#5ddf7a”}, {k:”air”,icon:”๐ช๏ธ”,color:”#c9e8ff”}, {k:”electric”,icon:”โก”,color:”#ffd93d”}, {k:”shadow”,icon:”๐”,color:”#9d6bff”}, {k:”light”,icon:”โจ”,color:”#fff2a8″}, {k:”ice”,icon:”โ๏ธ”,color:”#a8e8ff”}, {k:”metal”,icon:”โ๏ธ”,color:”#c0c8d4″}, {k:”cosmic”,icon:”๐”,color:”#b07bff”} ]; const CREATURE_ICONS=[“๐”,”๐ฒ”,”๐ฆ”,”๐ข”,”๐”,”๐ฆ”,”๐ท๏ธ”,”๐ฆ”,”๐”,”๐ฆ ”,”๐ฆ”,”๐ฆ”,”๐บ”,”๐ฆ”,”๐ฑ”,”๐ฏ”,”๐ฆ”,”๐ป”,”๐ผ”,”๐จ”,”๐ต”,”๐ฆ”,”๐”,”๐ฆ”,”๐ฎ”,”๐ท”,”๐”,”๐”,”๐ฆ”,”๐”,”๐ฟ๏ธ”,”๐ฆ”,”๐ญ”,”๐น”,”๐ฆก”,”๐ฆฆ”,”๐ฆฅ”,”๐”,”๐ฆ”,”๐ฆ”,”๐”,”๐ฆ”,”๐ฌ”,”๐ณ”,”๐”,”๐ฆ”,”๐ฆ”,”๐ฆ”,”๐ก”,”๐ ”,”๐ฆฉ”,”๐ฆ”,”๐ฆข”,”๐ฆ”,”๐ง”,”๐ฆ”,”๐ฆ”,”๐”,”๐ฆ”,”๐ฆ”]; const PET_PREFIX=[“Neon”,”Void”,”Solar”,”Lunar”,”Ember”,”Frost”,”Storm”,”Quantum”,”Cyber”,”Prism”,”Shadow”,”Crystal”,”Thunder”,”Venom”,”Astral”,”Nebula”,”Plasma”,”Iron”,”Silk”,”Magma”,”Radiant”,”Obsidian”,”Glacier”,”Tempest”,”Aurora”]; const PET_CORE=[“Drake”,”Wolf”,”Fox”,”Hawk”,”Tiger”,”Serpent”,”Golem”,”Wisp”,”Sprite”,”Phoenix”,”Wyrm”,”Kraken”,”Griffin”,”Panther”,”Raven”,”Lynx”,”Mantis”,”Sphinx”,”Hydra”,”Behemoth”]; const SPECIES=[ {id:”moth”,name:”Glimmer Moth”,icon:”๐ฆ”,r:”common”}, {id:”beetle”,name:”Byte Beetle”,icon:”๐ชฒ”,r:”common”}, {id:”frog”,name:”Pulse Frog”,icon:”๐ธ”,r:”common”}, {id:”bat”,name:”Echo Bat”,icon:”๐ฆ”,r:”common”}, {id:”snail”,name:”Circuit Snail”,icon:”๐”,r:”common”}, {id:”gecko”,name:”Neon Gecko”,icon:”๐ฆ”,r:”uncommon”}, {id:”hare”,name:”Static Hare”,icon:”๐”,r:”uncommon”}, {id:”crab”,name:”Prism Crab”,icon:”๐ฆ”,r:”uncommon”}, {id:”stag”,name:”Aurora Stag”,icon:”๐ฆ”,r:”rare”}, {id:”fox”,name:”Quantum Fox”,icon:”๐ฆ”,r:”rare”}, {id:”drake”,name:”Ember Drake”,icon:”๐”,r:”rare”}, {id:”whale”,name:”Cosmic Whale”,icon:”๐”,r:”epic”}, {id:”peacock”,name:”Nova Peacock”,icon:”๐ฆ”,r:”epic”}, {id:”dragon”,name:”Singularity Dragon”,icon:”๐ฒ”,r:”legendary”} ]; function hash3(a,b,c){ let h=(Math.imul(a|0,374761393)+Math.imul(b|0,668265263)+Math.imul(c|0,1442695041))|0; h=Math.imul(h^(h>>>13),1274126177); h=h^(h>>>16); h=Math.imul(h,2246822519); h=h^(h>>>13); return h>>>0; } function rnd01(a,b,c){return hash3(a,b,c)/4294967296;} function petRarity(i){ const h=hash3(i,77,99)%1000; if(h<520)return “common”; if(h<780)return “uncommon”; if(h<900)return “rare”; if(h<975)return “epic”; return “legendary”; } const PETS=[]; (function(){ let n=0; for(let a=0;a<PET_PREFIX.length;a++){ for(let b=0;b<PET_CORE.length;b++){ if(n>=500)break; const r=petRarity(n); const e=ELEMENTS[hash3(n,311,17)%ELEMENTS.length]; const icon=CREATURE_ICONS[hash3(n,533,29)%CREATURE_ICONS.length]; PETS.push({id:”g”+n,name:PET_PREFIX[a]+” “+PET_CORE[b],icon:icon,r:r,power:RARITY[r].power,el:e.k,elIcon:e.icon,elColor:e.color,cat:e.k}); n++; } if(n>=500)break; } })(); for(let i=0;i<SPECIES.length;i++){ const s=SPECIES[i]; const e=ELEMENTS[hash3(i,911,31)%ELEMENTS.length]; PETS.push({id:s.id,name:s.name,icon:s.icon,r:s.r,power:RARITY[s.r].power,el:e.k,elIcon:e.icon,elColor:e.color,cat:e.k}); } const PET_BY_ID={}; const PET_POOLS={common:[],uncommon:[],rare:[],epic:[],legendary:[]}; PETS.forEach(function(p){ PET_BY_ID[p.id]=p; PET_POOLS[p.r].push(p); }); function getPokeType(p){ if(!p)return “normal”; const mapped=ELEMENT_TO_TYPE[p.el]; return mapped||”normal”; } function getPokeMovesForPet(p,level){ const t=getPokeType(p); const pool=POKE_MOVES[t]||POKE_MOVES.normal; const normalPool=POKE_MOVES.normal; const moves=[]; const primary=pool.slice(0,Math.min(3,pool.length)); primary.forEach(function(mv){moves.push(cloneMove(mv));}); if(moves.length<4){ const secondary=normalPool[Math.floor(Math.random()*normalPool.length)]; moves.push(cloneMove(secondary)); } while(moves.length<4){ const extra=normalPool[Math.floor(Math.random()*normalPool.length)]; const exists=moves.some(function(m){return m.name===extra.name;}); if(!exists)moves.push(cloneMove(extra)); if(moves.length>=4)break; } return moves; } function cloneMove(mv){ return{name:mv.name,power:mv.power,acc:mv.acc,pp:mv.pp,maxPp:mv.pp,type:mv.type,cat:mv.cat||”phys”,status:mv.status||null,statusChance:mv.statusChance||0,statDrop:mv.statDrop||null,statChance:mv.statChance||0,selfBoost:mv.selfBoost||null,selfDrop:mv.selfDrop||null,priority:mv.priority||0,flinch:mv.flinch||false,critBoost:mv.critBoost||false,leech:mv.leech||false,recharge:mv.recharge||false,heal:mv.heal||false}; } function getTypeEffectiveness(moveType,defenderType){ const chart=POKE_TYPE_CHART[moveType]; if(!chart)return 1; if(chart[defenderType]!==undefined)return chart[defenderType]; return 1; } function shardsNeeded(r){ const base=RARITY[r].shards; if(gState.hired.indexOf(“medic”)!==-1&&(r===”common”||r===”uncommon”||r===”rare”))return Math.max(1,base-1); return base; } function petLevel(xp){return Math.min(50,Math.floor(Math.sqrt(xp/25))+1);} function petXpForLevel(lv){return Math.round(Math.pow(lv-1,2.5)*45);} function petPowerOf(p){ const o=gState.pets[p.id]; const lv=o?o.lv:1; let base=Math.round(p.power*(1+(lv-1)*0.42)); if(o&&o.anomaly){ base=Math.round(base*1.25); if(o.anomalyPerk===”power”)base=Math.round(base*1.15); } return base; } function npcPowerTotal(){ let s=0; for(let i=0;i<gState.hired.length;i++){ const n=NPC_BY_ID[gState.hired[i]]; if(n)s+=n.power; } return s; } function teamPowerTotal(){ let s=npcPowerTotal(); for(let i=0;i<gState.team.length;i++){ const p=PET_BY_ID[gState.team[i]]; if(p)s+=petPowerOf(p); } return s; } const NPCS=[ {id:”scout”,name:”Scout Vega”,icon:”๐ฐ๏ธ”,cost:500,desc:”Extends wild signal range and boosts spawn rate.”,power:5}, {id:”guard”,name:”Guard Rook”,icon:”๐ก๏ธ”,cost:800,desc:”Defends your blocks against rival attacks.”,power:12}, {id:”trainer”,name:”Trainer Juno”,icon:”๐”,cost:1200,desc:”Pets gain 50 percent more experience from training.”,power:8}, {id:”broker”,name:”Broker Nyx”,icon:”๐ฑ”,cost:1500,desc:”Adds 20 percent to all credit rewards.”,power:6}, {id:”medic”,name:”Medic Iri”,icon:”๐”,cost:1000,desc:”Reduces shard requirement for rare and below by 1.”,power:4}, {id:”buster”,name:”Buster Kane”,icon:”๐ฅ”,cost:2200,desc:”Adds 40 battle power against rival claims.”,power:40}, {id:”hacker”,name:”Hacker Zed”,icon:”๐ป”,cost:1800,desc:”Depots recharge twice as fast.”,power:10}, {id:”monk”,name:”Monk Sol”,icon:”๐ง”,cost:2600,desc:”Newly caught pets arrive one level higher.”,power:15} ]; const NPC_BY_ID={}; NPCS.forEach(function(n){NPC_BY_ID[n.id]=n;}); const SHOP=[ {id:”orb”,name:”Echo Weave x5″,desc:”A harmonic lattice for wild signal capture.”,cost:25,icon:”ใฐ๏ธ”,kind:”orb”,amount:5}, {id:”orb_enhanced”,name:”Prism Weave x5″,desc:”Wider pulse window and 3-shard extraction.”,cost:600,icon:”๐น”,kind:”orbx”,orbType:”orb_enhanced”,amount:5}, {id:”orb_precision”,name:”Arc Weave x5″,desc:”Minimum 2 shards per catch. Wide pulse window.”,cost:1600,icon:”โก”,kind:”orbx”,orbType:”orb_precision”,amount:5}, {id:”orb_master”,name:”Crown Weave x5″,desc:”Guarantees 3 or more shards per successful catch.”,cost:4500,icon:”๐ถ”,kind:”orbx”,orbType:”orb_master”,amount:5}, {id:”orb_legendary”,name:”Nova Weave x3″,desc:”Guarantees 5 shards. Massive pulse window.”,cost:7000,icon:”๐”,kind:”orbx”,orbType:”orb_legendary”,amount:3}, {id:”orb_anomaly”,name:”Void Weave x3″,desc:”High shard roll. Small chance to force an Anomaly pet.”,cost:14000,icon:”๐”,kind:”orbx”,orbType:”orb_anomaly”,amount:3}, {id:”shield”,name:”Turf Shield”,desc:”Stops rival attacks for 2 hours once activated.”,cost:700,icon:”๐ก๏ธ”,kind:”shield”,amount:1}, {id:”boost”,name:”Claim Boost”,desc:”+50% credits from claims, spins and income for 1 hour.”,cost:600,icon:”โก”,kind:”boost”,amount:1}, {id:”radar”,name:”Radar Pack”,desc:”Doubles wild signal range for 30 minutes.”,cost:450,icon:”๐ก”,kind:”radar”,amount:1}, {id:”armor”,name:”Block Armor”,desc:”Adds 1 level to all of your blocks, up to level 5.”,cost:1500,icon:”๐งฑ”,kind:”armor”,amount:1}, {id:”neon”,name:”Neon Marker”,desc:”Unlocks a glowing cyan player marker.”,cost:800,icon:”โจ”,kind:”neon”,amount:1}, {id:”shardpack”,name:”Shard Crate”,desc:”Adds 3 shards to your currently targeted signal species.”,cost:1200,icon:”๐ ”,kind:”shard”,amount:3}, {id:”trainpack”,name:”Training Kit”,desc:”Adds 120 experience to a random pet.”,cost:1100,icon:”๐งช”,kind:”train”,amount:120}, {id:”petroll”,name:”Pet Capsule”,desc:”Unlocks a random common or uncommon pet.”,cost:2000,icon:”๐”,kind:”pet”,amount:1}, {id:”xpburst”,name:”XP Burst”,desc:”Instantly grants 180 trainer experience.”,cost:1300,icon:”๐ท”,kind:”xp”,amount:180}, {id:”everstone”,name:”Everstone”,desc:”Prevents breeding stat loss for one egg.”,cost:900,icon:”๐ชจ”,kind:”breeditem”,amount:1}, {id:”ovalcharm”,name:”Oval Charm”,desc:”Doubles egg production speed for 24 hours.”,cost:3000,icon:”๐ฅ”,kind:”breeditem”,amount:1}, {id:”shinycharm”,name:”Shiny Charm”,desc:”Increases shiny chance on hatched eggs by 5% for 24 hours.”,cost:5000,icon:”โจ”,kind:”breeditem”,amount:1} ]; const SHOP_EXTRA=[]; (function(){ const A=[“Neon”,”Void”,”Solar”,”Lunar”,”Ember”,”Frost”,”Storm”,”Quantum”,”Cyber”,”Prism”]; const B=[“Orb”,”Core”,”Chip”,”Cell”,”Node”,”Rune”,”Sigil”,”Shard”,”Coil”,”Drive”]; const C=[“Pack”,”Kit”,”Bundle”,”Cache”,”Vault”,”Set”,”Case”,”Pod”,”Rig”,”Unit”]; const I=[“๐ฎ”,”๐ก๏ธ”,”โก”,”๐ก”,”๐งฑ”,”โจ”,”๐ ”,”๐ท”,”๐ถ”,”๐ฃ”,”๐ข”,”๐ด”,”๐งฟ”,”๐ด”,”๐”,”๐”,”๐ช”,”๐งช”,”โ๏ธ”,”๐”]; const K=[“orb”,”shield”,”boost”,”radar”,”armor”,”shard”,”train”,”credits”,”xp”,”pet”]; let n=0; for(let a=0;a<A.length;a++){ for(let b=0;b<B.length;b++){ for(let c=0;c<C.length;c++){ if(n>=300)break; const tier=1+(n%10); const kind=K[n%10]; const amount=1+(n%5); let base=Math.round((20+tier*18+(n%7)*11)*(1+(n%3)*0.35)); let cost=kind===”orb”?base:base*SHOP_COST_MULTIPLIER; SHOP_EXTRA.push({id:”x”+n,name:A[a]+” “+B[b]+” “+C[c],icon:I[n%I.length],desc:”Tier “+tier+” “+kind+” supply.”,cost:cost,kind:kind,amount:amount}); n++; } if(n>=300)break; } if(n>=300)break; } })(); const ALL_SHOP=SHOP.concat(SHOP_EXTRA); const MISSIONS={ hourly:[], daily:[], once:[] }; (function(){ const hBase=[ {id:”h_claim1″,name:”Fresh Ground”,desc:”Claim or capture 1 block.”,stat:”claimed”,target:1,reward:10,xp:8,shardReward:1}, {id:”h_walk300″,name:”Quick Stroll”,desc:”Walk 300 meters.”,stat:”distance”,target:300,reward:15,xp:10,shardReward:1}, {id:”h_spin1″,name:”Supply Run”,desc:”Spin 1 depot.”,stat:”spins”,target:1,reward:10,xp:8,shardReward:1}, {id:”h_catch1″,name:”Signal Hunter”,desc:”Harvest 1 signal shard.”,stat:”caught”,target:1,reward:12,xp:10,shardReward:1} ]; const dBase=[ {id:”d_claim5″,name:”Block Builder”,desc:”Claim or capture 5 blocks.”,stat:”claimed”,target:5,reward:40,xp:30,shardReward:2}, {id:”d_walk2k”,name:”Daily Miles”,desc:”Walk 2000 meters.”,stat:”distance”,target:2000,reward:60,xp:40,shardReward:2}, {id:”d_spin3″,name:”Depot Circuit”,desc:”Spin 3 depots.”,stat:”spins”,target:3,reward:30,xp:25,shardReward:2}, {id:”d_catch3″,name:”Collector”,desc:”Harvest 3 signal shards.”,stat:”caught”,target:3,reward:45,xp:35,shardReward:2}, {id:”d_capture2″,name:”Turf War”,desc:”Capture 2 rival blocks.”,stat:”captured”,target:2,reward:50,xp:40,shardReward:2}, {id:”d_battle1″,name:”Street Brawl”,desc:”Win 1 Street Claim battle.”,stat:”wins”,target:1,reward:55,xp:45,shardReward:2} ]; const oBase=[ {id:”o_claim1″,name:”First Steps”,desc:”Claim your first block.”,stat:”claimed”,target:1,reward:25,xp:20,shardReward:2}, {id:”o_claim10″,name:”Neighborhood”,desc:”Claim or capture 10 blocks.”,stat:”claimed”,target:10,reward:100,xp:80,shardReward:3}, {id:”o_claim50″,name:”District Lord”,desc:”Claim or capture 50 blocks.”,stat:”claimed”,target:50,reward:400,xp:300,shardReward:5}, {id:”o_capture5″,name:”Rival Breaker”,desc:”Capture 5 rival blocks.”,stat:”captured”,target:5,reward:150,xp:100,shardReward:4}, {id:”o_walk1k”,name:”First Kilometer”,desc:”Walk 1000 meters in total.”,stat:”distance”,target:1000,reward:50,xp:40,shardReward:2}, {id:”o_walk10k”,name:”Ten Kilometers”,desc:”Walk 10000 meters in total.”,stat:”distance”,target:10000,reward:300,xp:200,shardReward:5}, {id:”o_spin10″,name:”Depot Regular”,desc:”Spin 10 depots.”,stat:”spins”,target:10,reward:80,xp:60,shardReward:3}, {id:”o_catch5″,name:”Rookie Catcher”,desc:”Harvest 5 signal shards.”,stat:”caught”,target:5,reward:120,xp:90,shardReward:4}, {id:”o_catch25″,name:”Signal Master”,desc:”Harvest 25 signal shards.”,stat:”caught”,target:25,reward:500,xp:350,shardReward:8}, {id:”o_buy1″,name:”First Purchase”,desc:”Buy 1 item from the shop.”,stat:”purchases”,target:1,reward:30,xp:20,shardReward:1}, {id:”o_income1″,name:”Landlord”,desc:”Collect block income 1 time.”,stat:”collected”,target:1,reward:20,xp:15,shardReward:1}, {id:”o_reinforce5″,name:”Fortifier”,desc:”Reinforce 5 blocks.”,stat:”reinforced”,target:5,reward:75,xp:60,shardReward:3}, {id:”o_pet1″,name:”First Companion”,desc:”Catch your first pet.”,stat:”pets”,target:1,reward:60,xp:50,shardReward:4}, {id:”o_pet10″,name:”Keeper”,desc:”Catch 10 pets.”,stat:”pets”,target:10,reward:220,xp:160,shardReward:6}, {id:”o_pet50″,name:”Menagerie”,desc:”Catch 50 pets.”,stat:”pets”,target:50,reward:900,xp:600,shardReward:12}, {id:”o_merge1″,name:”First Fusion”,desc:”Merge 3 pets into one.”,stat:”merged”,target:1,reward:90,xp:70,shardReward:4}, {id:”o_merge10″,name:”Fusion Adept”,desc:”Merge pets 10 times.”,stat:”merged”,target:10,reward:420,xp:300,shardReward:8}, {id:”o_hire1″,name:”First Contract”,desc:”Hire your first NPC.”,stat:”hired”,target:1,reward:70,xp:55,shardReward:3}, {id:”o_hire4″,name:”Crew Leader”,desc:”Hire 4 NPCs.”,stat:”hired”,target:4,reward:340,xp:250,shardReward:6}, {id:”o_win5″,name:”Turf Champion”,desc:”Win 5 Street Claim battles.”,stat:”wins”,target:5,reward:260,xp:190,shardReward:5}, {id:”o_train10″,name:”Pet Trainer”,desc:”Train pets 10 times.”,stat:”trained”,target:10,reward:180,xp:130,shardReward:4} ]; for(let i=0;i<100;i++){ hBase.forEach(function(m){ MISSIONS.hourly.push({id:m.id+”_v”+i,name:m.name+(i>0?” “+(i+1):””),desc:m.desc,stat:m.stat,target:m.target*(i+1),reward:Math.round(m.reward*(1+i*0.3)),xp:Math.round(m.xp*(1+i*0.25)),shardReward:m.shardReward}); }); dBase.forEach(function(m){ MISSIONS.daily.push({id:m.id+”_v”+i,name:m.name+(i>0?” “+(i+1):””),desc:m.desc,stat:m.stat,target:m.target*(i+1),reward:Math.round(m.reward*(1+i*0.35)),xp:Math.round(m.xp*(1+i*0.3)),shardReward:m.shardReward}); }); oBase.forEach(function(m){ MISSIONS.once.push({id:m.id+”_v”+i,name:m.name+(i>0?” “+(i+1):””),desc:m.desc,stat:m.stat,target:m.target*(i+1),reward:Math.round(m.reward*(1+i*0.4)),xp:Math.round(m.xp*(1+i*0.35)),shardReward:m.shardReward}); }); } })(); const WALK_GOALS=[ {id:”w_500″,name:”First Steps Today”,desc:”Walk 500 meters today.”,target:500,reward:25,xp:15}, {id:”w_1500″,name:”Neighborhood Walk”,desc:”Walk 1500 meters today.”,target:1500,reward:50,xp:30}, {id:”w_3000″,name:”City Stroller”,desc:”Walk 3000 meters today.”,target:3000,reward:90,xp:55}, {id:”w_5000″,name:”Daily Explorer”,desc:”Walk 5000 meters today.”,target:5000,reward:150,xp:90}, {id:”w_8000″,name:”Long Haul”,desc:”Walk 8000 meters today.”,target:8000,reward:220,xp:130}, {id:”w_12000″,name:”Marathon Day”,desc:”Walk 12000 meters today.”,target:12000,reward:350,xp:200} ]; const PASS_TEMPLATES=[ {stat:”claimed”,name:”Block Claimer”,desc:”Claim or capture blocks.”,targets:[1,2,4,8,15,25,40,60,90,150]}, {stat:”captured”,name:”Rival Breaker”,desc:”Capture rival blocks.”,targets:[1,2,4,8,15,25,40,60,90,150]}, {stat:”distance”,name:”Walker”,desc:”Walk real meters outside.”,targets:[100,500,1000,2500,5000,10000,20000,40000,80000,150000]}, {stat:”spins”,name:”Depot Spinner”,desc:”Spin supply depots.”,targets:[1,3,6,12,20,35,55,80,120,200]}, {stat:”caught”,name:”Signal Hunter”,desc:”Harvest shards from wild signals.”,targets:[1,3,8,15,30,60,100,150,250,500]}, {stat:”wins”,name:”Battle Victor”,desc:”Win Street Claim battles.”,targets:[1,2,4,8,15,25,40,60,90,150]}, {stat:”purchases”,name:”Shop Patron”,desc:”Buy items from the shop.”,targets:[1,2,5,10,20,35,55,80,120,200]}, {stat:”collected”,name:”Landlord”,desc:”Collect block income.”,targets:[1,2,5,10,20,35,55,80,120,200]}, {stat:”merged”,name:”Fusion Master”,desc:”Merge pets.”,targets:[1,2,4,8,15,25,40,60,90,150]}, {stat:”hired”,name:”Crew Boss”,desc:”Hire NPC crew members.”,targets:[1,2,3,4,5,6,7,8,9,10]}, {stat:”arenaWins”,name:”Arena Challenger”,desc:”Win Arena battles.”,targets:[1,2,4,8,15,25,40,60,90,150]}, {stat:”duelsWon”,name:”Duel Victor”,desc:”Win pet duels.”,targets:[1,2,4,8,15,25,40,60,90,150]}, {stat:”hatched”,name:”Breeder”,desc:”Hatch eggs.”,targets:[1,2,4,8,15,25,40,60,90,150]} ]; const PASS_QUESTS=[]; (function(){ const roman=[“I”,”II”,”III”,”IV”,”V”,”VI”,”VII”,”VIII”,”IX”,”X”]; PASS_TEMPLATES.forEach(function(t,ti){ t.targets.forEach(function(tg,gi){ const credits=50+gi*45+ti*15; const xp=25+gi*20+ti*10; let item=null; if((gi+1)%5===0)item={type:”orb”,amount:5}; if(gi===9)item={type:”pet”,amount:1}; PASS_QUESTS.push({id:”p_”+ti+”_”+gi,name:t.name+” “+roman[gi],desc:t.desc+” Reach “+tg+”.”,stat:t.stat,target:tg,credits:credits,xp:xp,item:item}); }); }); })(); function currentSeasonId(){return Math.floor(Date.now()/SEASON_LENGTH_MS);} function ensureSeason(){ const s=currentSeasonId(); if(!gState.pass||typeof gState.pass!==”object”)gState.pass={season:s,claimed:{}}; if(gState.pass.season!==s){ gState.pass.season=s; gState.pass.claimed={}; logEvent(“New season pass started. All pass quests have reset.”); } } function passProgress(q){ if(q.stat===”pets”)return Object.keys(gState.pets).length; if(q.stat===”arenaWins”)return gState.stats.arenaWins||0; if(q.stat===”duelsWon”)return gState.stats.duelsWon||0; if(q.stat===”hatched”)return gState.stats.hatched||0; return gState.stats[q.stat]||0; } function cellOf(lat,lon){return{i:Math.floor(lat/CELL),j:Math.floor(lon/CELL)};} function factionById(id){ for(let n=0;n<FACTIONS.length;n++){if(FACTIONS[n].id===id)return FACTIONS[n];} return FACTIONS[0]; } function getBlock(i,j){ const k=i+”:”+j; if(gState.blocks[k])return gState.blocks[k]; if(hash3(i,j,7)%100<26){ return{o:FACTIONS[hash3(i,j,11)%FACTIONS.length].id,lv:1+(hash3(i,j,13)%3),npc:true,t:0}; } return null; } function ownedKeys(){ return Object.keys(gState.blocks).filter(function(k){return gState.blocks[k].o===”you”;}); } function incomePerHour(){ let s=0; ownedKeys().forEach(function(k){s+=1+gState.blocks[k].lv*2;}); if(gState.hired.indexOf(“broker”)!==-1)s=Math.round(s*1.2); return s; } function luckyInfo(now){ const cycleStart=Math.floor(now/LUCKY_CYCLE)*LUCKY_CYCLE; const start=cycleStart+LUCKY_CYCLE-LUCKY_DURATION; if(now>=start)return{active:true,endsIn:cycleStart+LUCKY_CYCLE-now,startsIn:0}; return{active:false,startsIn:start-now,endsIn:0}; } function boostMult(){ let m=1; if(gState.effects.boost>Date.now())m*=1.5; if(gState.hired.indexOf(“broker”)!==-1)m*=1.2; if(luckyInfo(Date.now()).active)m*=2; return m; } function incomeBoost(){ let m=1; if(gState.effects.boost>Date.now())m*=1.5; if(luckyInfo(Date.now()).active)m*=2; return m; } function levelOf(xp){return Math.floor(Math.sqrt(xp/40))+1;} function fmtDur(ms){ if(ms<0)ms=0; const s=Math.floor(ms/1000); const h=Math.floor(s/3600); const m=Math.floor((s%3600)/60); const r=s%60; if(h>0)return h+”h “+m+”m”; if(m>0)return m+”m “+r+”s”; return r+”s”; } function fmtStat(stat,v){return stat===”distance”?Math.round(v)+” m”:String(Math.round(v));} function pad2(n){return n<10?”0″+n:””+n;} function dayKey(d){return d.getFullYear()+”-“+pad2(d.getMonth()+1)+”-“+pad2(d.getDate());} function hourKey(d){return dayKey(d)+”-“+pad2(d.getHours());} function logEvent(msg){ gState.log.unshift({t:Date.now(),msg:msg}); if(gState.log.length>40)gState.log.length=40; } function ensurePeriods(){ const now=new Date(); const dk=dayKey(now); const hk=hourKey(now); if(gState.daily.key!==dk){gState.daily={key:dk,base:Object.assign({},gState.stats),claimed:{}};} if(gState.hourly.key!==hk){gState.hourly={key:hk,base:Object.assign({},gState.stats),claimed:{}};} if(gState.walkDaily.key!==dk){gState.walkDaily={key:dk,progress:0,claimed:{}};} ensureSeason(); } function progressOf(kind,m){ const cur=gState.stats[m.stat]||0; if(kind===”once”)return cur; const base=(kind===”daily”?gState.daily.base:gState.hourly.base)[m.stat]||0; return Math.max(0,cur-base); } function isClaimed(kind,m){ if(kind===”once”)return !!gState.once[m.id]; return !!(kind===”daily”?gState.daily.claimed:gState.hourly.claimed)[m.id]; } function claimableCount(){ ensurePeriods(); let n=0; [“hourly”,”daily”,”once”].forEach(function(kind){ MISSIONS[kind].forEach(function(m){ if(!isClaimed(kind,m)&&progressOf(kind,m)>=m.target)n++; }); }); WALK_GOALS.forEach(function(g){ if(!gState.walkDaily.claimed[g.id]&&gState.walkDaily.progress>=g.target)n++; }); PASS_QUESTS.forEach(function(q){ if(!gState.pass.claimed[q.id]&&passProgress(q)>=q.target)n++; }); return n; } function addXp(n){ const before=levelOf(gState.xp); gState.xp+=n; const after=levelOf(gState.xp); if(after>before){ const bonus=25*after; gState.credits+=bonus; logEvent(“Reached level “+after+”.”); showToast(“Level Up”,”Level “+after+” reached. +”+bonus+” credits.”); sfx(“level”); } } function grantShardsToTarget(amount){ if(amount<=0)return; let pool=PETS; const tgt=gState.shardTarget&&PET_BY_ID[gState.shardTarget]?PET_BY_ID[gState.shardTarget]:null; if(tgt){gState.shards[tgt.id]=(gState.shards[tgt.id]||0)+amount;showToast(“Shards Applied”,amount+” shard”+(amount>1?”s”:””)+” added to “+tgt.icon+” “+tgt.name+”.”);return;} const p=pool[Math.floor(Math.random()*pool.length)]; gState.shards[p.id]=(gState.shards[p.id]||0)+amount; showToast(“Shards Applied”,amount+” shard”+(amount>1?”s”:””)+” added to “+p.icon+” “+p.name+”.”); } function claimMission(kind,id){ ensurePeriods(); let m=null; MISSIONS[kind].forEach(function(x){if(x.id===id)m=x;}); if(!m)return; if(isClaimed(kind,m))return; if(progressOf(kind,m)<m.target)return; if(kind===”once”)gState.once[m.id]=true; else if(kind===”daily”)gState.daily.claimed[m.id]=true; else gState.hourly.claimed[m.id]=true; gState.credits+=m.reward; addXp(m.xp); if(m.shardReward)grantShardsToTarget(m.shardReward); logEvent(“Claimed mission reward: “+m.name+”.”); showToast(“Mission Complete”,m.name+”. +”+m.reward+” credits.”); sfx(“coin”); saveGameState(gState); updateGameHud(); } function claimWalkGoal(id){ ensurePeriods(); let g=null; WALK_GOALS.forEach(function(x){if(x.id===id)g=x;}); if(!g)return; if(gState.walkDaily.claimed[g.id])return; if(gState.walkDaily.progress<g.target)return; gState.walkDaily.claimed[g.id]=true; gState.credits+=g.reward; addXp(g.xp); grantShardsToTarget(1); logEvent(“Claimed walking goal: “+g.name+”.”); showToast(“Walking Goal”,g.name+”. +”+g.reward+” credits.”); sfx(“coin”); saveGameState(gState); updateGameHud(); } function updateLuckyBadge(){ if(!luckyBadge)return; const li=luckyInfo(Date.now()); if(li.active){ luckyBadge.className=”active”; luckyBadge.textContent=”โก LUCKY 2x ยท “+Math.ceil(li.endsIn/60000)+”m left”; }else{ luckyBadge.className=””; luckyBadge.textContent=”โก 2x bonus in “+Math.ceil(li.startsIn/60000)+”m”; } } function updateGameHud(){ const pets=Object.keys(gState.pets).length; gameCoinsEl.textContent=”LV “+levelOf(gState.xp)+” ยท “+gState.credits+” CR ยท “+ownedKeys().length+” BLOCKS ยท “+pets+” PETS”; const n=claimableCount(); const spinAvail=gState.dailySpinLast!==dayKey(new Date())?1:0; const alerts=n+spinAvail; if(gbTally){ gbTally.textContent=”๐ “+alerts; gbTally.classList.toggle(“zero”,alerts<=0); } updateLuckyBadge(); } function updateStatus(){ if(!inGame)return; if(!hasFix){ gameStatusEl.textContent=gpsMessage; return; } const c=cellOf(lastGeo.lat,lastGeo.lon); const b=getBlock(c.i,c.j); let owner=”Unclaimed”; if(b){owner=b.o===”you”?”Yours ยท Lv “+b.lv:factionById(b.o).name+” ยท Lv “+b.lv;} const stale=Date.now()-gpsLastSuccessAt>GPS_STALE_MS; let prefix=”GPS ยฑ”+Math.round(lastGeo.acc)+” m”; if(stale)prefix=”GPS (stale ยฑ”+Math.round(lastGeo.acc)+” m)”; gameStatusEl.textContent=prefix+” ยท “+(currentSpeed*3.6).toFixed(1)+” km/h ยท “+owner; } function updateSafety(){ if(currentSpeed>8){ speedLocked=false; safetyBanner.classList.add(“show”); safetyBanner.textContent=”SAFETY: You are moving at “+Math.round(currentSpeed*3.6)+” km/h. Do not play while driving. If you are a passenger, confirm when prompted.”; }else{ speedLocked=false; safetyBanner.classList.remove(“show”); } } function distTo(lat,lon){ if(!hasFix)return Infinity; return haversine(lastGeo.lat,lastGeo.lon,lat,lon); } function isCatchPanelOpen(){ return gamePanel.classList.contains(“active”)&&(panelTitle.textContent===”WILD SIGNAL”||panelTitle.textContent===”COSMIC RELIC”); } function afterChange(){ ensurePeriods(); saveGameState(gState); updateGameHud(); if(isCatchPanelOpen()){needsMapRefresh=true;}else{refreshMap();} updateStatus(); } function claimCell(i,j,manual){ const k=i+”:”+j; const b=getBlock(i,j); const now=Date.now(); let mult=boostMult(); const cellLat=(i+0.5)*CELL; const cellLon=(j+0.5)*CELL; const d=distTo(cellLat,cellLon); if(d<=RANGE){ mult*=1.2; }else if(d<=FAR_RANGE){ mult*=0.15; }else if(d>FAR_RANGE){ if(manual)showToast(“Out of Range”,”Walk closer. Bonus zone is “+RANGE+” m. Far stationary reach awards reduced rate.”); return “far”; } if(b&&b.o===”you”){ const wait=3600000-(now-(b.r||b.t||0)); if(wait<=0&&b.lv<5){ b.lv+=1; b.r=now; gState.stats.reinforced++; const cr=Math.round(5*mult); gState.credits+=cr; addXp(8); logEvent(“Reinforced a block to level “+b.lv+”.”); if(manual||!isCatchPanelOpen())showToast(“Reinforced”,”Block is now level “+b.lv+”. +”+cr+” credits.”); sfx(“claim”); afterChange(); return “reinforced”; } if(manual)showToast(“Your Block”,b.lv>=5?”This block is at max level.”:”Level “+b.lv+”. Reinforce again in “+fmtDur(wait)+”.”); return “own”; } let base; let label; if(b){ const f=factionById(b.o); base=20+b.lv*10; gState.stats.captured++; label=”Captured from the “+f.name; logEvent(“Captured a block from the “+f.name+”.”); }else{ base=15+Math.floor(Math.random()*15); label=”Claimed a new block”; logEvent(“Claimed a new block.”); } gState.stats.claimed++; const cr=Math.round(base*mult); gState.credits+=cr; gState.blocks[k]={o:”you”,lv:1,t:now,r:now}; addXp(b?25:15); if(manual||!isCatchPanelOpen())showToast(b?”Captured”:”Claimed”,label+”. +”+cr+” credits”+(d<=RANGE?” (1.2ร near bonus)”:(d<=FAR_RANGE?” (far stationary 15%)”:””))+”.”); sfx(b?”capture”:”claim”); afterChange(); return b?”captured”:”claimed”; } function doClaim(){ if(!hasFix){showToast(“GPS”,”Waiting for your location. Allow location access to play.”);sfx(“error”);return;} if(lastGeo.acc>80){showToast(“GPS”,”Signal is too weak. Move to an open area.”);sfx(“error”);return;} const c=cellOf(lastGeo.lat,lastGeo.lon); claimCell(c.i,c.j,true); } function runNpcSimulation(elapsedMs){ let lost=0; let defended=0; let blocked=0; const capturedFrom=[]; if(!ownedKeys().length)return{lost:0,defended:0,blocked:0,capturedFrom:capturedFrom}; gState.npcClock+=elapsedMs; let attacks=Math.floor(gState.npcClock/NPC_ATTACK_INTERVAL_MS); gState.npcClock-=attacks*NPC_ATTACK_INTERVAL_MS; if(attacks>3)attacks=3; const now=Date.now(); for(let n=0;n<attacks;n++){ const keys=ownedKeys(); if(!keys.length)break; const k=keys[Math.floor(Math.random()*keys.length)]; const b=gState.blocks[k]; if(!b)continue; if(b.lv<=1&&Math.random()<0.4)continue; const f=FACTIONS[Math.floor(Math.random()*FACTIONS.length)]; if(gState.effects.shield>now){ blocked++; logEvent(“Your Turf Shield stopped the “+f.name+”.”); }else{ let chance=NPC_ATTACK_CHANCE-0.03*(b.lv-1); if(gState.hired.indexOf(“guard”)!==-1)chance-=0.08; const petsBonus=Math.min(0.15,Object.keys(gState.pets).length*0.005); chance-=petsBonus; if(chance<0.03)chance=0.03; if(Math.random()<chance){ gState.blocks[k]={o:f.id,lv:Math.min(4,b.lv),t:now}; gState.stats.lost++; lost++; capturedFrom.push({k:k,f:f.name,lv:b.lv}); logEvent(“The “+f.name+” took one of your blocks.”); }else{ defended++; addXp(3); logEvent(“Your block held against the “+f.name+”.”); } } } return{lost:lost,defended:defended,blocked:blocked,capturedFrom:capturedFrom}; } function npcTick(){ const now=Date.now(); let elapsed=now-gState.lastTick; if(elapsed<0)elapsed=0; if(elapsed>86400000)elapsed=86400000; gState.lastTick=now; const res=runNpcSimulation(elapsed); saveGameState(gState); updateGameHud(); if(inGame&&(res.lost>0||res.defended>0||res.blocked>0))refreshMap(); } function dailyLogin(){ const today=dayKey(new Date()); if(gState.lastDay===today)return; const y=new Date(); y.setDate(y.getDate()-1); gState.streak=(gState.lastDay===dayKey(y))?gState.streak+1:1; gState.lastDay=today; const bonus=20+Math.min(gState.streak,7)*5; gState.credits+=bonus; let extra=””; if(gState.streak%7===0){gState.items.orb+=5;extra=” Plus 5 Echo Weaves.”;} logEvent(“Daily login bonus collected. Streak “+gState.streak+”.”); showToast(“Daily Login Bonus”,”Streak “+gState.streak+”. +”+bonus+” credits.”+extra); sfx(“coin”); saveGameState(gState); } function pruneSeen(){ const now=Date.now(); Object.keys(gState.seen).forEach(function(k){if(gState.seen[k]<now)delete gState.seen[k];}); Object.keys(gState.relicsSeen).forEach(function(k){if(gState.relicsSeen[k]<now)delete gState.relicsSeen[k];}); } function resonatorUsesLastHour(){ const cutoff=Date.now()-3600000; if(!Array.isArray(gState.resonatorUses))gState.resonatorUses=[]; gState.resonatorUses=gState.resonatorUses.filter(function(t){return t>cutoff;}); return gState.resonatorUses.length; } function resonatorUsesRemaining(){ return Math.max(0,RESONATOR_HOURLY_LIMIT-resonatorUsesLastHour()); } function homeBubbleMult(){ if(!gState.hasHome)return 1; const d=haversine(gState.homeLat,gState.homeLon,userLat,userLon); if(d>HOME_BUBBLE_METERS)return OUTSIDE_BUBBLE_SPAWN_MULT; return 1; } function getRelics(){ const now=Date.now(); const c=cellOf(userLat,userLon); const dr=6; const relics=[]; const bubbleMult=homeBubbleMult(); for(let di=-dr;di<=dr;di++){ for(let dj=-dr;dj<=dr;dj++){ const i=c.i+di; const j=c.j+dj; const b=getBlock(i,j); const claimable=(!b)||b.o===”you”; if(!claimable)continue; const windowMs=180000; const win=Math.floor(now/windowMs); const rate=RELIC_SPAWN_RATE*bubbleMult; if(rnd01(i,j,win+7100)>=rate)continue; const id=”r:”+i+”:”+j+”:”+win; if(gState.relicsSeen[id])continue; const roll=hash3(i,j,win+7200)%100; const rk=roll<45?”common”:roll<72?”uncommon”:roll<89?”rare”:roll<97?”epic”:”legendary”; const pool=PET_POOLS[rk]; const species=pool[hash3(i,j,win+7300)%pool.length]; const expires=(win+1)*windowMs; relics.push({ id:id, i:i, j:j, lat:(i+0.35+0.3*rnd01(i,j,win+7400))*CELL, lon:(j+0.35+0.3*rnd01(i,j,win+7500))*CELL, sp:species, expires:expires }); } } return relics; } function getSpawns(){ const now=Date.now(); const c=cellOf(userLat,userLon); let cr=gState.effects.radar>now?9:5; if(gState.hired.indexOf(“scout”)!==-1)cr+=2; const dr=8; const depots=[]; const crits=[]; const bubbleMult=homeBubbleMult(); for(let di=-dr;di<=dr;di++){ for(let dj=-dr;dj<=dr;dj++){ const i=c.i+di; const j=c.j+dj; if(hash3(i,j,21)%100<6){ depots.push({id:”d:”+i+”:”+j,lat:(i+0.2+0.6*rnd01(i,j,22))*CELL,lon:(j+0.2+0.6*rnd01(i,j,23))*CELL,name:DEPOT_NAMES[hash3(i,j,24)%DEPOT_NAMES.length]}); } if(Math.abs(di)<=cr&&Math.abs(dj)<=cr){ const off=hash3(i,j,5)%45000; const win=Math.floor((now+off)/45000); let rate=0.09*bubbleMult; if(gState.hired.indexOf(“scout”)!==-1)rate+=0.04*bubbleMult; if(rnd01(i,j,win+1000)<rate){ const id=”c:”+i+”:”+j+”:”+win; if(!gState.seen[id]){ const roll=hash3(i,j,win+3000)%100; const rk=roll<55?”common”:roll<80?”uncommon”:roll<92?”rare”:roll<98?”epic”:”legendary”; const pool=PET_POOLS[rk]; const species=pool[hash3(i,j,win+4000)%pool.length]; crits.push({id:id,lat:(i+0.15+0.7*rnd01(i,j,win+2000))*CELL,lon:(j+0.15+0.7*rnd01(i,j,win+2500))*CELL,sp:species,expires:(win+1)*45000-off}); } } } } } return{depots:depots,crits:crits}; } function getArenaLocation(){ if(!gState.hasHome)return null; const offsetMeters=ARENA_DISTANCE_METERS; const dLat=offsetMeters/111320; return{lat:gState.homeLat+dLat,lon:gState.homeLon}; } function youIcon(){ return L.divIcon({className:”mk-wrap”,html:”<div class=\”mk-you”+(gState.markerNeon?” neon”:””)+”\”></div>”,iconSize:[26,26],iconAnchor:[13,13]}); } function updateYouIcon(){ if(youMarker)youMarker.setIcon(youIcon()); } function npcPetsFor(factionId,level){ const faction=factionById(factionId); const seed=hash3(factionId.length,level,Math.floor(factionId.charCodeAt(0)||0)); const count=Math.min(3,Math.max(1,level)); const out=[]; const tier=level>=4?”rare”:level>=3?”uncommon”:”common”; const pool=PET_POOLS[tier]||PET_POOLS.common; for(let i=0;i<count;i++){ const idx=(seed+i*37)%pool.length; const sp=pool[idx]; const lv=Math.max(1,level*2+i); out.push({sp:sp,lv:lv,power:Math.round(sp.power*(1+(lv-1)*0.42))}); } return{name:faction.name,color:faction.color,pets:out,level:level}; } function blockPopup(b){ if(!b)return “<b>Unclaimed block</b><br>Walk in to claim it.”; if(b.o===”you”)return “<b>Your block</b><br>Level “+b.lv+” of 5<br>Held since “+new Date(b.t||Date.now()).toLocaleString(); const f=factionById(b.o); return “<b>”+f.name+” turf</b><br>Level “+b.lv+”<br>Walk in to capture it, or use the BATTLE panel.”; } function openTerritoryModal(i,j){ const b=getBlock(i,j); const cellLat=(i+0.5)*CELL; const cellLon=(j+0.5)*CELL; const d=distTo(cellLat,cellLon); let h=””; if(!b){ h+=”<div class=\”big\”>๐</div>”; h+=”<p style=\”text-align:center\”><b>UNCLAIMED TERRITORY</b></p>”; h+=”<p style=\”text-align:center\”>”+((isFinite(d))?(“Distance: “+Math.round(d)+” m”):”Waiting for GPS”)+”</p>”; if(d<=RANGE){ h+=”<button class=\”pbtn wide primary\” data-terr=\”claim\”>CLAIM TERRITORY</button>”; }else if(d<=FAR_RANGE){ h+=”<button class=\”pbtn wide\” data-terr=\”claim\”>CLAIM (15% FAR RATE)</button>”; }else{ h+=”<p style=\”text-align:center;color:#ff8a3d\”>Too far. Move within “+FAR_RANGE+” m to interact.</p>”; } }else if(b.o===”you”){ h+=”<div class=\”big\”>๐ด</div>”; h+=”<p style=\”text-align:center\”><b>YOUR TERRITORY</b></p>”; h+=”<p style=\”text-align:center\”>Level “+b.lv+” of 5 ยท Held since “+new Date(b.t||Date.now()).toLocaleString()+”</p>”; const wait=3600000-(Date.now()-(b.r||b.t||0)); if(b.lv<5){ if(wait<=0)h+=”<button class=\”pbtn wide primary\” data-terr=\”reinforce\”>REINFORCE NOW</button>”; else h+=”<p style=\”text-align:center\”>Reinforce available in “+fmtDur(wait)+”</p>”; }else{ h+=”<p style=\”text-align:center\”>This territory is at max level.</p>”; } }else{ const f=factionById(b.o); const npcInfo=npcPetsFor(b.o,b.lv); h+=”<div class=\”big\”>โ๏ธ</div>”; h+=”<p style=\”text-align:center\”><b style=\”color:”+f.color+”\”>”+f.name.toUpperCase()+”</b></p>”; h+=”<p style=\”text-align:center\”>Level “+b.lv+” rival claim”+(isFinite(d)?(” ยท “+Math.round(d)+” m away”):””)+”</p>”; h+=”<div style=\”border:1px solid rgba(255,140,120,0.4);border-radius:12px;padding:10px;background:rgba(40,10,10,0.35);margin:8px 0\”>”; h+=”<p style=\”text-align:center;font-size:11px;letter-spacing:1px;color:#ffb0b0;margin:0 0 8px\”><b>ENFORCER GARRISON ยท LEVEL “+b.lv+”</b></p>”; npcInfo.pets.forEach(function(pp){ h+=”<div class=\”npcrow\” style=\”margin-bottom:4px\”><div class=\”ti\” style=\”font-size:22px\”>”+pp.sp.icon+”</div><div style=\”flex:1\”><b>”+pp.sp.name+”</b><br><span style=\”font-size:10px;color:#a0c0e0\”>Lv “+pp.lv+” ยท Power “+pp.power+” ยท “+RARITY[pp.sp.r].label+”</span></div></div>”; }); h+=”</div>”; h+=”<button class=\”pbtn wide battle\” data-terr=\”battle\”>BATTLE TERRITORY</button>”; h+=”<button class=\”pbtn wide alt\” data-terr=\”challenge\”>CHALLENGE TERRITORY</button>”; } openPanel(“TERRITORY”,h); panelBody.onclick=function(e){ const b2=e.target.closest(“[data-terr]”); if(!b2)return; const act=b2.getAttribute(“data-terr”); if(act===”claim”){closePanel();claimCell(i,j,true);} else if(act===”reinforce”){closePanel();claimCell(i,j,true);} else if(act===”battle”||act===”challenge”){closePanel();setTimeout(function(){showBattleForTarget(i,j);},60);} }; } function refreshMap(){ if(!leafletMap||!blocksLayer||!spawnLayer)return; if(isCatchPanelOpen()){needsMapRefresh=true;return;} blocksLayer.clearLayers(); spawnLayer.clearLayers(); if(relicLayer)relicLayer.clearLayers(); if(arenaLayer)arenaLayer.clearLayers(); const c=cellOf(userLat,userLon); const R=7; for(let di=-R;di<=R;di++){ for(let dj=-R;dj<=R;dj++){ const i=c.i+di; const j=c.j+dj; const b=getBlock(i,j); const isCur=di===0&&dj===0; if(!b&&!isCur)continue; let color; let fill; let op; if(!b){color=”#00e5ff”;fill=”#00e5ff”;op=0.06;} else if(b.o===”you”){color=”#00ff9d”;fill=”#00cc7a”;op=0.2+b.lv*0.06;} else{const f=factionById(b.o);color=f.color;fill=f.color;op=0.18+b.lv*0.05;} const rect=L.rectangle([[i*CELL,j*CELL],[(i+1)*CELL,(j+1)*CELL]],{color:color,weight:isCur?3:1,dashArray:isCur?”6 4″:null,fillColor:fill,fillOpacity:op,interactive:true}); rect.on(“click”,(function(ci,cj){return function(){openTerritoryModal(ci,cj);};})(i,j)); rect.addTo(blocksLayer); } } const sp=getSpawns(); const now=Date.now(); sp.depots.forEach(function(d){ const cool=(gState.depots[d.id]||0)>now; const m=L.marker([d.lat,d.lon],{icon:L.divIcon({className:”mk-wrap”,html:”<div class=\”mk”+(cool?” cool”:””)+”\”>”+(cool?”๐ค”:”๐ฆ”)+”</div>”,iconSize:[40,40],iconAnchor:[20,20]}),zIndexOffset:500}); m.on(“click”,function(){openDepotPanel(d);}); m.addTo(spawnLayer); }); sp.crits.forEach(function(cr){ const col=RARITY[cr.sp.r].color; const m=L.marker([cr.lat,cr.lon],{icon:L.divIcon({className:”mk-wrap”,html:”<div class=\”mk\” style=\”border-color:”+col+”;box-shadow:0 0 14px “+col+”\”>”+cr.sp.icon+”</div>”,iconSize:[40,40],iconAnchor:[20,20]}),zIndexOffset:800}); m.on(“click”,function(){openCritterPanel(cr);}); m.addTo(spawnLayer); }); const relics=getRelics(); relics.forEach(function(rl){ const m=L.marker([rl.lat,rl.lon],{icon:L.divIcon({className:”mk-wrap”,html:”<div class=\”mk-relic\”><span>๐ </span></div>”,iconSize:[44,44],iconAnchor:[22,22]}),zIndexOffset:900}); m.on(“click”,function(){openRelicPanel(rl);}); m.addTo(relicLayer); }); const arenaLoc=getArenaLocation(); if(arenaLoc&&arenaLayer){ const distToArena=hasFix?haversine(lastGeo.lat,lastGeo.lon,arenaLoc.lat,arenaLoc.lon):Infinity; const withinBubble=gState.hasHome?haversine(gState.homeLat,gState.homeLon,arenaLoc.lat,arenaLoc.lon)<=HOME_BUBBLE_METERS:false; if(withinBubble){ const arenaIcon=L.divIcon({className:”mk-wrap”,html:”<div class=\”mk-arena\”>โ๏ธ</div>”,iconSize:[48,48],iconAnchor:[24,24]}); const am=L.marker([arenaLoc.lat,arenaLoc.lon],{icon:arenaIcon,zIndexOffset:1500}); am.on(“click”,function(){ if(!hasFix){showToast(“Arena”,”Waiting for GPS signal.”);return;} if(distToArena>ARENA_ENTER_RADIUS){ showToast(“Arena”,”You must walk within “+ARENA_ENTER_RADIUS+” m of the Arena. Currently “+Math.round(distToArena)+” m away.”); return; } openArenaGrounds(); }); am.addTo(arenaLayer); } } } function attachZoomTransitions(map,opts){ const container=map.getContainer(); let lock=0; let pinchStart=0; let pinchStartZoom=0; let gestureCounted=false; let outAttempts=0; let lastOutAttempt=0; let lastWheelEvent=0; const attemptsRequired=opts.outAttemptsRequired||1; const attemptWindow=opts.attemptWindow||30000; const idleThreshold=opts.idleThreshold||400; const minZoom=opts.min; const maxZoom=opts.max; function registerOutAttempt(){ const now=Date.now(); if(now-lastOutAttempt>attemptWindow)outAttempts=0; outAttempts++; lastOutAttempt=now; if(outAttempts>=attemptsRequired){ outAttempts=0; lock=now+1400; opts.onOut(); } } container.addEventListener(“wheel”,function(e){ const now=Date.now(); const wasIdle=(now-lastWheelEvent)>idleThreshold; lastWheelEvent=now; if(now<lock)return; if(opts.ready&&!opts.ready())return; const z=map.getZoom(); if(e.deltaY>0&&z<=minZoom+0.01){ if(wasIdle)registerOutAttempt(); }else if(e.deltaY<0&&z>=maxZoom-0.01){ lock=now+1400; opts.onIn(); }else{ outAttempts=0; } },{passive:true}); container.addEventListener(“touchstart”,function(e){ if(e.touches.length===2){ const dx=e.touches[0].clientX-e.touches[1].clientX; const dy=e.touches[0].clientY-e.touches[1].clientY; pinchStart=Math.sqrt(dx*dx+dy*dy); pinchStartZoom=map.getZoom(); gestureCounted=false; } },{passive:true}); container.addEventListener(“touchmove”,function(e){ if(e.touches.length===2&&pinchStart){ if(Date.now()<lock)return; if(opts.ready&&!opts.ready())return; const dx=e.touches[0].clientX-e.touches[1].clientX; const dy=e.touches[0].clientY-e.touches[1].clientY; const d=Math.sqrt(dx*dx+dy*dy); const z=map.getZoom(); if(d<pinchStart-80&&z<=minZoom+0.01&&!gestureCounted){ gestureCounted=true; if(pinchStartZoom<=minZoom+0.01)registerOutAttempt(); }else if(d>pinchStart+80&&z>=maxZoom-0.01&&!gestureCounted){ gestureCounted=true; lock=Date.now()+1400; opts.onIn(); } } },{passive:true}); container.addEventListener(“touchend”,function(){ pinchStart=0; gestureCounted=false; },{passive:true}); } function initGodsEyeMap(){ godseyeMap=L.map(“godseye-map”,{zoomControl:true,attributionControl:true,minZoom:2,maxZoom:19,zoomSnap:0.25,worldCopyJump:true}).setView([godseyeLat,godseyeLon],11); L.tileLayer(“https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}”,{maxZoom:19,maxNativeZoom:19,attribution:”Tiles ยฉ Esri”}).addTo(godseyeMap); L.tileLayer(“https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}”,{maxZoom:19,maxNativeZoom:19,opacity:0.85}).addTo(godseyeMap); attachZoomTransitions(godseyeMap,{min:2,max:19,outAttemptsRequired:3,attemptWindow:3000,idleThreshold:400,ready:function(){return performance.now()-godseyeOpenAt>700;},onOut:closeGodsEye,onIn:enterStreetFromGodsEye}); } function openGodsEye(lat,lon){ godseyeLat=(typeof lat===”number”&&isFinite(lat))?lat:userLat; godseyeLon=(typeof lon===”number”&&isFinite(lon))?lon:userLon; godseyeOpen=true; godseyeOpenAt=performance.now(); godseyeOverlay.classList.add(“active”); if(!godseyeMap)initGodsEyeMap(); godseyeMap.setView([godseyeLat,godseyeLon],11,{animate:false}); setTimeout(function(){if(godseyeMap)godseyeMap.invalidateSize();},150); state.targetDistMul=1.0; state.distMul=1.0; state.progress=0; scrollEl.scrollTop=0; setMusicMode(“godseye”); syncHud(); } function closeGodsEye(){ if(!godseyeOpen)return; godseyeOpen=false; godseyeOverlay.classList.remove(“active”); godseyeCloseAt=performance.now(); unlockDismissedIdx=-1; state.targetDistMul=1.2; state.distMul=1.2; state.progress=0; scrollEl.scrollTop=0; setMusicMode(“space”); syncHud(); } function enterStreetFromGodsEye(){ if(!godseyeOpen)return; godseyeOpen=false; godseyeOverlay.classList.remove(“active”); enterGame(0); } function closeStreetToGodsEye(){ if(!inGame)return; let la=userLat; let lo=userLon; if(leafletMap){ try{const c=leafletMap.getCenter();la=c.lat;lo=c.lng;}catch(e){} } exitGame(); openGodsEye(la,lo); } function godsEyeSearchGo(){ const text=godseyeSearch.value.trim(); if(!text||!godseyeMap)return; const ll=parseLatLon(text); if(ll){godseyeMap.setView([ll.lat,ll.lon],14,{animate:true});return;} fetch(“https://nominatim.openstreetmap.org/search?format=json&limit=1&q=”+encodeURIComponent(text),{headers:{“Accept”:”application/json”}}).then(function(r){return r.json();}).then(function(list){ if(list&&list.length&&godseyeMap){ godseyeMap.setView([parseFloat(list[0].lat),parseFloat(list[0].lon)],13,{animate:true}); }else{ showToast(“God’s Eye”,”No match found for that search.”); } }).catch(function(){showToast(“God’s Eye”,”Search is unavailable right now.”);}); } function initLeaflet(){ if(leafletMap){leafletMap.remove();leafletMap=null;} leafletMap=L.map(“leaflet-map”,{zoomControl:true,attributionControl:true,maxZoom:19,minZoom:10}).setView([userLat,userLon],17); const stadiaUrl=”https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png?api_key=”+STADIA_API_PRIMARY; L.tileLayer(stadiaUrl,{maxZoom:19,attribution:”ยฉ Stadia Maps ยฉ OpenMapTiles ยฉ OpenStreetMap”}).addTo(leafletMap); blocksLayer=L.layerGroup().addTo(leafletMap); spawnLayer=L.layerGroup().addTo(leafletMap); relicLayer=L.layerGroup().addTo(leafletMap); arenaLayer=L.layerGroup().addTo(leafletMap); youMarker=L.marker([userLat,userLon],{icon:youIcon(),zIndexOffset:2000,interactive:false}).addTo(leafletMap); rangeCircle=L.circle([userLat,userLon],{radius:RANGE,color:”#00e5ff”,weight:2,fillColor:”#00e5ff”,fillOpacity:0.06,interactive:false,dashArray:”8 6″}).addTo(leafletMap); followPlayer=true; centeredOnFix=false; leafletMap.on(“dragstart”,function(){followPlayer=false;}); refreshMap(); setTimeout(function(){if(leafletMap)leafletMap.invalidateSize();},300); streetOpenAt=performance.now(); attachZoomTransitions(leafletMap,{min:10,max:19,outAttemptsRequired:3,attemptWindow:3000,idleThreshold:450,ready:function(){return performance.now()-streetOpenAt>800;},onOut:closeStreetToGodsEye,onIn:function(){}}); } function awardWalkCredits(meters){ if(meters<=0)return; const mult=boostMult(); const earned=meters*WALK_REWARD_PER_METER*mult; gState.stats.walkCreditAcc=(gState.stats.walkCreditAcc||0)+earned; if(gState.stats.walkCreditAcc>=1){ const toAdd=Math.floor(gState.stats.walkCreditAcc); gState.stats.walkCreditAcc-=toAdd; gState.credits+=toAdd; gState.stats.walkCredits=(gState.stats.walkCredits||0)+toAdd; } } function checkPetMilestones(){ const count=Object.keys(gState.pets).length; for(let i=0;i<PET_MILESTONES.length;i++){ const m=PET_MILESTONES[i]; if(count>=m.count&&!gState.petMilestonesClaimed[“m”+m.count]){ gState.petMilestonesClaimed[“m”+m.count]=true; gState.credits+=m.reward; gState.stats.petMilestones=(gState.stats.petMilestones||0)+1; addXp(Math.round(m.reward*0.3)); logEvent(“Pet milestone reached: “+m.count+” pets. +”+m.reward+” credits.”); showToast(“Pet Milestone”,m.count+” pets collected. +”+m.reward+” credits.”); sfx(“level”); } } } function checkEggHatches(){ if(!Array.isArray(gState.eggs)||!gState.eggs.length)return; const now=Date.now(); for(let i=gState.eggs.length-1;i>=0;i–){ const egg=gState.eggs[i]; if(now>=egg.hatchTime){ gState.eggs.splice(i,1); const species=PET_BY_ID[egg.speciesId]; if(!species)continue; const isShiny=!!egg.isShiny; const isMutated=!!egg.isMutated; let newLv=1; const existing=gState.pets[egg.speciesId]; if(existing){ existing.caught=(existing.caught||0)+1; }else{ const anomalyPerk=isMutated?(egg.mutationPerk||rollAnomaly(true)):null; gState.pets[egg.speciesId]={lv:newLv,xp:petXpForLevel(newLv),caught:1,anomaly:!!anomalyPerk,anomalyPerk:anomalyPerk||””,stats:egg.inheritedStats||{str:10,agi:10,int:10,vit:10,luk:10}}; if(gState.team.length<3&&gState.team.indexOf(egg.speciesId)===-1)gState.team.push(egg.speciesId); } gState.stats.hatched=(gState.stats.hatched||0)+1; logEvent(“Egg hatched into “+species.name+”!”); showToast(“Egg Hatched”,species.icon+” “+species.name+” was born!”+(isShiny?” โจSHINY!”:””)+(isMutated?” โญANOMALY!”:””)); sfx(“level”); saveGameState(gState); updateGameHud(); } } } function onGeo(pos){ const c=pos.coords; const lat=c.latitude; const lon=c.longitude; const acc=typeof c.accuracy===”number”?c.accuracy:9999; const now=Date.now(); gpsFailCount=0; gpsLastSuccessAt=now; gpsPermissionGranted=true; gpsRetryAttempts=0; if(gpsRetryTimer){clearTimeout(gpsRetryTimer);gpsRetryTimer=null;} hideGpsPermissionButtons(); if(gameGpsBtn)gameGpsBtn.style.display=”none”; if(!gState.hasHome){ gState.homeLat=lat; gState.homeLon=lon; gState.hasHome=true; } if(lastGeo.lat!==null&&lastGeo.t&&acc<=50){ const dt=(now-lastGeo.t)/1000; if(dt>0.4){ const computed=haversine(lastGeo.lat,lastGeo.lon,lat,lon)/dt; let spd=computed; if(typeof c.speed===”number”&&!isNaN(c.speed)&&c.speed>=0&&computed<40)spd=c.speed; currentSpeed=spd; } } if(anchor===null){ anchor={lat:lat,lon:lon}; }else{ const ad=haversine(anchor.lat,anchor.lon,lat,lon); if(ad>=Math.max(4,acc*0.6)){ if(acc<=50&&ad<=300&ยคtSpeed<=8){ gState.stats.distance+=ad; gState.walkDaily.progress=(gState.walkDaily.progress||0)+ad; gState.stats.walkedToday=(gState.stats.walkedToday||0)+ad; awardWalkCredits(ad); } anchor={lat:lat,lon:lon}; } } lastGeo={lat:lat,lon:lon,t:now,acc:acc}; hasFix=true; setUserLocation(lat,lon); if(youMarker)youMarker.setLatLng([lat,lon]); if(rangeCircle)rangeCircle.setLatLng([lat,lon]); if(leafletMap){ if(!centeredOnFix){leafletMap.setView([lat,lon],17);centeredOnFix=true;} else if(followPlayer){leafletMap.panTo([lat,lon],{animate:true});} } updateSafety(); const cell=cellOf(lat,lon); const ck=cell.i+”:”+cell.j; if(lastCell===null){ lastCell=ck; if(!isCatchPanelOpen())showToast(“Location Locked”,”Walk into a new block to claim it, or tap CLAIM HERE.”); refreshMap(); }else if(ck!==lastCell){ lastCell=ck; if(acc<=60)claimCell(cell.i,cell.j,false); refreshMap(); } updateStatus(); updateGameHud(); checkStepGoal(); checkPassengerWarning(); checkWeather(); if(now-lastSave>5000)saveGameState(gState); } function hideGpsPermissionButtons(){ const tb=document.getElementById(“toast-gps-btn”); if(tb)tb.style.display=”none”; if(gameGpsBtn)gameGpsBtn.style.display=”none”; if(gameNotesEl){ const acts=gameNotesEl.querySelectorAll(“.gnote-action”); for(let i=0;i<acts.length;i++){acts[i].style.display=”none”;} } } function showGpsDeniedNotice(){ showToast(“Location Permission Required”,”Location is blocked for this site. Open your browser site settings, allow Location, then tap GRANT GPS PERMISSIONS.”,{action:”gps”}); } function updateGpsPermissionState(){ if(!navigator.permissions||!navigator.permissions.query)return; try{ navigator.permissions.query({name:”geolocation”}).then(function(status){ gpsPermissionState=status.state; if(status.state===”granted”)gpsPermissionGranted=true; else gpsPermissionGranted=false; status.onchange=function(){ gpsPermissionState=this.state; if(this.state===”granted”){gpsPermissionGranted=true;hideGpsPermissionButtons();} else{gpsPermissionGranted=false;} }; }).catch(function(){}); }catch(e){} } function attemptGpsPermission(){ if(!navigator.geolocation)return; gpsRetryAttempts++; const opts={enableHighAccuracy:true,maximumAge:0,timeout:20000}; navigator.geolocation.getCurrentPosition(function(pos){ gpsPermissionGranted=true; gpsRetryAttempts=0; gpsFailCount=0; gpsLastSuccessAt=Date.now(); hideGpsPermissionButtons(); onGeo(pos); if(watchId===null)startGeoWatch(); showToast(“GPS Active”,”Location acquired. You can play now.”); },function(err){ if(err&&err.code===1){ gpsPermissionGranted=false; gpsMessage=”Location permission denied. Enable location for this site in browser settings, then tap GRANT GPS PERMISSIONS.”; updateStatus(); if(gameGpsBtn)gameGpsBtn.style.display=”flex”; showGpsDeniedNotice(); }else{ gpsMessage=”GPS signal weak. Retrying (“+gpsRetryAttempts+”/5)…”; updateStatus(); if(gpsRetryAttempts<5){ const delay=1200+gpsRetryAttempts*800; gpsRetryTimer=setTimeout(attemptGpsPermission,delay); }else{ gpsMessage=”GPS unavailable. Move to an open area and tap the GPS button to retry.”; updateStatus(); showToast(“GPS Unavailable”,”Move to an open area with a clear view of the sky and tap RETRY GPS.”,{action:”gps”}); } } },opts); } function requestGpsPermission(){ if(!navigator.geolocation){ showToast(“GPS Unavailable”,”This device or browser does not support location services. Try a different browser or device.”); return; } updateGpsPermissionState(); gpsMessage=”Requesting GPS permission…”; updateStatus(); if(gameGpsBtn)gameGpsBtn.style.display=”flex”; if(gpsRetryTimer){clearTimeout(gpsRetryTimer);gpsRetryTimer=null;} gpsRetryAttempts=0; attemptGpsPermission(); } function onGeoError(err){ let msg=”Location unavailable.”; let permDenied=false; if(err&&err.code===1){ msg=”Location permission denied. Allow location access to play.”; permDenied=true; gpsPermissionGranted=false; if(gameGpsBtn)gameGpsBtn.style.display=”flex”; if(watchId!==null){try{navigator.geolocation.clearWatch(watchId);}catch(e){}watchId=null;} }else if(err&&err.code===2){ msg=”Location signal unavailable. Move to an open area. Retrying…”; }else if(err&&err.code===3){ msg=”Location request timed out. Retrying…”; } gpsMessage=msg; updateStatus(); if(permDenied){ showToast(“Location Permission Required”,”Enable location for this site, then tap GRANT GPS PERMISSIONS.”,{action:”gps”}); }else{ gpsFailCount++; if(gpsFailCount<3&&navigator.geolocation){ setTimeout(function(){ if(inGame&&!hasFix){navigator.geolocation.getCurrentPosition(onGeo,function(){},{enableHighAccuracy:false,maximumAge:10000,timeout:15000});} },2500); } } } function startGpsWatchdog(){ if(gpsWatchdogTimer)clearInterval(gpsWatchdogTimer); gpsWatchdogTimer=setInterval(function(){ if(!inGame)return; const now=Date.now(); const stale=!lastGeo.t||(now-lastGeo.t>GPS_STALE_MS); if(stale){ gpsFailCount++; if(gpsFailCount>=2){ gpsMessage=”GPS signal unstable. Auto-refreshing…”; updateStatus(); } if(navigator.geolocation){ navigator.geolocation.getCurrentPosition(function(pos){ gpsFailCount=0; onGeo(pos); },function(err){ if(err&&err.code===1){gpsMessage=”Location permission denied.”;updateStatus();if(gameGpsBtn)gameGpsBtn.style.display=”flex”;} },{enableHighAccuracy:false,maximumAge:10000,timeout:15000}); } }else{ if(gpsFailCount>0){gpsFailCount=0;} } },GPS_WATCHDOG_MS); } function startGeoWatch(){ if(watchId!==null)return; if(!navigator.geolocation){gpsMessage=”This device does not support location services.”;updateStatus();return;} updateGpsPermissionState(); gpsMessage=”Acquiring GPS signal”; gpsFailCount=0; gpsLastSuccessAt=Date.now(); try{ watchId=navigator.geolocation.watchPosition(onGeo,onGeoError,{enableHighAccuracy:true,maximumAge:1000,timeout:20000}); }catch(e){watchId=null;} if(gpsRefreshTimer)clearInterval(gpsRefreshTimer); gpsRefreshTimer=setInterval(function(){ if(!inGame||!navigator.geolocation)return; navigator.geolocation.getCurrentPosition(onGeo,function(){},{enableHighAccuracy:true,maximumAge:0,timeout:15000}); },GPS_REFRESH_MS); startGpsWatchdog(); } function stopGeoWatch(){ if(watchId!==null){navigator.geolocation.clearWatch(watchId);watchId=null;} if(gpsRefreshTimer){clearInterval(gpsRefreshTimer);gpsRefreshTimer=null;} if(gpsWatchdogTimer){clearInterval(gpsWatchdogTimer);gpsWatchdogTimer=null;} } function stopCatch(){ CATCH.active=false; CATCH.accepting=false; if(CATCH.timers&&CATCH.timers.length){ for(let i=0;i<CATCH.timers.length;i++)clearTimeout(CATCH.timers[i]); CATCH.timers=[]; } if(catchAnim){cancelAnimationFrame(catchAnim);catchAnim=0;} } function stopRelic(){ relicState=null; if(relicExtractTimer){clearInterval(relicExtractTimer);relicExtractTimer=null;} if(relicPulseTimer){clearInterval(relicPulseTimer);relicPulseTimer=null;} } function openPanel(title,html,compact){ stopCatch(); if(title!==”COSMIC RELIC”)stopRelic(); panelBody.onclick=null; panelTitle.textContent=title; panelBody.innerHTML=html; if(compact)gamePanelCard.classList.add(“compact”); else gamePanelCard.classList.remove(“compact”); gamePanel.classList.add(“active”); panelBody.scrollTop=0; gamePanelCard.scrollTop=0; updateFleeButton(); } function closePanel(){ stopCatch(); stopRelic(); panelBody.onclick=null; gamePanel.classList.remove(“active”); gamePanelCard.classList.remove(“compact”); gamePanelClose.textContent=”CLOSE”; gamePanelClose.classList.remove(“flee-mode”); if(needsMapRefresh){needsMapRefresh=false;setTimeout(refreshMap,80);} } function updateFleeButton(){ const inBattle=battleState&&!battleState.over; if(inBattle){ gamePanelClose.textContent=”FLEE”; gamePanelClose.classList.add(“flee-mode”); }else{ gamePanelClose.textContent=”CLOSE”; gamePanelClose.classList.remove(“flee-mode”); } } function fleeFromBattle(){ if(!battleState){closePanel();return;} if(battleState.over){battleState=null;closePanel();return;} battleState.over=true; battleState.resultText=”YOU FLED THE BATTLE”; battleState.log.push(“You fled from the battle!”); if(battleState.mode===”arena”){ gState.arenaWins.push(Date.now()); logEvent(“Fled from arena battle.”); }else if(battleState.mode===”street”){ logEvent(“Fled from street battle.”); }else if(battleState.mode===”duel”){ logEvent(“Fled from duel.”); } saveGameState(gState); renderBattleUI(); } gamePanelClose.addEventListener(“click”,function(){ if(battleState&&!battleState.over){fleeFromBattle();return;} closePanel(); }); gamePanelCloseX.addEventListener(“click”,function(){ if(battleState&&!battleState.over){fleeFromBattle();return;} closePanel(); }); gamePanel.addEventListener(“click”,function(e){if(e.target===gamePanel){if(battleState&&!battleState.over){fleeFromBattle();return;}closePanel();}}); function openDepotPanel(d){ const now=Date.now(); const ready=(gState.depots[d.id]||0)<=now; const dist=distTo(d.lat,d.lon); const inRange=dist<=RANGE; const inFarRange=dist<=FAR_RANGE; let h=”<div class=\”big\”>”+(ready?”๐ฆ”:”๐ค”)+”</div><p style=\”text-align:center\”><b>”+d.name+”</b></p>”; h+=”<p style=\”text-align:center\”>”+(isFinite(dist)?Math.round(dist)+” m away”:”Waiting for GPS”)+”</p>”; if(!ready){ h+=”<p style=\”text-align:center\”>Recharging. Ready in “+fmtDur(gState.depots[d.id]-now)+”.</p>”; }else if(!inFarRange){ h+=”<p style=\”text-align:center\”>Too far. Move within “+FAR_RANGE+” m to spin this depot.</p>”; }else{ if(!inRange){ h+=”<p style=\”text-align:center;color:#ff8a3d\”>Out of full range. Spin for 15% rewards. Walk within “+RANGE+” m for 100%.</p>”; } h+=”<button class=\”pbtn wide\” data-act=\”spin\”>SPIN DEPOT”+(inRange?””:” (15%)”)+”</button>”; } openPanel(“SUPPLY DEPOT”,h); panelBody.onclick=function(e){ const a=e.target.getAttribute(“data-act”); if(a===”spin”)spinDepot(d); }; } function spinDepot(d){ const now=Date.now(); if((gState.depots[d.id]||0)>now)return; const dist=distTo(d.lat,d.lon); let mult=boostMult(); let farMode=false; if(dist<=RANGE){ mult*=1.2; }else if(dist<=FAR_RANGE){ mult*=0.15; farMode=true; }else{ showToast(“Depot”,”Too far. Move within “+FAR_RANGE+” m.”); sfx(“error”); return; } const spinHtml=”<div style=\”text-align:center\”><div class=\”big spin-anim\” id=\”spin-box\”>๐ฆ</div><p style=\”text-align:center\”><b>”+d.name+”</b></p><p style=\”text-align:center;color:#ffd93d;font-size:16px;letter-spacing:3px\”>SPINNING</p>”+(farMode?”<p style=\”text-align:center;color:#ff8a3d;font-size:11px\”>15% far rate</p>”:””)+”</div>”; openPanel(“SUPPLY DEPOT”,spinHtml); setTimeout(function(){ const cr=Math.round((10+Math.floor(Math.random()*26))*mult); gState.credits+=cr; const loot=[cr+” credits”+(farMode?” (15% far rate)”:””)]; if(Math.random()<0.6){const n=1+Math.floor(Math.random()*3);gState.items.orb+=n;loot.push(n+” Echo Weave”+(n>1?”s”:””));} if(Math.random()<0.06){gState.items.orb_enhanced=(gState.items.orb_enhanced||0)+1;loot.push(“1 Prism Weave”);} if(Math.random()<0.08){gState.items.shield+=1;loot.push(“1 Turf Shield”);} if(Math.random()<0.1){gState.items.boost+=1;loot.push(“1 Claim Boost”);} if(Math.random()<0.1){gState.items.radar+=1;loot.push(“1 Radar Pack”);} if(Math.random()<0.14){gState.items.shard+=1;loot.push(“1 Wild Shard Token”);} if(Math.random()<0.1){gState.items.train+=60;loot.push(“60 Pet Training XP”);} let cool=60000; if(gState.hired.indexOf(“hacker”)!==-1)cool=45000; gState.depots[d.id]=Date.now()+cool; gState.stats.spins++; addXp(15); logEvent(“Spun “+d.name+”.”); sfx(“spin”); afterChange(); let h=”<div class=\”big\”>๐</div><p class=\”hl\”>DEPOT LOOT</p>”; loot.forEach(function(l){h+=”<div class=\”quest-item\”><span>”+l+”</span></div>”;}); if(gamePanel.classList.contains(“active”))openPanel(“SUPPLY DEPOT”,h); },1100); } function startCatch(c,orbType){ stopCatch(); CATCH.active=true; CATCH.critter=c; const orb=ORB_TYPE_BY_ID[orbType||selectedOrb]||ORB_TYPE_BY_ID.orb; catchBand=(RARITY[c.sp.r].band)*(orb.band||1.0); const t0=performance.now(); function loop(now){ if(!CATCH.active){catchAnim=0;return;} try{ const ring=document.getElementById(“catch-ring”); if(!ring){CATCH.active=false;catchAnim=0;return;} const p=((now-t0)%1700)/1700; const s=2.2-1.8*p; catchScale=s; ring.style.transform=”translate(-50%,-50%) scale(“+s.toFixed(3)+”)”; ring.className=Math.abs(s-1)<=catchBand?”hit”:””; }catch(e){CATCH.active=false;catchAnim=0;return;} catchAnim=requestAnimationFrame(loop); } catchAnim=requestAnimationFrame(loop); } function shardBarHtml(have,need,color){ const pct=Math.round(Math.min(1,have/need)*100); return “<div class=\”shardbar\”><div style=\”width:”+pct+”%;background:”+color+”\”></div></div><p style=\”text-align:center\”>Shards “+have+” / “+need+”</p>”; } function orbSelectorHtml(){ let h=”<div class=\”orbpick\”>”; ORB_TYPES.forEach(function(o){ const count=gState.items[o.id]||0; h+=”<button class=\””+(selectedOrb===o.id?”on”:””)+(count<=0?” empty”:””)+”\” data-orbpick=\””+o.id+”\”>”+o.icon+” “+o.name.split(” “)[0]+” ร”+count+”</button>”; }); h+=”</div>”; return h; } function autoSelectOrb(){ if((gState.items[selectedOrb]||0)>0)return; for(let i=0;i<ORB_TYPES.length;i++){ if((gState.items[ORB_TYPES[i].id]||0)>0){selectedOrb=ORB_TYPES[i].id;return;} } } function rollShardAmount(orb){ if(typeof orb.targetShard===”number”)return orb.targetShard; return Math.max(1,orb.minShard||1); } function addItemLog(text,focus){ if(!Array.isArray(gState.itemLog))gState.itemLog=[]; gState.itemLog.push({t:Date.now(),text:text,focus:focus||null}); if(gState.itemLog.length>200)gState.itemLog=gState.itemLog.slice(-200); saveGameState(gState); } function wildPetLoot(sp){ if(Math.random()>=0.35)return; const drops=[ {label:”+1 Echo Weave”,focus:”bag-orb”,fn:function(){gState.items.orb+=1;}}, {label:”+2 Echo Weaves”,focus:”bag-orb”,fn:function(){gState.items.orb+=2;}}, {label:”+80 credits”,focus:null,fn:function(){gState.credits+=80;}}, {label:”+150 credits”,focus:null,fn:function(){gState.credits+=150;}}, {label:”+1 Prism Weave”,focus:”bag-orb-orb_enhanced”,fn:function(){gState.items.orb_enhanced=(gState.items.orb_enhanced||0)+1;}}, {label:”+1 Wild Shard Token”,focus:”bag-shard”,fn:function(){gState.items.shard+=1;}}, {label:”+60 Pet Training XP”,focus:”bag-train”,fn:function(){gState.items.train+=60;}} ]; const d=drops[Math.floor(Math.random()*drops.length)]; d.fn(); saveGameState(gState); addItemLog(sp.icon+” “+sp.name+” dropped “+d.label,d.focus); updateGameHud(); } function setWeaveStatus(text){ const s=document.getElementById(“weave-status”); if(s)s.textContent=text; } function clearWeaveNodes(){ const nodes=document.querySelectorAll(“[data-weave-node]”); for(let i=0;i<nodes.length;i++){ nodes[i].classList.remove(“lit”); nodes[i].classList.remove(“hit”); } } function setWeaveNode(idx,cls){ const nodes=document.querySelectorAll(“[data-weave-node]”); for(let i=0;i<nodes.length;i++){ if(Number(nodes[i].getAttribute(“data-weave-node”))===idx)nodes[i].classList.add(cls); } } function startWeaveAttempt(c){ if(!c)return; if(CATCH.active&&CATCH.accepting){ setWeaveStatus(“Finish the current weave first.”); return; } if(Date.now()>=c.expires||gState.seen[c.id]){ setWeaveStatus(“This signal has faded.”); return; } const orb=ORB_TYPE_BY_ID[selectedOrb]||ORB_TYPE_BY_ID.orb; const stock=gState.items[orb.id]||0; if(stock<=0){ showOutOfOrbsModal(orb.id); sfx(“error”); return; } const dist=distTo(c.lat,c.lon); if(!(dist<=FAR_RANGE+25)){ setWeaveStatus(“You moved out of range.”); sfx(“error”); return; } gState.items[orb.id]=stock-1; saveGameState(gState); updateGameHud(); stopCatch(); CATCH.active=true; CATCH.critter=c; CATCH.orbType=orb; CATCH.input=[]; CATCH.accepting=false; CATCH.sequence=[0,1,2]; for(let i=CATCH.sequence.length-1;i>0;i–){ const j=Math.floor(Math.random()*(i+1)); const tmp=CATCH.sequence[i]; CATCH.sequence[i]=CATCH.sequence[j]; CATCH.sequence[j]=tmp; } clearWeaveNodes(); setWeaveStatus(“Watch the harmony…”); const base=280; CATCH.sequence.forEach(function(node,i){ CATCH.timers.push(setTimeout(function(){ clearWeaveNodes(); setWeaveNode(node,”lit”); sfx(“shard”); },base+i*620)); CATCH.timers.push(setTimeout(function(){ clearWeaveNodes(); },base+i*620+420)); }); CATCH.timers.push(setTimeout(function(){ CATCH.accepting=true; setWeaveStatus(“Repeat the harmony: tap the nodes in order.”); },base+CATCH.sequence.length*620)); const info=document.getElementById(“catch-info”); if(info)info.innerHTML=”Selected: <b>”+orb.icon+” “+orb.name+”</b> ยท extracts “+orb.targetShard+” shards. Stock: “+(gState.items[orb.id]||0); } function handleWeaveNode(idx){ if(!CATCH.active||!CATCH.accepting)return; CATCH.input.push(idx); const pos=CATCH.input.length-1; if(CATCH.sequence[pos]!==idx){ CATCH.accepting=false; setWeaveStatus(“Discord! The weave collapses.”); sfx(“error”); setTimeout(function(){ if(gamePanel.classList.contains(“active”))openCritterPanel(CATCH.critter); },900); return; } setWeaveNode(idx,”hit”); setTimeout(clearWeaveNodes,220); sfx(“shard”); if(CATCH.input.length===CATCH.sequence.length){ CATCH.accepting=false; setWeaveStatus(“Harmony locked!”); const orb=CATCH.orbType||ORB_TYPE_BY_ID[selectedOrb]||ORB_TYPE_BY_ID.orb; const rolled=rollShardAmount(orb); const extraAnomaly=orb.anomalyBonus>0&&Math.random()<orb.anomalyBonus; setTimeout(function(){ completeSignalCatch(CATCH.critter,rolled,extraAnomaly); },420); } } function completeSignalCatch(c,rolled,extraAnomaly){ if(!c)return; const r=RARITY[c.sp.r]; const need=shardsNeeded(c.sp.r); const dist=distTo(c.lat,c.lon); gState.shards[c.sp.id]=(gState.shards[c.sp.id]||0)+rolled; const have=gState.shards[c.sp.id]; gState.stats.caught++; addXp(Math.max(2,Math.round(r.xp*0.4))); sfx(“shard”); gState.seen[c.id]=c.expires; wildPetLoot(c.sp); if(have>=need){ gState.shards[c.sp.id]=0; gState.codex[c.sp.id]=(gState.codex[c.sp.id]||0)+1; let mult=1.0; if(dist>RANGE&&dist<=FAR_RANGE){mult=0.15;} const cr=Math.round(r.cr*boostMult()*mult); gState.credits+=cr; addXp(r.xp); const added=addPet(c.sp,true); if(extraAnomaly&&added&&!added.anomaly){ added.anomaly=true; added.anomalyPerk=rollAnomaly(false)||”power”; gState.stats.anomalies=(gState.stats.anomalies||0)+1; } logEvent(“Caught a “+c.sp.name+(added.anomaly?” ANOMALY”:””)+”.”); sfx(“catch”); afterChange(); const h=”<div class=\”big shardpop\”>”+c.sp.icon+(added.anomaly?” โญ”:””)+”</div><p style=\”text-align:center\”><b>”+c.sp.name+” assembled and caught!”+(added.anomaly?” ANOMALY!”:””)+”</b></p><p class=\”hl\”>+”+rolled+” SHARDS ยท +”+cr+” CREDITS ยท +”+r.xp+” XP</p><p style=\”text-align:center\”>Pet level “+added.lv+” ยท Power “+petPowerOf(c.sp)+(added.anomaly?” ยท โญ ANOMALY”:””)+”</p>”; openPanel(“PET SECURED”,h,true); return; }else{ const h=”<div class=\”big shardpop\”>”+c.sp.icon+”</div><p style=\”text-align:center\”><b>Successful weave! +”+rolled+” shard”+(rolled>1?”s”:””)+”</b></p>”+shardBarHtml(have,need,r.color)+”<p style=\”text-align:center\”>This encounter is complete. The signal will fade soon. Track another wild signal to continue assembling the pet.</p>”; openPanel(“SIGNAL HARVEST”,h,true); saveGameState(gState); updateGameHud(); return; } } function openCritterPanel(c){ const now=Date.now(); if(now>=c.expires||gState.seen[c.id]){showToast(“Signal Faded”,”That wild signal is gone.”);refreshMap();return;} const r=RARITY[c.sp.r]; const need=shardsNeeded(c.sp.r); const have=gState.shards[c.sp.id]||0; const dist=distTo(c.lat,c.lon); autoSelectOrb(); let h=”<p style=\”text-align:center\”><b>”+c.sp.icon+” “+c.sp.name+”</b><br><span style=\”color:”+r.color+”;letter-spacing:2px;font-size:11px\”>”+r.label+”</span> <span style=\”color:”+c.sp.elColor+”;font-size:11px\”>”+c.sp.elIcon+” “+c.sp.el.toUpperCase()+”</span></p>”; h+=shardBarHtml(have,need,r.color); if(!(dist<=FAR_RANGE)){ h+=”<div class=\”big\”>”+c.sp.icon+”</div><p style=\”text-align:center\”>”+(isFinite(dist)?Math.round(dist)+” m away. Walk within “+FAR_RANGE+” m to engage.”:”Waiting for GPS.”)+”</p>”; openPanel(“WILD SIGNAL”,h,true); return; } if(!(dist<=RANGE)){ h+=”<p style=\”text-align:center;color:#ff8a3d\”>Too far for full bonus. Walk within “+RANGE+” m for 100% rewards. Current range: 15% rewards.</p>”; } h+=”<div id=\”weave-stage\”><div class=\”weave-ring\”></div><button type=\”button\” class=\”weave-node\” data-weave-node=\”0\”>โ</button><button type=\”button\” class=\”weave-node\” data-weave-node=\”1\”>โฆ</button><button type=\”button\” class=\”weave-node\” data-weave-node=\”2\”>โ</button></div>”; h+=”<div id=\”weave-status\”>Select a weave and begin.</div>”; h+=orbSelectorHtml(); const selOrb=ORB_TYPE_BY_ID[selectedOrb]||ORB_TYPE_BY_ID.orb; h+=”<p id=\”catch-info\” style=\”text-align:center\”>Selected: <b>”+selOrb.icon+” “+selOrb.name+”</b> ยท extracts “+selOrb.targetShard+” shards. Stock: “+(gState.items[selOrb.id]||0)+”</p>”; h+=”<div style=\”display:flex; gap:8px; margin-top:8px;\”>”; h+=”<button class=\”pbtn wide\” data-act=\”weave\” style=\”flex:1;\”>BEGIN WEAVE</button>”; h+=”<button class=\”pbtn danger\” data-act=\”flee\” style=\”flex:1; margin-top:0;\”>FLEE</button>”; h+=”</div>”; openPanel(“WILD SIGNAL”,h,true); panelBody.onclick=function(e){ const pb=e.target.closest(“[data-orbpick]”); if(pb){ const oid=pb.getAttribute(“data-orbpick”); selectedOrb=oid; openCritterPanel(c); return; } const wb=e.target.closest(“[data-weave-node]”); if(wb){ handleWeaveNode(parseInt(wb.getAttribute(“data-weave-node”),10)); return; } if(e.target.getAttribute(“data-act”)===”weave”){ startWeaveAttempt(c); return; } if(e.target.getAttribute(“data-act”)===”flee”){ gState.seen[c.id]=c.expires; closePanel(); refreshMap(); showToast(“Wild Signal”,”You fled from the encounter.”); return; } }; } function throwOrb(c){ const info=document.getElementById(“catch-info”); if(!info)return; if(Date.now()>=c.expires||gState.seen[c.id]){info.textContent=”The signal has faded.”;stopCatch();return;} const orbType=ORB_TYPE_BY_ID[selectedOrb]||ORB_TYPE_BY_ID.orb; const stock=gState.items[orbType.id]||0; if(stock<=0){ showOutOfOrbsModal(orbType.id); sfx(“error”); return; } const dist=distTo(c.lat,c.lon); if(!(dist<=FAR_RANGE+25)){info.textContent=”You moved out of range.”;sfx(“error”);return;} gState.items[orbType.id]=stock-1; const r=RARITY[c.sp.r]; const effBand=r.band*(orbType.band||1.0); const hit=Math.abs(catchScale-1)<=effBand; if(hit){ const rolled=rollShardAmount(orbType); const need=shardsNeeded(c.sp.r); gState.shards[c.sp.id]=(gState.shards[c.sp.id]||0)+rolled; const have=gState.shards[c.sp.id]; gState.stats.caught++; addXp(Math.max(2,Math.round(r.xp*0.4))); sfx(“shard”); gState.seen[c.id]=c.expires; const extraAnomaly=orbType.anomalyBonus>0&&Math.random()<orbType.anomalyBonus; wildPetLoot(c.sp); if(have>=need){ gState.shards[c.sp.id]=0; gState.codex[c.sp.id]=(gState.codex[c.sp.id]||0)+1; let mult=1.0; if(dist>RANGE&&dist<=FAR_RANGE){mult=0.15;} const cr=Math.round(r.cr*boostMult()*mult); gState.credits+=cr; addXp(r.xp); const added=addPet(c.sp,true); if(extraAnomaly&&added&&!added.anomaly){ added.anomaly=true; added.anomalyPerk=rollAnomaly(false)||”power”; gState.stats.anomalies=(gState.stats.anomalies||0)+1; } logEvent(“Caught a “+c.sp.name+(added.anomaly?” ANOMALY”:””)+”.”); sfx(“catch”); afterChange(); const h=”<div class=\”big shardpop\”>”+c.sp.icon+(added.anomaly?” โญ”:””)+”</div><p style=\”text-align:center\”><b>”+c.sp.name+” assembled and caught!”+(added.anomaly?” ANOMALY!”:””)+”</b></p><p class=\”hl\”>+”+rolled+” SHARDS ยท +”+cr+” CREDITS ยท +”+r.xp+” XP</p><p style=\”text-align:center\”>Pet level “+added.lv+” ยท Power “+petPowerOf(c.sp)+(added.anomaly?” ยท โญ ANOMALY”:””)+”</p>”; openPanel(“PET SECURED”,h,true); return; }else{ const h=”<div class=\”big shardpop\”>”+c.sp.icon+”</div><p style=\”text-align:center\”><b>Successful catch! +”+rolled+” shard”+(rolled>1?”s”:””)+”</b></p>”+shardBarHtml(have,need,r.color)+”<p style=\”text-align:center\”>This encounter is complete. The signal will fade soon. Track another wild signal to continue assembling the pet.</p>”; openPanel(“SIGNAL HARVEST”,h,true); saveGameState(gState); updateGameHud(); return; } } sfx(“error”); if(Math.random()<r.flee){ gState.seen[c.id]=c.expires; stopCatch(); info.textContent=”It fled! Better luck next time.”; const btn=panelBody.querySelector(“[data-act=throw]”); if(btn)btn.style.display=”none”; refreshMap(); }else{ info.textContent=”Missed! “+orbType.name+” left: “+(gState.items[orbType.id]||0)+”. You may keep trying until you run out of weaves.”; autoSelectOrb(); const stockNow=gState.items[orbType.id]||0; if(stockNow<=0){ setTimeout(function(){showOutOfOrbsModal(orbType.id);},300); } } saveGameState(gState); updateGameHud(); } function openRelicPanel(relic){ const now=Date.now(); if(now>=relic.expires||gState.relicsSeen[relic.id]){ showToast(“Relic Faded”,”That Cosmic Relic has already been extracted.”); refreshMap(); return; } const dist=distTo(relic.lat,relic.lon); const r=RARITY[relic.sp.r]; const need=shardsNeeded(relic.sp.r); const have=gState.shards[relic.sp.id]||0; const remainingUses=resonatorUsesRemaining(); if(!(dist<=FAR_RANGE)){ let h=”<div class=\”big\” style=\”text-align:center\”>๐ </div>”; h+=”<p style=\”text-align:center\”><b>COSMIC RELIC</b></p>”; h+=”<p style=\”text-align:center\”>”+(isFinite(dist)?Math.round(dist)+” m away. Walk within “+FAR_RANGE+” m to engage.”:”Waiting for GPS.”)+”</p>”; h+=”<p style=\”text-align:center;font-size:11px;color:#a0c0e0\”>Cosmic Relics land on claimed or claimable blocks. They break into shards when you Tune & Extract with your Resonator.</p>”; openPanel(“COSMIC RELIC”,h,true); return; } relicState={ relic:relic, tuning:false, tuneStart:0, progress:0, shardsCollected:0, totalShards:RESONATOR_SHARD_COUNT, pulsePhase:0 }; let h=”<div style=\”text-align:center\”><div id=\”relic-crystal\” class=\”\”>๐ </div></div>”; h+=”<p style=\”text-align:center\”><b>COSMIC RELIC</b></p>”; h+=”<p style=\”text-align:center;font-size:11px\”>A temporary crystalline form from the cosmos. It has crystallized the essence of <b style=\”color:”+r.color+”\”>”+relic.sp.icon+” “+relic.sp.name+”</b> (“+r.label+”).</p>”; h+=”<p style=\”text-align:center;font-size:11px;color:#a0c0e0\”>Tool: <b style=\”color:#7fffd0\”>Resonator</b> ยท Action: <b style=\”color:#7fffd0\”>Tune & Extract</b></p>”; h+=”<div id=\”tune-meter\”><div id=\”tune-meter-fill\”></div><div id=\”tune-meter-label\”>HOLD TO TUNE</div></div>”; h+=”<p style=\”text-align:center;font-size:10.5px;color:#a0c0e0\”>Hold the button and keep the connection steady. The Relic resists in pulses. Release before it finishes and progress decays.</p>”; h+=”<div class=\”shard-row\” id=\”relic-shard-row\”></div>”; h+=”<p style=\”text-align:center;font-size:11px\”>Resonance capacity: <b style=\”color:#7fffd0\”>”+remainingUses+” / “+RESONATOR_HOURLY_LIMIT+”</b> this hour</p>”; h+=”<button id=\”tune-btn\” class=\”pbtn wide resonator\” “+(remainingUses<=0?”disabled”:””)+”>TUNE & EXTRACT</button>”; h+=”<button class=\”pbtn wide alt\” data-act=\”releasel\”>BACK AWAY</button>”; openPanel(“COSMIC RELIC”,h,true); renderRelicShardRow(); const tuneBtn=document.getElementById(“tune-btn”); if(tuneBtn){ const startHold=function(e){ e.preventDefault(); if(!relicState)return; if(relicState.tuning)return; if(resonatorUsesRemaining()<=0){ showToast(“Resonator”,”Hourly limit reached. Ten tunes per hour.”); sfx(“error”); return; } relicState.tuning=true; relicState.tuneStart=performance.now(); if(relicPulseTimer)clearInterval(relicPulseTimer); relicPulseTimer=setInterval(function(){ if(!relicState||!relicState.tuning)return; const crystal=document.getElementById(“relic-crystal”); if(crystal){ crystal.classList.toggle(“shake”); } sfx(“shard”); },RESONATOR_RESIST_PULSE_MS); if(relicExtractTimer)clearInterval(relicExtractTimer); relicExtractTimer=setInterval(function(){ if(!relicState)return; const meter=document.getElementById(“tune-meter-fill”); const label=document.getElementById(“tune-meter-label”); if(!meter||!label)return; if(relicState.tuning){ const elapsed=performance.now()-relicState.tuneStart; const prog=Math.min(1,elapsed/RESONATOR_TUNE_MS); relicState.progress=prog; meter.style.width=(prog*100)+”%”; label.textContent=prog>=1?”EXTRACTING!”:”TUNING “+Math.round(prog*100)+”%”; if(prog>=1){ completeRelicTune(); } }else{ if(relicState.progress>0){ relicState.progress=Math.max(0,relicState.progress-0.015); meter.style.width=(relicState.progress*100)+”%”; label.textContent=relicState.progress>0?”CONNECTION FADING”:”HOLD TO TUNE”; } } },50); }; const endHold=function(e){ if(e)e.preventDefault(); if(!relicState)return; relicState.tuning=false; if(relicPulseTimer){clearInterval(relicPulseTimer);relicPulseTimer=null;} const crystal=document.getElementById(“relic-crystal”); if(crystal)crystal.classList.remove(“shake”); }; tuneBtn.onpointerdown=startHold; tuneBtn.onpointerup=endHold; tuneBtn.onpointercancel=endHold; tuneBtn.onpointerleave=endHold; } panelBody.onclick=function(e){ const b=e.target.closest(“[data-act]”); if(!b)return; if(b.getAttribute(“data-act”)===”releasel”){ stopRelic(); closePanel(); refreshMap(); } }; } function renderRelicShardRow(){ const row=document.getElementById(“relic-shard-row”); if(!row||!relicState)return; let h=””; for(let i=0;i<relicState.totalShards;i++){ h+=”<span class=\”shard-icon”+(i<relicState.shardsCollected?” collected”:””)+”\”>๐ </span>”; } row.innerHTML=h; } function completeRelicTune(){ if(!relicState)return; if(resonatorUsesRemaining()<=0){ relicState.tuning=false; showToast(“Resonator”,”Hourly limit reached. Ten tunes per hour.”); sfx(“error”); return; } const relic=relicState.relic; relicState.tuning=false; if(relicPulseTimer){clearInterval(relicPulseTimer);relicPulseTimer=null;} if(relicExtractTimer){clearInterval(relicExtractTimer);relicExtractTimer=null;} gState.resonatorUses.push(Date.now()); relicState.shardsCollected=relicState.totalShards; const r=RARITY[relic.sp.r]; const need=shardsNeeded(relic.sp.r); const rolled=Math.max(1,Math.round((need/2))); gState.shards[relic.sp.id]=(gState.shards[relic.sp.id]||0)+rolled; gState.stats.relicsBroken=(gState.stats.relicsBroken||0)+1; gState.stats.caught++; gState.relicsSeen[relic.id]=relic.expires; sfx(“relic”); const have=gState.shards[relic.sp.id]; if(have>=need){ gState.shards[relic.sp.id]=0; gState.codex[relic.sp.id]=(gState.codex[relic.sp.id]||0)+1; const cr=Math.round(r.cr*boostMult()); gState.credits+=cr; addXp(r.xp); const added=addPet(relic.sp,true); logEvent(“Extracted a Cosmic Relic and assembled “+relic.sp.name+”.”); showToast(“Relic Extracted”,relic.sp.icon+” “+relic.sp.name+” materialized!”); const h=”<div class=\”big shardpop\” style=\”text-align:center\”>”+relic.sp.icon+”</div><p style=\”text-align:center\”><b>”+relic.sp.name+” materialized from the Cosmic Relic!</b></p><p class=\”hl\”>+”+rolled+” shards ยท +”+cr+” credits ยท +”+r.xp+” XP</p><p style=\”text-align:center\”>Pet level “+added.lv+” ยท Power “+petPowerOf(relic.sp)+”</p>”; stopRelic(); openPanel(“RELIC SECURED”,h,true); refreshMap(); saveGameState(gState); updateGameHud(); return; } const h=”<div class=\”big shardpop\” style=\”text-align:center\”>๐ </div><p style=\”text-align:center\”><b>Relic broken into shards!</b></p>”+shardBarHtml(have,need,r.color)+”<p style=\”text-align:center\”>+”+rolled+” shards from this Relic. Track more Cosmic Relics or wild signals to complete this pet.</p>”; stopRelic(); openPanel(“RELIC EXTRACTED”,h,true); refreshMap(); saveGameState(gState); updateGameHud(); } function rollAnomaly(isBreed){ const chance=isBreed?0.05:0.01; if(Math.random()>=chance)return null; const perks=[“power”,”xp”,”credit”,”defense”,”speed”]; return perks[Math.floor(Math.random()*perks.length)]; } function addPet(p,fromCatch){ const monkBonus=gState.hired.indexOf(“monk”)!==-1?1:0; const anomalyPerk=rollAnomaly(false); if(!gState.pets[p.id]){ gState.pets[p.id]={lv:1+monkBonus,xp:petXpForLevel(1+monkBonus),caught:0,anomaly:!!anomalyPerk,anomalyPerk:anomalyPerk||””,stats:{str:10,agi:10,int:10,vit:10,luk:10}}; if(anomalyPerk){ gState.stats.anomalies=(gState.stats.anomalies||0)+1; showToast(“Anomaly”,”โญ A rare anomaly pet appeared!”); } }else{ gState.pets[p.id].caught=(gState.pets[p.id].caught||0)+1; } if(gState.team.length<3&&gState.team.indexOf(p.id)===-1)gState.team.push(p.id); checkPetMilestones(); saveGameState(gState); return gState.pets[p.id]; } function showMissions(tab){ ensurePeriods(); tab=tab||missionTab||”hourly”; missionTab=tab; const names={hourly:”HOURLY”,daily:”DAILY”,once:”ONE-TIME”,walk:”WALK”}; let h=”<div class=\”tabs\”>”; [“hourly”,”daily”,”once”,”walk”].forEach(function(t){ h+=”<button class=\”tab”+(t===tab?” on”:””)+”\” data-tab=\””+t+”\”>”+names[t]+”</button>”; }); h+=”</div>”; const readyList=[]; const progressList=[]; if(tab===”walk”){ WALK_GOALS.forEach(function(g){ const pr=Math.min(g.target,gState.walkDaily.progress||0); const done=pr>=g.target; const cl=!!gState.walkDaily.claimed[g.id]; const item={type:”walk”,obj:g,pr:pr,done:done,cl:cl,target:g.target,reward:g.reward,xp:g.xp,desc:g.desc,name:g.name}; if(done&&!cl){readyList.push(item);}else if(!cl){progressList.push(item);} }); }else{ MISSIONS[tab].forEach(function(m){ const pr=Math.min(m.target,progressOf(tab,m)); const done=pr>=m.target; const cl=isClaimed(tab,m); const item={type:”mission”,obj:m,pr:pr,done:done,cl:cl,target:m.target,reward:m.reward,xp:m.xp,desc:m.desc,name:m.name}; if(done&&!cl){readyList.push(item);}else if(!cl){progressList.push(item);} }); } if(readyList.length){ h+=”<p class=\”hl\”>”+readyList.length+” READY TO CLAIM</p>”; h+=”<button class=\”pbtn wide gold\” data-act=\”claimall\”>CLAIM ALL</button>”; readyList.forEach(function(item){ h+=”<div class=\”mission\” style=\”border-color:#7fffc4\”><div class=\”mtop\”><b>”+item.name+”</b><span>+”+item.reward+” cr</span></div><div class=\”mdesc\”>”+item.desc+”</div><div class=\”pbar\”><div style=\”width:100%\”></div></div><div class=\”mbot\”><span>”+fmtStat(item.type===”walk”?”distance”:item.obj.stat,item.target)+” / “+fmtStat(item.type===”walk”?”distance”:item.obj.stat,item.target)+”</span><button class=\”pbtn\” data-“+(item.type===”walk”?”walk”:””)+”claim=\””+item.obj.id+”\”>CLAIM</button></div></div>”; }); } if(progressList.length){ h+=”<p style=\”margin-top:14px\”><b>IN PROGRESS</b></p>”; progressList.slice(0,80).forEach(function(item){ h+=”<div class=\”mission\”><div class=\”mtop\”><b>”+item.name+”</b><span>+”+item.reward+” cr</span></div><div class=\”mdesc\”>”+item.desc+”</div><div class=\”pbar\”><div style=\”width:”+Math.round(item.pr/item.target*100)+”%\”></div></div><div class=\”mbot\”><span>”+fmtStat(item.type===”walk”?”distance”:item.obj.stat,item.pr)+” / “+fmtStat(item.type===”walk”?”distance”:item.obj.stat,item.target)+”</span><em>IN PROGRESS</em></div></div>”; }); } if(!readyList.length&&!progressList.length){ h+=”<p>No missions available right now. Check back later.</p>”; } openPanel(“MISSIONS”,h); panelBody.onclick=function(e){ const t=e.target.closest(“[data-tab]”); if(t){showMissions(t.getAttribute(“data-tab”));return;} const ca=e.target.closest(“[data-act=claimall]”); if(ca){ readyList.forEach(function(item){ if(item.type===”walk”)claimWalkGoal(item.obj.id); else claimMission(tab,item.obj.id); }); showMissions(tab); return; } const cb=e.target.closest(“[data-claim]”); if(cb){claimMission(tab,cb.getAttribute(“data-claim”));showMissions(tab);} const wb=e.target.closest(“[data-walkclaim]”); if(wb){claimWalkGoal(wb.getAttribute(“data-walkclaim”));showMissions(“walk”);} }; } function passItemHtml(q,pr,done){ let reward=”+”+q.credits+” cr ยท +”+q.xp+” xp”; if(q.item){ if(q.item.type===”orb”)reward+=” ยท “+q.item.amount+” Weaves”; else if(q.item.type===”pet”)reward+=” ยท RANDOM RARE PET”; } let h=”<div class=\”mission\””+(done?” style=\”border-color:#7fffc4\””:””)+”>”; h+=”<div class=\”mtop\”><b>”+q.name+”</b><span>+”+q.credits+” cr</span></div>”; h+=”<div class=\”mdesc\”>”+q.desc+”</div>”; h+=”<div class=\”pbar\”><div style=\”width:”+Math.round(pr/q.target*100)+”%\”></div></div>”; h+=”<div class=\”mbot\”><span>”+fmtStat(q.stat,pr)+” / “+fmtStat(q.stat,q.target)+”</span>”; h+=done?”<button class=\”pbtn gold\” data-passclaim=\””+q.id+”\”>CLAIM</button>”:”<em>IN PROGRESS</em>”; h+=”</div><p style=\”font-size:10px;color:#7fffc4\”>”+reward+”</p>”; h+=”</div>”; return h; } function claimPassQuest(id){ ensureSeason(); const q=PASS_QUESTS.find(function(x){return x.id===id;}); if(!q)return; if(gState.pass.claimed[id])return; if(passProgress(q)<q.target)return; gState.pass.claimed[id]=true; gState.credits+=q.credits; addXp(q.xp); if(q.item){ if(q.item.type===”orb”)gState.items.orb+=q.item.amount; else if(q.item.type===”pet”){ const pool=PET_POOLS.rare.concat(PET_POOLS.epic); const p=pool[Math.floor(Math.random()*pool.length)]; addPet(p,false); } } logEvent(“Pass reward claimed: “+q.name+”.”); showToast(“Pass Reward”,q.name+” +”+q.credits+” credits.”); sfx(“pass”); saveGameState(gState); updateGameHud(); showPass(); } function showPass(){ ensureSeason(); const total=PASS_QUESTS.length; let done=0; PASS_QUESTS.forEach(function(q){if(gState.pass.claimed[q.id])done++;}); const ready=[]; const inprog=[]; PASS_QUESTS.forEach(function(q){ const pr=Math.min(q.target,passProgress(q)); const isDone=pr>=q.target; const isClaimed=!!gState.pass.claimed[q.id]; if(isDone&&!isClaimed)ready.push({q:q,pr:pr}); else if(!isClaimed)inprog.push({q:q,pr:pr}); }); const seasonEnd=(gState.pass.season+1)*SEASON_LENGTH_MS; const daysLeft=Math.max(0,Math.ceil((seasonEnd-Date.now())/86400000)); let h=”<p>Season <b>”+gState.pass.season+”</b> ยท Progress <b>”+done+” / “+total+”</b></p>”; h+=”<p style=\”font-size:11px;color:#7fffc4\”>Season pass quests reset automatically every 3 weeks. Gameplay progress feeds every objective.</p>”; h+=”<p>Season ends in roughly <b>”+daysLeft+”</b> day”+(daysLeft===1?””:”s”)+”.</p>”; h+=”<div class=\”pbar\”><div style=\”width:”+Math.round(done/total*100)+”%\”></div></div>”; if(ready.length){ h+=”<p class=\”hl\”>”+ready.length+” READY TO CLAIM</p>”; h+=”<button class=\”pbtn wide gold\” data-passclaimall=\”1\”>CLAIM ALL</button>”; ready.forEach(function(r){h+=passItemHtml(r.q,r.pr,true);}); } if(inprog.length){ h+=”<p style=\”margin-top:12px\”><b>IN PROGRESS</b></p>”; inprog.slice(0,80).forEach(function(r){h+=passItemHtml(r.q,r.pr,false);}); } if(!ready.length&&!inprog.length){ h+=”<p>All season pass quests completed. Amazing work.</p>”; } openPanel(“SEASON PASS”,h); panelBody.onclick=function(e){ const ca=e.target.closest(“[data-passclaimall]”); if(ca){ ready.forEach(function(r){claimPassQuest(r.q.id);}); return; } const cb=e.target.closest(“[data-passclaim]”); if(cb){claimPassQuest(cb.getAttribute(“data-passclaim”));} }; } function showDaily(){ const today=dayKey(new Date()); const canSpin=gState.dailySpinLast!==today; let h=”<p>Daily rewards keep your streak alive. Log in and spin every day to climb the calendar.</p>”; h+=”<p>Current streak: <b>”+gState.streak+”</b> day”+(gState.streak===1?””:”s”)+” ยท Last login: “+(gState.lastDay||”never”)+”</p>”; h+=”<p><b>7-DAY LOGIN CALENDAR</b></p>”; const streakPos=Math.min(7,((gState.streak-1)%7)+1); h+=”<div class=\”cgrid\” style=\”grid-template-columns:repeat(7,1fr)\”>”; const rewards=[“20″,”25″,”30″,”40″,”50″,”75″,”200 + 5 Weaves”]; for(let d=1;d<=7;d++){ const reached=d<=streakPos; h+=”<div class=\”ccell”+(reached?””:” off”)+”\” style=\”border-color:#ffd93d\”><span class=\”ci\” style=\”font-size:18px\”>๐</span>Day “+d+”<br><span style=\”font-size:9px;color:#ffd93d\”>”+rewards[d-1]+” cr</span></div>”; } h+=”</div>”; h+=”<p style=\”font-size:11px;color:#7fffc4\”>Daily login bonus is granted automatically the first time you open Street Claim each day. Day 7 grants 5 Echo Weaves.</p>”; h+=”<p><b>FREE DAILY SPIN</b></p>”; if(canSpin){ h+=”<button class=\”pbtn wide gold\” data-act=\”spin\”>SPIN THE WHEEL</button>”; }else{ h+=”<p style=\”text-align:center;color:#a0c0e0\”>You already spun today. Come back tomorrow for another free spin.</p>”; } h+=”<p style=\”font-size:11px;color:#a0c0e0\”>The wheel can award credits, weaves, shields, or the rare 2000 credit jackpot.</p>”; openPanel(“DAILY REWARDS”,h); panelBody.onclick=function(e){ if(e.target.getAttribute(“data-act”)===”spin”)doDailySpin(); }; } function applySpinPrize(p){ if(p.type===”credits”)gState.credits+=p.amount; else if(p.type===”orb”)gState.items.orb+=p.amount; else if(p.type===”orbx”)gState.items[p.orbType]=(gState.items[p.orbType]||0)+p.amount; else if(p.type===”shield”)gState.items.shield+=p.amount; } function doDailySpin(){ const today=dayKey(new Date()); if(gState.dailySpinLast===today)return; gState.dailySpinLast=today; let roll=Math.random()*100; let prize; if(roll<1)prize=SPIN_PRIZES[7]; else if(roll<6)prize=SPIN_PRIZES[6]; else if(roll<16)prize=SPIN_PRIZES[5]; else if(roll<31)prize=SPIN_PRIZES[4]; else if(roll<51)prize=SPIN_PRIZES[3]; else if(roll<71)prize=SPIN_PRIZES[2]; else if(roll<86)prize=SPIN_PRIZES[1]; else prize=SPIN_PRIZES[0]; applySpinPrize(prize); logEvent(“Daily spin: “+prize.label+”.”); showToast(“Daily Spin”,prize.label+”!”); sfx(“spin”); saveGameState(gState); updateGameHud(); showDaily(); } function shopItemsFiltered(){ let pool=ALL_SHOP; if(shopQuery){ const q=shopQuery.toLowerCase(); pool=pool.filter(function(it){return it.name.toLowerCase().indexOf(q)!==-1;}); } if(shopTab===”core”)pool=pool.filter(function(it){return SHOP.indexOf(it)!==-1;}); else if(shopTab===”orb”)pool=pool.filter(function(it){return it.kind===”orb”||it.kind===”orbx”;}); else if(shopTab===”pet”)pool=pool.filter(function(it){return it.kind===”pet”||it.kind===”shard”||it.kind===”train”||it.kind===”xp”;}); else if(shopTab===”def”)pool=pool.filter(function(it){return it.kind===”shield”||it.kind===”armor”;}); else if(shopTab===”boost”)pool=pool.filter(function(it){return it.kind===”boost”||it.kind===”radar”;}); else if(shopTab===”breed”)pool=pool.filter(function(it){return it.kind===”breeditem”;}); return pool; } function showShop(){ let h=”<p>Credits: <b>”+gState.credits+”</b> ยท Catalogue entries: <b>”+ALL_SHOP.length+”</b></p>”; h+=”<p style=\”font-size:11px;color:#7fffc4\”>Resonance weaves sold at various price tiers. Shard items apply to your currently targeted species.</p>”; h+=”<input class=\”shopsearch\” id=\”shop-search\” type=\”text\” placeholder=\”Search the shop\” value=\””+shopQuery.replace(/”/g,””)+”\”>”; h+=”<div class=\”shopcat\”>”; [[“base”,”ALL”],[“core”,”CORE”],[“orb”,”WEAVES”],[“pet”,”PETS”],[“def”,”DEFENSE”],[“boost”,”BOOSTS”],[“breed”,”BREED”]].forEach(function(t){ h+=”<button class=\””+(shopTab===t[0]?”on”:””)+”\” data-shoptab=\””+t[0]+”\”>”+t[1]+”</button>”; }); h+=”</div>”; const pool=shopItemsFiltered(); h+=”<p style=\”font-size:11px\”>Showing “+pool.length+” items.</p>”; pool.forEach(function(it){ const owned=it.id===”neon”&&gState.neon; h+=”<div class=\”shop-item\”><div><b>”+it.icon+” “+it.name+”</b><br><span style=\”font-size:11px;color:#a0c0e0\”>”+it.desc+” ยท “+it.cost+” cr</span></div>”+(owned?”<em>OWNED</em>”:”<button data-buy=\””+it.id+”\”>BUY</button>”)+”</div>”; }); openPanel(“SHOP”,h); const search=document.getElementById(“shop-search”); if(search){ search.oninput=function(){shopQuery=this.value;const pos=this.selectionStart;showShop();const s2=document.getElementById(“shop-search”);if(s2){s2.focus();try{s2.setSelectionRange(pos,pos);}catch(e){}}}; } panelBody.onclick=function(e){ const b=e.target.closest(“[data-buy]”); if(b){buyItem(b.getAttribute(“data-buy”));return;} const t=e.target.closest(“[data-shoptab]”); if(t){shopTab=t.getAttribute(“data-shoptab”);showShop();} }; } function findShopItem(id){ for(let i=0;i<ALL_SHOP.length;i++){if(ALL_SHOP[i].id===id)return ALL_SHOP[i];} return null; } function applyShopEffect(it){ const amt=it.amount||1; if(it.kind===”orb”)gState.items.orb+=amt; else if(it.kind===”orbx”)gState.items[it.orbType]=(gState.items[it.orbType]||0)+amt; else if(it.kind===”shield”)gState.items.shield+=amt; else if(it.kind===”boost”)gState.items.boost+=amt; else if(it.kind===”radar”)gState.items.radar+=amt; else if(it.kind===”armor”){ const keys=ownedKeys(); if(!keys.length){gState.items.orb+=amt;return;} for(let i=0;i<amt;i++){ const k=keys[Math.floor(Math.random()*keys.length)]; const b=gState.blocks[k]; if(b&&b.lv<5)b.lv+=1; } } else if(it.kind===”shard”){ if(gState.shardTarget&&PET_BY_ID[gState.shardTarget]){ gState.shards[gState.shardTarget]=(gState.shards[gState.shardTarget]||0)+amt; }else{ const p=PETS[Math.floor(Math.random()*PETS.length)]; gState.shards[p.id]=(gState.shards[p.id]||0)+amt; } } else if(it.kind===”train”){ const ids=Object.keys(gState.pets); if(!ids.length){gState.items.orb+=2;return;} const id=ids[Math.floor(Math.random()*ids.length)]; addPetXp(id,amt); } else if(it.kind===”credits”)gState.credits+=amt*30; else if(it.kind===”xp”)addXp(amt*15); else if(it.kind===”pet”){ const pool=PET_POOLS.common.concat(PET_POOLS.uncommon); const p=pool[Math.floor(Math.random()*pool.length)]; addPet(p,false); } else if(it.kind===”breeditem”){ if(it.id===”everstone”)gState.items.everstone=(gState.items.everstone||0)+amt; else if(it.id===”ovalcharm”)gState.items.ovalcharm=(gState.items.ovalcharm||0)+amt; else if(it.id===”shinycharm”)gState.items.shinycharm=(gState.items.shinycharm||0)+amt; } } function animateMoneyLoss(amount){ const rect=gameCoinsEl.getBoundingClientRect(); const el=document.createElement(“div”); el.className=”money-float”; el.textContent=”-“+amount+” CR”; el.style.left=(rect.left+rect.width/2)+”px”; el.style.top=(rect.top+rect.height/2)+”px”; document.body.appendChild(el); setTimeout(function(){if(el.parentNode)el.parentNode.removeChild(el);},1200); } function buyItem(id){ const it=findShopItem(id); if(!it)return; if(id===”neon”&&gState.neon){showToast(“Shop”,”Neon Marker already owned.”);return;} if(id===”armor”&&!ownedKeys().length){showToast(“Shop”,”You do not own any blocks yet.”);sfx(“error”);return;} if(gState.credits<it.cost){showToast(“Shop”,”Not enough credits. You need “+it.cost+”.”);sfx(“error”);return;} gState.credits-=it.cost; animateMoneyLoss(it.cost); if(id===”neon”){gState.neon=true;gState.markerNeon=true;updateYouIcon();} else applyShopEffect(it); gState.stats.purchases++; addXp(5); logEvent(“Bought “+it.name+”.”); sfx(“coin”); showToast(“Purchase Complete”,”You bought “+it.name+” for “+it.cost+” credits.”); afterChange(); const sc=panelBody.scrollTop; const sc2=gamePanelCard.scrollTop; showShop(); panelBody.scrollTop=sc; gamePanelCard.scrollTop=sc2; } function showInventory(focus){ const now=Date.now(); let h=”<p>Items you own. Activate power-ups to use them. Purchased items appear here.</p>”; h+=”<div class=\”shop-item\” id=\”bag-orb\”><div><b>ใฐ๏ธ Echo Weaves</b><br><span style=\”font-size:11px;color:#a0c0e0\”>Used to weave wild signals.</span></div><span>x”+gState.items.orb+”</span></div>”; h+=”<div class=\”shop-item\” id=\”bag-resonator\”><div><b>๐ก Resonance Capacity</b><br><span style=\”font-size:11px;color:#a0c0e0\”>Ten tunes per hour. Remaining: “+resonatorUsesRemaining()+” / “+RESONATOR_HOURLY_LIMIT+”.</span></div><span>”+resonatorUsesRemaining()+” / “+RESONATOR_HOURLY_LIMIT+”</span></div>”; ORB_TYPES.forEach(function(o){ if(o.id===”orb”)return; const count=gState.items[o.id]||0; if(count<=0)return; h+=”<div class=\”shop-item\” id=\”bag-orb-“+o.id+”\”><div><b>”+o.icon+” “+o.name+”</b><br><span style=\”font-size:11px;color:#a0c0e0\”>”+o.desc+”</span></div><span>x”+count+”</span></div>”; }); h+=”<div class=\”shop-item\” id=\”bag-shard\”><div><b>๐ Wild Shard Tokens</b><br><span style=\”font-size:11px;color:#a0c0e0\”>Adds one shard to your targeted species.</span></div><div>x”+(gState.items.shard||0)+” “+(gState.items.shard>0?”<button data-useshard=\”1\”>USE</button>”:””)+”</div></div>”; h+=”<div class=\”shop-item\” id=\”bag-train\”><div><b>๐งช Training XP</b><br><span style=\”font-size:11px;color:#a0c0e0\”>Applied to a random pet.</span></div><div>x”+(gState.items.train||0)+” “+(gState.items.train>0?”<button data-usetrain=\”1\”>USE</button>”:””)+”</div></div>”; [[“shield”,”๐ก๏ธ Turf Shield”],[“boost”,”โก Claim Boost”],[“radar”,”๐ก Radar Pack”]].forEach(function(p){ const active=gState.effects[p[0]]>now; h+=”<div class=\”shop-item\” id=\”bag-“+p[0]+”\”><div><b>”+p[1]+”</b><br><span style=\”font-size:11px;color:#a0c0e0\”>”+(active?”Active for “+fmtDur(gState.effects[p[0]]-now):”Not active”)+”</span></div><div>x”+gState.items[p[0]]+” “+(gState.items[p[0]]>0?”<button data-use=\””+p[0]+”\”>USE</button>”:””)+”</div></div>”; }); if(gState.neon){ h+=”<div class=\”shop-item\”><div><b>โจ Neon Marker</b><br><span style=\”font-size:11px;color:#a0c0e0\”>”+(gState.markerNeon?”Equipped”:”Not equipped”)+”</span></div><button data-toggle=\”neon\”>”+(gState.markerNeon?”UNEQUIP”:”EQUIP”)+”</button></div>”; } openPanel(“BAG”,h); if(focus){ const target=document.getElementById(focus); if(target){ target.classList.add(“bag-highlight”); setTimeout(function(){try{target.scrollIntoView({block:”center”,behavior:”smooth”});}catch(e){target.scrollIntoView();}},80); setTimeout(function(){target.classList.remove(“bag-highlight”);},2400); } } panelBody.onclick=function(e){ const u=e.target.closest(“[data-use]”); if(u){useItem(u.getAttribute(“data-use”));return;} const t=e.target.closest(“[data-toggle]”); if(t){gState.markerNeon=!gState.markerNeon;updateYouIcon();saveGameState(gState);showInventory();return;} const s=e.target.closest(“[data-useshard]”); if(s){useShardToken();return;} const tr=e.target.closest(“[data-usetrain]”); if(tr){useTrainToken();return;} }; } function useShardToken(){ if(!gState.items.shard||gState.items.shard<=0)return; gState.items.shard–; if(gState.shardTarget&&PET_BY_ID[gState.shardTarget]){ gState.shards[gState.shardTarget]=(gState.shards[gState.shardTarget]||0)+1; showToast(“Shard Token”,PET_BY_ID[gState.shardTarget].icon+” “+PET_BY_ID[gState.shardTarget].name+” shard added.”); }else{ const p=PETS[Math.floor(Math.random()*PETS.length)]; gState.shards[p.id]=(gState.shards[p.id]||0)+1; showToast(“Shard Token”,p.icon+” “+p.name+” shard added.”); } sfx(“shard”); afterChange(); showInventory(); } function useTrainToken(){ if(!gState.items.train||gState.items.train<=0)return; const ids=Object.keys(gState.pets); if(!ids.length){showToast(“Training”,”You have no pets yet.”);sfx(“error”);return;} const id=ids[Math.floor(Math.random()*ids.length)]; const amount=gState.items.train; gState.items.train=0; addPetXp(id,amount); sfx(“level”); afterChange(); showInventory(); } function useItem(id){ if(!gState.items[id]||gState.items[id]<=0)return; gState.items[id]–; const now=Date.now(); const dur=id===”shield”?7200000:id===”boost”?3600000:1800000; gState.effects[id]=Math.max(gState.effects[id],now)+dur; const names={shield:”Turf Shield”,boost:”Claim Boost”,radar:”Radar Pack”}; logEvent(“Activated “+names[id]+”.”); showToast(“Activated”,names[id]+” is now active.”); sfx(“claim”); afterChange(); showInventory(); } function addPetXp(id,amount){ const p=PET_BY_ID[id]; if(!p)return; const o=gState.pets[id]; if(!o)return; let amt=amount; if(gState.hired.indexOf(“trainer”)!==-1)amt=Math.round(amt*1.5); if(o.anomaly&&o.anomalyPerk===”xp”)amt=Math.round(amt*1.3); o.xp+=amt; gState.stats.trained++; const before=o.lv; o.lv=petLevel(o.xp); if(o.lv>before)showToast(“Pet Level Up”,p.icon+” “+p.name+” is now level “+o.lv+”.”); saveGameState(gState); } function petCardHtml(p,selected,equipped){ const o=gState.pets[p.id]; const lv=o?o.lv:1; const r=RARITY[p.r]; const anomalyMark=o&&o.anomaly?” <span class=\”anomaly\”>โญ</span>”:””; const pokeType=getPokeType(p); return “<div class=\”petcard”+(selected?” on”:””)+”\” data-petid=\””+p.id+”\” style=\”border-color:”+r.color+”\”>”+ “<span class=\”pi\”>”+p.icon+anomalyMark+”</span>”+ “<div class=\”pn\”>”+p.name+”</div>”+ “<div class=\”pr\” style=\”color:”+r.color+”\”>”+r.label+” ยท “+pokeType.toUpperCase()+”</div>”+ “<div class=\”pl\”>”+(equipped?”TEAM ยท “:””)+”Lv “+lv+” ยท “+petPowerOf(p)+”</div>”+ “</div>”; } function ownedPetIds(){ return Object.keys(gState.pets); } function showPets(tab){ tab=tab||petTab; petTab=tab; let h=”<div class=\”tabs\”>”; [[“collection”,”COLLECTION”],[“team”,”TEAM”],[“train”,”TRAIN”],[“stats”,”STATS”],[“merge”,”MERGE”],[“shards”,”SHARDS”],[“breed”,”BREED”],[“eggs”,”EGGS”]].forEach(function(t){ h+=”<button class=\”tab”+(tab===t[0]?” on”:””)+”\” data-pettab=\””+t[0]+”\”>”+t[1]+”</button>”; }); h+=”</div>”; if(tab===”collection”){ h+=”<p>Collected <b>”+ownedPetIds().length+”</b> of <b>”+PETS.length+”</b> pets. Tap a pet to select for merging.</p>”; h+=”<input class=\”shopsearch\” id=\”pet-search\” type=\”text\” placeholder=\”Search your pets\” value=\””+petQuery.replace(/”/g,””)+”\”>”; h+=”<div class=\”shopcat\”>”; [[“all”,”ALL”],[“fire”,”๐ฅ”],[“water”,”๐ง”],[“earth”,”๐ฟ”],[“air”,”๐ช๏ธ”],[“electric”,”โก”],[“shadow”,”๐”],[“light”,”โจ”],[“ice”,”โ๏ธ”],[“metal”,”โ๏ธ”],[“cosmic”,”๐”]].forEach(function(c){ h+=”<button class=\””+(petCat===c[0]?”on”:””)+”\” data-petcat=\””+c[0]+”\”>”+c[1]+”</button>”; }); h+=”</div>”; let list=ownedPetIds().map(function(id){return PET_BY_ID[id];}).filter(Boolean); if(petQuery){ const q=petQuery.toLowerCase(); list=list.filter(function(p){return p.name.toLowerCase().indexOf(q)!==-1;}); } if(petCat!==”all”){ list=list.filter(function(p){return p.cat===petCat||p.el===petCat;}); } if(petSort===”power”)list.sort(function(a,b){return petPowerOf(b)-petPowerOf(a);}); else if(petSort===”level”)list.sort(function(a,b){const oa=gState.pets[a.id],ob=gState.pets[b.id];return (ob?ob.lv:1)-(oa?oa.lv:1);}); else list.sort(function(a,b){return RARITY_INDEX[b.r]-RARITY_INDEX[a.r];}); h+=”<div class=\”shopcat\”>”; [[“power”,”POWER”],[“level”,”LEVEL”],[“rarity”,”RARITY”]].forEach(function(s){ h+=”<button class=\””+(petSort===s[0]?”on”:””)+”\” data-petsort=\””+s[0]+”\”>”+s[1]+”</button>”; }); h+=”</div>”; if(!list.length)h+=”<p>No pets match. Catch wild signals to grow your collection.</p>”; h+=”<div class=\”petgrid\”>”; list.forEach(function(p){ h+=petCardHtml(p,petSel.indexOf(p.id)!==-1,gState.team.indexOf(p.id)!==-1); }); h+=”</div>”; if(petSel.length){ h+=”<p>Selected: “+petSel.length+” pets.</p>”; h+=”<button class=\”pbtn wide\” data-act=\”clearsel\”>CLEAR SELECTION</button>”; } } else if(tab===”team”){ h+=”<p>Your battle team holds up to 3 pets. Team power: <b>”+teamPowerTotal()+”</b></p>”; for(let i=0;i<3;i++){ const id=gState.team[i]; const p=id?PET_BY_ID[id]:null; const o=p?gState.pets[p.id]:null; h+=”<div class=\”teamrow\”><div class=\”ti\”>”+(p?p.icon+(o&&o.anomaly?”โญ”:””):”โ”)+”</div><div style=\”flex:1\”>”+(p?”<b>”+p.name+”</b><br><span style=\”font-size:11px;color:#a0c0e0\”>Lv “+(o?o.lv:1)+” ยท Power “+petPowerOf(p)+” ยท “+RARITY[p.r].label+(o&&o.anomaly?” ยท ANOMALY”:””)+”</span>”:”Empty slot”)+”</div>”+(p?”<button data-remove=\””+i+”\”>REMOVE</button>”:””)+”</div>”; } h+=”<p>Tap a pet below to add or swap into the team.</p><div class=\”petgrid\”>”; ownedPetIds().forEach(function(id){ const p=PET_BY_ID[id]; if(!p)return; h+=petCardHtml(p,false,gState.team.indexOf(id)!==-1); }); h+=”</div>”; } else if(tab===”train”){ h+=”<p>Spend credits to train a pet. Training raises experience and level. Stats can also be trained.</p>”; const ids=ownedPetIds(); if(!ids.length)h+=”<p>You have no pets to train yet.</p>”; ids.slice(0,80).forEach(function(id){ const p=PET_BY_ID[id]; if(!p)return; const o=gState.pets[id]; const lv=o.lv; const cost=600+lv*450; const nextXp=petXpForLevel(Math.min(50,lv+1)); h+=”<div class=\”shop-item\”><div><b>”+p.icon+(o.anomaly?”โญ”:””)+” “+p.name+”</b><br><span style=\”font-size:11px;color:#a0c0e0\”>Lv “+lv+” ยท “+o.xp+” / “+nextXp+” XP ยท Power “+petPowerOf(p)+”</span></div><button data-train=\””+id+”\”>TRAIN “+cost+” CR</button></div>”; }); } else if(tab===”stats”){ h+=”<p>View and train individual pet stats. Each training costs credits and raises one stat.</p>”; const ids=ownedPetIds(); if(!ids.length)h+=”<p>You have no pets yet.</p>”; ids.slice(0,40).forEach(function(id){ const p=PET_BY_ID[id]; if(!p)return; const o=gState.pets[id]; if(!o.stats)o.stats={str:10,agi:10,int:10,vit:10,luk:10}; const cost=400+o.lv*200; h+=”<div class=\”mission\”><div class=\”mtop\”><b>”+p.icon+(o.anomaly?”โญ”:””)+” “+p.name+”</b><span>Lv “+o.lv+”</span></div>”; h+=”<div class=\”statrow\”><span>STR</span><b>”+o.stats.str+”</b><button data-stat=\””+id+”:str\” class=\”pbtn\”>+”+cost+”</button></div>”; h+=”<div class=\”statrow\”><span>AGI</span><b>”+o.stats.agi+”</b><button data-stat=\””+id+”:agi\” class=\”pbtn\”>+”+cost+”</button></div>”; h+=”<div class=\”statrow\”><span>INT</span><b>”+o.stats.int+”</b><button data-stat=\””+id+”:int\” class=\”pbtn\”>+”+cost+”</button></div>”; h+=”<div class=\”statrow\”><span>VIT</span><b>”+o.stats.vit+”</b><button data-stat=\””+id+”:vit\” class=\”pbtn\”>+”+cost+”</button></div>”; h+=”<div class=\”statrow\”><span>LUK</span><b>”+o.stats.luk+”</b><button data-stat=\””+id+”:luk\” class=\”pbtn\”>+”+cost+”</button></div>”; if(o.anomaly)h+=”<p style=\”font-size:11px;color:#ffd700\”>โญ ANOMALY ยท Perk: “+(o.anomalyPerk||”unknown”).toUpperCase()+”</p>”; h+=”</div>”; }); } else if(tab===”merge”){ h+=”<p>Select 3 pets of the same rarity to fuse them into one pet of the next rarity. Breeding has a 5% chance to create an Anomaly.</p>”; h+=”<p>Selected: <b>”+petSel.length+”</b> of 3</p>”; if(petSel.length===3){ let ok=true; let rk=null; for(let i=0;i<3;i++){ const p=PET_BY_ID[petSel[i]]; if(!p){ok=false;break;} if(rk===null)rk=p.r; else if(p.r!==rk)ok=false; } if(!ok)h+=”<p style=\”color:#ff8080\”>All three pets must share the same rarity.</p>”; else if(rk===”legendary”)h+=”<p style=\”color:#ff8080\”>Legendary pets cannot be merged further.</p>”; else h+=”<button class=\”pbtn wide gold\” data-act=\”domerge\”>FUSE INTO “+RARITY_ORDER[RARITY_INDEX[rk]+1].toUpperCase()+”</button>”; } h+=”<div class=\”petgrid\”>”; ownedPetIds().forEach(function(id){ const p=PET_BY_ID[id]; if(!p)return; h+=petCardHtml(p,petSel.indexOf(id)!==-1,false); }); h+=”</div>”; if(petSel.length)h+=”<button class=\”pbtn wide\” data-act=\”clearsel\”>CLEAR SELECTION</button>”; } else if(tab===”shards”){ h+=”<p>Partial signal shards waiting to be completed. Wild signal encounters grant a targeted shard amount determined by your weave.</p>”; h+=”<p>Tap a species below to set it as the target for shard items and mission rewards.</p>”; if(gState.shardTarget&&PET_BY_ID[gState.shardTarget]){ h+=”<p class=\”hl\”>TARGET: “+PET_BY_ID[gState.shardTarget].icon+” “+PET_BY_ID[gState.shardTarget].name+”</p>”; h+=”<button class=\”pbtn wide alt\” data-act=\”cleartarget\”>CLEAR TARGET</button>”; }else{ h+=”<p style=\”font-size:11px;color:#ffd93d\”>No target selected. Shard items and rewards will pick a random species.</p>”; } const keys=Object.keys(gState.shards).filter(function(k){return gState.shards[k]>0&&PET_BY_ID[k];}); if(!keys.length)h+=”<p>No partial shards right now.</p>”; keys.forEach(function(k){ const p=PET_BY_ID[k]; const need=shardsNeeded(p.r); const have=gState.shards[k]; const isTarget=gState.shardTarget===k; h+=”<div class=\”shop-item\” style=\”border-color:”+(isTarget?”#7fffc4″:”rgba(120,180,255,0.2)”)+”\”><div><b>”+p.icon+” “+p.name+”</b><br><span style=\”font-size:11px;color:#a0c0e0\”>”+RARITY[p.r].label+” ยท “+have+” / “+need+” shards</span></div><div><span>”+Math.round(have/need*100)+”%</span><br><button class=\”pbtn\” data-target=\””+k+”\”>”+(isTarget?”TARGETED”:”TARGET”)+”</button></div></div>”; }); h+=”<p>Shop shard items and shard tokens will apply to your target when one is set.</p>”; } else if(tab===”breed”){ h+=renderBreedTab(); } else if(tab===”eggs”){ h+=renderEggTab(); } openPanel(“PETS”,h); const search=document.getElementById(“pet-search”); if(search){ search.oninput=function(){petQuery=this.value;const pos=this.selectionStart;showPets(petTab);const s2=document.getElementById(“pet-search”);if(s2){s2.focus();try{s2.setSelectionRange(pos,pos);}catch(e){}}}; } panelBody.onclick=function(e){ const t=e.target.closest(“[data-pettab]”); if(t){petSel=[];showPets(t.getAttribute(“data-pettab”));return;} const s=e.target.closest(“[data-petsort]”); if(s){petSort=s.getAttribute(“data-petsort”);showPets(petTab);return;} const ct=e.target.closest(“[data-petcat]”); if(ct){petCat=ct.getAttribute(“data-petcat”);showPets(petTab);return;} const tgt=e.target.closest(“[data-target]”); if(tgt){gState.shardTarget=tgt.getAttribute(“data-target”);saveGameState(gState);showPets(“shards”);return;} const c=e.target.closest(“[data-petid]”); if(c){ const id=c.getAttribute(“data-petid”); if(petTab===”team”){ const at=gState.team.indexOf(id); if(at>=0){gState.team.splice(at,1);} else{ if(gState.team.length>=3)gState.team.shift(); gState.team.push(id); } saveGameState(gState); showPets(petTab); return; } if(petTab===”merge”||petTab===”collection”){ const at=petSel.indexOf(id); if(at>=0)petSel.splice(at,1); else{ if(petSel.length>=3)petSel.shift(); petSel.push(id); } showPets(petTab); return; } if(petTab===”breed”){ handleBreedSelect(id); return; } return; } const rm=e.target.closest(“[data-remove]”); if(rm){gState.team.splice(parseInt(rm.getAttribute(“data-remove”),10),1);saveGameState(gState);showPets(petTab);return;} const tr=e.target.closest(“[data-train]”); if(tr){trainPet(tr.getAttribute(“data-train”));return;} const st=e.target.closest(“[data-stat]”); if(st){trainStat(st.getAttribute(“data-stat”));return;} const cl=e.target.closest(“[data-act]”); if(cl){ const a=cl.getAttribute(“data-act”); if(a===”clearsel”){petSel=[];showPets(petTab);return;} if(a===”domerge”){doMerge();return;} if(a===”cleartarget”){gState.shardTarget=””;saveGameState(gState);showPets(“shards”);return;} if(a===”breedstart”){startBreedFlow();return;} if(a===”breedcancel”){breedState=null;showPets(“breed”);return;} if(a===”breedconfirm”){confirmBreed();return;} } }; } function renderBreedTab(){ if(!breedState)breedState={parent1:null,parent2:null}; const p1=breedState.parent1?PET_BY_ID[breedState.parent1]:null; const p2=breedState.parent2?PET_BY_ID[breedState.parent2]:null; let h=”<p>Breed two pets to create an egg. The egg hatches in 30-60 minutes and inherits stats and moves from both parents.</p>”; h+=”<p style=\”font-size:11px;color:#7fffc4\”>Chance of shiny: 5% (10% with Shiny Charm active). Chance of Anomaly: 10% (15% with Everstone).</p>”; h+=”<div style=\”display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:10px 0\”>”; h+=”<div class=\”breed-slot”+(p1?” filled”:””)+”\” data-breedparent=\”1\”>”+(p1?(“<div class=\”bsi\”>”+p1.icon+”</div><div>”+p1.name+”</div>”):(“<div class=\”bsi\”>โ</div><div>Parent 1</div>”))+”</div>”; h+=”<div class=\”breed-slot”+(p2?” filled”:””)+”\” data-breedparent=\”2\”>”+(p2?(“<div class=\”bsi\”>”+p2.icon+”</div><div>”+p2.name+”</div>”):(“<div class=\”bsi\”>โ</div><div>Parent 2</div>”))+”</div>”; h+=”</div>”; if(p1&&p2){ const cost=800; const p1cd=(gState.breedCooldowns[p1.id]||0); const p2cd=(gState.breedCooldowns[p2.id]||0); const now=Date.now(); const ready=p1cd<now&&p2cd<now; h+=”<div class=\”mission\”><div class=\”mtop\”><b>”+p1.icon+” “+p1.name+” ร “+p2.icon+” “+p2.name+”</b></div>”; h+=”<div class=\”mdesc\”>Result: random child from the combined gene pool.</div>”; h+=”<div class=\”mbot\”><span>Cost: “+cost+” CR</span><button class=\”pbtn gold\” data-act=\”breedconfirm\””+(ready&&gState.credits>=cost?””:” disabled”)+”>”+(ready?”BREED”:”COOLDOWN”)+”</button></div></div>”; if(!ready)h+=”<p style=\”color:#ffd93d\”>One or both parents are on cooldown. Wait for cooldown to expire.</p>”; } h+=”<p>Select from your pets below. Only one of each rarity may breed at a time, and Legendary pets cannot breed.</p>”; h+=”<div class=\”petgrid\”>”; ownedPetIds().forEach(function(id){ const p=PET_BY_ID[id]; if(!p)return; if(p.r===”legendary”)return; const sel=(breedState.parent1===id)||(breedState.parent2===id); h+=petCardHtml(p,sel,false); }); h+=”</div>”; if(p1||p2){ h+=”<button class=\”pbtn wide alt\” data-act=\”breedcancel\”>CLEAR PARENTS</button>”; } h+=renderEggListSmall(); return h; } function renderEggListSmall(){ if(!gState.eggs||!gState.eggs.length)return “”; let h=”<p style=\”margin-top:14px\”><b>ACTIVE EGGS</b></p>”; gState.eggs.forEach(function(egg,i){ const remaining=Math.max(0,egg.hatchTime-Date.now()); const sp=PET_BY_ID[egg.speciesId]; h+=”<div class=\”eggcard\”><div class=\”ename\”>๐ฅ Egg “+i+” โ “+(sp?sp.icon+” “+sp.name:”Unknown”)+”</div><div class=\”etime\”>Hatches in “+fmtDur(remaining)+”</div></div>”; }); return h; } function renderEggTab(){ let h=”<p>Eggs incubating from breeding. Each hatches into a new pet.</p>”; if(!gState.eggs||!gState.eggs.length){ h+=”<p>You have no eggs incubating. Visit the BREED tab to start breeding.</p>”; return h; } gState.eggs.forEach(function(egg,i){ const remaining=Math.max(0,egg.hatchTime-Date.now()); const sp=PET_BY_ID[egg.speciesId]; const badge=egg.isShiny?” โจ”:(egg.isMutated?” โญ”:””); h+=”<div class=\”eggcard\”><div class=\”ename\”>๐ฅ Egg “+i+” โ “+(sp?sp.icon+” “+sp.name+badge:”Unknown”)+”</div><div class=\”etime\”>Hatches in “+fmtDur(remaining)+”</div></div>”; }); return h; } function handleBreedSelect(id){ if(!breedState)breedState={parent1:null,parent2:null}; if(breedState.parent1===id){breedState.parent1=null;showPets(“breed”);return;} if(breedState.parent2===id){breedState.parent2=null;showPets(“breed”);return;} if(!breedState.parent1)breedState.parent1=id; else if(!breedState.parent2)breedState.parent2=id; else breedState.parent2=id; showPets(“breed”); } function startBreedFlow(){ if(!breedState||!breedState.parent1||!breedState.parent2){ showToast(“Breed”,”Select both parents first.”); return; } confirmBreed(); } function confirmBreed(){ if(!breedState||!breedState.parent1||!breedState.parent2)return; const p1=PET_BY_ID[breedState.parent1]; const p2=PET_BY_ID[breedState.parent2]; if(!p1||!p2)return; const now=Date.now(); const cost=800; if(gState.credits<cost){showToast(“Breed”,”Not enough credits.”);sfx(“error”);return;} const p1cd=gState.breedCooldowns[p1.id]||0; const p2cd=gState.breedCooldowns[p2.id]||0; if(p1cd>now||p2cd>now){showToast(“Breed”,”One parent is on cooldown.”);return;} gState.credits-=cost; const roll=Math.random(); let childSpecies; if(roll<0.4){ childSpecies=p1.r===p2.r?p1:(Math.random()<0.5?p1:p2); }else if(roll<0.85){ const pool=PET_POOLS[p1.r].concat(PET_POOLS[p2.r]).filter(function(p){return p.id!==p1.id&&p.id!==p2.id;}); childSpecies=pool.length?pool[Math.floor(Math.random()*pool.length)]:p1; }else{ const pool=PET_POOLS.common.concat(PET_POOLS.uncommon,PET_POOLS.rare); childSpecies=pool[Math.floor(Math.random()*pool.length)]; } const shinyCharmActive=(gState.items.shinycharm||0)>0; const shinyChance=shinyCharmActive?0.10:0.05; const isShiny=Math.random()<shinyChance; const everstoneActive=(gState.items.everstone||0)>0; const anomalyChance=everstoneActive?0.15:0.10; const isMutated=Math.random()<anomalyChance; if(shinyCharmActive){gState.items.shinycharm=Math.max(0,(gState.items.shinycharm||0)-1);} if(everstoneActive){gState.items.everstone=Math.max(0,(gState.items.everstone||0)-1);} const inheritedStats={str:10,agi:10,int:10,vit:10,luk:10}; const s1=gState.pets[p1.id]&&gState.pets[p1.id].stats?gState.pets[p1.id].stats:{str:10,agi:10,int:10,vit:10,luk:10}; const s2=gState.pets[p2.id]&&gState.pets[p2.id].stats?gState.pets[p2.id].stats:{str:10,agi:10,int:10,vit:10,luk:10}; [“str”,”agi”,”int”,”vit”,”luk”].forEach(function(k){ inheritedStats[k]=Math.floor((s1[k]+s2[k])/2)+Math.floor(Math.random()*3); }); const hatchDelay=BREED_HATCH_MIN_MS+Math.floor(Math.random()*(BREED_HATCH_MAX_MS-BREED_HATCH_MIN_MS)); gState.eggs.push({speciesId:childSpecies.id,hatchTime:now+hatchDelay,parent1:p1.id,parent2:p2.id,isShiny:isShiny,isMutated:isMutated,inheritedStats:inheritedStats,mutationPerk:isMutated?rollAnomaly(true):null}); gState.breedCooldowns[p1.id]=now+BREED_COOLDOWN_MS; gState.breedCooldowns[p2.id]=now+BREED_COOLDOWN_MS; gState.stats.breedsDone=(gState.stats.breedsDone||0)+1; logEvent(“Bred “+p1.name+” and “+p2.name+”. Egg created.”); showToast(“Egg Created”,p1.icon+” “+p1.name+” ร “+p2.icon+” “+p2.name+” โ egg incubating!”); sfx(“level”); breedState={parent1:null,parent2:null}; saveGameState(gState); updateGameHud(); showPets(“eggs”); } function trainPet(id){ const p=PET_BY_ID[id]; const o=gState.pets[id]; if(!p||!o)return; const cost=600+o.lv*450; if(gState.credits<cost){showToast(“Training”,”Not enough credits.”);sfx(“error”);return;} gState.credits-=cost; addPetXp(id,80+o.lv*12); sfx(“coin”); logEvent(“Trained “+p.name+”.”); afterChange(); showPets(petTab); } function trainStat(key){ const parts=key.split(“:”); const id=parts[0]; const stat=parts[1]; const p=PET_BY_ID[id]; const o=gState.pets[id]; if(!p||!o)return; if(!o.stats)o.stats={str:10,agi:10,int:10,vit:10,luk:10}; const cost=400+o.lv*200; if(gState.credits<cost){showToast(“Stat Train”,”Not enough credits.”);sfx(“error”);return;} gState.credits-=cost; o.stats[stat]=(o.stats[stat]||10)+1; gState.stats.trained++; sfx(“coin”); logEvent(“Trained “+stat.toUpperCase()+” on “+p.name+”.”); afterChange(); showPets(“stats”); } function doMerge(){ if(petSel.length!==3)return; let rk=null; for(let i=0;i<3;i++){ const p=PET_BY_ID[petSel[i]]; if(!p)return; if(rk===null)rk=p.r; else if(p.r!==rk)return; } if(rk===”legendary”)return; const next=RARITY_ORDER[RARITY_INDEX[rk]+1]; for(let i=0;i<3;i++)delete gState.pets[petSel[i]]; gState.team=gState.team.filter(function(id){return petSel.indexOf(id)===-1;}); const pool=PET_POOLS[next]; const result=pool[Math.floor(Math.random()*pool.length)]; const anomalyPerk=rollAnomaly(true); const added=addPet(result,false); if(anomalyPerk&&!added.anomaly){ added.anomaly=true; added.anomalyPerk=anomalyPerk; gState.stats.anomalies=(gState.stats.anomalies||0)+1; showToast(“Anomaly Fusion”,”โญ The fusion produced an Anomaly pet!”); } gState.stats.merged++; gState.credits+=Math.round(RARITY[next].cr*1.5); addXp(RARITY[next].xp); logEvent(“Fused 3 “+RARITY[rk].label+” pets into “+result.name+(added.anomaly?” ANOMALY”:””)+”.”); showToast(“Fusion Complete”,result.icon+” “+result.name+” joined your collection”+(added.anomaly?” as an ANOMALY”:””)+”.”); sfx(“level”); petSel=[]; afterChange(); showPets(“merge”); } function showCodex(){ let total=0; PETS.forEach(function(s){if(gState.codex[s.id])total++;}); let h=”<p>Discovered “+total+” of “+PETS.length+” signal species.</p>”; h+=”<input class=\”shopsearch\” id=\”codex-search\” type=\”text\” placeholder=\”Search the codex\” value=\”\”>”; h+=”<div id=\”codex-grid-wrap\”><div class=\”cgrid\” id=\”codex-grid\”>”; PETS.slice(0,120).forEach(function(s){ const n=gState.codex[s.id]||0; const r=RARITY[s.r]; const shave=gState.shards[s.id]||0; const sneed=shardsNeeded(s.r); h+=”<div class=\”ccell”+(n?””:” off”)+”\” style=\”border-color:”+r.color+”\”><span class=\”ci\”>”+(n?s.icon:”โ”)+”</span>”+(n?s.name+”<br>x”+n:”???”)+”<br><span style=\”color:”+r.color+”;font-size:9px;letter-spacing:1px\”>”+r.label+”</span><br><span style=\”color:#7fffc4;font-size:9px;letter-spacing:0.5px\”>Shards “+shave+” / “+sneed+”</span></div>”; }); h+=”</div></div>”; h+=”<p style=\”font-size:11px\”>Type in the search to narrow the codex.</p>”; openPanel(“CODEX”,h); const cs=document.getElementById(“codex-search”); const cg=document.getElementById(“codex-grid”); if(cs&&cg){ cs.oninput=function(){ const q=this.value.trim().toLowerCase(); let list=PETS; if(q)list=PETS.filter(function(s){return s.name.toLowerCase().indexOf(q)!==-1||(gState.codex[s.id]&&s.r===q);}); list=list.slice(0,160); let inner=””; list.forEach(function(s){ const n=gState.codex[s.id]||0; const r=RARITY[s.r]; const shave=gState.shards[s.id]||0; const sneed=shardsNeeded(s.r); inner+=”<div class=\”ccell”+(n?””:” off”)+”\” style=\”border-color:”+r.color+”\”><span class=\”ci\”>”+(n?s.icon:”โ”)+”</span>”+(n?s.name+”<br>x”+n:”???”)+”<br><span style=\”color:”+r.color+”;font-size:9px;letter-spacing:1px\”>”+r.label+”</span><br><span style=\”color:#7fffc4;font-size:9px;letter-spacing:0.5px\”>Shards “+shave+” / “+sneed+”</span></div>”; }); cg.innerHTML=inner; }; } } function showIncome(){ const now=Date.now(); const hrs=Math.min(8,(now-gState.lastIncome)/3600000); const iph=incomePerHour(); const pend=Math.floor(iph*hrs*incomeBoost()); let h=”<p>Your blocks generate credits over time. Up to 8 hours are stored.</p><p>Rate: <b>”+iph+”</b> credits per hour”+(incomeBoost()>1?” (boosted)”:””)+”</p><p class=\”hl\”>”+pend+” CREDITS READY</p>”; h+=”<p style=\”font-size:11px;color:#7fffc4\”>Walk reward rate: “+WALK_REWARD_PER_METER+” cr per meter. Walk credits earned: “+(gState.stats.walkCredits||0)+”.</p>”; h+=pend>=1?”<button class=\”pbtn wide\” data-act=\”collect\”>COLLECT</button>”:”<p style=\”text-align:center\”>Nothing to collect yet.</p>”; openPanel(“INCOME”,h); panelBody.onclick=function(e){ if(e.target.getAttribute(“data-act”)===”collect”)collectIncome(); }; } function collectIncome(){ const now=Date.now(); const hrs=Math.min(8,(now-gState.lastIncome)/3600000); const pend=Math.floor(incomePerHour()*hrs*incomeBoost()); if(pend<1)return; gState.credits+=pend; gState.lastIncome=now; gState.stats.collected++; addXp(10); logEvent(“Collected “+pend+” credits of block income.”); sfx(“coin”); showToast(“Income Collected”,”+”+pend+” credits.”); afterChange(); showIncome(); } function showOfflineReport(elapsedMs,incomeRes,npcRes){ const hrs=(elapsedMs/3600000).toFixed(1); let h=”<p>You were offline for <b>”+hrs+” hours</b>.</p>”; h+=”<p style=\”margin-top:10px\”><b>BLOCK INCOME</b></p>”; h+=”<p>Stored income: <b>”+incomeRes.income+”</b> credits</p>”; h+=”<p style=\”margin-top:10px\”><b>RIVAL ACTIVITY</b></p>”; if(npcRes.lost===0&&npcRes.defended===0&&npcRes.blocked===0){ h+=”<p>No rival activity. All of your turf held while you were away.</p>”; } if(npcRes.lost>0){ h+=”<p style=\”color:#ff8a3d\”>You lost <b>”+npcRes.lost+”</b> block(s) to rivals.</p>”; npcRes.capturedFrom.forEach(function(c){ h+=”<div class=\”logline\”>Lost “+c.k+” to “+c.f+” (was Lv “+c.lv+”)</div>”; }); } if(npcRes.defended>0){ h+=”<p style=\”color:#7fffc4\”>Your blocks held against <b>”+npcRes.defended+”</b> rival attack(s).</p>”; } if(npcRes.blocked>0){ h+=”<p style=\”color:#5aa8ff\”>Turf Shield blocked <b>”+npcRes.blocked+”</b> attack(s).</p>”; } h+=”<p style=\”margin-top:10px\”><b>STATUS</b></p>”; h+=”<p>Blocks owned now: <b>”+ownedKeys().length+”</b></p>”; h+=”<p>Credits now: <b>”+gState.credits+”</b></p>”; h+=”<p style=\”margin-top:10px\”>Tap COLLECT below to claim the stored income.</p>”; h+=”<button class=\”pbtn wide\” data-act=\”collect\”>COLLECT INCOME</button>”; openPanel(“OFFLINE REPORT”,h); panelBody.onclick=function(e){ const a=e.target.getAttribute(“data-act”); if(a===”collect”){ collectIncome(); closePanel(); } }; } function showTurf(){ const now=Date.now(); const mine=ownedKeys(); const byLevel=[0,0,0,0,0,0]; mine.forEach(function(k){byLevel[gState.blocks[k].lv]++;}); const c=cellOf(userLat,userLon); const counts={}; FACTIONS.forEach(function(f){counts[f.id]=0;}); let youNear=0; for(let di=-7;di<=7;di++){ for(let dj=-7;dj<=7;dj++){ const b=getBlock(c.i+di,c.j+dj); if(!b)continue; if(b.o===”you”)youNear++; else counts[b.o]++; } } const stepsToday=Math.round((gState.walkDaily.progress||0)*STEPS_PER_METER); const walkBuffPct=Math.min(50,Math.floor((gState.walkDaily.progress||0)/200)); const buffedTeamPower=Math.round(teamPowerTotal()*(1+walkBuffPct/100)); let h=”<p>Level <b>”+levelOf(gState.xp)+”</b> ยท XP <b>”+gState.xp+”</b></p>”; h+=”<p>Blocks owned: <b>”+mine.length+”</b> ยท Lost to rivals: <b>”+gState.stats.lost+”</b></p>”; h+=”<p>By level: L1 “+byLevel[1]+” ยท L2 “+byLevel[2]+” ยท L3 “+byLevel[3]+” ยท L4 “+byLevel[4]+” ยท L5 “+byLevel[5]+”</p>”; h+=”<p>Distance walked: <b>”+Math.round(gState.stats.distance)+” m</b></p>”; h+=”<p>Steps today: <b>”+stepsToday+”</b> ยท Step goal: <b>”+(gState.stepGoal||STEP_GOAL_DEFAULT)+”</b> ยท Step streak: <b>”+(gState.stepStreak||0)+”</b></p>”; h+=”<p>Login streak: <b>”+gState.streak+”</b> day”+(gState.streak===1?””:”s”)+”</p>”; h+=”<p>Walk Buff: <b style=\”color:#7fffc4\”>+”+walkBuffPct+”%</b> ยท Buffed team power: <b>”+buffedTeamPower+”</b> (base “+teamPowerTotal()+”)</p>”; h+=”<p>Walk reward credits: <b>”+(gState.stats.walkCredits||0)+”</b> ยท Pet milestones: <b>”+(gState.stats.petMilestones||0)+”</b></p>”; h+=”<p>Pets collected: <b>”+Object.keys(gState.pets).length+”</b> ยท Team power: <b>”+teamPowerTotal()+”</b> ยท Battles won: <b>”+gState.battles.wins+”</b></p>”; h+=”<p>Anomalies found: <b>”+(gState.stats.anomalies||0)+”</b> ยท Relics broken: <b>”+(gState.stats.relicsBroken||0)+”</b></p>”; h+=”<p>Arena wins: <b>”+(gState.stats.arenaWins||0)+”</b> ยท Duel wins: <b>”+(gState.stats.duelsWon||0)+”</b> ยท Eggs hatched: <b>”+(gState.stats.hatched||0)+”</b></p>”; h+=”<p>NPCs hired: <b>”+gState.hired.length+”</b> of “+NPCS.length+”</p>”; h+=”<p>Turf Shield: “+(gState.effects.shield>now?”active “+fmtDur(gState.effects.shield-now):”inactive”)+” ยท Claim Boost: “+(gState.effects.boost>now?”active “+fmtDur(gState.effects.boost-now):”inactive”)+”</p>”; h+=”<p><b>Nearby turf</b></p>”; h+=”<div class=\”logline\” style=\”color:#00ff9d\”>You: “+youNear+” blocks</div>”; FACTIONS.forEach(function(f){h+=”<div class=\”logline\” style=\”color:”+f.color+”\”>”+f.name+”: “+counts[f.id]+” blocks</div>”;}); h+=”<p style=\”margin-top:12px\”><b>Recent activity</b></p>”; if(!gState.log.length)h+=”<p>No activity yet.</p>”; gState.log.slice(0,12).forEach(function(l){h+=”<div class=\”logline\”>”+new Date(l.t).toLocaleTimeString()+” ยท “+l.msg+”</div>”;}); openPanel(“TURF REPORT”,h); } function showHire(){ let h=”<p>Hire NPC crew members. Each one adds passive benefits and battle power. Crew power: <b>”+npcPowerTotal()+”</b></p>”; NPCS.forEach(function(n){ const owned=gState.hired.indexOf(n.id)!==-1; h+=”<div class=\”npcrow\”><div class=\”ti\” style=\”font-size:24px\”>”+n.icon+”</div><div style=\”flex:1\”><b>”+n.name+”</b><br><span style=\”font-size:11px;color:#a0c0e0\”>”+n.desc+” ยท Power “+n.power+”</span></div>”+(owned?”<em>HIRED</em>”:”<button data-hire=\””+n.id+”\”>”+n.cost+” CR</button>”)+”</div>”; }); openPanel(“HIRE CREW”,h); panelBody.onclick=function(e){ const b=e.target.closest(“[data-hire]”); if(b)hireNpc(b.getAttribute(“data-hire”)); }; } function hireNpc(id){ const n=NPC_BY_ID[id]; if(!n)return; if(gState.hired.indexOf(id)!==-1)return; if(gState.credits<n.cost){showToast(“Hire”,”Not enough credits.”);sfx(“error”);return;} gState.credits-=n.cost; gState.hired.push(id); gState.stats.hired++; addXp(30); logEvent(“Hired “+n.name+”.”); showToast(“Crew Hired”,n.icon+” “+n.name+” joined your crew.”); sfx(“level”); afterChange(); showHire(); } function nearestRivalBlock(){ if(!hasFix)return null; const c=cellOf(lastGeo.lat,lastGeo.lon); let best=null; let bd=Infinity; for(let di=-3;di<=3;di++){ for(let dj=-3;dj<=3;dj++){ const i=c.i+di; const j=c.j+dj; const b=getBlock(i,j); if(b&&b.o!==”you”){ const lat=(i+0.5)*CELL; const lon=(j+0.5)*CELL; const d=distTo(lat,lon); if(d<bd){bd=d;best={i:i,j:j,b:b,lat:lat,lon:lon,d:d};} } } } return best; } function defenseOf(target){ if(!target)return 0; const f=factionById(target.b.o); let base=target.b.lv*22; if(f.id===”wolves”)base+=8; else if(f.id===”foxes”)base+=12; else if(f.id===”owls”)base+=16; else if(f.id===”hawks”)base+=10; return Math.round(base); } function showBattle(){ if(!hasFix){openPanel(“BATTLE”,”<p>Waiting for GPS. Allow location access to locate rival claims.</p>”);return;} const target=nearestRivalBlock(); let h=”<p>Battle rival Street Claims with your pet team and hired crew.</p>”; h+=”<p>Team power: <b>”+teamPowerTotal()+”</b> ยท Pets in team: <b>”+gState.team.length+”</b> ยท Crew: <b>”+gState.hired.length+”</b></p>”; h+=”<div class=\”battlebox\”>”; if(!target){ h+=”<p>No rival blocks within reach. Walk closer to a rival claim.</p>”; }else{ const def=defenseOf(target); const atk=teamPowerTotal(); const f=factionById(target.b.o); let chance=atk/(atk+def); if(chance<0.05)chance=0.05; if(chance>0.95)chance=0.95; h+=”<div class=\”vs\”><div class=\”side\”><div style=\”font-size:26px\”>๐พ</div><div class=\”bigpower\”>”+atk+”</div><div style=\”font-size:11px;color:#a0c0e0\”>YOUR TEAM</div></div><div class=\”mid\”>VS</div><div class=\”side\”><div style=\”font-size:26px\”>”+f.name.split(” “)[0]+”</div><div class=\”bigpower\” style=\”color:”+f.color+”\”>”+def+”</div><div style=\”font-size:11px;color:#a0c0e0\”>”+f.name.toUpperCase()+”</div></div></div>”; h+=”<p style=\”text-align:center\”>Target block level “+target.b.lv+” ยท “+Math.round(target.d)+” m away</p>”; h+=”<p style=\”text-align:center\”>Win chance “+Math.round(chance*100)+”%</p>”; h+=”<button class=\”pbtn wide battle\” data-act=\”fight\”>ATTACK CLAIM</button>”; } h+=”</div>”; h+=”<p>Battles won: <b>”+gState.battles.wins+”</b> ยท Battles lost: <b>”+gState.battles.losses+”</b></p>”; openPanel(“STREET BATTLE”,h); panelBody.onclick=function(e){ if(e.target.getAttribute(“data-act”)===”fight”)runBattle(target); }; } function showBattleForTarget(i,j){ const b=getBlock(i,j); if(!b||b.o===”you”){showBattle();return;} const cellLat=(i+0.5)*CELL; const cellLon=(j+0.5)*CELL; const d=distTo(cellLat,cellLon); const target={i:i,j:j,b:b,lat:cellLat,lon:cellLon,d:d}; let h=”<p>Challenge the rival territory claim.</p>”; h+=”<p>Team power: <b>”+teamPowerTotal()+”</b> ยท Crew: <b>”+gState.hired.length+”</b></p>”; const def=defenseOf(target); const atk=teamPowerTotal(); const f=factionById(target.b.o); let chance=atk/(atk+def); if(chance<0.05)chance=0.05; if(chance>0.95)chance=0.95; h+=”<div class=\”battlebox\”>”; h+=”<div class=\”vs\”><div class=\”side\”><div style=\”font-size:26px\”>๐พ</div><div class=\”bigpower\”>”+atk+”</div><div style=\”font-size:11px;color:#a0c0e0\”>YOUR TEAM</div></div><div class=\”mid\”>VS</div><div class=\”side\”><div style=\”font-size:26px\”>”+f.name.split(” “)[0]+”</div><div class=\”bigpower\” style=\”color:”+f.color+”\”>”+def+”</div><div style=\”font-size:11px;color:#a0c0e0\”>”+f.name.toUpperCase()+”</div></div></div>”; h+=”<p style=\”text-align:center\”>Target block level “+target.b.lv+” ยท “+(isFinite(d)?Math.round(d)+” m away”:”unknown distance”)+”</p>”; h+=”<p style=\”text-align:center\”>Win chance “+Math.round(chance*100)+”%</p>”; h+=”<button class=\”pbtn wide battle\” data-act=\”fight\”>ATTACK CLAIM</button>”; h+=”</div>”; openPanel(“STREET BATTLE”,h); panelBody.onclick=function(e){ if(e.target.getAttribute(“data-act”)===”fight”)runBattle(target); }; } function startStreetBattle(target) { if (!gState.team.length) { showToast(“Battle”, “Add pets to your team first.”); sfx(“error”); return; } const playerTeam = []; gState.team.forEach(function(id) { const sp = PET_BY_ID[id]; if (!sp) return; const o = gState.pets[id]; if (!o) return; const stats = o.stats || { str: 10, agi: 10, int: 10, vit: 10, luk: 10 }; const bp = makeBattlePet(sp, o.lv, stats, o.anomaly); bp.moves = getPokeMovesForPet(sp, o.lv); bp.currentPp = {}; bp.moves.forEach(function(mv, i) { bp.currentPp[i] = mv.pp; }); playerTeam.push(bp); }); const enemyTeam = []; const f = factionById(target.b.o); const enemyLv = target.b.lv * 3 + 2; const numEnemies = Math.min(3, Math.max(1, target.b.lv)); for (let i = 0; i < numEnemies; i++) { const rk = [‘common’, ‘uncommon’, ‘rare’][Math.floor(Math.random() * 3)]; const pool = PET_POOLS[rk]; const sp = pool[Math.floor(Math.random() * pool.length)]; const stats = { str: 8 + enemyLv * 2, agi: 8 + enemyLv * 2, int: 8 + enemyLv * 2, vit: 8 + enemyLv * 2, luk: 8 + enemyLv * 2 }; const bp = makeBattlePet(sp, enemyLv, stats, false); bp.moves = getPokeMovesForPet(sp, enemyLv); bp.currentPp = {}; bp.moves.forEach(function(mv, i) { bp.currentPp[i] = mv.pp; }); enemyTeam.push(bp); } battleState = { mode: “street”, target: target, trainerName: f.name + ” Enforcer”, trainerTier: target.b.lv, playerTeam: playerTeam, enemyTeam: enemyTeam, playerIndex: 0, enemyIndex: 0, log: [], over: false, multi: 1 }; battleState.log.push(“Battle for territory begins against “+f.name+”!”); battleState.log.push(“You send out “+playerTeam[0].name+”!”); battleState.log.push(battleState.trainerName+” sends out “+enemyTeam[0].name+”!”); sfx(“battle”); renderBattleUI(); } function runBattle(target){ if(!target)return; startStreetBattle(target); } function getArenaWinsRemaining(){ if(!Array.isArray(gState.arenaWins))gState.arenaWins=[]; const now=Date.now(); gState.arenaWins=gState.arenaWins.filter(function(t){return t>now-ARENA_WINDOW_MS;}); return Math.max(0,ARENA_MAX_WINS-gState.arenaWins.length); } function openArenaGrounds(){ if(!hasFix){showToast(“Arena”,”Waiting for GPS signal.”);return;} const loc=getArenaLocation(); if(!loc){showToast(“Arena”,”Set your home location first by walking.”);return;} const dist=haversine(lastGeo.lat,lastGeo.lon,loc.lat,loc.lon); if(dist>ARENA_ENTER_RADIUS){ showToast(“Arena”,”Walk within “+ARENA_ENTER_RADIUS+” m of the Arena. Currently “+Math.round(dist)+” m away.”); return; } showArenaPanel(); } function showArenaPanel(){ if(arenaState){arenaState=null;} const remaining=getArenaWinsRemaining(); const playerLv=Math.max(1,Math.round((gState.stats.arenaWins||0)/2)+1); let h=”<div class=\”battlebox\” style=\”text-align:center;\”><div style=\”font-size:34px\”>โ๏ธ</div><p style=\”font-size:14px;letter-spacing:2px;color:#ffd93d;margin:6px 0\”><b>ARENA GROUNDS</b></p><p style=\”font-size:12px\”>Wins Today: <b style=\”color:#7fffc4\”>”+(ARENA_MAX_WINS-remaining)+” / “+ARENA_MAX_WINS+”</b></p>”; if(remaining<=0){ h+=”<p style=\”color:#ff8a3d;font-size:11px\”>You have used all 10 arena attempts for today. Come back in 24 hours.</p>”; }else{ h+=”<p style=\”font-size:11px\”>Attempts Remaining: <b style=\”color:#7fffc4\”>”+remaining+”</b></p>”; } h+=”<p style=\”font-size:10px;color:#a0c0e0\”>Each victory scales rewards with your team’s strength. A powerful pet may even join you.</p></div>”; const tierUnlock=Math.max(1,Math.floor((gState.stats.arenaWins||0)/2)+1); ARENA_TRAINERS.forEach(function(tr,i){ const locked=i>=tierUnlock; const cardCls=”arena-card”+(locked?” locked”:””); h+=”<div class=\””+cardCls+”\” “+(locked?””:”data-arena=\””+i+”\””)+”>”; h+=”<div class=\”arow\”><div class=\”aname\”>”+(locked?”๐ “:”โ๏ธ “)+tr.name+”</div><div style=\”font-size:10px;color:#ffd93d\”>TIER “+tr.tier+”</div></div>”; h+=”<div class=\”ateam\”>”+tr.desc+”</div>”; h+=”<div class=\”areward\”>Reward: ~”+(tr.mult*300)+” credits ยท XP ร”+tr.mult+” ยท Rare pet chance</div>”; if(locked)h+=”<div style=\”font-size:10px;color:#a0c0e0;margin-top:4px\”>Win “+(tierUnlock*2-2)+”+ arena battles to unlock.</div>”; h+=”</div>”; }); h+=”<p style=\”font-size:10px;color:#a0c0e0;margin-top:10px\”>Arena battle is turn-based. Type effectiveness, PP, status conditions, and speed matter. Bring a strong, balanced team.</p>”; openPanel(“ARENA GROUNDS”,h); panelBody.onclick=function(e){ const card=e.target.closest(“[data-arena]”); if(!card)return; if(remaining<=0){showToast(“Arena”,”No attempts left today.”);return;} const idx=parseInt(card.getAttribute(“data-arena”),10); startArenaBattle(idx); }; } function startArenaBattle(trainerIdx){ const trainer=ARENA_TRAINERS[trainerIdx]; if(!trainer){return;} if(!gState.team.length||!gState.team.filter(function(id){return gState.pets[id]&&PET_BY_ID[id];}).length){ showToast(“Arena”,”Add at least one pet to your team first.”); return; } const now=Date.now(); const playerLv=Math.max(3,Math.round((gState.stats.arenaWins||0)/2)*3+3); const team=[]; trainer.team.forEach(function(sid,lvOverride){ const sp=PET_BY_ID[sid]||PET_POOLS.common[0]; const lv=playerLv+(trainer.tier-1)*2+Math.floor(Math.random()*3); team.push(makeBattlePet(sp,lv,{str:8+lv,vit:8+lv,agi:8+lv,int:8+lv,luk:8+lv},Math.random()<0.05)); }); const playerTeam=[]; gState.team.forEach(function(id){ const sp=PET_BY_ID[id]; if(!sp)return; const o=gState.pets[id]; if(!o)return; const stats=o.stats||{str:10,agi:10,int:10,vit:10,luk:10}; const battlePet=makeBattlePet(sp,o.lv,stats,o.anomaly); battlePet.moves=getPokeMovesForPet(sp,o.lv); battlePet.currentPp={}; battlePet.moves.forEach(function(mv,i){battlePet.currentPp[i]=mv.pp;}); playerTeam.push(battlePet); }); battleState={ mode:”arena”, trainerName:trainer.name, trainerTier:trainer.tier, playerTeam:playerTeam, enemyTeam:team, playerIndex:0, enemyIndex:0, log:[], over:false, multi:trainer.mult, lastActionTime:now }; battleState.log.push(“Arena battle begins against “+trainer.name+”!”); battleState.log.push(“You send out “+playerTeam[0].name+”!”); battleState.log.push(trainer.name+” sends out “+team[0].name+”!”); sfx(“arena”); renderBattleUI(); } function makeBattlePet(species,level,stats,isAnomaly){ const pokeType=getPokeType(species); const hp=Math.floor((60+stats.vit*6+level*8)*(isAnomaly?1.15:1)); const atk=Math.floor(10+stats.str*2+level*2); const def=Math.floor(5+stats.vit*2+level*2); const spd=Math.floor(8+stats.agi*2+level*1.5); return{ species:species, name:species.name, icon:species.icon, type:pokeType, level:level, maxHp:hp, hp:hp, atk:atk, def:def, spd:spd, isAnomaly:!!isAnomaly, status:null, statusTurns:0, toxCounter:0, moves:[], currentPp:{}, statStages:{atk:0,def:0,spd:0}, flinch:false, recharge:false, leech:false }; } function renderBattleUI(anim){ if(!battleState){closePanel();return;} const ps=battleState.playerTeam[battleState.playerIndex]; const es=battleState.enemyTeam[battleState.enemyIndex]; if(!ps||!es){closePanel();return;} const psHpPct=Math.max(0,Math.min(100,Math.round(ps.hp/ps.maxHp*100))); const esHpPct=Math.max(0,Math.min(100,Math.round(es.hp/es.maxHp*100))); const psHpClass=psHpPct<20?”crit”:psHpPct<50?”warn”:””; const esHpClass=esHpPct<20?”crit”:esHpPct<50?”warn”:””; const psStatus=ps.status?”<span class=\”status-tag “+STATUS_CLASS[ps.status]+”\”>”+STATUS_LABEL[ps.status]+”</span>”:””; const esStatus=es.status?”<span class=\”status-tag “+STATUS_CLASS[es.status]+”\”>”+STATUS_LABEL[es.status]+”</span>”:””; let h=””; h+=”<div class=\”battle-side foe\”><div style=\”font-size:10px;letter-spacing:1px;color:#ffb0b0\”>FOE”+esStatus+”</div><div class=\”battle-sprite\”>”+es.icon+”</div><div style=\”font-size:12px;font-weight:700\”>”+es.name+” Lv “+es.level+”</div><div class=\”hpwrap\”><div class=\””+esHpClass+”\” style=\”width:”+esHpPct+”%\”></div></div><div style=\”font-size:10px;color:#a0c0e0\”>”+es.hp+” / “+es.maxHp+” HP</div></div>”; h+=”<div class=\”battle-side you\”><div style=\”font-size:10px;letter-spacing:1px;color:#a0ffd0\”>YOU”+psStatus+”</div><div class=\”battle-sprite\”>”+ps.icon+”</div><div style=\”font-size:12px;font-weight:700\”>”+ps.name+” Lv “+ps.level+”</div><div class=\”hpwrap\”><div class=\””+psHpClass+”\” style=\”width:”+psHpPct+”%\”></div></div><div style=\”font-size:10px;color:#a0c0e0\”>”+ps.hp+” / “+ps.maxHp+” HP</div></div>”; h+=”<div class=\”duel-log\” id=\”battle-log\”>”; battleState.log.slice(-30).forEach(function(l){h+=”<div>”+l+”</div>”;}); h+=”</div>”; if(battleState.over){ h+=”<p class=\”hl\”>”+battleState.resultText+”</p>”; h+=”<button class=\”pbtn wide\” data-battleact=\”close\”>RETURN</button>”; }else{ h+=”<div class=\”duel-moves\”>”; ps.moves.forEach(function(mv,i){ const ppLeft=ps.currentPp[i]||0; const disabled=ppLeft<=0?” disabled”:””; const eff=getTypeEffectiveness(mv.type,es.type); const effText=eff>1?” โฒ”:eff<1&&eff>0?” โผ”:””; h+=”<button data-move=\””+i+”\””+disabled+”><span class=\”pptag\”>”+ppLeft+”/”+mv.maxPp+”</span>”+mv.name+”<small>PWR “+mv.power+” ยท “+mv.type.toUpperCase()+effText+”</small></button>”; }); h+=”</div>”; h+=”<p style=\”font-size:10px;color:#7fffc4;text-align:center\”>Your team: “+battleState.playerTeam.filter(function(p){return p.hp>0;}).length+” ยท Foe team: “+battleState.enemyTeam.filter(function(p){return p.hp>0;}).length+”</p>”; } openPanel(“BATTLE”,h,true); updateFleeButton(); const lg=document.getElementById(“battle-log”); if(lg)lg.scrollTop=lg.scrollHeight; if(anim){ setTimeout(function(){ const pSprite=document.querySelector(‘.battle-side.you .battle-sprite’); const eSprite=document.querySelector(‘.battle-side.foe .battle-sprite’); if(anim===’player’){ if(pSprite)pSprite.classList.add(‘anim-attack-right’); if(eSprite)eSprite.classList.add(‘anim-damage’); }else if(anim===’enemy’){ if(eSprite)eSprite.classList.add(‘anim-attack-left’); if(pSprite)pSprite.classList.add(‘anim-damage’); } }, 50); } panelBody.onclick=function(e){ const mvBtn=e.target.closest(“[data-move]”); if(mvBtn&&!battleState.over){ const mi=parseInt(mvBtn.getAttribute(“data-move”),10); executeBattleTurn(mi); return; } const closeBtn=e.target.closest(“[data-battleact=\”close\”]”); if(closeBtn){ const mode = battleState.mode; battleState=null; closePanel(); if (mode === “arena”) { setTimeout(function(){showArenaPanel();},100); } else if (mode === “street”) { setTimeout(function(){showBattle();},100); } } }; } function executeBattleTurn(moveIndex){ if(!battleState||battleState.over)return; let ps=battleState.playerTeam[battleState.playerIndex]; let es=battleState.enemyTeam[battleState.enemyIndex]; if(!ps||!es)return; if(ps.flinch){ battleState.log.push(ps.name+” flinched and cannot move!”); ps.flinch=false; resolveStatusTurnAndEnd(); return; } if(ps.recharge){ battleState.log.push(ps.name+” must recharge!”); ps.recharge=false; resolveStatusTurnAndEnd(); return; } if(!checkStatusBeforeMove(ps,battleState.log)){ resolveStatusTurnAndEnd(); return; } const playerMove=ps.moves[moveIndex]; if(!playerMove||(ps.currentPp[moveIndex]||0)<=0){ battleState.log.push(ps.name+” has no PP for that move!”); renderBattleUI(); return; } ps.currentPp[moveIndex]–; const enemyMove=chooseEnemyMove(es,ps); const playerPriority=playerMove.priority||0; const enemyPriority=enemyMove?enemyMove.priority||0:0; let playerFirst=ps.spd>=es.spd; if(playerPriority>enemyPriority)playerFirst=true; else if(enemyPriority>playerPriority)playerFirst=false; const actions=playerFirst?[“player”,”enemy”]:[“enemy”,”player”]; let actionIdx = 0; function processNextAction() { if (battleState.over) return; ps = battleState.playerTeam[battleState.playerIndex]; es = battleState.enemyTeam[battleState.enemyIndex]; if (actionIdx >= actions.length) { if(!battleState.over){ endOfTurnEffects(ps,battleState.log); endOfTurnEffects(es,battleState.log); if(ps.hp<=0){ const nextIdx=battleState.playerTeam.findIndex(function(p){return p.hp>0;}); if(nextIdx===-1){endBattle(false);return;} battleState.playerIndex=nextIdx; battleState.log.push(“You send out “+battleState.playerTeam[battleState.playerIndex].name+”!”); } if(es.hp<=0){ battleState.enemyIndex++; if(battleState.enemyIndex>=battleState.enemyTeam.length){ endBattle(true); return; } battleState.log.push(battleState.trainerName+” sends out “+battleState.enemyTeam[battleState.enemyIndex].name+”!”); } renderBattleUI(); } return; } const who = actions[actionIdx]; actionIdx++; if (who === “player”) { if (ps.hp <= 0) { processNextAction(); return; } battleState.log.push(ps.name+” used “+playerMove.name+”!”); applyMoveToTarget(playerMove,ps,es,battleState.log); if(es.hp<=0){ battleState.log.push(es.name+” fainted!”); battleState.enemyIndex++; if(battleState.enemyIndex>=battleState.enemyTeam.length){ renderBattleUI(‘player’); setTimeout(function(){ endBattle(true); }, 900); return; } const nextFoe=battleState.enemyTeam[battleState.enemyIndex]; battleState.log.push(battleState.trainerName+” sends out “+nextFoe.name+”!”); } renderBattleUI(‘player’); setTimeout(processNextAction, 900); } else { if (es.hp <= 0) { processNextAction(); return; } const em = enemyMove; if(!em) { processNextAction(); return; } battleState.log.push(es.name+” used “+em.move.name+”!”); applyMoveToTarget(em.move,es,ps,battleState.log); if(ps.hp<=0){ battleState.log.push(ps.name+” fainted!”); const nextIdx=battleState.playerTeam.findIndex(function(p,i){return i>battleState.playerIndex&&p.hp>0;}); if(nextIdx===-1){ const anyIdx=battleState.playerTeam.findIndex(function(p){return p.hp>0;}); if(anyIdx===-1){ renderBattleUI(‘enemy’); setTimeout(function(){ endBattle(false); }, 900); return; } battleState.playerIndex=anyIdx; }else{ battleState.playerIndex=nextIdx; } const nextP=battleState.playerTeam[battleState.playerIndex]; battleState.log.push(“You send out “+nextP.name+”!”); } renderBattleUI(‘enemy’); setTimeout(processNextAction, 900); } } processNextAction(); } function checkStatusBeforeMove(mon,log){ if(mon.status===”slp”){ mon.statusTurns–; if(mon.statusTurns>0){ log.push(mon.name+” is fast asleep!”); return false; } mon.status=null; log.push(mon.name+” woke up!”); } if(mon.status===”frz”){ if(Math.random()<0.2){ mon.status=null; log.push(mon.name+” thawed out!”); }else{ log.push(mon.name+” is frozen solid!”); return false; } } if(mon.status===”par”){ if(Math.random()<0.25){ log.push(mon.name+” is fully paralyzed!”); return false; } } if(mon.status===”cnf”){ mon.statusTurns–; if(mon.statusTurns<=0){ mon.status=null; }else if(Math.random()<0.33){ const dmg=Math.max(1,Math.floor(mon.atk*0.4)); mon.hp=Math.max(0,mon.hp-dmg); log.push(mon.name+” is confused and hurt itself! (-“+dmg+”)”); return false; } } return true; } function endOfTurnEffects(mon,log){ if(!mon||mon.hp<=0)return; if(mon.status===”brn”){ const dmg=Math.max(1,Math.floor(mon.maxHp/16)); mon.hp=Math.max(0,mon.hp-dmg); log.push(mon.name+” is hurt by burn! (-“+dmg+”)”); } if(mon.status===”psn”){ const dmg=Math.max(1,Math.floor(mon.maxHp/8)); mon.hp=Math.max(0,mon.hp-dmg); log.push(mon.name+” is hurt by poison! (-“+dmg+”)”); } if(mon.status===”tox”){ mon.toxCounter=(mon.toxCounter||0)+1; const dmg=Math.max(1,Math.floor(mon.maxHp*(mon.toxCounter/16))); mon.hp=Math.max(0,mon.hp-dmg); log.push(mon.name+” is hurt by toxic! (-“+dmg+”)”); } if(mon.leech){ const dmg=Math.max(1,Math.floor(mon.maxHp/8)); mon.hp=Math.max(0,mon.hp-dmg); log.push(mon.name+” is hurt by leech seed! (-“+dmg+”)”); } } function applyMoveToTarget(move,attacker,defender,log){ if(move.acc&&Math.random()>move.acc){ log.push(“The attack missed!”); return; } if(move.power===0){ if(move.status&&!defender.status){ if(Math.random()<(move.statusChance||1)){ defender.status=move.status; if(move.status===”slp”)defender.statusTurns=2+Math.floor(Math.random()*2); if(move.status===”cnf”)defender.statusTurns=2+Math.floor(Math.random()*2); if(move.status===”tox”)defender.toxCounter=0; log.push(defender.name+” was inflicted with “+STATUS_LABEL[move.status]+”!”); } }else if(move.selfBoost){ move.selfBoost.forEach(function(stat){ attacker.statStages[stat]=(attacker.statStages[stat]||0)+1; }); log.push(attacker.name+”‘s “+move.selfBoost.join(“, “)+” rose!”); }else if(move.leech){ defender.leech=true; log.push(defender.name+” was seeded!”); } return; } const eff=getTypeEffectiveness(move.type,defender.type); if(eff===0){ log.push(“It doesn’t affect “+defender.name+”…”); return; } let atkValue=attacker.atk*Math.pow(1.5,attacker.statStages.atk||0); let defValue=Math.max(1,defender.def*Math.pow(1.5,defender.statStages.def||0)); const levelFactor=(2*attacker.level/5+2); let baseDmg=(levelFactor*move.power*(atkValue/defValue)/50)+2; const isCrit=Math.random()<((move.critBoost?0.125:0.0625)); if(isCrit)baseDmg*=1.5; baseDmg*=eff; const stab=move.type===attacker.type?1.5:1; baseDmg*=stab; const variance=0.85+Math.random()*0.15; let dmg=Math.max(1,Math.floor(baseDmg*variance)); if(attacker.status===”brn”&&move.cat===”phys”)dmg=Math.max(1,Math.floor(dmg*0.5)); defender.hp=Math.max(0,defender.hp-dmg); let effText=””; if(eff>1)effText=” It’s super effective!”; else if(eff<1)effText=” It’s not very effective…”; const critText=isCrit?” Critical hit!”:””; log.push(“Dealt “+dmg+” damage!”+effText+critText); if(move.flinch&&Math.random()<0.3)defender.flinch=true; if(move.recharge)attacker.recharge=true; if(move.recoil){ const rec=Math.floor(dmg*(move.recoil===1?1:move.recoil)); attacker.hp=Math.max(0,attacker.hp-rec); log.push(attacker.name+” takes “+rec+” recoil!”); } if(move.selfDrop){ move.selfDrop.forEach(function(stat){ attacker.statStages[stat]=(attacker.statStages[stat]||0)-1; }); log.push(attacker.name+”‘s “+move.selfDrop.join(“, “)+” fell!”); } if(move.statDrop&&Math.random()<(move.statChance||0.1)){ const stat=move.statDrop; defender.statStages[stat]=(defender.statStages[stat]||0)-1; log.push(defender.name+”‘s “+stat+” fell!”); } if(move.status&&!defender.status&&Math.random()<(move.statusChance||0)){ defender.status=move.status; if(move.status===”slp”)defender.statusTurns=2+Math.floor(Math.random()*2); if(move.status===”cnf”)defender.statusTurns=2+Math.floor(Math.random()*2); log.push(defender.name+” was inflicted with “+STATUS_LABEL[move.status]+”!”); } } function chooseEnemyMove(enemy,player){ const available=[]; enemy.moves.forEach(function(mv,i){ if(!mv)return; if((enemy.currentPp[i]||0)<=0)return; available.push({move:mv,idx:i}); }); if(!available.length)return null; let best=available[0]; let bestScore=-1; available.forEach(function(opt){ const mv=opt.move; if(mv.power===0){ const score=25+(mv.status?35:0); if(score>bestScore){bestScore=score;best=opt;} return; } const eff=getTypeEffectiveness(mv.type,player.type); const stab=mv.type===enemy.type?1.5:1; const acc=mv.acc||1; const score=mv.power*eff*stab*acc*(0.9+Math.random()*0.2); if(score>bestScore){bestScore=score;best=opt;} }); enemy.currentPp[best.idx]–; return best; } function resolveStatusTurnAndEnd(){ endOfTurnEffects(battleState.playerTeam[battleState.playerIndex],battleState.log); endOfTurnEffects(battleState.enemyTeam[battleState.enemyIndex],battleState.log); if(battleState.playerTeam[battleState.playerIndex].hp<=0){ const nextIdx=battleState.playerTeam.findIndex(function(p){return p.hp>0;}); if(nextIdx===-1){endBattle(false);return;} battleState.playerIndex=nextIdx; battleState.log.push(“You send out “+battleState.playerTeam[battleState.playerIndex].name+”!”); } if(battleState.enemyTeam[battleState.enemyIndex].hp<=0){ battleState.enemyIndex++; if(battleState.enemyIndex>=battleState.enemyTeam.length){ endBattle(true); return; } battleState.log.push(battleState.trainerName+” sends out “+battleState.enemyTeam[battleState.enemyIndex].name+”!”); } renderBattleUI(); } function endBattle(won){ if(!battleState)return; battleState.over=true; battleState.won=won; if (battleState.mode === “arena”) { if(won){ battleState.resultText=”ARENA VICTORY!”; gState.arenaWins.push(Date.now()); gState.stats.arenaWins=(gState.stats.arenaWins||0)+1; const playerLv=Math.max(1,Math.round((gState.stats.arenaWins||0)/2)+1); const tier=battleState.trainerTier; const baseCredits=300+tier*400; const lvlBonus=1+Math.min(2,playerLv*0.15); const credits=Math.floor(baseCredits*battleState.multi*lvlBonus); const xp=Math.floor((60+tier*40)*battleState.multi); gState.credits+=credits; addXp(xp); let petRewardMsg=””; if(Math.random()<0.35*(1+tier*0.2)){ const pool=PET_POOLS.rare.concat(PET_POOLS.epic); if(tier>=4)pool.push.apply(pool,PET_POOLS.legendary); const pet=pool[Math.floor(Math.random()*pool.length)]; const reward=addPet(pet,false); if(reward){ const isShiny=Math.random()<0.10; const isMutated=Math.random()<0.25; if(reward&&(isShiny||isMutated)){ gState.pets[pet.id].lv=Math.max(gState.pets[pet.id].lv||1,playerLv+2); } petRewardMsg=” ๐ REWARD: “+pet.icon+” “+pet.name+” joined!”; } } battleState.log.push(“Victory! +”+credits+” credits ยท +”+xp+” XP”+petRewardMsg); sfx(“win”); logEvent(“Arena victory vs “+battleState.trainerName+”. +”+credits+” cr.”); }else{ battleState.resultText=”ARENA DEFEAT”; const penalty=Math.min(50,(gState.stats.arenaWins||0)*10); gState.credits=Math.max(0,gState.credits-penalty); battleState.log.push(“Defeat. You lost “+penalty+” credits. Try again with a stronger team.”); sfx(“attack”); logEvent(“Arena defeat vs “+battleState.trainerName+”.”); } } else if (battleState.mode === “street”) { const target = battleState.target; if (won) { const f = factionById(target.b.o); const cr = Math.round((40 + target.b.lv * 24) * boostMult()); gState.credits += cr; gState.blocks[target.i + “:” + target.j] = { o: “you”, lv: Math.min(5, target.b.lv), t: Date.now(), r: Date.now() }; gState.stats.claimed++; gState.stats.captured++; gState.stats.wins++; gState.battles.wins++; addXp(60); logEvent(“Won a Street Claim battle against the ” + f.name + “.”); sfx(“win”); battleState.resultText = “VICTORY! You took the block. +” + cr + ” credits.”; } else { const loss = Math.min(gState.credits, 20 + target.b.lv * 12); gState.credits -= loss; gState.battles.losses++; addXp(15); logEvent(“Lost a Street Claim battle.”); sfx(“attack”); battleState.resultText = “DEFEAT! Your team fell back. -” + loss + ” credits.”; } } else if (battleState.mode === “duel”) { if (won) { battleState.resultText = “DUEL VICTORY!”; gState.stats.duelsWon = (gState.stats.duelsWon || 0) + 1; gState.credits += 150; addXp(40); sfx(“win”); } else { battleState.resultText = “DUEL DEFEAT”; sfx(“attack”); } } saveGameState(gState); updateGameHud(); renderBattleUI(); } function showStory(){ let h=”<p>Follow the AFBN Street Claim storyline. Complete missions to unlock the next chapter and earn rare rewards.</p>”; h+=”<p style=\”font-size:11px;color:#7fffc4\”>Progress is tracked automatically. Complete each mission to unlock the next.</p>”; let anyVisible=false; STORY_MISSIONS.forEach(function(s,i){ const prereqDone=!s.prereq||!!gState.storyClaimed[s.prereq]; const claimed=!!gState.storyClaimed[s.id]; const cur=storyProgressOf(s); const done=cur>=s.target; if(!prereqDone&&!claimed)return; anyVisible=true; const cardCls=”story-card”+(claimed?” done”:””); h+=”<div class=\””+cardCls+”\”>”; h+=”<div class=\”stitle\”>”+(claimed?”โ “:”๐ “)+s.title+”</div>”; h+=”<div class=\”sdesc\”>”+s.desc+”</div>”; h+=”<div class=\”pbar\”><div style=\”width:”+Math.min(100,Math.round(cur/s.target*100))+”%\”></div></div>”; h+=”<div style=\”font-size:10px;color:#a0c0e0\”>”+Math.min(cur,s.target)+” / “+s.target+”</div>”; h+=”<div class=\”sreward\”>Reward: +”+s.reward+” CR ยท +”+s.xp+” XP”+(s.item?” ยท “+s.item.amount+”ร “+s.item.type:””)+”</div>”; if(done&&!claimed){ h+=”<button class=\”pbtn wide gold\” data-storyclaim=\””+s.id+”\”>CLAIM REWARD</button>”; }else if(claimed){ h+=”<p style=\”color:#7fffc4;font-size:10px;text-align:center\”>โ COMPLETED</p>”; }else if(!prereqDone){ h+=”<p style=\”color:#a0c0e0;font-size:10px;text-align:center\”>๐ Complete previous missions to unlock</p>”; }else{ h+=”<p style=\”color:#ffd93d;font-size:10px;text-align:center\”>IN PROGRESS</p>”; } h+=”</div>”; }); if(!anyVisible){ h+=”<p>No storyline missions available. Start by catching your first pet.</p>”; } openPanel(“STORYLINE”,h); panelBody.onclick=function(e){ const b=e.target.closest(“[data-storyclaim]”); if(!b)return; const id=b.getAttribute(“data-storyclaim”); claimStoryReward(id); }; } function storyProgressOf(s){ if(s.stat===”pets”)return Object.keys(gState.pets).length; if(s.stat===”arenaWins”)return gState.stats.arenaWins||0; if(s.stat===”hatched”)return gState.stats.hatched||0; if(s.stat===”wins”)return gState.stats.duelsWon||0; if(s.stat===”distance”)return gState.stats.distance||0; return gState.stats[s.stat]||0; } function claimStoryReward(id){ const s=STORY_MISSIONS.find(function(x){return x.id===id;}); if(!s)return; if(gState.storyClaimed[s.id])return; if(storyProgressOf(s)<s.target)return; gState.storyClaimed[s.id]=true; gState.credits+=s.reward; addXp(s.xp); if(s.item){ if(s.item.type===”orb”)gState.items.orb+=s.item.amount; else if(s.item.type===”orb_enhanced”)gState.items.orb_enhanced=(gState.items.orb_enhanced||0)+s.item.amount; else if(s.item.type===”orb_precision”)gState.items.orb_precision=(gState.items.orb_precision||0)+s.item.amount; else if(s.item.type===”orb_master”)gState.items.orb_master=(gState.items.orb_master||0)+s.item.amount; else if(s.item.type===”orb_legendary”)gState.items.orb_legendary=(gState.items.orb_legendary||0)+s.item.amount; else if(s.item.type===”orb_anomaly”)gState.items.orb_anomaly=(gState.items.orb_anomaly||0)+s.item.amount; else if(s.item.type===”pet”){ const pool=PET_POOLS.rare.concat(PET_POOLS.epic,PET_POOLS.legendary); const p=pool[Math.floor(Math.random()*pool.length)]; addPet(p,false); } } logEvent(“Storyline reward claimed: “+s.title+”.”); showToast(“Story Complete”,s.title+” ยท +”+s.reward+” cr!”); sfx(“level”); saveGameState(gState); updateGameHud(); showStory(); } function showHelp(){ let h=”<div class=\”helpsection\”><h4>What is Street Claim?</h4><p>Street Claim is a real-world walking game inside AFBN COSMOS. You walk outside with your phone. The map shows streets around you. You claim little blocks of land by walking into them. You catch signal pets, fight rival groups, breed creatures, run an arena team, and complete story missions. Everything is played through the same phone screen.</p></div>”; h+=”<div class=\”helpsection\”><h4>How do I start?</h4><p>1. Allow location access when your phone asks. 2. Walk outside where GPS works. 3. The blue square on the map is the block you are standing on. 4. Tap CLAIM in the command menu or just walk into a new block to claim it. 5. Glowing pet icons are wild signals. Walk within 60 meters and tap one to begin a Resonance Weave. 6. Cosmic Relics are crystalline diamond markers. Walk within 60 meters, tap one, then hold TUNE & EXTRACT to break it into shards. 7. Tap the COMMAND MENU at the bottom to reach every panel.</p></div>”; h+=”<div class=\”helpsection\”><h4>Resonance Weave Capture</h4><p>Wild signals are captured by matching a three-pulse harmony. When you begin a weave, three nodes light in a sequence. Watch the sequence, then tap the same nodes in the same order. A correct sequence extracts shards. A wrong tap collapses the weave and you may try again if you have more weaves. Higher-tier weaves extract more shards and have more forgiving timing. This is the core capture system. No throwing. No orbs.</p></div>”; h+=”<div class=\”helpsection\”><h4>Cosmic Relics</h4><p>Pets arrive from the Cosmos as temporary crystalline forms that land on claimed or claimable blocks. These forms are called Cosmic Relics. Use your Resonator to Tune & Extract. The Relic resists in pulses for 2-4 seconds; keep holding the button to keep the connection steady. Release too early and progress decays. When the meter fills, the Relic breaks into shards and you collect them. When all shards are gathered the pet materializes. The Resonator is limited to ten successful tunes per hour. That limit exists because Relics output large shard amounts.</p></div>”; h+=”<div class=\”helpsection\”><h4>Claiming Blocks</h4><p>Every block on the map can belong to you or to one of the four rival factions: Rust Wolves, Neon Foxes, Void Owls, and Iron Hawks. Walk into an empty block or a rival block to claim it. If you are within 60 meters you get a 1.2 times near bonus. If you are between 60 meters and 25 kilometers you can still interact from a stationary position at a 15 percent far rate. Beyond 25 kilometers you cannot interact at all. Blocks you own can be reinforced once per hour up to level 5. Tap a rival block to see its faction’s pets and levels before you battle.</p></div>”; h+=”<div class=\”helpsection\”><h4>Ways to Earn Credits</h4><p>1. Claim and hold blocks for passive hourly income. 2. Walk. Every meter walked pays “+WALK_REWARD_PER_METER+” credits. 3. Complete hourly, daily, one-time, walking, season pass, and storyline objectives. 4. Reach pet collection milestones for large payouts. 5. Win Street Claim battles, pet duels, and Arena matches. 6. Spin supply depots. 7. Collect the daily login bonus. 8. Use the free daily spin. 9. Catch the Lucky Hour 2x window that appears every four hours for 15 minutes. 10. Hit your daily step goal for streak-scaled rewards.</p></div>”; h+=”<div class=\”helpsection\”><h4>Daily Step Goal & Streaks</h4><p>Open SETTINGS to adjust your daily step goal from 500 to 50000 steps. When you hit your goal each day you earn credits. Consecutive days meeting the goal build a streak, and streak rewards grow: 100, 150, 225, 340, 510, 760, 1140 credits. Miss a day and the streak resets to 1. Steps are estimated from real distance walked.</p></div>”; h+=”<div class=\”helpsection\”><h4>Pokemon-Style Battle Mechanics</h4><p>Arena battles, Street Claim battles, and pet duels use full Pokemon-style combat. Each pet has a Pokemon type derived from its element. Type effectiveness, STAB, critical hits, accuracy rolls, PP tracking per move, stat stages, status conditions, flinch, recharge, leeching, and end-of-turn effects all apply. When you tap the X or CLOSE in the battle panel, the button changes to FLEE and pressing it forfeits the battle safely.</p></div>”; h+=”<div class=\”helpsection\”><h4>Type Chart Summary</h4><p>Fire beats Grass and Ice. Water beats Fire and Ground. Grass beats Water and Ground. Electric beats Water and Flying. Ice beats Dragon and Ground. Fighting beats Normal and Rock. Poison beats Grass and Fairy. Ground beats Fire and Electric. Flying beats Grass and Fighting. Psychic beats Fighting and Poison. Bug beats Grass and Dark. Rock beats Fire and Flying. Ghost beats Psychic. Dragon beats Dragon. Dark beats Psychic. Steel beats Ice and Fairy. Fairy beats Dragon and Dark. Full 18-type chart applies inside the battle engine.</p></div>”; h+=”<div class=\”helpsection\”><h4>Status Conditions</h4><p>BRN: halves physical attack and deals 1/16 maximum HP per turn. PSN: deals 1/8 maximum HP per turn. TOX: deals increasing damage each turn. PAR: 25 percent chance to skip a turn and halves speed. SLP: cannot move for 2 to 3 turns. FRZ: cannot move until a 20 percent thaw roll succeeds. CNF: 33 percent chance to hurt itself instead of attacking. All statuses clear when the battle ends.</p></div>”; h+=”<div class=\”helpsection\”><h4>Arena Grounds</h4><p>The Arena Grounds appear on your map as a golden crossed-swords icon. You must physically walk within “+ARENA_ENTER_RADIUS+” meters of the Arena to enter. It sits about “+ARENA_DISTANCE_METERS+” meters away from your home bubble. Each day you get up to “+ARENA_MAX_WINS+” Arena attempts. Win to earn credits, XP, and a chance at a rare, shiny, or Anomaly pet reward. Higher Arena tiers unlock as you win more battles. Difficulty and rewards both scale.</p></div>”; h+=”<div class=\”helpsection\”><h4>Breeding and Eggs</h4><p>Open PETS, go to BREED, and select two parents. The cost is 800 credits per breeding. Eggs hatch after 30 to 60 minutes. Children inherit averaged stats with variance, plus a 5 percent chance to be shiny and a 10 percent chance to be an Anomaly. Everstone raises Anomaly chance to 15 percent and Shiny Charm raises shiny chance to 10 percent. Each consumes one charge on use. Legendary pets cannot breed. Every parent has a 30 minute cooldown after breeding.</p></div>”; h+=”<div class=\”helpsection\”><h4>Storyline</h4><p>Open STORYLINE from the command menu to follow the AFBN Street Claim story. Each mission unlocks the next. Rewards scale into the thousands of credits and grant rare weaves and pets. Progress is tracked automatically across all gameplay.</p></div>”; h+=”<div class=\”helpsection\”><h4>Resonance Weaves</h4><p>There are six weave tiers. Echo Weave delivers exactly 2 shards per successful weave. Prism Weave and Arc Weave deliver exactly 3. Crown Weave delivers exactly 4. Nova Weave and Void Weave deliver exactly 5. Higher tiers have much wider pulse windows. Void Weave has a 5 percent chance to force an Anomaly pet on a successful weave. Buy weaves from the shop or earn them from missions and depots.</p></div>”; h+=”<div class=\”helpsection\”><h4>Pets and Shards</h4><p>Wild signals appear as creature icons on the map. Walk within 60 meters to begin a weave. Every successful weave gives a fixed shard amount determined by the weave. You need multiple successful weaves across multiple encounters to finish a pet. Rarer pets need more shards: Common 20, Uncommon 35, Rare 60, Epic 100, Legendary 160. The Medic crew member reduces the requirement by 1 for Common, Uncommon, and Rare.</p></div>”; h+=”<div class=\”helpsection\”><h4>Team, Training, and Battle</h4><p>Put up to three pets on your TEAM. Train them with credits to raise level and stats (STR, AGI, INT, VIT, LUK). Hire NPCs for passive bonuses and extra battle power. Use BATTLE near a rival block to fight for it with your team. Use DUEL to fight a rival trainer in a turn-based Pokemon-style battle. Use ARENA to fight the Arena trainers for scaling rewards.</p></div>”; h+=”<div class=\”helpsection\”><h4>Wild Pet Spawning</h4><p>Wild pet spawning is 60 percent faster when you explore more than 60 kilometers away from your home location. Inside your home bubble spawn rates remain normal. The Radar Pack doubles wild signal range for 30 minutes. The Scout crew member raises both spawn rate and range.</p></div>”; h+=”<div class=\”helpsection\”><h4>On-Chain Shop</h4><p>Open the ON-CHAIN SHOP from the top bar. Connect a Solana wallet via Mobile Wallet Adapter, Phantom, Solflare, Backpack, or any injected browser wallet. All purchases are paid in SOL directly to the treasury address CA4n7Lc8g6S9E7CMvY234xsovtsEYAjtuJZuf7yasGND. The shop shows live SOL price from CoinGecko and USD equivalents update in real time. On desktop, injected wallets are detected automatically. On mobile, MWA is attempted first.</p></div>”; h+=”<div class=\”helpsection\”><h4>Daily Rewards and Lucky Hours</h4><p>Open DAILY to see your 7-day login calendar and claim a free spin once per day. A Lucky Hour triggers every four hours for 15 minutes and doubles credits from claims, spins, battles, duels, walking, and income. The lucky badge at the top of the map shows the next window.</p></div>”; h+=”<div class=\”helpsection\”><h4>Notifications and Item Log</h4><p>The bell in the bottom bar shows how many completed objectives are ready to claim. Tap it to see every ready mission, walking goal, season pass quest, and free spin. The bell panel also lists every item you have found while catching wild pets, in the order you found them. Tap any item in that list to open the bag with that item highlighted.</p></div>”; h+=”<div class=\”helpsection\”><h4>Popups and Toasts</h4><p>You can close any open popup or modal by tapping outside the popup card. The same applies to toast notifications and in-game notification cards.</p></div>”; h+=”<div class=\”helpsection\”><h4>GPS Resilience</h4><p>The game watches for stale GPS fixes and automatically tries to refresh your position every 20 seconds when the signal looks stale. If your device cannot get a fresh fix, the map stops panning, your status line shows a stale signal warning, and the game keeps running without freezing. When a fresh fix arrives, the game resumes normal play automatically. If location permission is denied, a GRANT GPS PERMISSIONS button appears in the notification and in the top bar. Tap it to re-request location access.</p></div>”; h+=”<div class=\”helpsection\”><h4>Player Card</h4><p>Open CARD from the command menu to see a downloadable player card that includes all-time stats: total steps, total distance, blocks, pets, arena wins, duel wins, purchases, spins, eggs, breeds, fusions, anomalies, and more. Tap DOWNLOAD PNG to save it to your device.</p></div>”; h+=”<div class=\”helpsection\”><h4>Safety Rules</h4><p>Always watch the real world around you. Never walk into traffic. Never play while driving a car, bike, or any vehicle. Never trespass. Only walk in public areas where you are allowed to be. Weather conditions are checked based on your location and an alert pops up when dangerous conditions are detected. A speed warning appears above 8 km/h and the passenger confirmation appears above 6 km/h when you are moving.</p></div>”; h+=”<div class=\”helpsection\”><h4>Legal</h4><p>Open LEGAL from the command menu to read the copyright notice, user agreement, privacy policy, liability disclaimer, and cookie policy. By continuing to play you accept all of these documents.</p></div>”; openPanel(“HELP & WALKTHROUGH”,h); } function showTerms(){ let h=”<div class=\”helpsection\”><h4>Street Claim User Agreement</h4><p>By entering and using Street Claim inside AFBN COSMOS you agree to all of the following rules. This agreement is between you and A Few Bad Newbies (AFBN). If you do not agree, do not use the game.</p></div>”; h+=”<div class=\”helpsection\”><h4>1. Safety First</h4><p>You must always pay attention to the real world. Do not walk into streets, roads, or dangerous places while looking at your phone. Do not play Street Claim while driving any vehicle. You are fully responsible for your own safety and the safety of those around you.</p></div>”; h+=”<div class=\”helpsection\”><h4>2. Location Permission</h4><p>Street Claim needs your device location to work. Location data stays on your device for gameplay and is not sold. You can turn location off at any time but the game will stop working correctly.</p></div>”; h+=”<div class=\”helpsection\”><h4>3. Virtual Property Only</h4><p>Blocks, pets, credits, and items inside Street Claim are virtual game objects only. They have no real-world money value. Claiming a block does not give you any legal rights over real property.</p></div>”; h+=”<div class=\”helpsection\”><h4>4. Fair Play</h4><p>Do not cheat, use fake GPS, bots, or scripts. Do not exploit bugs. Cheating can result in loss of progress or permanent ban.</p></div>”; h+=”<div class=\”helpsection\”><h4>5. No Real Money Purchases Required</h4><p>Street Claim is free to play. Credits and items are earned by walking, claiming, catching, and completing missions. On-chain purchases are optional and pay SOL to the treasury.</p></div>”; h+=”<div class=\”helpsection\”><h4>6. Content and Behavior</h4><p>Be respectful. Do not use the game to harass anyone. Do not use offensive names or content.</p></div>”; h+=”<div class=\”helpsection\”><h4>7. Data and Progress</h4><p>Your progress is saved on your device using browser local storage. Clearing browser data can erase progress. We do not store an account or restore progress for you.</p></div>”; h+=”<div class=\”helpsection\”><h4>8. Changes and Availability</h4><p>AFBN can change the rules, the game, the rewards, or shut the game down at any time without notice.</p></div>”; h+=”<div class=\”helpsection\”><h4>9. Age</h4><p>Street Claim is intended for players who can safely walk outdoors and understand these rules. If under 13, ask a parent or guardian before playing.</p></div>”; h+=”<div class=\”helpsection\”><h4>10. Limitation of Liability</h4><p>Street Claim is provided as-is. AFBN is not liable for any loss, injury, damage, or claim arising from your use of the game.</p></div>”; h+=”<div class=\”helpsection\”><h4>11. Acceptance</h4><p>By continuing to play you accept this entire agreement.</p></div>”; openPanel(“USER AGREEMENT”,h); } function showCopyright(){ let h=”<div class=\”helpsection\”><h4>Copyright Notice</h4><p>Copyright ยฉ 2026 A Few Bad Newbies (AFBN). All rights reserved.</p></div>”; h+=”<div class=\”helpsection\”><h4>Ownership</h4><p>All source code, artwork, sound assets, game design, written text, layouts, and interactive experiences inside AFBN COSMOS and Street Claim are the exclusive property of A Few Bad Newbies unless otherwise credited.</p></div>”; h+=”<div class=\”helpsection\”><h4>Limited License</h4><p>You are granted a limited, non-exclusive, non-transferable license to view and play this game for personal, non-commercial entertainment. You may not copy, redistribute, resell, reverse engineer, decompile, publicly perform, or create derivative works from any portion of the game without prior written permission from AFBN.</p></div>”; h+=”<div class=\”helpsection\”><h4>Trademarks</h4><p>AFBN, A Few Bad Newbies, AFBN COSMOS, Street Claim, and all associated names, logos, and marks are trademarks of A Few Bad Newbies. All other trademarks are the property of their respective owners.</p></div>”; h+=”<div class=\”helpsection\”><h4>Third-Party Assets</h4><p>This game uses third-party libraries and services including Three.js, Leaflet, OpenStreetMap, Stadia Maps, Esri, Open-Meteo, CoinGecko, and Solana. Their use is governed by their own licenses and terms. Three.js example textures are used under their respective terms.</p></div>”; h+=”<div class=\”helpsection\”><h4>Reporting Infringement</h4><p>If you believe any content inside this game infringes your copyright, contact AFBN through the official channels listed on afbn.me with a description of the material and your contact information.</p></div>”; openPanel(“COPYRIGHT NOTICE”,h); } function showPrivacy(){ let h=”<div class=\”helpsection\”><h4>Privacy Policy</h4><p>This policy explains what information AFBN COSMOS and Street Claim collect and how it is used. It is written to be understood by anyone aged 8 and above without unnecessary jargon.</p></div>”; h+=”<div class=\”helpsection\”><h4>What We Store On Your Device</h4><p>Street Claim stores your game progress in your browser’s local storage. This includes your character name, your detected nationality label, your credits, your blocks, your pets, your inventory, your mission progress, your item log, your play time, your walking distance, your step estimate, your daily step goal, your step streak, and your settings. This data stays on your device and is not sent to AFBN servers.</p></div>”; h+=”<div class=\”helpsection\”><h4>Location Data</h4><p>Street Claim reads your device location to draw the map and place you on it. Location is read on your device by your browser’s geolocation service. Street Claim does not send your precise location to AFBN servers. Two external services receive a rounded latitude and longitude when needed: Open-Meteo receives coordinates for the weather check and Esri and Stadia Maps receive coordinates for map tiles as part of normal tile loading. Nominatim receives the search text you type into the God’s Eye search box. No account is created and no advertising identifier is collected.</p></div>”; h+=”<div class=\”helpsection\”><h4>On-Chain Wallet Data</h4><p>When you connect a Solana wallet, your public key is stored in browser memory for the current session only. Payment transactions are broadcast to the Solana network and are public by design. AFBN does not store your private keys. AFBN does not have access to your funds. All purchases pay SOL directly to the treasury address.</p></div>”; h+=”<div class=\”helpsection\”><h4>Audio and Camera</h4><p>Street Claim does not access your microphone or camera. Audio is generated locally by the Web Audio API. No audio is uploaded.</p></div>”; h+=”<div class=\”helpsection\”><h4>Children</h4><p>Street Claim does not knowingly collect personal information from children. If a child uses the game, they should do so with a parent or guardian present and only in safe walking areas.</p></div>”; h+=”<div class=\”helpsection\”><h4>Your Choices</h4><p>You can clear your data at any time by clearing your browser storage for this site. You can deny or revoke location permission in your browser settings. Denying location prevents gameplay.</p></div>”; h+=”<div class=\”helpsection\”><h4>Changes</h4><p>AFBN may update this policy at any time. The latest version is shown inside the game under LEGAL.</p></div>”; openPanel(“PRIVACY POLICY”,h); } function showLiability(){ let h=”<div class=\”helpsection\”><h4>Liability Disclaimer</h4><p>Street Claim is a location-based walking game. By using it you accept the following disclaimers.</p></div>”; h+=”<div class=\”helpsection\”><h4>Physical Safety</h4><p>You are solely responsible for your physical safety while playing. AFBN is not responsible for accidents, injuries, collisions, property damage, or any harm caused by walking while distracted, walking into traffic, walking in unsafe areas, or playing while driving or operating any vehicle. Do not play while driving. Do not trespass. Respect private property and local laws.</p></div>”; h+=”<div class=\”helpsection\”><h4>On-Chain Transactions</h4><p>On-chain purchases are irreversible. Once a Solana transaction is confirmed, AFBN cannot refund your SOL. Verify the item, the amount, and the treasury address before you confirm. AFBN is not responsible for losses caused by wallet errors, wrong networks, or sending funds to the wrong address. All purchases pay SOL to CA4n7Lc8g6S9E7CMvY234xsovtsEYAjtuJZuf7yasGND.</p></div>”; h+=”<div class=\”helpsection\”><h4>Data Loss</h4><p>Game progress is stored in your browser’s local storage. AFBN is not responsible for lost progress caused by clearing browser data, switching devices, browser updates, private browsing mode, storage quota limits, or device failure.</p></div>”; h+=”<div class=\”helpsection\”><h4>Third-Party Services</h4><p>Street Claim relies on third-party map tiles, geocoding, weather services, price feeds, and RPC endpoints. If those services are unavailable, slow, or inaccurate, the game may not work correctly. AFBN is not responsible for third-party service failures.</p></div>”; h+=”<div class=\”helpsection\”><h4>Virtual Items</h4><p>Blocks, pets, credits, shards, and all other in-game items are virtual and have no real monetary value. They cannot be exchanged for money. They may be changed, reset, or removed at any time.</p></div>”; h+=”<div class=\”helpsection\”><h4>Game Changes</h4><p>AFBN may modify, suspend, or discontinue Street Claim or any part of it at any time without notice. AFBN is not liable for any resulting loss.</p></div>”; h+=”<div class=\”helpsection\”><h4>As-Is Service</h4><p>The game is provided as-is and as-available without warranties of any kind, express or implied, including merchantability, fitness for a particular purpose, and non-infringement.</p></div>”; h+=”<div class=\”helpsection\”><h4>Maximum Liability</h4><p>To the maximum extent permitted by law, AFBN’s total liability for any claim related to Street Claim is limited to the amount you have paid AFBN to use Street Claim.</p></div>”; openPanel(“LIABILITY DISCLAIMER”,h); } function showCookie(){ let h=”<div class=\”helpsection\”><h4>Cookie Policy</h4><p>AFBN COSMOS and Street Claim do not use cookies for advertising, tracking, or analytics.</p></div>”; h+=”<div class=\”helpsection\”><h4>What We Use Instead</h4><p>We use browser local storage. Local storage is a small storage area on your device that survives page reloads. It is used to remember your game progress, your mute setting, your character creation status, whether you have seen the intro, and your settings. It is not a cookie and it is not sent to any server.</p></div>”; h+=”<div class=\”helpsection\”><h4>Third-Party Cookies</h4><p>Map tiles and other third-party resources loaded by the game may set their own cookies or similar technologies in accordance with their own policies. AFBN does not control those cookies. You can block them in your browser if you choose.</p></div>”; h+=”<div class=\”helpsection\”><h4>Clearing Local Storage</h4><p>You can clear local storage at any time through your browser settings. Clearing local storage resets your game progress and settings. Use this option only if you intend to start over.</p></div>”; h+=”<div class=\”helpsection\”><h4>Do Not Track</h4><p>Because we do not track you across sites, we do not respond to Do Not Track signals differently from normal browsing.</p></div>”; h+=”<div class=\”helpsection\”><h4>Contact</h4><p>Questions about this cookie policy can be sent through afbn.me.</p></div>”; openPanel(“COOKIE POLICY”,h); } function showLegalMenu(){ let h=”<p>Legal documents for AFBN COSMOS and Street Claim. Read each one carefully.</p>”; h+=”<button class=\”pbtn wide\” data-legal=\”copyright\”>COPYRIGHT NOTICE</button>”; h+=”<button class=\”pbtn wide\” data-legal=\”terms\”>USER AGREEMENT</button>”; h+=”<button class=\”pbtn wide\” data-legal=\”privacy\”>PRIVACY POLICY</button>”; h+=”<button class=\”pbtn wide\” data-legal=\”liability\”>LIABILITY DISCLAIMER</button>”; h+=”<button class=\”pbtn wide\” data-legal=\”cookie\”>COOKIE POLICY</button>”; openPanel(“LEGAL”,h); panelBody.onclick=function(e){ const b=e.target.closest(“[data-legal]”); if(!b)return; const v=b.getAttribute(“data-legal”); if(v===”copyright”)showCopyright(); else if(v===”terms”)showTerms(); else if(v===”privacy”)showPrivacy(); else if(v===”liability”)showLiability(); else if(v===”cookie”)showCookie(); }; } function recenter(){ followPlayer=true; if(leafletMap)leafletMap.setView([userLat,userLon],Math.max(leafletMap.getZoom(),16),{animate:true}); } function showDuelHome(){ openPanel(“PET DUEL”,”<p>Turn-based pet versus pet battle with full Pokemon-style mechanics. Choose a team of up to three pets and fight a rival trainer.</p><p>Your team: <b>”+gState.team.filter(function(id){return gState.pets[id];}).length+”</b> pets ready.</p><button class=\”pbtn wide\” data-duelact=\”start\”>START DUEL</button>”,true); panelBody.onclick=function(e){ const ac=e.target.closest(“[data-duelact]”); if(ac&&ac.getAttribute(“data-duelact”)===”start”)startDuel(); }; } function startDuel(){ const ids=gState.team.filter(function(id){return gState.pets[id]&&PET_BY_ID[id];}); if(!ids.length){showToast(“Pet Duel”,”Add pets to your team first.”);sfx(“error”);return;} const playerTeam=[]; let sumLv=0; ids.slice(0,3).forEach(function(id){ const sp=PET_BY_ID[id]; const o=gState.pets[id]; const stats=o.stats||{str:10,agi:10,int:10,vit:10,luk:10}; const bp=makeBattlePet(sp,o.lv,stats,o.anomaly); bp.moves=getPokeMovesForPet(sp,o.lv); bp.moves.forEach(function(mv,i){bp.currentPp[i]=mv.pp;}); playerTeam.push(bp); sumLv+=o.lv; }); const avgLv=Math.max(2,Math.round(sumLv/playerTeam.length)); const enemyTeam=[]; for(let i=0;i<3;i++){ const roll=Math.random(); let rk; if(avgLv<6)rk=roll<0.65?”common”:roll<0.92?”uncommon”:”rare”; else if(avgLv<16)rk=roll<0.45?”common”:roll<0.8?”uncommon”:roll<0.96?”rare”:”epic”; else rk=roll<0.3?”uncommon”:roll<0.7?”rare”:roll<0.94?”epic”:”legendary”; const pool=PET_POOLS[rk]; const sp=pool[Math.floor(Math.random()*pool.length)]; const stats={str:8+avgLv*2,agi:8+avgLv*2,int:8+avgLv*2,vit:8+avgLv*2,luk:8+avgLv*2}; const bp=makeBattlePet(sp,avgLv+Math.floor(Math.random()*3)-1,stats,Math.random()<0.04); bp.moves=getPokeMovesForPet(sp,bp.level); bp.moves.forEach(function(mv,i){bp.currentPp[i]=mv.pp;}); enemyTeam.push(bp); } battleState={ mode:”duel”, trainerName:”Rival Trainer”, trainerTier:1, playerTeam:playerTeam, enemyTeam:enemyTeam, playerIndex:0, enemyIndex:0, log:[], over:false, multi:1 }; battleState.log.push(“A rival trainer challenges you to a duel!”); battleState.log.push(“You send out “+playerTeam[0].name+”!”); battleState.log.push(“Rival sends out “+enemyTeam[0].name+”!”); sfx(“battle”); renderBattleUI(); } function showCommandMenu(){ const n=claimableCount(); let h=”<p style=\”text-align:center;margin:0 0 12px;font-size:11px;letter-spacing:2px;color:#7fffc4\”>STREET CLAIM COMMAND MENU</p>”; h+=”<div class=\”cmd-section\”><div class=\”cmd-head\”>Street Claim</div><div class=\”cmd-grid\”>”; h+=”<button class=\”cmd-btn primary\” data-cmd=\”claim\”>๐ CLAIM</button>”; h+=”<button class=\”cmd-btn\” data-cmd=\”income\”>๐ฐ INCOME</button>”; h+=”<button class=\”cmd-btn”+(n>0?” gold”:””)+”\” data-cmd=\”missions\”>๐ MISSIONS”+(n>0?” (“+n+”)”:””)+”</button>”; h+=”<button class=\”cmd-btn pass\” data-cmd=\”pass\”>๐ซ SEASON PASS</button>”; h+=”</div></div>”; h+=”<div class=\”cmd-section\”><div class=\”cmd-head\”>On-Chain</div><div class=\”cmd-grid\”>”; h+=”<button class=\”cmd-btn onchain\” data-cmd=\”onchain\”>๐ ON-CHAIN SHOP</button>”; h+=”<button class=\”cmd-btn onchain\” data-cmd=\”connectwallet\”>๐ CONNECT WALLET</button>”; h+=”</div></div>”; h+=”<div class=\”cmd-section\”><div class=\”cmd-head\”>Market</div><div class=\”cmd-grid\”>”; h+=”<button class=\”cmd-btn\” data-cmd=\”shop\”>๐ SHOP</button>”; h+=”<button class=\”cmd-btn\” data-cmd=\”inventory\”>๐ BAG</button>”; h+=”<button class=\”cmd-btn resonator\” data-cmd=\”relics\”>๐ก RELICS</button>”; h+=”</div></div>”; h+=”<div class=\”cmd-section\”><div class=\”cmd-head\”>Creatures</div><div class=\”cmd-grid\”>”; h+=”<button class=\”cmd-btn pet\” data-cmd=\”pets\”>๐พ PETS</button>”; h+=”<button class=\”cmd-btn pet\” data-cmd=\”team\”>๐ฅ TEAM</button>”; h+=”<button class=\”cmd-btn pet\” data-cmd=\”breed\”>๐ฅ BREED</button>”; h+=”<button class=\”cmd-btn battle\” data-cmd=\”battle\”>โ๏ธ BATTLE</button>”; h+=”<button class=\”cmd-btn duel\” data-cmd=\”duel\”>๐ฏ DUEL</button>”; h+=”<button class=\”cmd-btn battle\” data-cmd=\”hire\”>๐ค HIRE</button>”; h+=”</div></div>”; h+=”<div class=\”cmd-section\”><div class=\”cmd-head\”>Arena & Story</div><div class=\”cmd-grid\”>”; h+=”<button class=\”cmd-btn arena\” data-cmd=\”arena\”>๐๏ธ ARENA</button>”; h+=”<button class=\”cmd-btn story\” data-cmd=\”story\”>๐ STORYLINE</button>”; h+=”</div></div>”; h+=”<div class=\”cmd-section\”><div class=\”cmd-head\”>World</div><div class=\”cmd-grid\”>”; h+=”<button class=\”cmd-btn daily\” data-cmd=\”daily\”>๐ DAILY</button>”; h+=”<button class=\”cmd-btn\” data-cmd=\”codex\”>๐ CODEX</button>”; h+=”<button class=\”cmd-btn\” data-cmd=\”turf\”>๐ด TURF</button>”; h+=”<button class=\”cmd-btn\” data-cmd=\”playercard\”>๐ CARD</button>”; h+=”<button class=\”cmd-btn\” data-cmd=\”profile\”>๐ค PROFILE</button>”; h+=”<button class=\”cmd-btn\” data-cmd=\”settings\”>โ๏ธ SETTINGS</button>”; h+=”<button class=\”cmd-btn\” data-cmd=\”help\”>โ HELP</button>”; h+=”<button class=\”cmd-btn\” data-cmd=\”terms\”>๐ TERMS</button>”; h+=”<button class=\”cmd-btn\” data-cmd=\”legal\”>โ๏ธ LEGAL</button>”; h+=”<button class=\”cmd-btn\” data-cmd=\”center\”>๐ CENTER</button>”; h+=”</div></div>”; openPanel(“COMMAND MENU”,h); panelBody.onclick=function(e){ const b=e.target.closest(“[data-cmd]”); if(!b)return; const cmd=b.getAttribute(“data-cmd”); closePanel(); setTimeout(function(){dispatchCmd(cmd);},60); }; } function dispatchCmd(cmd){ if(cmd===”claim”)doClaim(); else if(cmd===”income”)showIncome(); else if(cmd===”missions”)showMissions(); else if(cmd===”pass”)showPass(); else if(cmd===”shop”)showShop(); else if(cmd===”inventory”)showInventory(); else if(cmd===”relics”){showRelicInfo();} else if(cmd===”pets”)showPets(“collection”); else if(cmd===”team”)showPets(“team”); else if(cmd===”breed”){petTab=”breed”;showPets(“breed”);} else if(cmd===”battle”)showBattle(); else if(cmd===”duel”)startDuel(); else if(cmd===”hire”)showHire(); else if(cmd===”daily”)showDaily(); else if(cmd===”codex”)showCodex(); else if(cmd===”turf”)showTurf(); else if(cmd===”playercard”)showPlayerCard(); else if(cmd===”profile”)showProfile(); else if(cmd===”settings”)showStepSettings(); else if(cmd===”help”)showHelp(); else if(cmd===”terms”)showTerms(); else if(cmd===”legal”)showLegalMenu(); else if(cmd===”center”)recenter(); else if(cmd===”arena”)openArenaGrounds(); else if(cmd===”story”)showStory(); else if(cmd===”onchain”)openOnchainShop(); else if(cmd===”connectwallet”)openWalletModal(); } function showRelicInfo(){ const remainingUses=resonatorUsesRemaining(); const relics=getRelics(); let h=”<p style=\”text-align:center;font-size:11px;letter-spacing:2px;color:#7fffd0\”>COSMIC RELIC FIELD MANUAL</p>”; h+=”<div class=\”battlebox\” style=\”text-align:center;\”><div style=\”font-size:40px\”>๐ </div><p style=\”font-size:12px;color:#a0c0e0;text-align:left\”>Pets arrive from the Cosmos as temporary crystalline forms that land on claimed or claimable blocks. These forms are called Cosmic Relics. When you find one, use your Resonator to break it into shards. Once every shard is collected, the Relic finishes reforming and the pet appears in your roster.</p></div>”; h+=”<p style=\”font-size:12px\”><b>Tool:</b> Resonator โ a frequency-tuned handheld extractor.</p>”; h+=”<p style=\”font-size:12px\”><b>Action:</b> Tune & Extract โ tap the Relic, then hold the Tune button while the Relic resists in pulses. Release too early and progress decays.</p>”; h+=”<p style=\”font-size:12px\”><b>Resonance capacity:</b> <b style=\”color:#7fffc4\”>”+remainingUses+” / “+RESONATOR_HOURLY_LIMIT+”</b> this hour.</p>”; h+=”<p style=\”font-size:12px\”><b>Nearby Relics detected:</b> “+relics.length+”</p>”; if(relics.length){ relics.forEach(function(rl){ const dist=distTo(rl.lat,rl.lon); h+=”<div class=\”shop-item\”><div><b>๐ “+rl.sp.icon+” “+rl.sp.name+”</b><br><span style=\”font-size:11px;color:#a0c0e0\”>”+RARITY[rl.sp.r].label+” ยท “+(isFinite(dist)?Math.round(dist)+” m away”:”waiting for GPS”)+”</span></div></div>”; }); } h+=”<p style=\”font-size:11px;color:#a0c0e0\”>Walk near a Relic and tap its diamond marker on the map to begin Tune & Extract.</p>”; openPanel(“COSMIC RELICS”,h); } function showNotifications(){ ensurePeriods(); const readyMissions=[]; [“hourly”,”daily”,”once”].forEach(function(kind){ MISSIONS[kind].forEach(function(m){ if(!isClaimed(kind,m)&&progressOf(kind,m)>=m.target){ readyMissions.push({kind:kind,m:m}); } }); }); const readyWalk=[]; WALK_GOALS.forEach(function(g){ if(!gState.walkDaily.claimed[g.id]&&gState.walkDaily.progress>=g.target)readyWalk.push(g); }); const readyPass=[]; PASS_QUESTS.forEach(function(q){ if(!gState.pass.claimed[q.id]&&passProgress(q)>=q.target)readyPass.push(q); }); const spinReady=gState.dailySpinLast!==dayKey(new Date()); const total=readyMissions.length+readyWalk.length+readyPass.length+(spinReady?1:0); const log=(Array.isArray(gState.itemLog)?gState.itemLog.slice().reverse():[]); let h=”<p style=\”text-align:center;font-size:11px;letter-spacing:1px;color:#7fffc4\”>COMPLETED OBJECTIVES READY TO CLAIM</p>”; if(total===0&&log.length===0){ h+=”<div class=\”big\”>โ </div><p style=\”text-align:center\”>All caught up. No completed objectives waiting right now.</p>”; h+=”<p style=\”text-align:center;font-size:11px;color:#a0c0e0\”>Keep walking, claiming, and catching to unlock more rewards.</p>”; openPanel(“NOTIFICATIONS”,h); return; } if(total>0){ h+=”<p class=\”hl\”>”+total+” READY</p>”; h+=”<button class=\”pbtn wide gold\” data-notifall=\”1\”>CLAIM ALL</button>”; } if(spinReady){ h+=”<div class=\”mission\” style=\”border-color:#7fffc4\”><div class=\”mtop\”><b>๐ Free Daily Spin</b><span>READY</span></div><div class=\”mdesc\”>Spin the daily wheel for a free reward.</div><div class=\”mbot\”><span>DAILY</span><button class=\”pbtn gold\” data-notif=\”spin\”>OPEN DAILY</button></div></div>”; } readyMissions.forEach(function(item){ h+=”<div class=\”mission\” style=\”border-color:#7fffc4\”><div class=\”mtop\”><b>”+item.m.name+”</b><span>+”+item.m.reward+” cr</span></div><div class=\”mdesc\”>”+item.m.desc+”</div><div class=\”mbot\”><span>”+item.kind.toUpperCase()+”</span><button class=\”pbtn\” data-notif=\”mission:”+item.kind+”:”+item.m.id+”\”>CLAIM</button></div></div>”; }); readyWalk.forEach(function(g){ h+=”<div class=\”mission\” style=\”border-color:#7fffc4\”><div class=\”mtop\”><b>”+g.name+”</b><span>+”+g.reward+” cr</span></div><div class=\”mdesc\”>”+g.desc+”</div><div class=\”mbot\”><span>WALK</span><button class=\”pbtn\” data-notif=\”walk:”+g.id+”\”>CLAIM</button></div></div>”; }); readyPass.forEach(function(q){ h+=”<div class=\”mission\” style=\”border-color:#7fffc4\”><div class=\”mtop\”><b>”+q.name+”</b><span>+”+q.credits+” cr</span></div><div class=\”mdesc\”>”+q.desc+”</div><div class=\”mbot\”><span>PASS</span><button class=\”pbtn gold\” data-notif=\”pass:”+q.id+”\”>CLAIM</button></div></div>”; }); h+=”<p style=\”margin-top:16px\”><b>RECENT ITEMS FOUND</b></p>”; if(!log.length){ h+=”<p style=\”font-size:11px;color:#a0c0e0\”>No items found yet. Catch wild pets to find loot. Found items are listed here in the order you obtained them.</p>”; }else{ log.forEach(function(item,i){ const dt=new Date(item.t); h+=”<div class=\”itemlogrow\” data-itemlog=\””+i+”\”><span class=\”itemlogtime\”>”+dt.toLocaleTimeString()+”</span><span style=\”flex:1\”>”+item.text+”</span><span style=\”color:#7fffc4\”>OPEN โบ</span></div>”; }); } openPanel(“NOTIFICATIONS”,h); panelBody.onclick=function(e){ const ca=e.target.closest(“[data-notifall]”); if(ca){ readyMissions.forEach(function(item){claimMission(item.kind,item.m.id);}); readyWalk.forEach(function(g){claimWalkGoal(g.id);}); readyPass.forEach(function(q){claimPassQuest(q.id);}); if(spinReady)doDailySpin(); showNotifications(); return; } const il=e.target.closest(“[data-itemlog]”); if(il){ const idx=parseInt(il.getAttribute(“data-itemlog”),10); const item=log[idx]; closePanel(); setTimeout(function(){showInventory(item?item.focus:null);},60); return; } const b=e.target.closest(“[data-notif]”); if(!b)return; const v=b.getAttribute(“data-notif”); const parts=v.split(“:”); if(parts[0]===”spin”){closePanel();setTimeout(showDaily,60);return;} if(parts[0]===”mission”){claimMission(parts[1],parts[2]);showNotifications();return;} if(parts[0]===”walk”){claimWalkGoal(parts[1]);showNotifications();return;} if(parts[0]===”pass”){claimPassQuest(parts[1]);showNotifications();return;} }; } gbToggle.addEventListener(“click”,showCommandMenu); gbTally.addEventListener(“click”,showNotifications); function gameTick(){ if(!inGame)return; tickCount++; ensurePeriods(); gState.playSeconds=(gState.playSeconds||0)+5; if(tickCount%4===0)npcTick(); if(tickCount%6===0){pruneSeen();refreshMap();} if(tickCount%12===0)checkTrespass(); checkEggHatches(); checkStepGoal(); updateStatus(); updateGameHud(); updateLuckyBadge(); checkPassengerWarning(); if(Date.now()-lastSave>10000)saveGameState(gState); } function enterGame(idx){ const d=ecoData.find(function(f){return f.idx===idx;}); if(!d)return; pendingZoom=-1; unlockDismissedIdx=-1; inGame=true; lsSet(“cosmosInGame”,”1″); clearGameNotes(); toastEl.classList.remove(“visible”); if(toastTimer){clearTimeout(toastTimer);toastTimer=0;} lastToast=null; lastToastBody=null; lastToastAction=null; const tb=document.getElementById(“toast-gps-btn”); if(tb)tb.style.display=”none”; gState.levelName=d.level||”Earth”; gameTitleEl.textContent=”STREET CLAIM ยท “+gState.levelName.toUpperCase(); hasFix=false; lastCell=null; anchor=null; currentSpeed=0; speedLocked=false; lastGeo={lat:null,lon:null,t:0,acc:9999}; gpsMessage=”Acquiring GPS signal”; if(gameGpsBtn)gameGpsBtn.style.display=”none”; safetyBanner.classList.remove(“show”); gameOverlay.classList.add(“active”); setMusicMode(“game”); ensurePeriods(); dailyLogin(); const now=Date.now(); const offlineMs=Math.max(0,now-(gState.lastOnline||now)); const incomeNow=now; const hoursSinceIncome=Math.min(8,(incomeNow-gState.lastIncome)/3600000); const pendingIncome=Math.floor(incomePerHour()*hoursSinceIncome*incomeBoost()); const npcRes=runNpcSimulation(offlineMs); gState.lastTick=now; gState.lastOnline=now; saveGameState(gState); updateGameHud(); updateLuckyBadge(); initLeaflet(); updateGpsPermissionState(); startGeoWatch(); updateStatus(); if(gameTimer)clearInterval(gameTimer); tickCount=0; gameTimer=setInterval(gameTick,5000); if(offlineMs>=60000){ setTimeout(function(){ showOfflineReport(offlineMs,{income:pendingIncome},npcRes); },700); }else{ npcTick(); } syncHud(); } function exitGame(){ inGame=false; lsSet(“cosmosInGame”,”0″); pendingZoom=-1; unlockDismissedIdx=-1; gameOverlay.classList.remove(“active”); closePanel(); hideOutOfOrbsModal(); clearGameNotes(); stopGeoWatch(); if(gameTimer){clearInterval(gameTimer);gameTimer=null;} if(passengerWarnTimer){clearInterval(passengerWarnTimer);passengerWarnTimer=null;} gState.lastOnline=Date.now(); saveGameState(gState); if(leafletMap){leafletMap.remove();leafletMap=null;youMarker=null;rangeCircle=null;blocksLayer=null;spawnLayer=null;relicLayer=null;arenaLayer=null;} hasFix=false; lastCell=null; anchor=null; setMusicMode(“space”); state.targetDistMul=1; state.distMul=1; syncHud(); } gameExit.addEventListener(“click”,closeStreetToGodsEye); function busy(){ return modal.classList.contains(“active”)||outpostEl.classList.contains(“active”)||worldsPanel.classList.contains(“active”)||!welcome.classList.contains(“hidden”)||inGame||godseyeOpen||unlockModal.classList.contains(“active”)||introModal.classList.contains(“active”)||outOrbsModal.classList.contains(“active”)||charModal.classList.contains(“active”)||passengerModal.classList.contains(“active”)||weatherModal.classList.contains(“active”)||cardModal.classList.contains(“active”)||settingsModal.classList.contains(“active”); } function syncHud(){ const b=busy(); const key=activeIdx+”|”+b+”|”+inGame; if(key===lastHudKey)return; lastHudKey=key; if(activeIdx>=0&&!b){ const d=ecoData.find(function(f){return f.idx===activeIdx;}); const c=d&&d.cat?CATS[d.cat]:null; if(c&&c.items.length){ bunnyBtn.textContent=c.title; bunnyBtn.classList.add(“show”); }else{ bunnyBtn.classList.remove(“show”); } }else{ bunnyBtn.classList.remove(“show”); } if(!inGame&&!b&&!godseyeOpen){ cosmosControls.style.display = “flex”; }else{ cosmosControls.style.display = “none”; } } function renderTiles(items){ outpostGrid.innerHTML=””; for(let i=0;i<items.length;i++){ const it=items[i]; const tile=document.createElement(“div”); tile.className=”otile”; tile.innerHTML=”<div class=\”oicon\”>”+it.icon+”</div><div class=\”olabel\”>”+it.name+”</div>”; tile.addEventListener(“click”,(function(item){return function(e){ if(performance.now()<uiLockUntil){e.preventDefault();e.stopPropagation();return;} activateItem(item); };})(it)); outpostGrid.appendChild(tile); } outpostEmpty.style.display=items.length?”none”:”block”; } function filterOutpost(){ const q=outpostSearch.value.trim().toLowerCase(); if(!q){renderTiles(outpostItems);return;} const out=[]; for(let i=0;i<outpostItems.length;i++){ if(cleanName(outpostItems[i].name).toLowerCase().indexOf(q)!==-1)out.push(outpostItems[i]); } renderTiles(out); } outpostSearch.addEventListener(“input”,filterOutpost); function zoomInto(idx){ unlockDismissedIdx=-1; unlockClosedAt=0; godseyeCloseAt=0; const fi=state.progress*N; if(activeIdx===idx&&Math.abs(fi-idx)<0.05){ pendingZoom=-1; state.targetDistMul=0.01; const d=ecoData.find(function(f){return f.idx===idx;}); if(d&&d.level===”Earth”&&!godseyeOpen&&!inGame){ let tgt = godseyeTarget || getEarthCenterCoords(); if(!tgt) tgt = {lat:userLat,lon:userLon}; openGodsEye(tgt.lat,tgt.lon); } else if(d&&d.level!==”Earth”){ showToast(“Unlockable Content”,”This planet’s Street Claim is coming soon.”); openUnlockModal(); } }else{ pendingZoom=idx; pendingZoomAt=performance.now(); goTo(idx); } } function openOutpost(idx){ const d=ecoData.find(function(f){return f.idx===idx;}); if(!d)return; const c=d.cat?CATS[d.cat]:null; if(!c||!c.items.length){zoomInto(idx);return;} outpostItems=c.items; outpostIcon.textContent=c.icon; outpostTitle.textContent=d.name; outpostCat.textContent=c.title+” ยท “+c.items.length+” destinations”; outpostFact.textContent=d.fact; outpostSearch.value=””; renderTiles(outpostItems); document.getElementById(“outpost-body”).scrollTop=0; outpostEl.style.pointerEvents=”none”; uiLockUntil=performance.now()+750; outpostEl.classList.add(“active”); setTimeout(function(){outpostEl.style.pointerEvents=””;},750); syncHud(); } function closeOutpost(){ outpostEl.classList.remove(“active”); outpostEl.style.pointerEvents=””; syncHud(); } outpostClose.addEventListener(“click”,function(){ if(performance.now()<uiLockUntil)return; closeOutpost(); }); outpostEl.addEventListener(“click”,function(e){ if(performance.now()<uiLockUntil)return; if(e.target===outpostEl)closeOutpost(); }); bunnyBtn.addEventListener(“click”,function(){ if(activeIdx>=0)openOutpost(activeIdx); }); function activateItem(item){ if(item.type===”web”)openModal(item.url,cleanName(item.name)); else if(item.type===”link”)window.open(item.url,”_blank”,”noopener”); else if(item.type===”info”)openModalInfo(item.text,cleanName(item.name)); else if(item.type===”action”)runAction(item.action); } function runAction(action){ if(action===”sound”){toggleSound();showToast(“Sound”,audioMuted?”Audio muted.”:”Audio enabled.”);} else if(action===”intro”){lsDel(“cosmosEntered”);closeOutpost();welcome.classList.remove(“hidden”);syncHud();} else if(action===”playercard”){closeOutpost();showPlayerCard();} } function buildWorlds(){ worldsList.innerHTML=””; for(let i=0;i<ecoData.length;i++){ const d=ecoData[i]; if(d.idx===1)continue; const c=d.cat?CATS[d.cat]:null; const row=document.createElement(“div”); row.className=”wrow”; row.setAttribute(“data-idx”,d.idx); row.innerHTML=”<div class=\”wicon\”></div><div><div class=\”wname\”></div><div class=\”wcat\”></div></div>”; row.querySelector(“.wicon”).textContent=c?c.icon:d.icon; row.querySelector(“.wname”).textContent=d.name; const wc=row.querySelector(“.wcat”); if(c){wc.textContent=c.title;}else{wc.style.display=”none”;} row.addEventListener(“click”,(function(idx){return function(){closeWorlds();goTo(idx);};})(d.idx)); worldsList.appendChild(row); } } function openWorlds(){ const rows=worldsList.querySelectorAll(“.wrow”); for(let i=0;i<rows.length;i++){ const ri=parseInt(rows[i].getAttribute(“data-idx”),10); const cur=activeIdx===ri||(activeIdx===1&&ri===0); rows[i].classList.toggle(“current”,cur); } worldsPanel.classList.add(“active”); syncHud(); } function closeWorlds(){ worldsPanel.classList.remove(“active”); syncHud(); } worldsBtn.addEventListener(“click”,openWorlds); worldsClose.addEventListener(“click”,closeWorlds); worldsQuick.addEventListener(“click”,function(){ closeWorlds(); if(godseyeOpen){ godseyeOpen=false; godseyeOverlay.classList.remove(“active”); godseyeCloseAt=performance.now(); } if(!inGame){ enterGame(0); } }); buildWorlds(); function goTo(idx){ const i=Math.max(0,Math.min(N,idx)); snapTargetIdx = i; const maxScroll=scrollEl.scrollHeight-H; state.targetDistMul=1; state.yaw=0; state.pitch=0; scrollEl.scrollTo({top:(i/N)*maxScroll,behavior:”smooth”}); } const raycaster=new THREE.Raycaster(); raycaster.params.Points={threshold:6}; const mouse=new THREE.Vector2(); function resolvePick(hits,map){ for(let i=0;i<hits.length;i++){ let o=hits[i].object; while(o&&!map.has(o))o=o.parent; if(o)return map.get(o); } return -1; } function pickAt(cx,cy){ mouse.x=(cx/W)*2-1; mouse.y=-(cy/H)*2+1; raycaster.setFromCamera(mouse,camera); const eh=raycaster.intersectObject(earth,false); if(eh&&eh.length){ const lp=earth.worldToLocal(eh[0].point.clone()); const rr=5.7; const la=Math.asin(Math.max(-1,Math.min(1,lp.y/rr)))*180/Math.PI; let lo=Math.atan2(lp.z,-lp.x)*180/Math.PI-180; if(lo<-180)lo+=360; if(lo>180)lo-=360; godseyeTarget={lat:la,lon:lo}; openGodsEye(la,lo); return; } let idx=resolvePick(raycaster.intersectObjects(primaryObjs,true),primaryMap); if(idx<0)idx=resolvePick(raycaster.intersectObjects(secondaryObjs,false),secondaryMap); if(idx<0)return; const d=ecoData.find(function(f){return f.idx===idx;}); if(!d)return; showToast(d.name,d.fact); const c=d.cat?CATS[d.cat]:null; if(c&&c.items.length){openOutpost(idx);} else{zoomInto(idx);} } function getEarthCenterCoords() { mouse.x = 0; mouse.y = 0; raycaster.setFromCamera(mouse, camera); const eh = raycaster.intersectObject(earth, false); if (eh && eh.length) { const lp = earth.worldToLocal(eh[0].point.clone()); const rr = 5.7; const la = Math.asin(Math.max(-1, Math.min(1, lp.y / rr))) * 180 / Math.PI; let lo = Math.atan2(lp.z, -lp.x) * 180 / Math.PI – 180; if (lo < -180) lo += 360; if (lo > 180) lo -= 360; return { lat: la, lon: lo }; } return null; } function onDown(e){ pendingZoom=-1; if(e.touches&&e.touches.length===2){ state.pinch=true;state.drag=false;tap=null; const dx=e.touches[0].clientX-e.touches[1].clientX; const dy=e.touches[0].clientY-e.touches[1].clientY; state.lp=Math.sqrt(dx*dx+dy*dy); twoFingerStartDist=state.lp; lastPanCenter={x:(e.touches[0].clientX+e.touches[1].clientX)/2,y:(e.touches[0].clientY+e.touches[1].clientY)/2}; return; } state.drag=true;state.pinch=false; state.lx=e.touches?e.touches[0].clientX:e.clientX; state.ly=e.touches?e.touches[0].clientY:e.clientY; tap={x:state.lx,y:state.ly,t:performance.now(),moved:false}; state.free=true; unlockAudio(); } function onMove(e){ if(state.pinch&&e.touches&&e.touches.length===2){ const dx=e.touches[0].clientX-e.touches[1].clientX; const dy=e.touches[0].clientY-e.touches[1].clientY; const d=Math.sqrt(dx*dx+dy*dy); const distDelta=d-state.lp; const before=state.targetDistMul; state.targetDistMul=Math.max(0.0008,Math.min(80,state.targetDistMul-distDelta*0.008)); if(state.targetDistMul>before)unlockDismissedIdx=-1; const cx=(e.touches[0].clientX+e.touches[1].clientX)/2; const cy=(e.touches[0].clientY+e.touches[1].clientY)/2; if(lastPanCenter){ const pdx=cx-lastPanCenter.x; const pdy=cy-lastPanCenter.y; if(Math.abs(pdx)>1||Math.abs(pdy)>1){ state.yaw+=pdx*0.003; state.pitch+=pdy*0.003; state.pitch=Math.max(-1.5,Math.min(1.5,state.pitch)); } } lastPanCenter={x:cx,y:cy}; state.lp=d; return; } if(!state.drag)return; const x=e.touches?e.touches[0].clientX:e.clientX; const y=e.touches?e.touches[0].clientY:e.clientY; if(tap&&!tap.moved){ const ddx=x-tap.x;const ddy=y-tap.y; if(ddx*ddx+ddy*ddy>64)tap.moved=true; } state.yaw+=(x-state.lx)*0.008; state.pitch+=(y-state.ly)*0.008; state.pitch=Math.max(-1.5,Math.min(1.5,state.pitch)); state.lx=x;state.ly=y; state.free=true; } function onUp(){state.drag=false;state.pinch=false;lastPanCenter=null;} function onTapEnd(){ if(!tap)return; const t=tap; tap=null; if(t.moved)return; if(performance.now()-t.t>700)return; if(performance.now()<uiLockUntil)return; if(busy())return; pickAt(t.x,t.y); } scrollEl.addEventListener(“mousedown”,onDown,{passive:true}); window.addEventListener(“mousemove”,onMove,{passive:true}); window.addEventListener(“mouseup”,onUp,{passive:true}); scrollEl.addEventListener(“mouseup”,onTapEnd,{passive:true}); scrollEl.addEventListener(“touchstart”,onDown,{passive:true}); scrollEl.addEventListener(“touchmove”,onMove,{passive:true}); scrollEl.addEventListener(“touchend”,function(){onUp();onTapEnd();},{passive:true}); scrollEl.addEventListener(“touchcancel”,function(){tap=null;onUp();},{passive:true}); scrollEl.addEventListener(“wheel”,function(e){ if(e.ctrlKey||e.metaKey||Math.abs(e.deltaY)>0){ e.preventDefault(); const before=state.targetDistMul; state.targetDistMul=Math.max(0.0008,Math.min(80,state.targetDistMul-e.deltaY*0.002)); if(state.targetDistMul>before)unlockDismissedIdx=-1; } unlockAudio(); },{passive:false}); window.addEventListener(“keydown”,function(e){ const tag=e.target&&e.target.tagName; if(tag===”INPUT”||tag===”TEXTAREA”)return; if(e.code===”Space”){e.preventDefault();state.warp=true;} else if(e.key===”=”||e.key===”+”){state.targetDistMul=Math.max(0.0008,state.targetDistMul*0.85);unlockDismissedIdx=-1;} else if(e.key===”-“||e.key===”_”){state.targetDistMul=Math.min(80,state.targetDistMul*1.18);unlockDismissedIdx=-1;} else if(e.key===”r”||e.key===”R”){state.yaw=0;state.pitch=0;state.targetDistMul=1;} else if(e.key===”Escape”){ if(cardModal.classList.contains(“active”))cardModal.classList.remove(“active”); else if(settingsModal.classList.contains(“active”))settingsModal.classList.remove(“active”); else if(weatherModal.classList.contains(“active”))weatherModal.classList.remove(“active”); else if(passengerModal.classList.contains(“active”))passengerModal.classList.remove(“active”); else if(charModal.classList.contains(“active”)){} else if(outOrbsModal.classList.contains(“active”))hideOutOfOrbsModal(); else if(document.getElementById(“onchain-shop”).classList.contains(“active”))closeOnchainShop(); else if(document.getElementById(“wallet-modal”).classList.contains(“active”))closeWalletModal(); else if(modal.classList.contains(“active”))closeModal(); else if(unlockModal.classList.contains(“active”))closeUnlockModal(); else if(introModal.classList.contains(“active”))introOk.click(); else if(godseyeOpen)closeGodsEye(); else if(worldsPanel.classList.contains(“active”))closeWorlds(); else if(outpostEl.classList.contains(“active”))closeOutpost(); else if(inGame){ if(gamePanel.classList.contains(“active”))closePanel(); else closeStreetToGodsEye(); } } else if(!busy()){ const cur=Math.max(0,Math.round(state.progress*N)); if(e.key===”ArrowRight”||e.key===”ArrowDown”){e.preventDefault();goTo(cur+1);} else if(e.key===”ArrowLeft”||e.key===”ArrowUp”){e.preventDefault();goTo(cur-1);} else if(e.key===”Enter”&&activeIdx>=0){zoomInto(activeIdx);} } }); window.addEventListener(“keyup”,function(e){ if(e.code===”Space”){state.warp=false;} }); function catmullRom(p0,p1,p2,p3,t){ const t2=t*t;const t3=t2*t; return 0.5*((2*p1)+(-p0+p2)*t+(2*p0-5*p1+4*p2-p3)*t2+(-p0+3*p1-3*p2+p3)*t3); } function getCameraAt(p){ const n=waypoints.length-1; const f=p*n; const i=Math.min(n-1,Math.floor(f)); const t=f-i; const i0=Math.max(0,i-1); const i1=i; const i2=Math.min(n,i+1); const i3=Math.min(n,i+2); const a=waypoints[i0];const b=waypoints[i1];const c=waypoints[i2];const d=waypoints[i3]; const x=catmullRom(a.pos.x,b.pos.x,c.pos.x,d.pos.x,t); const y=catmullRom(a.pos.y,b.pos.y,c.pos.y,d.pos.y,t); const z=catmullRom(a.pos.z,b.pos.z,c.pos.z,d.pos.z,t); const lx=catmullRom(a.look.x,b.look.x,c.look.x,d.look.x,t); const ly=catmullRom(a.look.y,b.look.y,c.look.y,d.look.y,t); const lz=catmullRom(a.look.z,b.look.z,c.look.z,d.look.z,t); const fov=catmullRom(a.fov,b.fov,c.fov,d.fov,t); return{x:x,y:y,z:z,lx:lx,ly:ly,lz:lz,fov:fov}; } function showToast(title,body,opts){ opts=opts||{}; if(isCatchPanelOpen())return; if(inGame&&gameOverlay.classList.contains(“active”)){ showGameNote(title,body,opts); return; } const action=opts.action||””; if(title===lastToast&&body===lastToastBody&&action===lastToastAction&&toastEl.classList.contains(“visible”))return; lastToast=title; lastToastBody=body; lastToastAction=action; toastTitle.textContent=title; toastBody.textContent=body; const gpsBtn=document.getElementById(“toast-gps-btn”); if(gpsBtn){ gpsBtn.style.display=(action===”gps”)?”inline-block”:”none”; } toastEl.classList.add(“visible”); if(toastTimer)clearTimeout(toastTimer); const hideAfter=(action===”gps”)?15000:3800; toastTimer=setTimeout(function(){ toastEl.classList.remove(“visible”); lastToast=null; lastToastBody=null; lastToastAction=null; if(gpsBtn)gpsBtn.style.display=”none”; },hideAfter); } const tmpVec=new THREE.Vector3(); function frame(now){ if(!COSMOS.alive)return; try{ state.t=now*0.001; const maxScroll=scrollEl.scrollHeight-H; state.targetProgress=Math.max(0,Math.min(0.9999,scrollEl.scrollTop/Math.max(1,maxScroll))); const prev=state.progress; const lerp=state.warp?0.09:(state.free?0.018:0.028); state.progress+=(state.targetProgress-state.progress)*lerp; state.velocity=Math.abs(state.progress-prev)*1400; if(Math.abs(state.progress-lastProgress)<0.0001){pauseTime+=0.016;}else{pauseTime=0;state.free=false;} lastProgress=state.progress; state.distMul+=(state.targetDistMul-state.distMul)*0.06; const cam=getCameraAt(state.progress); const baseDist=Math.sqrt((cam.x-cam.lx)*(cam.x-cam.lx)+(cam.y-cam.ly)*(cam.y-cam.ly)+(cam.z-cam.lz)*(cam.z-cam.lz)); const dirX=(cam.x-cam.lx)/Math.max(0.001,baseDist); const dirY=(cam.y-cam.ly)/Math.max(0.001,baseDist); const dirZ=(cam.z-cam.lz)/Math.max(0.001,baseDist); const finalDist=baseDist*state.distMul; let cx=cam.lx+dirX*finalDist; let cy=cam.ly+dirY*finalDist; let cz=cam.lz+dirZ*finalDist; const cyaw=Math.cos(state.yaw);const syaw=Math.sin(state.yaw); const cpitch=Math.cos(state.pitch);const spitch=Math.sin(state.pitch); const ox=cx-cam.lx;const oy=cy-cam.ly;const oz=cz-cam.lz; const rx=ox*cyaw-oz*syaw; const rz=ox*syaw+oz*cyaw; const ry=oy*cpitch-rz*spitch; const rz2=oy*spitch+rz*cpitch; cx=cam.lx+rx;cy=cam.ly+ry;cz=cam.lz+rz2; camera.position.set(cx,cy,cz); camera.lookAt(cam.lx,cam.ly,cam.lz); camera.fov=cam.fov/Math.sqrt(Math.max(0.03,state.distMul))*(state.warp?1.12:1); camera.updateProjectionMatrix(); if(state.velocity>3.2){ stars.material.size=4.2+Math.min(14,state.velocity*1.1); }else{ stars.material.size=4.2; } const n=waypoints.length-1; const seg=Math.min(n,Math.round(state.progress*n)); if(pauseTime>2.2||(Math.abs(state.progress-(seg/n))<0.01&&state.velocity<1.2)){ if(seg!==currentSnapIdx){ currentSnapIdx=seg; snapTargetIdx=seg; const d=ecoData.find(function(f){return f.idx===seg;}); if(d)showToast(d.name,d.fact); } } const fi=state.progress*n; const ri=Math.round(fi); const act=Math.abs(fi-ri)<0.22?ri:-1; if(act!==activeIdx)activeIdx=act; const currentlyZoomedIn=act>=0&&ecoData[act]&&finalDist<(ecoData[act].zd||999); if(!currentlyZoomedIn){ unlockDismissedIdx=-1; } if(pendingZoom>=0){ if(Math.abs(fi-pendingZoom)<0.03){ const pendingTargetIdx=pendingZoom; state.targetDistMul=0.01; pendingZoom=-1; godseyeCloseAt=0; const d=ecoData.find(function(f){return f.idx===pendingTargetIdx;}); if(d){ if(d.level!==”Earth”){ if(unlockDismissedIdx!==pendingTargetIdx){ unlockTriggerIdx=pendingTargetIdx; showToast(“Unlockable Content”,”This planet’s Street Claim is coming soon.”); openUnlockModal(); } }else{ if(!godseyeOpen&&!inGame){ let tgt=godseyeTarget || getEarthCenterCoords(); if(!tgt) tgt={lat:userLat,lon:userLon}; openGodsEye(tgt.lat,tgt.lon); } } } }else if(performance.now()-pendingZoomAt>4000){ pendingZoom=-1; } } const canReopen=(performance.now()-unlockClosedAt>900); if(!busy()&&!zoomAnimationActive&&act>=0&&Math.abs(fi-ri)<0.06){ const zd=ecoData[act]?ecoData[act].zd:0; if(zd&&finalDist<zd){ const d=ecoData[act]; if(d.level!==”Earth”){ if(canReopen){ unlockTriggerIdx=act; showToast(“Unlockable Content”,”This planet’s Street Claim is coming soon.”); openUnlockModal(); } }else{ if(!godseyeOpen&&performance.now()-godseyeCloseAt>1500){ let tgt=godseyeTarget || getEarthCenterCoords(); if(!tgt) tgt={lat:userLat,lon:userLon}; openGodsEye(tgt.lat,tgt.lon); } } } } syncHud(); const nearEarth=state.progress<0.15&&state.distMul<2.5; youLabel.style.opacity=nearEarth?0.95:0; if(nearEarth){ userMarker.getWorldPosition(tmpVec); tmpVec.project(camera); const sx=(tmpVec.x*0.5+0.5)*W; const sy=(-tmpVec.y*0.5+0.5)*H; youLabel.style.left=(sx+14)+”px”; youLabel.style.top=(sy-10)+”px”; } if(state.progress<0.1){ const phi=(90-userLat)*(Math.PI/180); const theta=(userLon+180)*(Math.PI/180); const px=-Math.sin(phi)*Math.cos(theta); const pz=Math.sin(phi)*Math.sin(theta); const targetY=Math.atan2(-px,pz); let diff=targetY-earth.rotation.y; while(diff>Math.PI)diff-=Math.PI*2; while(diff<-Math.PI)diff+=Math.PI*2; earth.rotation.y+=diff*0.08; earthClouds.rotation.y+=diff*0.08; }else{ earth.rotation.y+=0.00072; earthClouds.rotation.y+=0.00048; } moon.rotation.y+=0.00034; mercury.rotation.y+=0.0012; venus.rotation.y+=0.0003; mars.rotation.y+=0.00072; jupiter.rotation.y+=0.00024; saturnGroup.rotation.y+=0.00019; uranus.rotation.y+=0.00036; neptune.rotation.y+=0.00032; pluto.rotation.y+=0.00044; disk.rotation.z=state.t*0.6; diskHot.rotation.z=-state.t*0.42; diskMid.rotation.z=state.t*0.28; diskOuter.rotation.z=-state.t*0.18; diskFar.rotation.z=state.t*0.11; photonRing.rotation.z=state.t*0.11; bhDust.rotation.y=state.t*0.44; snPhase+=0.02; const snS=1+Math.sin(snPhase)*0.8+(snPhase*0.052)%4.2; snCore.scale.setScalar(snS); snShell.scale.setScalar(snS*2.55); snShell2.scale.setScalar(snS*4.7); snShell3.scale.setScalar(snS*7.6); snShell4.scale.setScalar(snS*11.5); snShell.material.opacity=Math.max(0.02,0.82/snS); snShell2.material.opacity=Math.max(0.01,0.52/snS); snShell3.material.opacity=Math.max(0.005,0.24/snS); snShell4.material.opacity=Math.max(0.002,0.09/snS); snParticles.scale.setScalar(snS*1.18); galaxy1.rotation.y=state.t*0.0082; galaxy2.rotation.y=state.t*0.0066; galaxy3.rotation.y=state.t*0.0098; galaxy4.rotation.y=state.t*0.0056; galaxy5.rotation.y=state.t*0.0078; neb1.rotation.y=state.t*0.018; neb2.rotation.y=-state.t*0.015; neb3.rotation.y=state.t*0.017; neb4.rotation.y=-state.t*0.012; neb5.rotation.y=state.t*0.01; neb6.rotation.y=-state.t*0.013; const arr=asteroids.geometry.attributes.position.array; for(let i=0;i<astCount;i++){ const a=astData[i]; a.angle+=a.speed; arr[i*3]=Math.cos(a.angle)*a.radius; arr[i*3+1]=a.y; arr[i*3+2]=Math.sin(a.angle)*a.radius; } asteroids.geometry.attributes.position.needsUpdate=true; for(let i=0;i<comets.length;i++){ const c=comets[i]; const ud=c.userData; ud.angle+=ud.speed; c.position.x=Math.cos(ud.angle)*ud.radius; c.position.y=ud.y+Math.sin(ud.angle*2)*8; c.position.z=Math.sin(ud.angle)*ud.radius-200; c.lookAt(0,0,-200); } stars.rotation.y=state.t*0.00042; milky.rotation.y=state.t*0.00026; corona.rotation.y=state.t*0.024; textPoints.material.opacity=0.7+Math.sin(state.t*1.5)*0.25; updateAudio(); if(now-lastFpsCheck>1000){ const fps=Math.round(1000/(now-(frame.last||now))+0.5)||60; fpsSamples.push(fps); if(fpsSamples.length>5)fpsSamples.shift(); avgFps=fpsSamples.reduce(function(a,b){return a+b;},0)/fpsSamples.length; fpsEl.textContent=Math.round(avgFps)+” FPS”; if(avgFps<26&&!isLowEnd){ isLowEnd=true; stars.material.size=3.0; } lastFpsCheck=now; } frame.last=now; renderer.render(scene,camera); }catch(e){} requestAnimationFrame(frame); } requestAnimationFrame(frame); window.addEventListener(“visibilitychange”,function(){ if(document.hidden){ if(Snd.ctx&&Snd.ctx.state===”running”)Snd.ctx.suspend(); gState.lastOnline=Date.now(); saveGameState(gState); }else{ resize(); if(Snd.ctx&&!audioMuted&&Snd.ctx.state===”suspended”)Snd.ctx.resume(); } },{passive:true}); window.addEventListener(“beforeunload”,function(){ gState.lastOnline=Date.now(); saveGameState(gState); }); if(gameGpsBtn){ gameGpsBtn.addEventListener(“click”,function(e){ e.preventDefault(); e.stopPropagation(); requestGpsPermission(); }); } gameStatusEl.addEventListener(“click”,function(){ if(!hasFix||gpsFailCount>0){ requestGpsPermission(); } }); const toastGpsBtn=document.getElementById(“toast-gps-btn”); if(toastGpsBtn){ toastGpsBtn.addEventListener(“click”,function(e){ e.preventDefault(); e.stopPropagation(); requestGpsPermission(); }); } console.log(“AFBN COSMOS “+BUILD+” online ยท on-chain shop ready ยท treasury “+TREASURY_WALLET); })(); </script> </body> </html>